diff --git a/README.md b/README.md index d86f6b6..8502839 100644 --- a/README.md +++ b/README.md @@ -337,6 +337,23 @@ Yamdb. Клиент может получить только разрешенн ``` python manage.py fill_db -m [Model] -f [file] ``` +Важно отметить, что название модели нужно вводить строго +с заглавной буквы. Так же для заполнения БД необходимо по следующем критериям: +1. Первым делом заполнить модель User; +2. Заполнить модели Category / Genre; +3. Заполнить модель Title; +4. Заполнить модель GenreTitle; +5. Последующие модели. +Если не учесть данную последовательность, то возникнет ошибка, +т.к. все модели с полями ForeignKey / ManyToManyField ожидают +экземпляр класса связующей ею моделью. + +Просьба - учесть данный факт! + +### Пример команды +``` +python manage.py fill_db -m Category -f category +``` Для подробной информации используйте: ``` python manage.py fill_db -h diff --git a/api_yamdb/api/permissions.py b/api_yamdb/api/permissions.py index aa5c597..3ee47fa 100644 --- a/api_yamdb/api/permissions.py +++ b/api_yamdb/api/permissions.py @@ -2,51 +2,48 @@ from rest_framework.permissions import SAFE_METHODS, BasePermission -class OwnerOrAdmins(permissions.BasePermission): +class PermissionAdmins(permissions.BasePermission): def has_permission(self, request, view): return ( - request.user.is_authenticated - and ( - request.user.is_admin - or request.user.is_superuser) + request.user.is_authenticated + and ( + request.user.is_admin + or request.user.is_superuser + or request.user.is_staff + ) ) - def has_object_permission(self, request, view, obj): - return ( - obj == request.user - or request.user.is_admin - or request.user.is_superuser) - class IsAdminOrReadOnly(BasePermission): """Разрешение на уровне админ.""" def has_permission(self, request, view): return ( - request.method in SAFE_METHODS - or ( - request.user.is_authenticated - and request.user.is_admin - ) + request.method in SAFE_METHODS + or ( + request.user.is_authenticated + and request.user.is_admin + ) ) -class AuthorAndStaffOrReadOnly(BasePermission): +class ModeratorReadOnly(BasePermission): def has_permission(self, request, view): return ( - request.method in SAFE_METHODS - or request.user.is_authenticated + request.method in SAFE_METHODS + or request.user.is_authenticated ) def has_object_permission(self, request, view, obj): return ( - request.method in SAFE_METHODS - or ( - request.user.is_authenticated - and ( - obj.author == request.user - or request.user.is_moderator + request.method in SAFE_METHODS + or ( + request.user.is_authenticated + and ( + obj.author == request.user + or request.user.is_moderator + or request.user.is_admin + ) ) - ) ) diff --git a/api_yamdb/api/serializers.py b/api_yamdb/api/serializers.py index 6c33ec0..786524e 100644 --- a/api_yamdb/api/serializers.py +++ b/api_yamdb/api/serializers.py @@ -1,4 +1,4 @@ -import datetime as dt +from django.core.validators import MaxValueValidator, MinValueValidator from rest_framework import serializers from rest_framework.relations import SlugRelatedField @@ -9,6 +9,7 @@ Review, Title, ) +from reviews.validators import validate_year from users.models import User @@ -16,8 +17,8 @@ class SignUpSerializer(serializers.Serializer): email = serializers.EmailField(max_length=254, required=True) username = serializers.CharField(max_length=150, required=True) - def validate(self, data): - if data['username'] == 'me': + def validate_username(self, data): + if data.lower() == 'me': raise serializers.ValidationError('Нельзя использовать логин me') return data @@ -29,9 +30,6 @@ class TokenSerializer(serializers.Serializer): username = serializers.CharField(max_length=150, required=True) confirmation_code = serializers.CharField(required=True) - class Meta: - fields = ('username', 'confirmation_code') - class UserSerializer(serializers.ModelSerializer): class Meta: @@ -95,9 +93,7 @@ class Meta: model = Title def validate_year(self, value): - current_year = dt.date.today().year - if value > current_year: - raise serializers.ValidationError('Проверьте год') + validate_year(value) return value @@ -156,8 +152,8 @@ def validate(self, data): return data def validate_score(self, value): - if 0 >= value >= 10: - raise serializers.ValidationError('Проверьте оценку') + MaxValueValidator(value), + MinValueValidator(value) return value diff --git a/api_yamdb/api/urls.py b/api_yamdb/api/urls.py index b567dc7..f091bed 100644 --- a/api_yamdb/api/urls.py +++ b/api_yamdb/api/urls.py @@ -20,18 +20,18 @@ router.register('genres', GenresViewSet, basename='genres') router.register('categories', CategoriesViewSet, basename='categories') router.register( - r'titles/(?P[\d]+)/reviews', + r'titles/(?P\d+)/reviews', ReviewViewSet, basename='reviews' ) router.register( - r'titles/(?P[\d]+)/reviews/(?P[\d]+)/comments', + r'titles/(?P\d+)/reviews/(?P\d+)/comments', CommentViewSet, basename='comments', ) urlpatterns = [ - path('v1/auth/token/', token_post), - path('v1/auth/signup/', signup_post), + path('v1/auth/token/', token_post, name='token'), + path('v1/auth/signup/', signup_post, name='signup'), path('v1/', include(router.urls)), ] diff --git a/api_yamdb/api/views.py b/api_yamdb/api/views.py index e22c639..0382f09 100644 --- a/api_yamdb/api/views.py +++ b/api_yamdb/api/views.py @@ -16,8 +16,8 @@ from users.models import User from api.paginator import CommentPagination from api.filters import TitleFilter -from api.permissions import (AuthorAndStaffOrReadOnly, - IsAdminOrReadOnly, OwnerOrAdmins) +from api.permissions import (ModeratorReadOnly, + IsAdminOrReadOnly, PermissionAdmins) from api.serializers import (CategoriesSerializer, CommentsSerializer, GenresSerializer, ReviewsSerializer, SignUpSerializer, @@ -45,7 +45,8 @@ def signup_post(request): user.confirmation_code = confirmation_code user.save() send_mail( - 'Код подверждения', confirmation_code, + 'Confirmation code', + f'Ваш код подтверждения для получения токена: {confirmation_code}', ['admin@email.com'], (email, ), fail_silently=False ) return Response(serializer.data, status=status.HTTP_200_OK) @@ -67,7 +68,7 @@ def token_post(request): class UserViewSet(viewsets.ModelViewSet): queryset = User.objects.all().order_by('id') serializer_class = UserSerializer - permission_classes = (OwnerOrAdmins, ) + permission_classes = (PermissionAdmins, ) filter_backends = (filters.SearchFilter, ) filterset_fields = ('username') search_fields = ('username', ) @@ -93,7 +94,7 @@ def get_patch_me(self, request): class TitlesViewSet(viewsets.ModelViewSet): queryset = Title.objects.annotate( - rating=Avg('reviews__score')).order_by('id') + rating=Avg('reviews__score')) serializer_class = TitlesSerializer permission_classes = [IsAdminOrReadOnly] filterset_class = TitleFilter @@ -122,24 +123,23 @@ class ReviewGenreModelMixin( class CategoriesViewSet(ReviewGenreModelMixin): - queryset = Category.objects.all().order_by('id') + queryset = Category.objects.all() serializer_class = CategoriesSerializer class GenresViewSet(ReviewGenreModelMixin): - queryset = Genre.objects.all().order_by('id') + queryset = Genre.objects.all() serializer_class = GenresSerializer class ReviewViewSet(viewsets.ModelViewSet): serializer_class = ReviewsSerializer pagination_class = CommentPagination - permission_classes = [AuthorAndStaffOrReadOnly] + permission_classes = [ModeratorReadOnly] def get_queryset(self): title = get_object_or_404(Title, id=self.kwargs.get('title_id')) - new_queryset = title.reviews.all() - return new_queryset + return title.reviews.all() def perform_create(self, serializer): title = get_object_or_404(Title, id=self.kwargs.get('title_id')) @@ -149,21 +149,14 @@ def perform_create(self, serializer): class CommentViewSet(viewsets.ModelViewSet): serializer_class = CommentsSerializer pagination_class = CommentPagination - permission_classes = [AuthorAndStaffOrReadOnly] + permission_classes = [ModeratorReadOnly] def get_queryset(self): title = get_object_or_404(Title, id=self.kwargs.get('title_id')) - try: - review = title.reviews.get(id=self.kwargs.get('review_id')) - except TypeError: - TypeError('У произведения нет такого отзыва') - queryset = review.comments.all().order_by('-pub_date') - return queryset + review = title.reviews.get(id=self.kwargs.get('review_id')) + return review.comments.all() def perform_create(self, serializer): title = get_object_or_404(Title, id=self.kwargs.get('title_id')) - try: - review = title.reviews.get(id=self.kwargs.get('review_id')) - except TypeError: - TypeError('У произведения нет такого отзыва') + review = title.reviews.get(id=self.kwargs.get('review_id')) serializer.save(author=self.request.user, review=review) diff --git a/api_yamdb/reviews/migrations/0001_initial.py b/api_yamdb/reviews/migrations/0001_initial.py deleted file mode 100644 index 24a548a..0000000 --- a/api_yamdb/reviews/migrations/0001_initial.py +++ /dev/null @@ -1,90 +0,0 @@ -# Generated by Django 2.2.16 on 2022-07-16 09:35 - -from django.conf import settings -import django.core.validators -from django.db import migrations, models -import django.db.models.deletion -import reviews.validators - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Category', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=256)), - ('slug', models.SlugField(unique=True)), - ], - ), - migrations.CreateModel( - name='Genre', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=256)), - ('slug', models.SlugField(unique=True)), - ], - ), - migrations.CreateModel( - name='GenreTitle', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('genre', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='reviews.Genre')), - ], - ), - migrations.CreateModel( - name='Title', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.TextField()), - ('year', models.IntegerField(help_text='Введите год релиза', validators=[reviews.validators.validate_year], verbose_name='Год релиза')), - ('description', models.TextField(null=True, verbose_name='Описание')), - ('category', models.ForeignKey(blank=True, help_text='Введите категорию произведения', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='titles', to='reviews.Category', verbose_name='Категория')), - ('genre', models.ManyToManyField(through='reviews.GenreTitle', to='reviews.Genre')), - ], - options={ - 'verbose_name': 'Произведение', - 'verbose_name_plural': 'Произведения', - }, - ), - migrations.CreateModel( - name='Review', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('pub_date', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Дата отзыва')), - ('text', models.TextField()), - ('score', models.IntegerField(default=0, validators=[django.core.validators.MaxValueValidator(10), django.core.validators.MinValueValidator(1)], verbose_name='Оценка')), - ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reviews', to=settings.AUTH_USER_MODEL)), - ('title', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reviews', to='reviews.Title')), - ], - options={ - 'ordering': ['-pub_date'], - }, - ), - migrations.AddField( - model_name='genretitle', - name='title', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='reviews.Title'), - ), - migrations.CreateModel( - name='Comment', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('pub_date', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Дата комментария')), - ('text', models.TextField()), - ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to=settings.AUTH_USER_MODEL)), - ('review', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='reviews.Review')), - ], - ), - migrations.AddConstraint( - model_name='review', - constraint=models.UniqueConstraint(fields=('author', 'title'), name='unique_review'), - ), - ] diff --git a/api_yamdb/reviews/models.py b/api_yamdb/reviews/models.py index 39eb085..579ca7e 100644 --- a/api_yamdb/reviews/models.py +++ b/api_yamdb/reviews/models.py @@ -10,6 +10,9 @@ class Genre(models.Model): name = models.CharField(max_length=256) slug = models.SlugField(max_length=50, unique=True) + class Meta: + ordering = ['pk'] + def __str__(self): return self.slug @@ -19,6 +22,9 @@ class Category(models.Model): name = models.CharField(max_length=256) slug = models.SlugField(max_length=50, unique=True) + class Meta: + ordering = ['pk'] + def __str__(self) -> str: return self.slug @@ -26,7 +32,7 @@ def __str__(self) -> str: class Title(models.Model): """Модель Произведение, базовая модель""" - name = models.TextField() + name = models.CharField(max_length=250) year = models.IntegerField( 'Год релиза', validators=[validate_year], @@ -50,6 +56,7 @@ class Title(models.Model): class Meta: verbose_name = 'Произведение' verbose_name_plural = 'Произведения' + ordering = ['name'] def __str__(self) -> str: return self.name @@ -96,6 +103,9 @@ class Meta: fields=['author', 'title'], name="unique_review") ] + def __str__(self): + return self.title + class Comment(models.Model): review = models.ForeignKey( @@ -113,7 +123,10 @@ class Comment(models.Model): auto_now_add=True, db_index=True ) - text = models.TextField() + text = models.TextField('Введите текст комментария') + + class Meta: + ordering = ['-pub_date'] def __str__(self): - return self.author + return self.text diff --git a/api_yamdb/reviews/validators.py b/api_yamdb/reviews/validators.py index 8cfe9f7..91b14ef 100644 --- a/api_yamdb/reviews/validators.py +++ b/api_yamdb/reviews/validators.py @@ -1,7 +1,9 @@ import datetime as dt +from django.core.exceptions import ValidationError + def validate_year(year): now_year = dt.date.today() if year > now_year.year: - raise ValueError(f'Некорректный год {year}') + raise ValidationError(f'Некорректный год {year}') diff --git a/api_yamdb/users/migrations/0001_initial.py b/api_yamdb/users/migrations/0001_initial.py deleted file mode 100644 index d678f93..0000000 --- a/api_yamdb/users/migrations/0001_initial.py +++ /dev/null @@ -1,47 +0,0 @@ -# Generated by Django 2.2.16 on 2022-07-15 09:29 - -import django.contrib.auth.models -from django.db import migrations, models -import django.utils.timezone -import users.validators - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('auth', '0011_update_proxy_permissions'), - ] - - operations = [ - migrations.CreateModel( - name='User', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('username', models.CharField(max_length=150, unique=True, validators=[users.validators.UsernameValidator()], verbose_name='Имя пользователя')), - ('first_name', models.CharField(blank=True, max_length=150)), - ('last_name', models.CharField(blank=True, max_length=150)), - ('email', models.EmailField(max_length=254, unique=True, verbose_name='Email')), - ('role', models.CharField(choices=[('user', 'user'), ('moderator', 'moderator'), ('admin', 'admin')], default='user', max_length=9, verbose_name='Роль пользователя')), - ('bio', models.TextField(blank=True, verbose_name='Биография')), - ('confirmation_code', models.CharField(max_length=100, null=True, verbose_name='Код подтверждения')), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), - ], - options={ - 'verbose_name': 'user', - 'verbose_name_plural': 'users', - 'abstract': False, - }, - managers=[ - ('objects', django.contrib.auth.models.UserManager()), - ], - ), - ] diff --git a/tests/test_07_files.py b/tests/test_07_files.py index e9581a5..4d54d28 100644 --- a/tests/test_07_files.py +++ b/tests/test_07_files.py @@ -21,7 +21,7 @@ f'В корне проекта не найден файл `{filename}`' ) -with open(filename, 'r') as f: +with open(filename, 'r', encoding="utf-8") as f: file = f.read() assert file != default_md, ( f'Не забудьте оформить `{filename}`'