diff --git a/apps/account/views/common.py b/apps/account/views/common.py index d29ce2bb..8b066742 100644 --- a/apps/account/views/common.py +++ b/apps/account/views/common.py @@ -63,7 +63,7 @@ class SendConfirmationEmailView(generics.GenericAPIView): return Response(status=status.HTTP_200_OK) -class ConfirmEmailView(JWTGenericViewMixin): +class ConfirmEmailView(JWTGenericViewMixin, generics.GenericAPIView): """View for confirm changing email""" permission_classes = (permissions.AllowAny,) diff --git a/apps/account/views/web.py b/apps/account/views/web.py index 9f2ebcfd..0fc762f5 100644 --- a/apps/account/views/web.py +++ b/apps/account/views/web.py @@ -33,7 +33,7 @@ class PasswordResetView(generics.GenericAPIView): return Response(status=status.HTTP_200_OK) -class PasswordResetConfirmView(JWTGenericViewMixin): +class PasswordResetConfirmView(JWTGenericViewMixin, generics.GenericAPIView): """View for confirmation new password""" serializer_class = serializers.PasswordResetConfirmSerializer permission_classes = (permissions.AllowAny,) diff --git a/apps/advertisement/views/back.py b/apps/advertisement/views/back.py index d11615ba..a2973589 100644 --- a/apps/advertisement/views/back.py +++ b/apps/advertisement/views/back.py @@ -45,7 +45,7 @@ class AdvertisementPageListCreateView(AdvertisementBackOfficeViewMixin, ad_qs = Advertisement.objects.all() filtered_ad_qs = self.filter_queryset(ad_qs) - ad = get_object_or_404(filtered_ad_qs, pk=self.kwargs['pk']) + ad = get_object_or_404(filtered_ad_qs, pk=self.kwargs.get('pk')) # May raise a permission denied self.check_object_permissions(self.request, ad) @@ -68,8 +68,8 @@ class AdvertisementPageRUDView(AdvertisementBackOfficeViewMixin, ad_qs = Advertisement.objects.all() filtered_ad_qs = self.filter_queryset(ad_qs) - ad = get_object_or_404(filtered_ad_qs, pk=self.kwargs['ad_pk']) - page = get_object_or_404(ad.pages.all(), pk=self.kwargs['page_pk']) + ad = get_object_or_404(filtered_ad_qs, pk=self.kwargs.get('ad_pk')) + page = get_object_or_404(ad.pages.all(), pk=self.kwargs.get('page_pk')) # May raise a permission denied self.check_object_permissions(self.request, page) diff --git a/apps/authorization/views/common.py b/apps/authorization/views/common.py index 9d2069f2..8b466acb 100644 --- a/apps/authorization/views/common.py +++ b/apps/authorization/views/common.py @@ -71,7 +71,7 @@ class OAuth2ViewMixin(CsrfExemptMixin, OAuthLibMixin, BaseOAuth2ViewMixin): # Sign in via Facebook -class OAuth2SignUpView(OAuth2ViewMixin, JWTGenericViewMixin): +class OAuth2SignUpView(OAuth2ViewMixin, JWTGenericViewMixin, generics.GenericAPIView): """ Implements an endpoint to convert a provider token to an access token @@ -142,7 +142,7 @@ class SignUpView(generics.GenericAPIView): return Response(status=status.HTTP_201_CREATED) -class ConfirmationEmailView(JWTGenericViewMixin): +class ConfirmationEmailView(JWTGenericViewMixin, generics.GenericAPIView): """View for confirmation email""" permission_classes = (permissions.AllowAny, ) @@ -174,7 +174,7 @@ class ConfirmationEmailView(JWTGenericViewMixin): # Login by username|email + password -class LoginByUsernameOrEmailView(JWTGenericViewMixin): +class LoginByUsernameOrEmailView(JWTGenericViewMixin, generics.GenericAPIView): """Login by email and password""" permission_classes = (permissions.AllowAny,) serializer_class = serializers.LoginByUsernameOrEmailSerializer @@ -197,7 +197,7 @@ class LoginByUsernameOrEmailView(JWTGenericViewMixin): # Logout -class LogoutView(JWTGenericViewMixin): +class LogoutView(JWTGenericViewMixin, generics.GenericAPIView): """Logout user""" permission_classes = (IsAuthenticatedAndTokenIsValid, ) @@ -215,7 +215,7 @@ class LogoutView(JWTGenericViewMixin): # Refresh token -class RefreshTokenView(JWTGenericViewMixin): +class RefreshTokenView(JWTGenericViewMixin, generics.GenericAPIView): """Refresh access_token""" permission_classes = (permissions.AllowAny, ) serializer_class = serializers.RefreshTokenSerializer diff --git a/apps/collection/serializers/back.py b/apps/collection/serializers/back.py index 4cc76b2f..bb88a778 100644 --- a/apps/collection/serializers/back.py +++ b/apps/collection/serializers/back.py @@ -1,8 +1,14 @@ from rest_framework import serializers + +from collection import models +from collection.serializers.common import CollectionBaseSerializer +from establishment.models import Establishment from location.models import Country from location.serializers import CountrySimpleSerializer -from collection.serializers.common import CollectionBaseSerializer -from collection import models +from product.models import Product +from utils.exceptions import ( + BindingObjectNotFound, RemovedBindingObjectNotFound, ObjectAlreadyAdded +) class CollectionBackOfficeSerializer(CollectionBaseSerializer): @@ -31,3 +37,54 @@ class CollectionBackOfficeSerializer(CollectionBaseSerializer): 'start', 'end', ] + + +class CollectionBindObjectSerializer(serializers.Serializer): + """Serializer for binding collection and objects""" + + ESTABLISHMENT = 'establishment' + PRODUCT = 'product' + + TYPE_CHOICES = ( + (ESTABLISHMENT, 'Establishment'), + (PRODUCT, 'Product'), + ) + + type = serializers.ChoiceField(TYPE_CHOICES) + object_id = serializers.IntegerField() + + def validate(self, attrs): + view = self.context.get('view') + request = self.context.get('request') + + obj_type = attrs.get('type') + obj_id = attrs.get('object_id') + + collection = view.get_object() + attrs['collection'] = collection + + if obj_type == self.ESTABLISHMENT: + establishment = Establishment.objects.filter(pk=obj_id).\ + first() + if not establishment: + raise BindingObjectNotFound() + if request.method == 'POST' and collection.establishments.\ + filter(pk=establishment.pk).exists(): + raise ObjectAlreadyAdded() + if request.method == 'DELETE' and not collection.\ + establishments.filter(pk=establishment.pk).\ + exists(): + raise RemovedBindingObjectNotFound() + attrs['related_object'] = establishment + elif obj_type == self.PRODUCT: + product = Product.objects.filter(pk=obj_id).first() + if not product: + raise BindingObjectNotFound() + if request.method == 'POST' and collection.products.\ + filter(pk=product.pk).exists(): + raise ObjectAlreadyAdded() + if request.method == 'DELETE' and not collection.products.\ + filter(pk=product.pk).exists(): + raise RemovedBindingObjectNotFound() + attrs['related_object'] = product + return attrs diff --git a/apps/collection/urls/back.py b/apps/collection/urls/back.py index eee40327..6a6dbd54 100644 --- a/apps/collection/urls/back.py +++ b/apps/collection/urls/back.py @@ -1,11 +1,10 @@ """Collection common urlpaths.""" -from django.urls import path +from rest_framework.routers import SimpleRouter from collection.views import back as views app_name = 'collection' +router = SimpleRouter() +router.register(r'', views.CollectionBackOfficeViewSet) -urlpatterns = [ - path('', views.CollectionListCreateView.as_view(), name='list-create'), - path('/', views.CollectionRUDView.as_view(), name='rud-collection'), -] +urlpatterns = router.urls diff --git a/apps/collection/views/back.py b/apps/collection/views/back.py index 78a2dcb0..a989ec56 100644 --- a/apps/collection/views/back.py +++ b/apps/collection/views/back.py @@ -1,19 +1,49 @@ -from rest_framework import generics, permissions +from rest_framework import permissions +from rest_framework import viewsets, mixins + from collection import models -from collection.serializers import back +from collection.serializers import back as serializers +from utils.views import BindObjectMixin -class CollectionListCreateView(generics.ListCreateAPIView): - """Collection list-create view.""" +class CollectionViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): + """ViewSet for Collection model.""" + + pagination_class = None + permission_classes = (permissions.AllowAny,) queryset = models.Collection.objects.all() - serializer_class = back.CollectionBackOfficeSerializer - # todo: conf. permissions by TT - permission_classes = (permissions.IsAuthenticated, ) + serializer_class = serializers.CollectionBackOfficeSerializer -class CollectionRUDView(generics.RetrieveUpdateDestroyAPIView): - """Collection list-create view.""" +class CollectionBackOfficeViewSet(mixins.CreateModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + mixins.RetrieveModelMixin, + BindObjectMixin, + CollectionViewSet): + """ViewSet for Collection model for BackOffice users.""" + + permission_classes = (permissions.IsAuthenticated,) queryset = models.Collection.objects.all() - serializer_class = back.CollectionBackOfficeSerializer - # todo: conf. permissions by TT - permission_classes = (permissions.IsAuthenticated, ) + serializer_class = serializers.CollectionBackOfficeSerializer + bind_object_serializer_class = serializers.CollectionBindObjectSerializer + + def perform_binding(self, serializer): + data = serializer.validated_data + collection = data.pop('collection') + obj_type = data.get('type') + related_object = data.get('related_object') + if obj_type == self.bind_object_serializer_class.ESTABLISHMENT: + collection.establishments.add(related_object) + elif obj_type == self.bind_object_serializer_class.PRODUCT: + collection.products.add(related_object) + + def perform_unbinding(self, serializer): + data = serializer.validated_data + collection = data.pop('collection') + obj_type = data.get('type') + related_object = data.get('related_object') + if obj_type == self.bind_object_serializer_class.ESTABLISHMENT: + collection.establishments.remove(related_object) + elif obj_type == self.bind_object_serializer_class.PRODUCT: + collection.products.remove(related_object) diff --git a/apps/establishment/admin.py b/apps/establishment/admin.py index e6b0d991..45716f32 100644 --- a/apps/establishment/admin.py +++ b/apps/establishment/admin.py @@ -7,7 +7,7 @@ from comment.models import Comment from utils.admin import BaseModelAdminMixin from establishment import models from main.models import Award -from product.models import Product +from product.models import Product, PurchasedProduct from review import models as review_models @@ -69,13 +69,19 @@ class EstablishmentNote(admin.TabularInline): extra = 0 +class PurchasedProduct(admin.TabularInline): + model = PurchasedProduct + extra = 0 + + @admin.register(models.Establishment) class EstablishmentAdmin(BaseModelAdminMixin, admin.ModelAdmin): """Establishment admin.""" list_display = ['id', '__str__', 'image_tag', ] search_fields = ['id', 'name', 'index_name', 'slug'] list_filter = ['public_mark', 'toque_number'] - inlines = [GalleryImageInline, CompanyInline, EstablishmentNote] + inlines = [GalleryImageInline, CompanyInline, EstablishmentNote, + PurchasedProduct] # inlines = [ # AwardInline, ContactPhoneInline, ContactEmailInline, diff --git a/apps/establishment/migrations/0065_establishment_purchased_products.py b/apps/establishment/migrations/0065_establishment_purchased_products.py new file mode 100644 index 00000000..25088455 --- /dev/null +++ b/apps/establishment/migrations/0065_establishment_purchased_products.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.7 on 2019-11-20 12:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0018_purchasedproduct'), + ('establishment', '0064_auto_20191119_1546'), + ] + + operations = [ + migrations.AddField( + model_name='establishment', + name='purchased_products', + field=models.ManyToManyField(blank=True, help_text='Attribute from legacy db.\nMust be deleted after the implementation of the market.', related_name='establishments', through='product.PurchasedProduct', to='product.Product', verbose_name='purchased plaques'), + ), + ] diff --git a/apps/establishment/models.py b/apps/establishment/models.py index a6737850..f0260cf9 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -399,6 +399,13 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, currency = models.ForeignKey(Currency, blank=True, null=True, default=None, on_delete=models.PROTECT, verbose_name=_('currency')) + purchased_products = models.ManyToManyField('product.Product', blank=True, + through='product.PurchasedProduct', + related_name='establishments', + verbose_name=_('purchased plaques'), + help_text=_('Attribute from legacy db.\n' + 'Must be deleted after the ' + 'implementation of the market.')) objects = EstablishmentQuerySet.as_manager() @@ -411,6 +418,12 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, def __str__(self): return f'id:{self.id}-{self.name}' + def clean_fields(self, exclude=None): + super().clean_fields(exclude) + if self.purchased_products.filter(product_type__index_name='souvenir').exists(): + raise ValidationError( + _('Only souvenirs.')) + def delete(self, using=None, keep_parents=False): """Overridden delete method""" # Delete all related companies diff --git a/apps/establishment/transfer_data.py b/apps/establishment/transfer_data.py index b3dc58f9..741a9989 100644 --- a/apps/establishment/transfer_data.py +++ b/apps/establishment/transfer_data.py @@ -4,7 +4,9 @@ from django.db.models import Q, F from establishment.models import Establishment from location.models import Address -from transfer.models import Establishments, Dishes, EstablishmentNotes +from product.models import PurchasedProduct, Product +from transfer.models import Establishments, Dishes, EstablishmentNotes, \ + EstablishmentMerchandises from transfer.serializers.establishment import EstablishmentSerializer, \ EstablishmentNoteSerializer from transfer.serializers.plate import PlateSerializer @@ -140,6 +142,43 @@ def transfer_establishment_note(): pprint(f"transfer_establishment_note errors: {errors}") +def transfer_purchased_plaques(): + update_products_counter = 0 + already_updated_counter = 0 + not_existed_establishment_counter = 0 + + purchased = EstablishmentMerchandises.objects.values_list( + 'establishment_id', + 'merchandise__vintage', + 'gifted', + 'quantity' + ) + for old_est_id, vintage, gifted, quantity in purchased: + establishment_qs = Establishment.objects.filter(old_id=old_est_id) + product_qs = Product.objects.filter(name='Plaque restaurants', + vintage=vintage) + if establishment_qs.exists() and product_qs.exists(): + product = product_qs.first() + establishment = establishment_qs.first() + + purchases, created = PurchasedProduct.objects.get_or_create( + establishment=establishment, + product=product, + is_gifted=gifted, + quantity=quantity + ) + if created: + update_products_counter += 1 + else: + already_updated_counter += 1 + else: + not_existed_establishment_counter += 1 + + print(f'Updated products: {update_products_counter}\n' + f'Already updated: {already_updated_counter}\n' + f'Not existed establishment: {not_existed_establishment_counter}') + + data_types = { "establishment": [ transfer_establishment, @@ -149,4 +188,7 @@ data_types = { transfer_establishment_addresses ], "menu": [transfer_menu], + "purchased_plaques": [ + transfer_purchased_plaques + ], } diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index ad38e806..d1897397 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -6,6 +6,7 @@ from establishment import filters, models, serializers from timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer from utils.permissions import IsCountryAdmin, IsEstablishmentManager from utils.views import CreateDestroyGalleryViewMixin +from timetable.models import Timetable from rest_framework import status from rest_framework.response import Response @@ -36,13 +37,14 @@ class EstablishmentRUDView(generics.RetrieveUpdateDestroyAPIView): class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView): """Establishment schedule RUD view""" serializer_class = ScheduleRUDSerializer + permission_classes = [IsEstablishmentManager] def get_object(self): """ Returns the object the view is displaying. """ - establishment_pk = self.kwargs['pk'] - schedule_id = self.kwargs['schedule_id'] + establishment_pk = self.kwargs.get('pk') + schedule_id = self.kwargs.get('schedule_id') establishment = get_object_or_404(klass=models.Establishment.objects.all(), pk=establishment_pk) @@ -59,6 +61,8 @@ class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView): class EstablishmentScheduleCreateView(generics.CreateAPIView): """Establishment schedule Create view""" serializer_class = ScheduleCreateSerializer + queryset = Timetable.objects.all() + permission_classes = [IsEstablishmentManager] class MenuListCreateView(generics.ListCreateAPIView): @@ -200,8 +204,9 @@ class EstablishmentGalleryCreateDestroyView(EstablishmentMixinViews, """ establishment_qs = self.filter_queryset(self.get_queryset()) - establishment = get_object_or_404(establishment_qs, pk=self.kwargs['pk']) - gallery = get_object_or_404(establishment.establishment_gallery, image_id=self.kwargs['image_id']) + establishment = get_object_or_404(establishment_qs, pk=self.kwargs.get('pk')) + gallery = get_object_or_404(establishment.establishment_gallery, + image_id=self.kwargs.get('image_id')) # May raise a permission denied self.check_object_permissions(self.request, gallery) @@ -217,7 +222,7 @@ class EstablishmentGalleryListView(EstablishmentMixinViews, def get_object(self): """Override get_object method.""" qs = super(EstablishmentGalleryListView, self).get_queryset() - establishment = get_object_or_404(qs, pk=self.kwargs['pk']) + establishment = get_object_or_404(qs, pk=self.kwargs.get('pk')) # May raise a permission denied self.check_object_permissions(self.request, establishment) @@ -240,7 +245,7 @@ class EstablishmentCompanyListCreateView(EstablishmentMixinViews, establishment_qs = models.Establishment.objects.all() filtered_ad_qs = self.filter_queryset(establishment_qs) - establishment = get_object_or_404(filtered_ad_qs, pk=self.kwargs['pk']) + establishment = get_object_or_404(filtered_ad_qs, pk=self.kwargs.get('pk')) # May raise a permission denied self.check_object_permissions(self.request, establishment) @@ -263,8 +268,8 @@ class EstablishmentCompanyRUDView(EstablishmentMixinViews, establishment_qs = models.Establishment.objects.all() filtered_ad_qs = self.filter_queryset(establishment_qs) - establishment = get_object_or_404(filtered_ad_qs, pk=self.kwargs['pk']) - company = get_object_or_404(establishment.companies.all(), pk=self.kwargs['company_pk']) + establishment = get_object_or_404(filtered_ad_qs, pk=self.kwargs.get('pk')) + company = get_object_or_404(establishment.companies.all(), pk=self.kwargs.get('company_pk')) # May raise a permission denied self.check_object_permissions(self.request, company) @@ -273,7 +278,7 @@ class EstablishmentCompanyRUDView(EstablishmentMixinViews, class EstablishmentNoteListCreateView(EstablishmentMixinViews, - generics.ListCreateAPIView): + generics.ListCreateAPIView): """Retrieve|Update|Destroy establishment note view.""" serializer_class = serializers.EstablishmentNoteListCreateSerializer @@ -283,7 +288,7 @@ class EstablishmentNoteListCreateView(EstablishmentMixinViews, establishment_qs = models.Establishment.objects.all() filtered_establishment_qs = self.filter_queryset(establishment_qs) - establishment = get_object_or_404(filtered_establishment_qs, pk=self.kwargs['pk']) + establishment = get_object_or_404(filtered_establishment_qs, pk=self.kwargs.get('pk')) # May raise a permission denied self.check_object_permissions(self.request, establishment) @@ -306,7 +311,7 @@ class EstablishmentNoteRUDView(EstablishmentMixinViews, establishment_qs = models.Establishment.objects.all() filtered_establishment_qs = self.filter_queryset(establishment_qs) - establishment = get_object_or_404(filtered_establishment_qs, pk=self.kwargs['pk']) + establishment = get_object_or_404(filtered_establishment_qs, pk=self.kwargs.get('pk')) note = get_object_or_404(establishment.notes.all(), pk=self.kwargs['note_pk']) # May raise a permission denied diff --git a/apps/location/transfer_data.py b/apps/location/transfer_data.py index 0d90461a..658d2d20 100644 --- a/apps/location/transfer_data.py +++ b/apps/location/transfer_data.py @@ -1,8 +1,8 @@ from transfer.serializers import location as location_serializers from transfer import models as transfer_models -from location.models import Country +from location.models import Country, CityGallery, City +from gallery.models import Image from pprint import pprint - from requests import get @@ -179,6 +179,42 @@ def update_flags(): query.save() +def transfer_city_gallery(): + created_counter = 0 + cities_not_exists = {} + gallery_obj_exists_counter = 0 + + city_gallery = transfer_models.CityPhotos.objects.exclude(city__isnull=True) \ + .exclude(city__country_code_2__isnull=True) \ + .exclude(city__country_code_2__iexact='') \ + .exclude(city__region_code__isnull=True) \ + .exclude(city__region_code__iexact='') \ + .values_list('city_id', 'attachment_suffix_url') + for old_city_id, image_suffix_url in city_gallery: + city = City.objects.filter(old_id=old_city_id) + if city.exists(): + city = city.first() + image, _ = Image.objects.get_or_create(image=image_suffix_url, + defaults={ + 'image': image_suffix_url, + 'orientation': Image.HORIZONTAL, + 'title': f'{city.name} - {image_suffix_url}', + }) + city_gallery, created = CityGallery.objects.get_or_create(image=image, + city=city, + is_main=True) + if created: + created_counter += 1 + else: + gallery_obj_exists_counter += 1 + else: + cities_not_exists.update({'city_old_id': old_city_id}) + + print(f'Created: {created_counter}\n' + f'City not exists: {cities_not_exists}\n' + f'Already added: {gallery_obj_exists_counter}') + + data_types = { "dictionaries": [ transfer_countries, @@ -192,4 +228,5 @@ data_types = { "update_country_flag": [ update_flags ], + "fill_city_gallery": [transfer_city_gallery] } diff --git a/apps/location/views/back.py b/apps/location/views/back.py index b6677837..4d420154 100644 --- a/apps/location/views/back.py +++ b/apps/location/views/back.py @@ -51,8 +51,8 @@ class CityGalleryCreateDestroyView(common.CityViewMixin, """ city_qs = self.filter_queryset(self.get_queryset()) - city = get_object_or_404(city_qs, pk=self.kwargs['pk']) - gallery = get_object_or_404(city.city_gallery, image_id=self.kwargs['image_id']) + city = get_object_or_404(city_qs, pk=self.kwargs.get('pk')) + gallery = get_object_or_404(city.city_gallery, image_id=self.kwargs.get('image_id')) # May raise a permission denied self.check_object_permissions(self.request, gallery) diff --git a/apps/main/filters.py b/apps/main/filters.py new file mode 100644 index 00000000..72636d9e --- /dev/null +++ b/apps/main/filters.py @@ -0,0 +1,40 @@ +from django.core.validators import EMPTY_VALUES +from django_filters import rest_framework as filters + +from main import models + + +class AwardFilter(filters.FilterSet): + """Award filter set.""" + + establishment_id = filters.NumberFilter(field_name='object_id', ) + product_id = filters.NumberFilter(field_name='object_id', ) + employee_id = filters.NumberFilter(field_name='object_id', ) + + class Meta: + """Meta class.""" + + model = models.Award + fields = ( + 'establishment_id', + 'product_id', + 'employee_id', + 'state', + 'award_type', + 'vintage_year', + ) + + def by_establishment_id(self, queryset, name, value): + if value not in EMPTY_VALUES: + return queryset.by_establishment_id(value, content_type='establishment') + return queryset + + def by_product_id(self, queryset, name, value): + if value not in EMPTY_VALUES: + return queryset.by_product_id(value, content_type='product') + return queryset + + def by_employee_id(self, queryset, name, value): + if value not in EMPTY_VALUES: + return queryset.by_employee_id(value, content_type='establishmentemployee') + return queryset diff --git a/apps/main/models.py b/apps/main/models.py index 1bd39a6d..61a4d447 100644 --- a/apps/main/models.py +++ b/apps/main/models.py @@ -153,7 +153,7 @@ class Award(TranslatedFieldsMixin, URLImageMixin, models.Model): PUBLISHED = 1 STATE_CHOICES = ( - (WAITING,'waiting'), + (WAITING, 'waiting'), (PUBLISHED, 'published') ) diff --git a/apps/main/serializers.py b/apps/main/serializers.py index 6b8e2ba4..572aff31 100644 --- a/apps/main/serializers.py +++ b/apps/main/serializers.py @@ -38,7 +38,7 @@ class SiteFeatureSerializer(serializers.ModelSerializer): 'route', 'source', 'nested', - ) + ) class CurrencySerializer(ProjectModelSerializer): @@ -145,6 +145,19 @@ class AwardSerializer(AwardBaseSerializer): fields = AwardBaseSerializer.Meta.fields + ['award_type', ] +class BackAwardSerializer(AwardBaseSerializer): + """Award serializer.""" + + class Meta: + model = models.Award + fields = AwardBaseSerializer.Meta.fields + [ + 'award_type', + 'state', + 'content_type', + 'object_id', + ] + + class CarouselListSerializer(serializers.ModelSerializer): """Serializer for retrieving list of carousel items.""" @@ -202,4 +215,4 @@ class PageTypeBaseSerializer(serializers.ModelSerializer): fields = [ 'id', 'name', - ] \ No newline at end of file + ] diff --git a/apps/main/tests.py b/apps/main/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/apps/main/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/apps/main/tests/__init__.py b/apps/main/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/main/tests/tests_back.py b/apps/main/tests/tests_back.py new file mode 100644 index 00000000..e09c7b4b --- /dev/null +++ b/apps/main/tests/tests_back.py @@ -0,0 +1,73 @@ +from http.cookies import SimpleCookie + +from django.contrib.contenttypes.models import ContentType +from rest_framework import status +from rest_framework.test import APITestCase + +from account.models import User +from location.models import Country +from main.models import Award, AwardType + + +class AwardTestCase(APITestCase): + + def setUp(self): + self.user = User.objects.create_user( + username='alex', + email='alex@mail.com', + password='alex_password', + is_staff=True, + ) + + # get tokens + tokens = User.create_jwt_tokens(self.user) + self.client.cookies = SimpleCookie( + {'access_token': tokens.get('access_token'), + 'refresh_token': tokens.get('refresh_token')}) + + self.country_ru = Country.objects.create( + name={'en-GB': 'Russian'}, + code='RU', + ) + + self.content_type = ContentType.objects.get(app_label="establishment", model="establishment") + + self.award_type = AwardType.objects.create( + country=self.country_ru, + name="Test award type", + ) + + self.award = Award.objects.create( + award_type=self.award_type, + vintage_year='2017', + state=Award.PUBLISHED, + object_id=1, + content_type_id=1, + ) + + def test_award_CRUD(self): + response = self.client.get('/api/back/main/awards/', format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + data = { + 'award_type': self.award_type.pk, + 'state': 1, + 'object_id': 1, + 'content_type': 1, + } + + response = self.client.post('/api/back/main/awards/', data=data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + response = self.client.get(f'/api/back/main/awards/{self.award.id}/', format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + update_data = { + 'vintage_year': '2019' + } + + response = self.client.patch(f'/api/back/main/awards/{self.award.id}/', data=update_data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + response = self.client.delete(f'/api/back/main/awards/{self.award.id}/') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) diff --git a/apps/main/urls/back.py b/apps/main/urls/back.py new file mode 100644 index 00000000..d92bddf8 --- /dev/null +++ b/apps/main/urls/back.py @@ -0,0 +1,11 @@ +"""Back main URLs""" +from django.urls import path + +from main.views import back as views + +app_name = 'main' + +urlpatterns = [ + path('awards/', views.AwardLstView.as_view(), name='awards-list-create'), + path('awards//', views.AwardRUDView.as_view(), name='awards-rud'), +] diff --git a/apps/main/views/back.py b/apps/main/views/back.py new file mode 100644 index 00000000..bbbfad53 --- /dev/null +++ b/apps/main/views/back.py @@ -0,0 +1,21 @@ +from rest_framework import generics, permissions + +from main import serializers +from main.filters import AwardFilter +from main.models import Award + + +class AwardLstView(generics.ListCreateAPIView): + """Award list create view.""" + queryset = Award.objects.all() + serializer_class = serializers.BackAwardSerializer + permission_classes = (permissions.IsAdminUser,) + filterset_class = AwardFilter + + +class AwardRUDView(generics.RetrieveUpdateDestroyAPIView): + """Award RUD view.""" + queryset = Award.objects.all() + serializer_class = serializers.BackAwardSerializer + permission_classes = (permissions.IsAdminUser,) + lookup_field = 'id' diff --git a/apps/main/views/common.py b/apps/main/views/common.py index 15f89510..18ee0d8d 100644 --- a/apps/main/views/common.py +++ b/apps/main/views/common.py @@ -70,7 +70,7 @@ class CarouselListView(generics.ListAPIView): def get_queryset(self): country_code = self.request.country_code - if hasattr(settings, 'CAROUSEL_ITEMS') and country_code in ['www', 'main']: + if hasattr(settings, 'CAROUSEL_ITEMS') and country_code in settings.INTERNATIONAL_COUNTRY_CODES: qs = models.Carousel.objects.filter(id__in=settings.CAROUSEL_ITEMS) return qs qs = models.Carousel.objects.is_parsed().active() diff --git a/apps/news/filters.py b/apps/news/filters.py index 6ade7eeb..e8e35307 100644 --- a/apps/news/filters.py +++ b/apps/news/filters.py @@ -20,6 +20,16 @@ class NewsListFilterSet(filters.FilterSet): tag_value__in = filters.CharFilter(method='in_tags') type = filters.CharFilter(method='by_type') + state = filters.NumberFilter() + + SORT_BY_CREATED_CHOICE = "created" + SORT_BY_START_CHOICE = "start" + SORT_BY_CHOICES = ( + (SORT_BY_CREATED_CHOICE, "created"), + (SORT_BY_START_CHOICE, "start"), + ) + sort_by = filters.ChoiceFilter(method='sort_by_field', choices=SORT_BY_CHOICES) + class Meta: """Meta class""" model = models.News @@ -29,6 +39,8 @@ class NewsListFilterSet(filters.FilterSet): 'tag_group', 'tag_value__exclude', 'tag_value__in', + 'state', + 'sort_by', ) def in_tags(self, queryset, name, value): @@ -58,3 +70,6 @@ class NewsListFilterSet(filters.FilterSet): return queryset.filter(news_type__name=value) else: return queryset + + def sort_by_field(self, queryset, name, value): + return queryset.order_by(f'-{value}') diff --git a/apps/news/serializers.py b/apps/news/serializers.py index 4eaeaeb4..2264b9ec 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -162,6 +162,7 @@ class NewsDetailWebSerializer(NewsDetailSerializer): class NewsBackOfficeBaseSerializer(NewsBaseSerializer): """News back office base serializer.""" + is_published = serializers.BooleanField(source='is_publish', read_only=True) class Meta(NewsBaseSerializer.Meta): """Meta class.""" @@ -169,6 +170,7 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer): fields = NewsBaseSerializer.Meta.fields + ( 'title', 'subtitle', + 'is_published', ) diff --git a/apps/news/views.py b/apps/news/views.py index a215947a..bdb75fc7 100644 --- a/apps/news/views.py +++ b/apps/news/views.py @@ -108,8 +108,8 @@ class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView, """ news_qs = self.filter_queryset(self.get_queryset()) - news = get_object_or_404(news_qs, pk=self.kwargs['pk']) - gallery = get_object_or_404(news.news_gallery, image_id=self.kwargs['image_id']) + news = get_object_or_404(news_qs, pk=self.kwargs.get('pk')) + gallery = get_object_or_404(news.news_gallery, image_id=self.kwargs.get('image_id')) # May raise a permission denied self.check_object_permissions(self.request, gallery) @@ -125,7 +125,7 @@ class NewsBackOfficeGalleryListView(NewsBackOfficeMixinView, def get_object(self): """Override get_object method.""" qs = super(NewsBackOfficeGalleryListView, self).get_queryset() - news = get_object_or_404(qs, pk=self.kwargs['pk']) + news = get_object_or_404(qs, pk=self.kwargs.get('pk')) # May raise a permission denied self.check_object_permissions(self.request, news) diff --git a/apps/partner/admin.py b/apps/partner/admin.py index f2973f75..9161837b 100644 --- a/apps/partner/admin.py +++ b/apps/partner/admin.py @@ -6,3 +6,4 @@ from partner import models @admin.register(models.Partner) class PartnerModelAdmin(admin.ModelAdmin): """Model admin for Partner model.""" + raw_id_fields = ('establishment',) diff --git a/apps/partner/migrations/0003_auto_20191121_1059.py b/apps/partner/migrations/0003_auto_20191121_1059.py new file mode 100644 index 00000000..c2e98fbe --- /dev/null +++ b/apps/partner/migrations/0003_auto_20191121_1059.py @@ -0,0 +1,50 @@ +# Generated by Django 2.2.7 on 2019-11-21 10:59 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('establishment', '0065_establishment_purchased_products'), + ('partner', '0002_auto_20191101_0939'), + ] + + operations = [ + migrations.AddField( + model_name='partner', + name='establishment', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='partners', to='establishment.Establishment', verbose_name='Establishment'), + ), + migrations.AddField( + model_name='partner', + name='expiry_date', + field=models.DateField(blank=True, null=True, verbose_name='expiry date'), + ), + migrations.AddField( + model_name='partner', + name='name', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='name'), + ), + migrations.AddField( + model_name='partner', + name='old_id', + field=models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='old id'), + ), + migrations.AddField( + model_name='partner', + name='price_per_month', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='price per month'), + ), + migrations.AddField( + model_name='partner', + name='starting_date', + field=models.DateField(blank=True, null=True, verbose_name='starting date'), + ), + migrations.AddField( + model_name='partner', + name='type', + field=models.PositiveSmallIntegerField(choices=[(0, 'Partner'), (1, 'Sponsor')], default=0), + ), + ] diff --git a/apps/partner/models.py b/apps/partner/models.py index 1eb70845..5c5766c8 100644 --- a/apps/partner/models.py +++ b/apps/partner/models.py @@ -1,13 +1,36 @@ from django.db import models from django.utils.translation import gettext_lazy as _ +from establishment.models import Establishment from utils.models import ImageMixin, ProjectBaseMixin class Partner(ProjectBaseMixin): """Partner model.""" + + PARTNER = 0 + SPONSOR = 1 + MODEL_TYPES = ( + (PARTNER, _('Partner')), + (SPONSOR, _('Sponsor')), + ) + + old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None) + name = models.CharField(_('name'), max_length=255, blank=True, null=True) url = models.URLField(verbose_name=_('Partner URL')) image = models.URLField(verbose_name=_('Partner image URL'), null=True) + establishment = models.ForeignKey( + Establishment, + verbose_name=_('Establishment'), + related_name='partners', + on_delete=models.CASCADE, + blank=True, + null=True, + ) + type = models.PositiveSmallIntegerField(choices=MODEL_TYPES, default=PARTNER) + starting_date = models.DateField(_('starting date'), blank=True, null=True) + expiry_date = models.DateField(_('expiry date'), blank=True, null=True) + price_per_month = models.DecimalField(_('price per month'), max_digits=10, decimal_places=2, blank=True, null=True) class Meta: verbose_name = _('partner') diff --git a/apps/partner/transfer_data.py b/apps/partner/transfer_data.py index e3219838..acda6c2c 100644 --- a/apps/partner/transfer_data.py +++ b/apps/partner/transfer_data.py @@ -1,19 +1,37 @@ -from django.db.models import Value, IntegerField, F from pprint import pprint + +from establishment.models import Establishment +from partner.models import Partner from transfer.models import EstablishmentBacklinks from transfer.serializers.partner import PartnerSerializer def transfer_partner(): - queryset = EstablishmentBacklinks.objects.filter(type="Partner") + """ + Transfer data to Partner model only after transfer Establishment + """ + establishments = Establishment.objects.filter(old_id__isnull=False).values_list('old_id', flat=True) + queryset = EstablishmentBacklinks.objects.filter( + establishment_id__in=list(establishments), + ).values( + 'id', + 'establishment_id', + 'partnership_name', + 'partnership_icon', + 'backlink_url', + 'created_at', + 'type', + 'starting_date', + 'expiry_date', + 'price_per_month', + ) - # queryset = EstablishmentBacklinks.objects.all() # Partner and Sponsor - - serialized_data = PartnerSerializer(data=list(queryset.values()), many=True) + serialized_data = PartnerSerializer(data=list(queryset), many=True) if serialized_data.is_valid(): + Partner.objects.all().delete() # TODO: закоментить, если требуется сохранить старые записи serialized_data.save() else: - pprint(f"News serializer errors: {serialized_data.errors}") + pprint(f"Partner serializer errors: {serialized_data.errors}") data_types = { diff --git a/apps/product/management/commands/add_product_tag.py b/apps/product/management/commands/add_product_tag.py index dc54f35c..6377fcac 100644 --- a/apps/product/management/commands/add_product_tag.py +++ b/apps/product/management/commands/add_product_tag.py @@ -40,7 +40,6 @@ class Command(BaseCommand): TagCategory.objects.bulk_create(objects) self.stdout.write(self.style.WARNING(f'Add or get tag category objects.')) - def product_type_category_sql(self): with connections['legacy'].cursor() as cursor: cursor.execute(''' @@ -56,9 +55,9 @@ class Command(BaseCommand): def add_type_product_category(self): for c in tqdm(self.product_type_category_sql(), desc='Add type product category'): - type = ProductType.objects.get(index_name='wine') + type = ProductType.objects.get(index_name=ProductType.WINE) category = TagCategory.objects.get(index_name=c.tag_category) - if type and category not in type.tag_categories.all(): + if category not in type.tag_categories.all(): type.tag_categories.add(category) self.stdout.write(self.style.WARNING(f'Add type product category objects.')) @@ -116,7 +115,7 @@ class Command(BaseCommand): select DISTINCT m.product_id, - lower(trim(CONVERT(m.value USING utf8))) as tag_value, + trim(CONVERT(m.value USING utf8)) as tag_value, trim(CONVERT(v.key_name USING utf8)) as tag_category FROM product_metadata m JOIN product_key_value_metadata v on v.id = m.product_key_value_metadatum_id @@ -133,7 +132,7 @@ class Command(BaseCommand): ) product = Product.objects.get(old_id=t.product_id) for tag in tags: - if product not in tag.products.all(): + if tag not in product.tags.all(): product.tags.add(tag) self.stdout.write(self.style.WARNING(f'Add or get tag objects.')) diff --git a/apps/product/migrations/0018_purchasedproduct.py b/apps/product/migrations/0018_purchasedproduct.py new file mode 100644 index 00000000..b1c23646 --- /dev/null +++ b/apps/product/migrations/0018_purchasedproduct.py @@ -0,0 +1,30 @@ +# Generated by Django 2.2.7 on 2019-11-20 12:49 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('establishment', '0064_auto_20191119_1546'), + ('product', '0017_auto_20191119_1546'), + ] + + operations = [ + migrations.CreateModel( + name='PurchasedProduct', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_gifted', models.NullBooleanField(default=None, verbose_name='is gifted')), + ('quantity', models.PositiveSmallIntegerField(verbose_name='quantity')), + ('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='purchased_plaques', to='establishment.Establishment', verbose_name='establishment')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='purchased_by_establishments', to='product.Product', verbose_name='plaque')), + ], + options={ + 'verbose_name': 'purchased plaque', + 'verbose_name_plural': 'purchased plaques', + 'unique_together': {('establishment', 'product')}, + }, + ), + ] diff --git a/apps/product/models.py b/apps/product/models.py index 896eb211..561280fc 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -264,12 +264,23 @@ class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes, def grape_variety(self): return self.tags.filter(category__index_name='grape-variety') + @property + def bottle_sizes(self): + return self.tags.filter(category__index_name='bottle_size') + + @property + def alcohol_percentage(self): + qs = self.tags.filter(category__index_name='alcohol_percentage') + if qs.exists(): + return qs.first() + @property def related_tags(self): return super().visible_tags.exclude(category__index_name__in=[ 'sugar-content', 'wine-color', 'bottles-produced', - 'serial-number', 'grape-variety'] - ) + 'serial-number', 'grape-variety', 'serial_number', + 'alcohol_percentage', 'bottle_size', + ]) @property def display_name(self): @@ -316,6 +327,26 @@ class OnlineProduct(Product): verbose_name_plural = _('Online products') +class PurchasedProduct(models.Model): + """Model for storing establishment purchased plaques.""" + + establishment = models.ForeignKey('establishment.Establishment', on_delete=models.CASCADE, + related_name='purchased_plaques', + verbose_name=_('establishment')) + product = models.ForeignKey('product.Product', on_delete=models.CASCADE, + related_name='purchased_by_establishments', + verbose_name=_('plaque')) + is_gifted = models.NullBooleanField(default=None, + verbose_name=_('is gifted')) + quantity = models.PositiveSmallIntegerField(verbose_name=_('quantity')) + + class Meta: + """Meta class.""" + verbose_name = _('purchased plaque') + verbose_name_plural = _('purchased plaques') + unique_together = ('establishment', 'product') + + class Unit(models.Model): """Product unit model.""" name = models.CharField(max_length=255, diff --git a/apps/product/serializers/common.py b/apps/product/serializers/common.py index a0a56337..14eff642 100644 --- a/apps/product/serializers/common.py +++ b/apps/product/serializers/common.py @@ -128,6 +128,8 @@ class ProductDetailSerializer(ProductBaseSerializer): bottles_produced = TagBaseSerializer(many=True, read_only=True) sugar_contents = TagBaseSerializer(many=True, read_only=True) grape_variety = TagBaseSerializer(many=True, read_only=True) + bottle_sizes = TagBaseSerializer(many=True, read_only=True) + alcohol_percentage = TagBaseSerializer(read_only=True) image_url = serializers.URLField(allow_null=True, read_only=True) new_image = ImageBaseSerializer(source='crop_main_image', allow_null=True, read_only=True) @@ -146,6 +148,8 @@ class ProductDetailSerializer(ProductBaseSerializer): 'new_image', 'grape_variety', 'average_price', + 'bottle_sizes', + 'alcohol_percentage', ] diff --git a/apps/product/transfer_data.py b/apps/product/transfer_data.py index 8e4a26fa..84a8a524 100644 --- a/apps/product/transfer_data.py +++ b/apps/product/transfer_data.py @@ -2,17 +2,6 @@ from pprint import pprint from transfer import models as transfer_models from transfer.serializers import product as product_serializers -from transfer.serializers.partner import PartnerSerializer - - -def transfer_partner(): - queryset = transfer_models.EstablishmentBacklinks.objects.filter(type="Partner") - - serialized_data = PartnerSerializer(data=list(queryset.values()), many=True) - if serialized_data.is_valid(): - serialized_data.save() - else: - pprint(f"News serializer errors: {serialized_data.errors}") def transfer_wine_color(): @@ -50,8 +39,8 @@ def transfer_wine_bottles_produced(): ) queryset = [vars(query) for query in raw_queryset] serialized_data = product_serializers.WineBottlesProducedSerializer( - data=queryset, - many=True) + data=queryset, + many=True) if serialized_data.is_valid(): serialized_data.save() else: @@ -69,8 +58,8 @@ def transfer_wine_classification_type(): ) queryset = [vars(query) for query in raw_queryset] serialized_data = product_serializers.WineClassificationTypeSerializer( - data=queryset, - many=True) + data=queryset, + many=True) if serialized_data.is_valid(): serialized_data.save() else: @@ -79,10 +68,10 @@ def transfer_wine_classification_type(): def transfer_wine_standard(): queryset = transfer_models.ProductClassification.objects.filter(parent_id__isnull=True) \ - .exclude(type='Classification') + .exclude(type='Classification') serialized_data = product_serializers.ProductStandardSerializer( - data=list(queryset.values()), - many=True) + data=list(queryset.values()), + many=True) if serialized_data.is_valid(): serialized_data.save() else: @@ -92,8 +81,8 @@ def transfer_wine_standard(): def transfer_wine_classifications(): queryset = transfer_models.ProductClassification.objects.filter(type='Classification') serialized_data = product_serializers.ProductClassificationSerializer( - data=list(queryset.values()), - many=True) + data=list(queryset.values()), + many=True) if serialized_data.is_valid(): serialized_data.save() else: @@ -104,8 +93,8 @@ def transfer_product(): errors = [] queryset = transfer_models.Products.objects.all() serialized_data = product_serializers.ProductSerializer( - data=list(queryset.values()), - many=True) + data=list(queryset.values()), + many=True) if serialized_data.is_valid(): serialized_data.save() else: @@ -117,8 +106,8 @@ def transfer_product_note(): errors = [] queryset = transfer_models.ProductNotes.objects.exclude(text='') serialized_data = product_serializers.ProductNoteSerializer( - data=list(queryset.values()), - many=True) + data=list(queryset.values()), + many=True) if serialized_data.is_valid(): serialized_data.save() else: @@ -130,8 +119,8 @@ def transfer_plate(): errors = [] queryset = transfer_models.Merchandise.objects.all() serialized_data = product_serializers.PlateSerializer( - data=list(queryset.values()), - many=True) + data=list(queryset.values()), + many=True) if serialized_data.is_valid(): serialized_data.save() else: @@ -143,8 +132,8 @@ def transfer_plate_image(): errors = [] queryset = transfer_models.Merchandise.objects.all() serialized_data = product_serializers.PlateImageSerializer( - data=list(queryset.values()), - many=True) + data=list(queryset.values()), + many=True) if serialized_data.is_valid(): serialized_data.save() else: @@ -153,7 +142,6 @@ def transfer_plate_image(): data_types = { - "partner": [transfer_partner], "wine_characteristics": [ transfer_wine_sugar_content, transfer_wine_color, @@ -161,12 +149,12 @@ data_types = { transfer_wine_classification_type, transfer_wine_standard, transfer_wine_classifications, - ], + ], "product": [ transfer_product, ], "product_note": [ - transfer_product_note, + transfer_product_note, ], "souvenir": [ transfer_plate, diff --git a/apps/product/views/back.py b/apps/product/views/back.py index ac780849..fc5e108f 100644 --- a/apps/product/views/back.py +++ b/apps/product/views/back.py @@ -57,8 +57,8 @@ class ProductBackOfficeGalleryCreateDestroyView(ProductBackOfficeMixinView, """ product_qs = self.filter_queryset(self.get_queryset()) - product = get_object_or_404(product_qs, pk=self.kwargs['pk']) - gallery = get_object_or_404(product.product_gallery, image_id=self.kwargs['image_id']) + product = get_object_or_404(product_qs, pk=self.kwargs.get('pk')) + gallery = get_object_or_404(product.product_gallery, image_id=self.kwargs.get('image_id')) # May raise a permission denied self.check_object_permissions(self.request, gallery) @@ -75,7 +75,7 @@ class ProductBackOfficeGalleryListView(ProductBackOfficeMixinView, def get_object(self): """Override get_object method.""" qs = super(ProductBackOfficeGalleryListView, self).get_queryset() - product = get_object_or_404(qs, pk=self.kwargs['pk']) + product = get_object_or_404(qs, pk=self.kwargs.get('pk')) # May raise a permission denied self.check_object_permissions(self.request, product) @@ -149,7 +149,7 @@ class ProductNoteListCreateView(ProductBackOfficeMixinView, product_qs = models.Product.objects.all() filtered_product_qs = self.filter_queryset(product_qs) - product = get_object_or_404(filtered_product_qs, pk=self.kwargs['pk']) + product = get_object_or_404(filtered_product_qs, pk=self.kwargs.get('pk')) # May raise a permission denied self.check_object_permissions(self.request, product) @@ -173,8 +173,8 @@ class ProductNoteRUDView(ProductBackOfficeMixinView, product_qs = models.Product.objects.all() filtered_product_qs = self.filter_queryset(product_qs) - product = get_object_or_404(filtered_product_qs, pk=self.kwargs['pk']) - note = get_object_or_404(product.notes.all(), pk=self.kwargs['note_pk']) + product = get_object_or_404(filtered_product_qs, pk=self.kwargs.get('pk')) + note = get_object_or_404(product.notes.all(), pk=self.kwargs.get('note_pk')) # May raise a permission denied self.check_object_permissions(self.request, note) diff --git a/apps/review/serializers/common.py b/apps/review/serializers/common.py index 2889db6e..da7b624b 100644 --- a/apps/review/serializers/common.py +++ b/apps/review/serializers/common.py @@ -14,7 +14,9 @@ class ReviewBaseSerializer(serializers.ModelSerializer): 'child', 'published_at', 'vintage', - 'country' + 'country', + 'content_type', + 'object_id', ) diff --git a/apps/review/tests.py b/apps/review/tests.py index e04f5281..eebc1e86 100644 --- a/apps/review/tests.py +++ b/apps/review/tests.py @@ -19,6 +19,7 @@ class BaseTestCase(APITestCase): username=self.username, email=self.email, password=self.password, + is_staff=True, ) tokens = User.create_jwt_tokens(self.user) @@ -61,6 +62,49 @@ class BaseTestCase(APITestCase): ) +class ReviewTestCase(BaseTestCase): + def setUp(self): + super().setUp() + + def test_review_list(self): + response = self.client.get('/api/back/review/') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_review_post(self): + test_review = { + 'reviewer': self.user.id, + 'status': Review.READY, + 'vintage': 2019, + 'country': self.country_ru.id, + 'object_id': 1, + 'content_type': 1, + } + response = self.client.post('/api/back/review/', data=test_review) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_review_detail(self): + response = self.client.get(f'/api/back/review/{self.test_review.id}/', format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_review_detail_put(self): + data = { + 'id': self.test_review.id, + 'vintage': 2018, + 'reviewer': self.user.id, + 'status': Review.READY, + 'country': self.country_ru.id, + 'object_id': 1, + 'content_type': 1, + } + + response = self.client.put(f'/api/back/review/{self.test_review.id}/', data=data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_review_delete(self): + response = self.client.delete(f'/api/back/review/{self.test_review.id}/') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + class InquiriesTestCase(BaseTestCase): def setUp(self): super().setUp() diff --git a/apps/review/views/back.py b/apps/review/views/back.py index 5e38f7ab..27f9af0d 100644 --- a/apps/review/views/back.py +++ b/apps/review/views/back.py @@ -1,9 +1,8 @@ -from django_filters.rest_framework import DjangoFilterBackend from rest_framework import generics, permissions +from review import filters from review import models from review import serializers -from review import filters from utils.permissions import IsReviewerManager, IsRestaurantReviewer @@ -12,7 +11,6 @@ class ReviewLstView(generics.ListCreateAPIView): serializer_class = serializers.ReviewBaseSerializer queryset = models.Review.objects.all() permission_classes = [permissions.IsAuthenticatedOrReadOnly, ] - filter_backends = (DjangoFilterBackend,) filterset_class = filters.ReviewFilter @@ -20,7 +18,7 @@ class ReviewRUDView(generics.RetrieveUpdateDestroyAPIView): """Comment RUD view.""" serializer_class = serializers.ReviewBaseSerializer queryset = models.Review.objects.all() - permission_classes = [IsReviewerManager | IsRestaurantReviewer] + permission_classes = [permissions.IsAdminUser | IsReviewerManager | IsRestaurantReviewer] lookup_field = 'id' diff --git a/apps/search_indexes/filters.py b/apps/search_indexes/filters.py index 28341502..ab47ef84 100644 --- a/apps/search_indexes/filters.py +++ b/apps/search_indexes/filters.py @@ -71,14 +71,21 @@ class CustomSearchFilterBackend(SearchFilterBackend): Q("match", **{k: v}) ) __queries.append( - Q('wildcard', **{k: f'*{search_term.lower()}*'}) + Q('wildcard', + **{k: { + 'value': f'*{search_term.lower()}*', + 'boost': v.get('boost', 1) + 30 + } + } + ) ) else: __queries.append( Q("match", **field_kwargs) ) __queries.append( - Q('wildcard', **{field: f'*{search_term.lower()}*'}) + Q('wildcard', **{field: {'value': f'*{search_term.lower()}*', + 'boost': field_kwargs[field].get('boost', 1) + 30}}) ) else: for field in view.search_fields: @@ -99,13 +106,20 @@ class CustomSearchFilterBackend(SearchFilterBackend): Q("match", **{k: v}) ) __queries.append( - Q('wildcard', **{k: f'*{search_term.lower()}*'}) + Q('wildcard', + **{k: { + 'value': f'*{search_term.lower()}*', + 'boost': v.get('boost', 1) + 30 + } + } + ) ) else: __queries.append( Q("match", **field_kwargs) ) __queries.append( - Q('wildcard', **{field: f'*{search_term.lower()}*'}) + Q('wildcard', **{field: {'value': f'*{search_term.lower()}*', + 'boost': field_kwargs[field].get('boost', 1) + 30}}) ) return __queries \ No newline at end of file diff --git a/apps/tag/filters.py b/apps/tag/filters.py index 8d2343ec..7bd22ec2 100644 --- a/apps/tag/filters.py +++ b/apps/tag/filters.py @@ -43,8 +43,8 @@ class TagCategoryFilterSet(TagsBaseFilterSet): 'product_type', ) def by_product_type(self, queryset, name, value): - if value == product_models.ProductType.WINE: - queryset = queryset.filter(index_name='wine-color').filter(tags__products__isnull=False) + # if value == product_models.ProductType.WINE: + # queryset = queryset.filter(index_name='wine-color').filter(tags__products__isnull=False) queryset = queryset.by_product_type(value) return queryset diff --git a/apps/timetable/serialziers.py b/apps/timetable/serialziers.py index 37725e1d..533bca70 100644 --- a/apps/timetable/serialziers.py +++ b/apps/timetable/serialziers.py @@ -8,6 +8,9 @@ from timetable.models import Timetable class ScheduleRUDSerializer(serializers.ModelSerializer): """Serializer for Establishment model.""" + NULLABLE_FIELDS = ['lunch_start', 'lunch_end', 'dinner_start', + 'dinner_end', 'opening_at', 'closed_at'] + weekday_display = serializers.CharField(source='get_weekday_display', read_only=True) @@ -18,9 +21,6 @@ class ScheduleRUDSerializer(serializers.ModelSerializer): opening_at = serializers.TimeField(required=False) closed_at = serializers.TimeField(required=False) - NULLABLE_FIELDS = ['lunch_start', 'lunch_end', 'dinner_start', - 'dinner_end', 'opening_at', 'closed_at'] - class Meta: """Meta class.""" model = Timetable diff --git a/apps/transfer/management/commands/transfer.py b/apps/transfer/management/commands/transfer.py index d69dd908..2d0ce399 100644 --- a/apps/transfer/management/commands/transfer.py +++ b/apps/transfer/management/commands/transfer.py @@ -39,6 +39,8 @@ class Command(BaseCommand): 'rating_count', 'product_review', 'newsletter_subscriber', # подписчики на рассылку - переносить после переноса пользователей №1 + 'purchased_plaques', # №6 - перенос купленных тарелок + 'fill_city_gallery', # №3 - перенос галереи городов ] def handle(self, *args, **options): diff --git a/apps/transfer/models.py b/apps/transfer/models.py index d8be36ce..a8190879 100644 --- a/apps/transfer/models.py +++ b/apps/transfer/models.py @@ -217,10 +217,10 @@ class CityNames(MigrateMixin): class CityPhotos(MigrateMixin): using = 'legacy' - # city_id = models.IntegerField(blank=True, null=True) city = models.ForeignKey(Cities, models.DO_NOTHING, blank=True, null=True) attachment_file_name = models.CharField(max_length=255, blank=True, null=True) attachment_content_type = models.CharField(max_length=255, blank=True, null=True) + attachment_suffix_url = models.CharField(max_length=255) geometries = models.CharField(max_length=1024, blank=True, null=True) attachment_file_size = models.IntegerField(blank=True, null=True) attachment_updated_at = models.DateTimeField(blank=True, null=True) @@ -581,22 +581,19 @@ class EstablishmentInfos(MigrateMixin): db_table = 'establishment_infos' -# class EstablishmentMerchandises(MigrateMixin): -# using = 'legacy' -# -# establishment = models.ForeignKey('Establishments', models.DO_NOTHING, blank=True, null=True) +class EstablishmentMerchandises(MigrateMixin): + using = 'legacy' -# TODO: модели Merchandises нету в гугл таблице Check Migrations + establishment = models.ForeignKey('Establishments', models.DO_NOTHING, blank=True, null=True) + merchandise = models.ForeignKey('Merchandise', models.DO_NOTHING, blank=True, null=True) + gifted = models.NullBooleanField(blank=True, null=True) + quantity = models.IntegerField(blank=True, null=True) + created_at = models.DateTimeField() + updated_at = models.DateTimeField() -# merchandise = models.ForeignKey('Merchandises', models.DO_NOTHING, blank=True, null=True) -# gifted = models.IntegerField(blank=True, null=True) -# quantity = models.IntegerField(blank=True, null=True) -# created_at = models.DateTimeField() -# updated_at = models.DateTimeField() -# -# class Meta: -# managed = False -# db_table = 'establishment_merchandises' + class Meta: + managed = False + db_table = 'establishment_merchandises' class Menus(MigrateMixin): diff --git a/apps/transfer/serializers/partner.py b/apps/transfer/serializers/partner.py index 7de61486..61f56dea 100644 --- a/apps/transfer/serializers/partner.py +++ b/apps/transfer/serializers/partner.py @@ -1,28 +1,53 @@ from rest_framework import serializers + +from establishment.models import Establishment from partner.models import Partner -class PartnerSerializer(serializers.ModelSerializer): - backlink_url = serializers.CharField(source="url") - partnership_icon = serializers.CharField() - partnership_name = serializers.CharField() - - class Meta: - model = Partner - fields = ( - "backlink_url", - "partnership_icon", - "partnership_name" - ) +class PartnerSerializer(serializers.Serializer): + id = serializers.IntegerField() + establishment_id = serializers.IntegerField() + partnership_name = serializers.CharField(allow_null=True) + partnership_icon = serializers.CharField(allow_null=True) + backlink_url = serializers.CharField(allow_null=True) + created_at = serializers.DateTimeField(format='%m-%d-%Y %H:%M:%S') + type = serializers.CharField(allow_null=True) + starting_date = serializers.DateField(allow_null=True) + expiry_date = serializers.DateField(allow_null=True) + price_per_month = serializers.DecimalField(max_digits=10, decimal_places=2, allow_null=True) def validate(self, data): - data["image"] = partnership_to_image_url.get(data["partnership_name"]).get(data["partnership_icon"]) - data.pop("partnership_name") - data.pop("partnership_icon") + data.update({ + 'old_id': data.pop('id'), + 'name': data['partnership_name'], + 'url': data.pop('backlink_url'), + 'image': self.get_image(data), + 'establishment': self.get_establishment(data), + 'type': Partner.PARTNER if data['type'] == 'Partner' else Partner.SPONSOR, + 'created': data.pop('created_at'), + }) + data.pop('partnership_icon') + data.pop('partnership_name') + data.pop('establishment_id') return data + @staticmethod + def get_image(data): + return partnership_to_image_url.get(data['partnership_name']).get(data['partnership_icon']) + + @staticmethod + def get_establishment(data): + establishment = Establishment.objects.filter(old_id=data['establishment_id']).first() + if not establishment: + raise ValueError(f"Establishment not found with old_id {data['establishment_id']}: ") + return establishment + def create(self, validated_data): - return Partner.objects.create(**validated_data) + obj, _ = Partner.objects.update_or_create( + old_id=validated_data['old_id'], + defaults=validated_data, + ) + return obj partnership_to_image_url = { diff --git a/apps/transfer/serializers/product.py b/apps/transfer/serializers/product.py index a0f3ef8a..86c6720a 100644 --- a/apps/transfer/serializers/product.py +++ b/apps/transfer/serializers/product.py @@ -265,7 +265,7 @@ class ProductSerializer(TransferSerializerMixin): state = serializers.CharField() bottles_produced = serializers.CharField(allow_null=True, allow_blank=True) unique_key = serializers.CharField(allow_null=True) - price = serializers.DecimalField(max_digits=14, decimal_places=2) + price = serializers.DecimalField(max_digits=14, decimal_places=2, allow_null=True) class Meta: model = models.Product diff --git a/apps/utils/views.py b/apps/utils/views.py index 0f5f74cd..fef14c08 100644 --- a/apps/utils/views.py +++ b/apps/utils/views.py @@ -4,6 +4,7 @@ from django.conf import settings from django.db.transaction import on_commit from django.shortcuts import get_object_or_404 from rest_framework import generics, status +from rest_framework.decorators import action from rest_framework.response import Response from gallery.tasks import delete_image @@ -12,7 +13,7 @@ from search_indexes.documents import es_update # JWT # Login base view mixins -class JWTGenericViewMixin(generics.GenericAPIView): +class JWTGenericViewMixin: """JWT view mixin""" ACCESS_TOKEN_HTTP_ONLY = False @@ -39,30 +40,31 @@ class JWTGenericViewMixin(generics.GenericAPIView): """ COOKIES = [] - if hasattr(self.request, 'locale'): - COOKIES.append(self.COOKIE(key='locale', - value=self.request.locale, - http_only=self.ACCESS_TOKEN_HTTP_ONLY, - secure=self.LOCALE_SECURE, - max_age=settings.COOKIES_MAX_AGE if permanent else None)) - if hasattr(self.request, 'country_code'): - COOKIES.append(self.COOKIE(key='country_code', - value=self.request.country_code, - http_only=self.COUNTRY_CODE_HTTP_ONLY, - secure=self.COUNTRY_CODE_SECURE, - max_age=settings.COOKIES_MAX_AGE if permanent else None)) - if access_token: - COOKIES.append(self.COOKIE(key='access_token', - value=access_token, - http_only=self.ACCESS_TOKEN_HTTP_ONLY, - secure=self.ACCESS_TOKEN_SECURE, - max_age=settings.COOKIES_MAX_AGE if permanent else None)) - if refresh_token: - COOKIES.append(self.COOKIE(key='refresh_token', - value=refresh_token, - http_only=self.REFRESH_TOKEN_HTTP_ONLY, - secure=self.REFRESH_TOKEN_SECURE, - max_age=settings.COOKIES_MAX_AGE if permanent else None)) + if hasattr(self, 'request'): + if hasattr(self.request, 'locale'): + COOKIES.append(self.COOKIE(key='locale', + value=self.request.locale, + http_only=self.ACCESS_TOKEN_HTTP_ONLY, + secure=self.LOCALE_SECURE, + max_age=settings.COOKIES_MAX_AGE if permanent else None)) + if hasattr(self.request, 'country_code'): + COOKIES.append(self.COOKIE(key='country_code', + value=self.request.country_code, + http_only=self.COUNTRY_CODE_HTTP_ONLY, + secure=self.COUNTRY_CODE_SECURE, + max_age=settings.COOKIES_MAX_AGE if permanent else None)) + if access_token: + COOKIES.append(self.COOKIE(key='access_token', + value=access_token, + http_only=self.ACCESS_TOKEN_HTTP_ONLY, + secure=self.ACCESS_TOKEN_SECURE, + max_age=settings.COOKIES_MAX_AGE if permanent else None)) + if refresh_token: + COOKIES.append(self.COOKIE(key='refresh_token', + value=refresh_token, + http_only=self.REFRESH_TOKEN_HTTP_ONLY, + secure=self.REFRESH_TOKEN_SECURE, + max_age=settings.COOKIES_MAX_AGE if permanent else None)) return COOKIES def _put_cookies_in_response(self, cookies: list, response: Response): @@ -156,3 +158,29 @@ class FavoritesCreateDestroyMixinView(generics.CreateAPIView, instance.delete() self.es_update_base_object() + +# BackOffice user`s views & viewsets +class BindObjectMixin: + """Bind object mixin.""" + + def get_serializer_class(self): + if self.action == 'bind_object': + return self.bind_object_serializer_class + return self.serializer_class + + def perform_binding(self, serializer): + raise NotImplemented + + def perform_unbinding(self, serializer): + raise NotImplemented + + @action(methods=['post', 'delete'], detail=True, url_path='bind-object') + def bind_object(self, request, pk=None): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + if request.method == 'POST': + self.perform_binding(serializer) + return Response(serializer.data, status=status.HTTP_201_CREATED) + elif request.method == 'DELETE': + self.perform_unbinding(serializer) + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/project/settings/base.py b/project/settings/base.py index 792ad281..2e29c92d 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -509,9 +509,8 @@ FALLBACK_LOCALE = 'en-GB' # TMP TODO remove it later # Временный хардкод для демонстрации > 15 ноября, потом удалить! -CAROUSEL_ITEMS = [230, 231, 232] +CAROUSEL_ITEMS = [465] ESTABLISHMENT_CHOSEN_TAGS = ['gastronomic', 'en_vogue', 'terrace', 'streetfood', 'business', 'bar_cocktail', 'brunch', 'pop'] NEWS_CHOSEN_TAGS = ['eat', 'drink', 'cook', 'style', 'international', 'event', 'partnership'] INTERNATIONAL_COUNTRY_CODES = ['www', 'main', 'next'] -ELASTICSEARCH_DSL_AUTOSYNC = False \ No newline at end of file diff --git a/project/settings/development.py b/project/settings/development.py index 669bc485..06f1199b 100644 --- a/project/settings/development.py +++ b/project/settings/development.py @@ -33,6 +33,7 @@ ELASTICSEARCH_INDEX_NAMES = { 'search_indexes.documents.product': 'development_product', } +# ELASTICSEARCH_DSL_AUTOSYNC = False sentry_sdk.init( dsn="https://35d9bb789677410ab84a822831c6314f@sentry.io/1729093", diff --git a/project/urls/back.py b/project/urls/back.py index 04af4a53..fdd3d10a 100644 --- a/project/urls/back.py +++ b/project/urls/back.py @@ -15,5 +15,5 @@ urlpatterns = [ path('products/', include(('product.urls.back', 'product'), namespace='product')), path('re_blocks/', include(('advertisement.urls.back', 'advertisement'), namespace='advertisement')), + path('main/', include('main.urls.back')), ] -