From 350fe429b58ef0aa65929abaadb4881a8f8d77aa Mon Sep 17 00:00:00 2001 From: David Saidov Date: Fri, 9 Aug 2024 21:25:24 +0300 Subject: [PATCH] add news --- backend/_db.sqlite3 | Bin 204800 -> 212992 bytes backend/api/serializers.py | 2 +- backend/api/urls.py | 1 + backend/api/utils.py | 5 +++ backend/api/views.py | 25 +++++++++++++-- backend/backend/settings.py | 6 ++-- backend/news/__init__.py | 0 backend/news/admin.py | 17 ++++++++++ backend/news/apps.py | 7 ++++ backend/news/migrations/0001_initial.py | 30 ++++++++++++++++++ .../migrations/0002_alter_news_options.py | 17 ++++++++++ backend/news/migrations/__init__.py | 0 backend/news/models.py | 30 ++++++++++++++++++ .../0004_alter_taskanswer_is_started.py | 18 +++++++++++ ..._alter_user_email_alter_user_tasks_type.py | 23 ++++++++++++++ 15 files changed, 176 insertions(+), 5 deletions(-) create mode 100644 backend/api/utils.py create mode 100644 backend/news/__init__.py create mode 100644 backend/news/admin.py create mode 100644 backend/news/apps.py create mode 100644 backend/news/migrations/0001_initial.py create mode 100644 backend/news/migrations/0002_alter_news_options.py create mode 100644 backend/news/migrations/__init__.py create mode 100644 backend/news/models.py create mode 100644 backend/tasks/migrations/0004_alter_taskanswer_is_started.py create mode 100644 backend/users/migrations/0009_alter_user_email_alter_user_tasks_type.py diff --git a/backend/_db.sqlite3 b/backend/_db.sqlite3 index 4a4528bfa5ef6c18fd31aa55e1addc5050d40eb4..064a8e9d7b85239dd55f14bf79fb8daacca2fca9 100644 GIT binary patch delta 4026 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 V5QBIv<}rm4BM4d!QIAIO{{gwH&949e diff --git a/backend/api/serializers.py b/backend/api/serializers.py index cf7fd70..f9c8f42 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -26,4 +26,4 @@ class Meta: class NewsSerializer(serializers.ModelSerializer): class Meta: model = News - fields = ('id', 'title', ' description', 'image', 'text', 'created_at') + fields = ('id', 'title', 'description', 'image', 'text', 'created_at') diff --git a/backend/api/urls.py b/backend/api/urls.py index 0b7de26..31335bb 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -15,6 +15,7 @@ router.register(r'answers/(?P\d+)', task_views.TaskAnswerViewSet, basename='answers') router.register(r'regions', views.RegionViewSet, basename='regions') +router.register(r'news', views.NewsViewSet, basename='news') urlpatterns = [ diff --git a/backend/api/utils.py b/backend/api/utils.py new file mode 100644 index 0000000..e2ea54a --- /dev/null +++ b/backend/api/utils.py @@ -0,0 +1,5 @@ +from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination + + +class Limit100OffsetPagination(LimitOffsetPagination): + max_limit = 100 diff --git a/backend/api/views.py b/backend/api/views.py index 07403f8..a1d55c6 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -5,7 +5,9 @@ from rest_framework import permissions from api.mixins import RetrieveListViewSet -from api.serializers import TaskSerializer, RegionSerializer +from api.serializers import TaskSerializer, RegionSerializer, ShortNewsSerializer, NewsSerializer +from api.utils import Limit100OffsetPagination +from news.models import News from tasks.models import Task from users.models import Region @@ -15,6 +17,25 @@ class TaskViewSet(RetrieveListViewSet): serializer_class = TaskSerializer permission_classes = (permissions.AllowAny,) + @method_decorator(cache_page(settings.TASKS_LIST_TTL)) + def list(self, request, *args, **kwargs): + return super().list(request, *args, **kwargs) + + +class NewsViewSet(RetrieveListViewSet): + queryset = News.objects.all() + permission_classes = (permissions.AllowAny,) + pagination_class = Limit100OffsetPagination + + def get_serializer_class(self): + match self.action: + case 'list': + return ShortNewsSerializer + case 'retrieve': + return NewsSerializer + case _: + return NewsSerializer + class RegionViewSet(RetrieveListViewSet): """Представляет регионы. Доступны только операции чтения.""" @@ -23,7 +44,7 @@ class RegionViewSet(RetrieveListViewSet): serializer_class = RegionSerializer filter_backends = (filters.SearchFilter,) search_fields = ('name',) - permission_classes = (permissions.AllowAny,) + permission_classes = (permissions.IsAuthenticated,) ordering = ('name',) @method_decorator(cache_page(settings.REGIONS_LIST_TTL)) diff --git a/backend/backend/settings.py b/backend/backend/settings.py index 17a9bfa..8d61963 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -9,7 +9,8 @@ # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent -REGIONS_LIST_TTL = 60 +REGIONS_LIST_TTL = 120 +TASKS_LIST_TTL = 120 SECRET_KEY = os.getenv('SECRET_KEY', default='key') @@ -43,7 +44,8 @@ INSTALLED_APPS += [ 'users.apps.UsersConfig', 'tasks.apps.TasksConfig', - 'api.apps.ApiConfig' + 'api.apps.ApiConfig', + 'news.apps.NewsConfig' ] MIDDLEWARE = [ diff --git a/backend/news/__init__.py b/backend/news/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/news/admin.py b/backend/news/admin.py new file mode 100644 index 0000000..d0c7564 --- /dev/null +++ b/backend/news/admin.py @@ -0,0 +1,17 @@ +from django.contrib import admin +from django.utils.safestring import mark_safe + +from news.models import News + + +@admin.register(News) +class NewsAdmin(admin.ModelAdmin): + list_display = ('id', 'title', 'image', 'created_at',) + search_fields = ('title', 'description') + + def image_tag(self, obj): + if obj.image: + return mark_safe(f'') + return "No Image" + + image_tag.short_description = 'Изображение' diff --git a/backend/news/apps.py b/backend/news/apps.py new file mode 100644 index 0000000..a3f7d52 --- /dev/null +++ b/backend/news/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class NewsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'news' + verbose_name = 'Новости' diff --git a/backend/news/migrations/0001_initial.py b/backend/news/migrations/0001_initial.py new file mode 100644 index 0000000..9af1345 --- /dev/null +++ b/backend/news/migrations/0001_initial.py @@ -0,0 +1,30 @@ +# Generated by Django 5.0.6 on 2024-08-09 18:10 + +import news.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='News', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255, verbose_name='Заголовок')), + ('image', models.ImageField(upload_to=news.models.news_files_path, verbose_name='Изображение')), + ('description', models.CharField(max_length=500, verbose_name='Краткое содержание')), + ('text', models.TextField(verbose_name='Текст')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), + ], + options={ + 'verbose_name': 'Новость', + 'verbose_name_plural': 'Новости', + }, + ), + ] diff --git a/backend/news/migrations/0002_alter_news_options.py b/backend/news/migrations/0002_alter_news_options.py new file mode 100644 index 0000000..9793d2c --- /dev/null +++ b/backend/news/migrations/0002_alter_news_options.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0.6 on 2024-08-09 18:15 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('news', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='news', + options={'ordering': ('-created_at',), 'verbose_name': 'Новость', 'verbose_name_plural': 'Новости'}, + ), + ] diff --git a/backend/news/migrations/__init__.py b/backend/news/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/news/models.py b/backend/news/models.py new file mode 100644 index 0000000..5c73358 --- /dev/null +++ b/backend/news/models.py @@ -0,0 +1,30 @@ +from django.db import models + + +def news_files_path(instance, filename) -> str: + """Функция для формирования пути сохранения изображения. + + :param instance: Экземпляр модели. + :param filename: Имя файла. + :return: Путь к файлу. + """ + filename = filename.split('.') + last_news_instance = News.objects.last() + instance_id = last_news_instance.id+1 if last_news_instance else 1 + return f'news/{instance_id}/{filename[0][:20]}.{filename[1]}' + + +class News(models.Model): + title = models.CharField(max_length=255, verbose_name='Заголовок') + image = models.ImageField(verbose_name='Изображение', upload_to=news_files_path) + description = models.CharField(max_length=500, verbose_name='Краткое содержание') + text = models.TextField(verbose_name='Текст') + created_at = models.DateTimeField(verbose_name='Дата создания', auto_now_add=True) + + class Meta: + verbose_name = 'Новость' + verbose_name_plural = 'Новости' + ordering = ('-created_at',) + + def __str__(self): + return self.title diff --git a/backend/tasks/migrations/0004_alter_taskanswer_is_started.py b/backend/tasks/migrations/0004_alter_taskanswer_is_started.py new file mode 100644 index 0000000..891baf9 --- /dev/null +++ b/backend/tasks/migrations/0004_alter_taskanswer_is_started.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.6 on 2024-08-09 18:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0003_alter_task_name_alter_taskanswer_is_correct'), + ] + + operations = [ + migrations.AlterField( + model_name='taskanswer', + name='is_started', + field=models.BooleanField(default=True, verbose_name='Задание начато'), + ), + ] diff --git a/backend/users/migrations/0009_alter_user_email_alter_user_tasks_type.py b/backend/users/migrations/0009_alter_user_email_alter_user_tasks_type.py new file mode 100644 index 0000000..4b59935 --- /dev/null +++ b/backend/users/migrations/0009_alter_user_email_alter_user_tasks_type.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.6 on 2024-08-09 18:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0008_rename_user_id_child_user_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='email', + field=models.EmailField(max_length=254, unique=True, verbose_name='Email'), + ), + migrations.AlterField( + model_name='user', + name='tasks_type', + field=models.CharField(choices=[('индивидуальный', 'индивидуальный'), ('групповой', 'групповой')], max_length=20, verbose_name='Формат занятий'), + ), + ]