From 7caed2fa34d8f378a1c6636fe103a4f4f81272b2 Mon Sep 17 00:00:00 2001 From: qwertttyyy Date: Sat, 9 Mar 2024 21:54:59 +0300 Subject: [PATCH 1/7] added comment to content --- backend/ambassadors/factories.py | 3 +- backend/ambassadors/models.py | 17 ++- backend/api/v1/constants.py | 30 ++--- backend/api/v1/filters.py | 3 + backend/api/v1/pagination.py | 4 + backend/api/v1/schemas.py | 192 +++++++++++++-------------- backend/api/v1/serializers.py | 1 + backend/api/v1/tests/test_content.py | 4 + backend/api/v1/views.py | 25 ++-- 9 files changed, 138 insertions(+), 141 deletions(-) diff --git a/backend/ambassadors/factories.py b/backend/ambassadors/factories.py index 7dbde94..a3a400a 100644 --- a/backend/ambassadors/factories.py +++ b/backend/ambassadors/factories.py @@ -95,6 +95,7 @@ class Meta: elements=[choice[0] for choice in CONTENT_STATUS_CHOICES], ) ambassador = factory.SubFactory(AmbassadorFactory) + comment = Faker('text', max_nb_chars=200) class PromoCodeFactory(DjangoModelFactory): @@ -105,5 +106,5 @@ class Meta: name = Faker('lexify', text='?' * 16) status = Faker( 'random_element', - elements=[choice[0] for choice in PROMO_CODE_STATUS_CHOICES] + elements=[choice[0] for choice in PROMO_CODE_STATUS_CHOICES], ) diff --git a/backend/ambassadors/models.py b/backend/ambassadors/models.py index d8fcc22..bb71bd5 100644 --- a/backend/ambassadors/models.py +++ b/backend/ambassadors/models.py @@ -15,6 +15,7 @@ class City(models.Model): """ Модель для названий городов. """ + name = models.CharField(max_length=255, verbose_name='Название города') def __str__(self): @@ -29,6 +30,7 @@ class Country(models.Model): """ Модель для названий стран. """ + name = models.CharField(max_length=255, verbose_name='Название страны') def __str__(self): @@ -92,13 +94,10 @@ class Ambassador(models.Model): on_delete=models.SET_NULL, null=True, related_name='ambassadors', - verbose_name='Страна' + verbose_name='Страна', ) city = models.ForeignKey( - City, - on_delete=models.SET_NULL, - null=True, - verbose_name='Город' + City, on_delete=models.SET_NULL, null=True, verbose_name='Город' ) address = models.CharField(max_length=255, verbose_name='Адрес проживания') postal_code = models.CharField(max_length=20, verbose_name='Индекс') @@ -196,7 +195,13 @@ class Content(models.Model): null=True, blank=True, related_name='content', - verbose_name='амбассадор' + verbose_name='амбассадор', + ) + comment = models.TextField( + max_length=200, + verbose_name='Комментарий менеджера', + null=True, + blank=True, ) created_date = models.DateField( auto_now_add=True, verbose_name='Дата создания' diff --git a/backend/api/v1/constants.py b/backend/api/v1/constants.py index be00708..768e531 100644 --- a/backend/api/v1/constants.py +++ b/backend/api/v1/constants.py @@ -11,7 +11,8 @@ "id": 1, "status": "new", "created_date": SCHEMA_DATE, - "ambassador": 1 + "ambassador": 1, + "comment": "Пример комментария", } | CONTENT_REQ_EXAMPLE CONTENT_PATCH_EXAMPLE = {"status": "rejected"} @@ -20,19 +21,16 @@ "status_send": "new", "comment": "Комментарий", "name_merch": 1, - "ambassador": 1 + "ambassador": 1, } MERCH_RESP_EXAMPLE = {"id": 1, "created_date": SCHEMA_DATE} | MERCH_REQ_EXAMPLE -MERCH_PATCH_EXAMPLE = { - "status_send": "new", - "comment": "Комментарий" -} +MERCH_PATCH_EXAMPLE = {"status_send": "new", "comment": "Комментарий"} PROMO_CODE_REQ_EXAMPLE = { "status": "active", "name": "promo_code", - "ambassador": 1 + "ambassador": 1, } PROMO_CODE_RESP_EXAMPLE = {"id": 1} | PROMO_CODE_REQ_EXAMPLE @@ -56,26 +54,16 @@ "additional_comments": "Пример комментария", "status": "active", "ya_edu": 1, - "amb_goals": [ - 1 - ] + "amb_goals": [1], } AMBASSADOR_RESP_EXAMPLE = AMBASSADOR_REQ_EXAMPLE | { "id": 1, - "ya_edu": { - "id": 1, - "name": "Пример программы обучения" - }, - "amb_goals": [ - { - "id": 1, - "name": "Пример цели" - } - ], + "ya_edu": {"id": 1, "name": "Пример программы обучения"}, + "amb_goals": [{"id": 1, "name": "Пример цели"}], "promo_code": "promo_code_example", "content_count": 0, "reg_date": f"{SCHEMA_DATE}T20:00:00.000+03:00", "country": 1, - "city": 1 + "city": 1, } diff --git a/backend/api/v1/filters.py b/backend/api/v1/filters.py index fc06147..92893b8 100644 --- a/backend/api/v1/filters.py +++ b/backend/api/v1/filters.py @@ -18,6 +18,9 @@ class ContentFilter(FilterSet): status = ChoiceFilter(choices=CONTENT_STATUS_CHOICES) full_name = CharFilter(field_name='full_name', lookup_expr='icontains') + ya_edu = CharFilter( + field_name='ambassador__ya_edu__name', lookup_expr='icontains' + ) class Meta: model = Content diff --git a/backend/api/v1/pagination.py b/backend/api/v1/pagination.py index 7d5fd7f..314c83e 100644 --- a/backend/api/v1/pagination.py +++ b/backend/api/v1/pagination.py @@ -4,3 +4,7 @@ class AmbassadorPagination(LimitOffsetPagination): default_limit = 30 max_limit = 100 + + +class ContentPagination(AmbassadorPagination): + pass diff --git a/backend/api/v1/schemas.py b/backend/api/v1/schemas.py index b025ab9..12ca8e0 100644 --- a/backend/api/v1/schemas.py +++ b/backend/api/v1/schemas.py @@ -23,7 +23,7 @@ def get_unique_id_param(name): name='id', description=f'Уникальный идентификатор {name}', required=True, - location=OpenApiParameter.PATH + location=OpenApiParameter.PATH, ) @@ -31,23 +31,23 @@ def get_unique_id_param(name): 'retrieve': extend_schema( summary='Получение конкретного объекта амбассадора', description='Возвращает объект амбассадора, ' - 'по переданному ID в параметре пути.', + 'по переданному ID в параметре пути.', examples=[ OpenApiExample( 'retrieve_ambassador_example', summary='Пример ответа на получение амбассадора', value=AMBASSADOR_RESP_EXAMPLE, - response_only=True + response_only=True, ) ], - parameters=[get_unique_id_param('амбассадора')] + parameters=[get_unique_id_param('амбассадора')], ), 'list': extend_schema( summary='Получение списка амбассадоров', description='Возвращает пагинированный список объектов амбассадора с ' - 'возможностью фильтрации по городу, стране, гендеру, ' - 'статусу и образовательной программе ' - 'с возможностью применения сортировки.', + 'возможностью фильтрации по городу, стране, гендеру, ' + 'статусу и образовательной программе ' + 'с возможностью применения сортировки.', parameters=[ OpenApiParameter( name='status', @@ -79,39 +79,37 @@ def get_unique_id_param(name): OpenApiParameter( name='city', description='Параметр фильтра по городу(-ам).\n\nНесколько ' - 'значений могут быть разделены запятыми.', + 'значений могут быть разделены запятыми.', type=int, - many=True + many=True, ), OpenApiParameter( name='country', description='Параметр фильтра по стране(-ам).\n\nНесколько ' - 'значений могут быть разделены запятыми.', + 'значений могут быть разделены запятыми.', type=int, - many=True + many=True, ), OpenApiParameter( name='gender', description='Параметр фильтра по полу.', - enum=[ - 'Мужской', 'Женский' - ], + enum=['Мужской', 'Женский'], ), OpenApiParameter( name='ya_edu', description='Параметр фильтра по образовательной ' - 'программе(-ам).\n\nНесколько ' - 'значений могут быть разделены запятыми.', + 'программе(-ам).\n\nНесколько ' + 'значений могут быть разделены запятыми.', type=int, - many=True + many=True, ), OpenApiParameter( name='status', description='Параметр фильтра по статусу.\n\n' - '- `active` - активные \n\n' - '- `paused` - на паузе\n\n' - '- `not_ambassador` - не амбассадор\n\n' - '- `pending` - уточняется', + '- `active` - активные \n\n' + '- `paused` - на паузе\n\n' + '- `not_ambassador` - не амбассадор\n\n' + '- `pending` - уточняется', examples=[ OpenApiExample('Активный', value='active'), OpenApiExample('На паузе', value='paused'), @@ -122,14 +120,12 @@ def get_unique_id_param(name): OpenApiParameter( name='order', description='Параметр сортировки по дате.\n\n' - '- ` date` - по возрастанию даты\n\n' - '- `-date` - по убыванию даты\n\n' - '- ` name` - по возрастанию имени\n\n' - '- `-name` - по убыванию имени', - enum=[ - 'date', '-date', 'name', '-name' - ], - many=True + '- ` date` - по возрастанию даты\n\n' + '- `-date` - по убыванию даты\n\n' + '- ` name` - по возрастанию имени\n\n' + '- `-name` - по убыванию имени', + enum=['date', '-date', 'name', '-name'], + many=True, ), ], examples=[ @@ -137,33 +133,33 @@ def get_unique_id_param(name): name='list_ambassador_example', summary='Пример ответа на получение списка амбассадоров', value=[AMBASSADOR_RESP_EXAMPLE], - response_only=True + response_only=True, ) ], ), 'create': extend_schema( summary='Создание нового объекта амбассадора', description='Создает новый объект амбассадора с данными, ' - 'предоставленными в запросе.', + 'предоставленными в запросе.', examples=[ OpenApiExample( name='create_ambassador_example', summary='Пример запроса на создание амбассадора', value=AMBASSADOR_REQ_EXAMPLE, - request_only=True + request_only=True, ), OpenApiExample( name='create_ambassador_example', summary='Пример ответа на создание амбассадора', value=AMBASSADOR_RESP_EXAMPLE, - response_only=True - ) - ] + response_only=True, + ), + ], ), 'partial_update': extend_schema( summary='Частичное обновление объекта амбассадора', description='Обновляет часть данных объекта амбассадора с ' - 'указанным ID. \n\n', + 'указанным ID. \n\n', examples=[ OpenApiExample( 'patch_ambassador_example', @@ -178,12 +174,12 @@ def get_unique_id_param(name): response_only=True, ), ], - parameters=[get_unique_id_param('амбассадора')] + parameters=[get_unique_id_param('амбассадора')], ), 'destroy': extend_schema( summary='Удаление амбассадора', description='Удаляет объект существующего амбассадора с указанным ID.', - parameters=[get_unique_id_param('амбассадора')] + parameters=[get_unique_id_param('амбассадора')], ), } @@ -198,54 +194,54 @@ def get_unique_id_param(name): value={ 'ya_edu': { 'name': 'Программа обучения', - 'values': [{'id': 1, 'name': 'Пример программы'}] + 'values': [{'id': 1, 'name': 'Пример программы'}], }, 'country': { 'name': 'Страна', - 'values': [{'id': 1, 'name': 'Пример страны'}] + 'values': [{'id': 1, 'name': 'Пример страны'}], }, 'city': { 'name': 'Город', - 'values': [{'id': 1, 'name': 'Пример города'}] + 'values': [{'id': 1, 'name': 'Пример города'}], }, 'status': { 'name': 'Статус амбассадора', - 'values': [{'id': 'status', 'name': 'Пример статуса'}] + 'values': [{'id': 'status', 'name': 'Пример статуса'}], }, 'gender': { 'name': 'Пол', - 'values': [{'id': 'E', 'name': 'Пример пола'}] + 'values': [{'id': 'E', 'name': 'Пример пола'}], }, 'order': { 'name': 'Сортировать', 'values': [ {'id': 'example_sort', 'name': 'Пример сортировки'}, - ] - } - } + ], + }, + }, ) - ] + ], } content_schema = { 'retrieve': extend_schema( summary='Получение конкретного объекта контента', description='Возвращает объект контента, ' - 'по переданному ID в параметре пути.', + 'по переданному ID в параметре пути.', examples=[ OpenApiExample( 'retrieve_content_example', summary='Пример ответа на получение контента', value=CONTENT_RESP_EXAMPLE, - response_only=True + response_only=True, ) ], - parameters=[get_unique_id_param('контента')] + parameters=[get_unique_id_param('контента')], ), 'list': extend_schema( summary='Получение списка контента', description='Возвращает пагинированный список объектов контента с ' - 'возможностью фильтрации по статусу и поиска по имени.', + 'возможностью фильтрации по статусу и поиска по имени.', parameters=[ OpenApiParameter( name='status', @@ -260,6 +256,10 @@ def get_unique_id_param(name): name='full_name', description='Поиск по имени и фамилии', ), + OpenApiParameter( + name='ya_edu', + description='Поиск по программе обучения амбассадора', + ), OpenApiParameter( name='limit', description='Лимит объектов на странице', @@ -277,12 +277,12 @@ def get_unique_id_param(name): summary='Пример ответа получения списка контента.', value=CONTENT_RESP_EXAMPLE, ) - ] + ], ), 'create': extend_schema( summary='Создание нового объекта контента', description='Создает новый объект контента с данными, ' - 'предоставленными в запросе.', + 'предоставленными в запросе.', examples=[ OpenApiExample( 'create_content_example', @@ -301,7 +301,7 @@ def get_unique_id_param(name): 'partial_update': extend_schema( summary='Частичное обновление объекта контента', description='Обновляет часть данных объекта контента с указанным ID. ' - '\n\nИспользовать для обновления статуса', + '\n\nИспользовать для обновления статуса', examples=[ OpenApiExample( 'patch_content_example', @@ -316,7 +316,7 @@ def get_unique_id_param(name): response_only=True, ), ], - parameters=[get_unique_id_param('контента')] + parameters=[get_unique_id_param('контента')], ), } @@ -324,16 +324,16 @@ def get_unique_id_param(name): 'retrieve': extend_schema( summary='Получение конкретного промокода', description='Возвращает объект промокода, ' - 'по переданному ID в параметре пути.', + 'по переданному ID в параметре пути.', examples=[ OpenApiExample( 'retrieve_promo_example', summary='Пример ответа на получение промокода', value=PROMO_CODE_RESP_EXAMPLE, - response_only=True + response_only=True, ) ], - parameters=[get_unique_id_param('промокода')] + parameters=[get_unique_id_param('промокода')], ), 'list': extend_schema( summary='Получение списка промокодов', @@ -343,9 +343,9 @@ def get_unique_id_param(name): 'list_promo_example', summary='Пример ответа на получение списка промокодов', value=[PROMO_CODE_RESP_EXAMPLE], - response_only=True + response_only=True, ), - ] + ], ), 'partial_update': extend_schema( summary='Изменение существующего промокода', @@ -355,40 +355,40 @@ def get_unique_id_param(name): 'patch_promo_example', summary='Пример запроса изменения промокода', value=PROMO_CODE_REQ_EXAMPLE, - request_only=True + request_only=True, ), OpenApiExample( 'patch_promo_example', summary='Пример ответа на изменение промокода', value=PROMO_CODE_RESP_EXAMPLE, - response_only=True + response_only=True, ), ], - parameters=[get_unique_id_param('промокода')] + parameters=[get_unique_id_param('промокода')], ), 'create': extend_schema( summary='Создание нового промокода', description='Создает новый объект промокода с данными, ' - 'предоставленными в запросе.', + 'предоставленными в запросе.', examples=[ OpenApiExample( 'create_promo_example', summary='Пример запроса на создание промокода', value=PROMO_CODE_REQ_EXAMPLE, - request_only=True + request_only=True, ), OpenApiExample( 'create_promo_example', summary='Пример ответа на создание промокода', value=PROMO_CODE_RESP_EXAMPLE, - response_only=True - ) - ] + response_only=True, + ), + ], ), 'destroy': extend_schema( summary='Удаление промокода', description='Удаляет объект существующего промокода с указанным ID.', - parameters=[get_unique_id_param('промокода')] + parameters=[get_unique_id_param('промокода')], ), } @@ -396,16 +396,16 @@ def get_unique_id_param(name): 'retrieve': extend_schema( summary='Получение конкретной заявки', description='Возвращает объект заявки, ' - 'по переданному ID в параметре пути.', + 'по переданному ID в параметре пути.', examples=[ OpenApiExample( 'retrieve_merchandise_example', summary='Пример ответа на получение заявки', value=MERCH_RESP_EXAMPLE, - response_only=True + response_only=True, ) ], - parameters=[get_unique_id_param('заявки')] + parameters=[get_unique_id_param('заявки')], ), 'list': extend_schema( summary='Получение списка заявок', @@ -415,9 +415,9 @@ def get_unique_id_param(name): 'list_merchandise_example', summary='Пример ответа на получение списка заявок', value=MERCH_RESP_EXAMPLE, - response_only=True + response_only=True, ), - ] + ], ), 'partial_update': extend_schema( summary='Изменение существующей заявки', @@ -427,35 +427,35 @@ def get_unique_id_param(name): 'patch_merchandise_example', summary='Пример запроса изменения заявки', value=MERCH_PATCH_EXAMPLE, - request_only=True + request_only=True, ), OpenApiExample( 'patch_merchandise_example', summary='Пример ответа на изменение заявки', value=MERCH_RESP_EXAMPLE, - response_only=True + response_only=True, ), ], - parameters=[get_unique_id_param('заявки')] + parameters=[get_unique_id_param('заявки')], ), 'create': extend_schema( summary='Создание новой заявки', description='Создает новый объект заявки с данными, ' - 'предоставленными в запросе.', + 'предоставленными в запросе.', examples=[ OpenApiExample( 'create_merchandise_example', summary='Пример запроса на создание заявки', value=MERCH_REQ_EXAMPLE, - request_only=True + request_only=True, ), OpenApiExample( 'create_merchandise_example', summary='Пример ответа на создание заявки', value=MERCH_RESP_EXAMPLE, - response_only=True - ) - ] + response_only=True, + ), + ], ), } @@ -466,21 +466,16 @@ def get_unique_id_param(name): OpenApiExample( 'list_goals_example', summary='Пример ответа на получение целей', - value=[ - { - "id": 1, - "name": "Пример цели" - } - ], - response_only=True + value=[{"id": 1, "name": "Пример цели"}], + response_only=True, ) - ] + ], } loyalty_schema = { 'summary': 'Получение списка амбассадоров в программе лояльности', 'description': 'Возвращает список объектов амбассадоров ' - 'в программе лояльности.', + 'в программе лояльности.', 'examples': [ OpenApiExample( 'list_loyalty_example', @@ -490,12 +485,12 @@ def get_unique_id_param(name): "id": 1, "full_name": "Иван Иванов", "content_count": 1, - "shipped_merch": "Пример названия мерча" + "shipped_merch": "Пример названия мерча", } ], response_only=True, ) - ] + ], } training_program_schema = { @@ -505,13 +500,8 @@ def get_unique_id_param(name): OpenApiExample( 'list_training_program_example', summary='Пример ответа на получение программ обучения', - value=[ - { - "id": 1, - "name": "Пример программы" - } - ], - response_only=True + value=[{"id": 1, "name": "Пример программы"}], + response_only=True, ) - ] + ], } diff --git a/backend/api/v1/serializers.py b/backend/api/v1/serializers.py index 3b576e0..e3fb8be 100644 --- a/backend/api/v1/serializers.py +++ b/backend/api/v1/serializers.py @@ -80,6 +80,7 @@ class Meta: extra_kwargs = { 'ambassador': {'required': False}, 'status': {'required': False}, + 'comment': {'required': False}, } def create(self, validated_data): diff --git a/backend/api/v1/tests/test_content.py b/backend/api/v1/tests/test_content.py index acad065..3c3d01e 100644 --- a/backend/api/v1/tests/test_content.py +++ b/backend/api/v1/tests/test_content.py @@ -21,12 +21,14 @@ def setUp(self): self.client.force_authenticate(user=self.user) def test_get_all_content(self): + """Тест на получение списка всего контента амбассадоров.""" self.content2 = ContentFactory() response = self.client.get(self.list_url) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data), 2) def test_create_content(self): + """Тест на создание контента.""" new_content_data = { 'full_name': self.ambassador.full_name, 'telegram': self.ambassador.telegram, @@ -42,6 +44,7 @@ def test_create_content(self): self.assertEqual(response.data['status'], 'new') def test_create_content_with_unexpected_telegram(self): + """Тест на создание контента с telegram несуществующего амбассадора.""" new_content_data = { 'full_name': self.ambassador.full_name, 'telegram': '@wrongtestuser', @@ -57,6 +60,7 @@ def test_create_content_with_unexpected_telegram(self): self.assertEqual(response.data['status'], 'new') def test_update_content_status(self): + """Тест на обновление статуса контента.""" new_status_request = {'status': 'rejected'} response = self.client.patch(self.detail_url, new_status_request) self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/backend/api/v1/views.py b/backend/api/v1/views.py index 02d96d6..78d9d74 100644 --- a/backend/api/v1/views.py +++ b/backend/api/v1/views.py @@ -19,7 +19,7 @@ from rest_framework.response import Response from .filters import AmbassadorFilter, ContentFilter -from .pagination import AmbassadorPagination +from .pagination import AmbassadorPagination, ContentPagination from .permissions import IsAuthenticatedOrYandexForms from .schemas import ( ambassador_schema, @@ -73,8 +73,9 @@ def get_queryset(self): case 'list' | 'retrieve': active_promocodes = PromoCode.objects.filter(status='active') prefetch_active_promocodes = Prefetch( - 'promo_code', queryset=active_promocodes, - to_attr='prefetched_promo_codes' + 'promo_code', + queryset=active_promocodes, + to_attr='prefetched_promo_codes', ) return Ambassador.objects.select_related( @@ -107,17 +108,17 @@ def filters(self, request): 'ya_edu': { 'name': 'Программа обучения', 'type': 'checkbox', - 'values': ya_edu_options + 'values': ya_edu_options, }, 'country': { 'name': 'Страна', 'type': 'checkbox', - 'values': country_options + 'values': country_options, }, 'city': { 'name': 'Город', 'type': 'checkbox', - 'values': city_options + 'values': city_options, }, 'status': { 'name': 'Статус амбассадора', @@ -154,14 +155,10 @@ class PromoCodeViewSet(viewsets.ModelViewSet): """ Вьюсет для модели промокода. """ + queryset = PromoCode.objects.all() serializer_class = PromoCodeSerializer - http_method_names = ( - 'get', - 'post', - 'patch', - 'delete' - ) + http_method_names = ('get', 'post', 'patch', 'delete') @extend_schema(tags=["Контент"]) @@ -219,6 +216,8 @@ class AmbassadorLoyaltyViewSet(ListAPIView): queryset = Ambassador.objects.prefetch_related( content_prefetch, shipped_merch_prefetch ) + pagination_class = ContentPagination + permission_classes = (IsAuthenticatedOrYandexForms,) serializer_class = LoyaltyAmbassadorSerializer @@ -227,6 +226,7 @@ class AmbassadorGoalView(ListAPIView): """ View для просмотра списка целей амбассадорства. """ + queryset = AmbassadorGoal.objects.all() serializer_class = AmbassadorGoalSerializer @@ -236,5 +236,6 @@ class TrainingProgramView(ListAPIView): """ View для просмотра списка программ обучения. """ + queryset = TrainingProgram.objects.all() serializer_class = TrainingProgramSerializer From c3bc00045738b68ba3ad267ae63c17952676bdc3 Mon Sep 17 00:00:00 2001 From: qwertttyyy Date: Sat, 9 Mar 2024 22:20:51 +0300 Subject: [PATCH 2/7] fix tests --- ...0002_alter_merchandise_options_and_more.py | 37 +++++++++++++++++++ .../migrations/0003_content_comment.py | 23 ++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 backend/ambassadors/migrations/0002_alter_merchandise_options_and_more.py create mode 100644 backend/ambassadors/migrations/0003_content_comment.py diff --git a/backend/ambassadors/migrations/0002_alter_merchandise_options_and_more.py b/backend/ambassadors/migrations/0002_alter_merchandise_options_and_more.py new file mode 100644 index 0000000..757b37a --- /dev/null +++ b/backend/ambassadors/migrations/0002_alter_merchandise_options_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 4.2.10 on 2024-03-08 17:48 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("ambassadors", "0001_initial"), + ] + + operations = [ + migrations.AlterModelOptions( + name="merchandise", + options={"verbose_name": "Мерч", "verbose_name_plural": "Мерч"}, + ), + migrations.AlterField( + model_name="merchandiseshippingrequest", + name="ambassador", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="merch_shipping_requests", + to="ambassadors.ambassador", + verbose_name="Амбассадор", + ), + ), + migrations.AlterField( + model_name="merchandiseshippingrequest", + name="name_merch", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + to="ambassadors.merchandise", + verbose_name="Название мерча", + ), + ), + ] diff --git a/backend/ambassadors/migrations/0003_content_comment.py b/backend/ambassadors/migrations/0003_content_comment.py new file mode 100644 index 0000000..b60c7cd --- /dev/null +++ b/backend/ambassadors/migrations/0003_content_comment.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.10 on 2024-03-09 17:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("ambassadors", "0002_alter_merchandise_options_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="content", + name="comment", + field=models.TextField( + blank=True, + max_length=200, + null=True, + verbose_name="Комментарий менеджера", + ), + ), + ] From e0947d46eb454f21d93f0736ffd7c4f14a064878 Mon Sep 17 00:00:00 2001 From: qwertttyyy Date: Sun, 10 Mar 2024 15:13:23 +0300 Subject: [PATCH 3/7] pep8 fix --- backend/api/v1/schemas.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/backend/api/v1/schemas.py b/backend/api/v1/schemas.py index fdc5ed0..a6c268c 100644 --- a/backend/api/v1/schemas.py +++ b/backend/api/v1/schemas.py @@ -481,14 +481,7 @@ def get_unique_id_param(name): OpenApiExample( 'list_loyalty_example', summary='Пример ответа на получение амбассадоров', - value=[ - { - "id": 1, - "full_name": "Иван Иванов", - "content_count": 1, - "shipped_merch": "Пример названия мерча", - } - ], + value=LOYALTY_RESP_EXAMPLE, response_only=True, ) ], From 3b5b6f548f53472c5fb9cffe827e34abc4a388ce Mon Sep 17 00:00:00 2001 From: Sherif Ragimov Date: Sun, 10 Mar 2024 16:15:44 +0400 Subject: [PATCH 4/7] postgresql --- .../ambassadors/migrations/0001_initial.py | 9 +- backend/config/settings.py | 19 ++- backend/requirements.txt | 2 + backend/users/migrations/0001_initial.py | 149 +++--------------- .../0002_alter_user_phone_number.py | 19 --- 5 files changed, 45 insertions(+), 153 deletions(-) delete mode 100644 backend/users/migrations/0002_alter_user_phone_number.py diff --git a/backend/ambassadors/migrations/0001_initial.py b/backend/ambassadors/migrations/0001_initial.py index b5f8adb..2acafbd 100644 --- a/backend/ambassadors/migrations/0001_initial.py +++ b/backend/ambassadors/migrations/0001_initial.py @@ -1,7 +1,7 @@ -# Generated by Django 4.2.10 on 2024-03-06 05:56 +# Generated by Django 4.2.10 on 2024-03-10 11:58 -import django.db.models.deletion from django.db import migrations, models +import django.db.models.deletion class Migration(migrations.Migration): @@ -80,6 +80,7 @@ class Migration(migrations.Migration): ], options={ 'verbose_name': 'Мерч', + 'verbose_name_plural': 'Мерч', }, ), migrations.CreateModel( @@ -113,8 +114,8 @@ class Migration(migrations.Migration): ('status_send', models.CharField(choices=[('new', 'Новая заявка'), ('address_verified', 'Адрес проверен'), ('sent_to_logisticians', 'Отправлена логистам')], default='new', max_length=25, verbose_name='Статус отправки')), ('created_date', models.DateField(auto_now_add=True, verbose_name='Дата создания')), ('comment', models.TextField(max_length=200, verbose_name='Комментарий менеджера')), - ('ambassador', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ambassadors.ambassador', verbose_name='Амбассадор')), - ('name_merch', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ambassadors.merchandise', verbose_name='Название мерча')), + ('ambassador', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='merch_shipping_requests', to='ambassadors.ambassador', verbose_name='Амбассадор')), + ('name_merch', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='ambassadors.merchandise', verbose_name='Название мерча')), ], options={ 'verbose_name': 'Заявка на отправку мерча', diff --git a/backend/config/settings.py b/backend/config/settings.py index 19a7b38..d65f85a 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -5,9 +5,9 @@ SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-key') -DEBUG = os.environ.get('DEBUG', default='True').lower() == 'true' +DEBUG = os.environ.get('DEBUG', 'True').lower() == 'true' -ALLOWED_HOSTS = os.getenv("DJANGO_ALLOWED_HOSTS", '*').split() +ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", '*').split() CSRF_TRUSTED_ORIGINS = ['https://crm.ragimov700.ru'] INSTALLED_APPS = [ @@ -66,10 +66,21 @@ WSGI_APPLICATION = 'config.wsgi.application' +# DATABASES = { +# 'default': { +# 'ENGINE': 'django.db.backends.sqlite3', +# 'NAME': BASE_DIR / 'db.sqlite3', +# } +# } + DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': os.getenv('POSTGRES_DB', 'postgres'), + 'USER': os.getenv('POSTGRES_USER', 'admin'), + 'PASSWORD': os.getenv('POSTGRES_PASSWORD', 'password'), + 'HOST': os.getenv('DB_HOST', 'localhost'), + 'PORT': os.getenv('DB_PORT', 5432) } } diff --git a/backend/requirements.txt b/backend/requirements.txt index 643249d..69354de 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -18,6 +18,7 @@ djangorestframework==3.14.0 djangorestframework-simplejwt==5.3.1 djoser==2.2.2 drf-spectacular==0.27.1 +et-xmlfile==1.1.0 factory-boy==3.3.0 Faker==24.0.0 flake8==7.0.0 @@ -31,6 +32,7 @@ jsonschema-specifications==2023.12.1 mccabe==0.7.0 oauthlib==3.2.2 openpyxl==3.1.2 +psycopg2-binary==2.9.9 pyasn1==0.5.1 pyasn1-modules==0.3.0 pycodestyle==2.11.1 diff --git a/backend/users/migrations/0001_initial.py b/backend/users/migrations/0001_initial.py index db07fd3..b8548c2 100644 --- a/backend/users/migrations/0001_initial.py +++ b/backend/users/migrations/0001_initial.py @@ -1,10 +1,10 @@ -# Generated by Django 4.2.10 on 2024-02-23 13:09 +# Generated by Django 4.2.10 on 2024-03-10 11:58 import django.contrib.auth.models import django.contrib.auth.validators import django.core.validators -import django.utils.timezone from django.db import migrations, models +import django.utils.timezone class Migration(migrations.Migration): @@ -12,139 +12,36 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ("auth", "0012_alter_user_first_name_max_length"), + ('auth', '0012_alter_user_first_name_max_length'), ] operations = [ migrations.CreateModel( - name="User", + name='User', fields=[ - ( - "id", - models.BigAutoField( - 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", - ), - ), - ( - "username", - models.CharField( - error_messages={ - "unique": "A user with that username already exists." - }, - help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", - max_length=150, - unique=True, - validators=[ - django.contrib.auth.validators.UnicodeUsernameValidator() - ], - verbose_name="username", - ), - ), - ( - "first_name", - models.CharField( - blank=True, max_length=150, verbose_name="first name" - ), - ), - ( - "last_name", - models.CharField( - blank=True, max_length=150, verbose_name="last name" - ), - ), - ( - "email", - models.EmailField( - blank=True, max_length=254, verbose_name="email address" - ), - ), - ( - "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" - ), - ), - ( - "patronymic", - models.CharField(max_length=150, verbose_name="Отчество"), - ), - ( - "phone_number", - models.CharField( - max_length=16, - unique=True, - validators=[ - django.core.validators.RegexValidator( - message="Номер телефона необходимо вводить в формате: «+999999999». Допускается до 15 цифр.", - regex="'^\\+\\d{8,15}$'", - ) - ], - ), - ), - ( - "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", - ), - ), + ('id', models.BigAutoField(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')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('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')), + ('patronymic', models.CharField(max_length=150, verbose_name='Отчество')), + ('phone_number', models.CharField(max_length=16, unique=True, validators=[django.core.validators.RegexValidator(message='Номер телефона необходимо вводить в формате: «+999999999». Допускается до 15 цифр.', regex='^\\+\\d{8,15}$')])), + ('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, + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, }, managers=[ - ("objects", django.contrib.auth.models.UserManager()), + ('objects', django.contrib.auth.models.UserManager()), ], ), ] diff --git a/backend/users/migrations/0002_alter_user_phone_number.py b/backend/users/migrations/0002_alter_user_phone_number.py deleted file mode 100644 index eb0f3af..0000000 --- a/backend/users/migrations/0002_alter_user_phone_number.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 4.2.10 on 2024-02-24 09:32 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('users', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='user', - name='phone_number', - field=models.CharField(max_length=16, unique=True, validators=[django.core.validators.RegexValidator(message='Номер телефона необходимо вводить в формате: «+999999999». Допускается до 15 цифр.', regex='^\\+\\d{8,15}$')]), - ), - ] From a404b7198128c00af99287f2af9b5197abf3c4ad Mon Sep 17 00:00:00 2001 From: Sherif Ragimov Date: Sun, 10 Mar 2024 16:18:48 +0400 Subject: [PATCH 5/7] fix CI for postgre --- .github/workflows/main.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8892d8f..0e639cf 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,8 +8,17 @@ on: jobs: tests: - + name: Test with flake8 and django tests runs-on: ubuntu-latest + services: + postgres: + image: postgres:13 + env: + POSTGRES_DB: postgres + POSTGRES_USER: admin + POSTGRES_PASSWORD: password + ports: + - 5432:5432 steps: - name: Check out code From db96f18906ec0d63a9268409270d059753c51f20 Mon Sep 17 00:00:00 2001 From: Sherif Ragimov Date: Sun, 10 Mar 2024 16:33:33 +0400 Subject: [PATCH 6/7] fix factories --- backend/ambassadors/factories.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/ambassadors/factories.py b/backend/ambassadors/factories.py index 7dbde94..a6c991c 100644 --- a/backend/ambassadors/factories.py +++ b/backend/ambassadors/factories.py @@ -59,10 +59,10 @@ class Meta: ya_edu = factory.SubFactory(TrainingProgramFactory) country = factory.SubFactory(CountryFactory) city = factory.SubFactory(CityFactory) - address = Faker('street_address') + address = str(Faker('street_address'))[:20] postal_code = Faker('postcode') email = Faker('email') - phone_number = Faker('phone_number') + phone_number = str(Faker('phone_number'))[:20] telegram = Faker('user_name') edu = Faker('text', max_nb_chars=200) work = Faker('company') From b5bf630a7a31c0c43bbd646f55c98710996a5750 Mon Sep 17 00:00:00 2001 From: Sherif Ragimov Date: Sun, 10 Mar 2024 16:36:00 +0400 Subject: [PATCH 7/7] migrations --- .../ambassadors/migrations/0001_initial.py | 3 +- ...0002_alter_merchandise_options_and_more.py | 37 ------------------- .../migrations/0003_content_comment.py | 23 ------------ backend/users/migrations/0001_initial.py | 2 +- 4 files changed, 3 insertions(+), 62 deletions(-) delete mode 100644 backend/ambassadors/migrations/0002_alter_merchandise_options_and_more.py delete mode 100644 backend/ambassadors/migrations/0003_content_comment.py diff --git a/backend/ambassadors/migrations/0001_initial.py b/backend/ambassadors/migrations/0001_initial.py index 2acafbd..a285794 100644 --- a/backend/ambassadors/migrations/0001_initial.py +++ b/backend/ambassadors/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.10 on 2024-03-10 11:58 +# Generated by Django 4.2.10 on 2024-03-10 12:35 from django.db import migrations, models import django.db.models.deletion @@ -131,6 +131,7 @@ class Migration(migrations.Migration): ('link', models.CharField(max_length=200, verbose_name='Ссылка на контент')), ('guide', models.BooleanField(verbose_name='В рамках Гайда?')), ('status', models.CharField(choices=[('new', 'Новая публикация'), ('approved', 'Одобрена'), ('rejected', 'Не одобрена')], default='new', max_length=50, verbose_name='Статус контента')), + ('comment', models.TextField(blank=True, max_length=200, null=True, verbose_name='Комментарий менеджера')), ('created_date', models.DateField(auto_now_add=True, verbose_name='Дата создания')), ('ambassador', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='content', to='ambassadors.ambassador', verbose_name='амбассадор')), ], diff --git a/backend/ambassadors/migrations/0002_alter_merchandise_options_and_more.py b/backend/ambassadors/migrations/0002_alter_merchandise_options_and_more.py deleted file mode 100644 index 757b37a..0000000 --- a/backend/ambassadors/migrations/0002_alter_merchandise_options_and_more.py +++ /dev/null @@ -1,37 +0,0 @@ -# Generated by Django 4.2.10 on 2024-03-08 17:48 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("ambassadors", "0001_initial"), - ] - - operations = [ - migrations.AlterModelOptions( - name="merchandise", - options={"verbose_name": "Мерч", "verbose_name_plural": "Мерч"}, - ), - migrations.AlterField( - model_name="merchandiseshippingrequest", - name="ambassador", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="merch_shipping_requests", - to="ambassadors.ambassador", - verbose_name="Амбассадор", - ), - ), - migrations.AlterField( - model_name="merchandiseshippingrequest", - name="name_merch", - field=models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - to="ambassadors.merchandise", - verbose_name="Название мерча", - ), - ), - ] diff --git a/backend/ambassadors/migrations/0003_content_comment.py b/backend/ambassadors/migrations/0003_content_comment.py deleted file mode 100644 index b60c7cd..0000000 --- a/backend/ambassadors/migrations/0003_content_comment.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 4.2.10 on 2024-03-09 17:21 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("ambassadors", "0002_alter_merchandise_options_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="content", - name="comment", - field=models.TextField( - blank=True, - max_length=200, - null=True, - verbose_name="Комментарий менеджера", - ), - ), - ] diff --git a/backend/users/migrations/0001_initial.py b/backend/users/migrations/0001_initial.py index b8548c2..eae4e73 100644 --- a/backend/users/migrations/0001_initial.py +++ b/backend/users/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.10 on 2024-03-10 11:58 +# Generated by Django 4.2.10 on 2024-03-10 12:35 import django.contrib.auth.models import django.contrib.auth.validators