diff --git a/apps/account/models.py b/apps/account/models.py index 9573c92e..78c3c284 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -100,7 +100,6 @@ class User(AbstractUser): newsletter = models.NullBooleanField(default=True) old_id = models.IntegerField(null=True, blank=True, default=None) - EMAIL_FIELD = 'email' USERNAME_FIELD = 'username' REQUIRED_FIELDS = ['email'] @@ -261,15 +260,31 @@ class User(AbstractUser): def favorite_establishment_ids(self): """Return establishment IDs that in favorites for current user.""" return self.favorites.by_content_type(app_label='establishment', - model='establishment')\ - .values_list('object_id', flat=True) + model='establishment') \ + .values_list('object_id', flat=True) @property def favorite_recipe_ids(self): """Return recipe IDs that in favorites for current user.""" return self.favorites.by_content_type(app_label='recipe', - model='recipe')\ - .values_list('object_id', flat=True) + model='recipe') \ + .values_list('object_id', flat=True) + + @property + def favorite_news_ids(self): + """Return news IDs that in favorites for current user.""" + return self.favorites.by_content_type( + app_label='news', + model='news', + ).values_list('object_id', flat=True) + + @property + def favorite_product_ids(self): + """Return news IDs that in favorites for current user.""" + return self.favorites.by_content_type( + app_label='product', + model='product', + ).values_list('object_id', flat=True) class UserRole(ProjectBaseMixin): @@ -277,4 +292,4 @@ class UserRole(ProjectBaseMixin): user = models.ForeignKey(User, verbose_name=_('User'), on_delete=models.CASCADE) role = models.ForeignKey(Role, verbose_name=_('Role'), on_delete=models.SET_NULL, null=True) establishment = models.ForeignKey(Establishment, verbose_name=_('Establishment'), - on_delete=models.SET_NULL, null=True, blank=True) \ No newline at end of file + on_delete=models.SET_NULL, null=True, blank=True) diff --git a/apps/advertisement/admin.py b/apps/advertisement/admin.py index 74bc6cdc..3754dca9 100644 --- a/apps/advertisement/admin.py +++ b/apps/advertisement/admin.py @@ -2,8 +2,15 @@ from django.contrib import admin from advertisement import models +from main.models import Page + + +class PageInline(admin.TabularInline): + model = Page + extra = 0 @admin.register(models.Advertisement) class AdvertisementModelAdmin(admin.ModelAdmin): """Admin model for model Advertisement""" + inlines = (PageInline, ) diff --git a/apps/advertisement/migrations/0006_auto_20191115_0750.py b/apps/advertisement/migrations/0006_auto_20191115_0750.py new file mode 100644 index 00000000..a043cab2 --- /dev/null +++ b/apps/advertisement/migrations/0006_auto_20191115_0750.py @@ -0,0 +1,34 @@ +# Generated by Django 2.2.7 on 2019-11-15 07:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('advertisement', '0005_auto_20191108_0923'), + ] + + operations = [ + migrations.RemoveField( + model_name='advertisement', + name='height', + ), + migrations.RemoveField( + model_name='advertisement', + name='image_url', + ), + migrations.RemoveField( + model_name='advertisement', + name='source', + ), + migrations.RemoveField( + model_name='advertisement', + name='width', + ), + migrations.AddField( + model_name='advertisement', + name='end', + field=models.DateTimeField(blank=True, default=None, null=True, verbose_name='end'), + ), + ] diff --git a/apps/advertisement/migrations/0007_auto_20191115_0750.py b/apps/advertisement/migrations/0007_auto_20191115_0750.py new file mode 100644 index 00000000..67617b68 --- /dev/null +++ b/apps/advertisement/migrations/0007_auto_20191115_0750.py @@ -0,0 +1,30 @@ +# Generated by Django 2.2.7 on 2019-11-15 07:50 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('advertisement', '0006_auto_20191115_0750'), + ('main', '0036_auto_20191115_0750'), + ] + + operations = [ + migrations.AddField( + model_name='advertisement', + name='page_type', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='advertisements', to='main.PageType', verbose_name='page type'), + ), + migrations.AddField( + model_name='advertisement', + name='sites', + field=models.ManyToManyField(related_name='advertisements', to='main.SiteSettings', verbose_name='site'), + ), + migrations.AddField( + model_name='advertisement', + name='start', + field=models.DateTimeField(null=True, verbose_name='start'), + ), + ] diff --git a/apps/advertisement/models.py b/apps/advertisement/models.py index 3822522d..574aff56 100644 --- a/apps/advertisement/models.py +++ b/apps/advertisement/models.py @@ -6,18 +6,47 @@ from django.utils.translation import gettext_lazy as _ from translation.models import Language from utils.models import ProjectBaseMixin, ImageMixin, PlatformMixin, URLImageMixin +from main.models import Page -class Advertisement(URLImageMixin, ProjectBaseMixin, PlatformMixin): +class AdvertisementQuerySet(models.QuerySet): + """QuerySet for model Advertisement.""" + + def with_base_related(self): + """Return QuerySet with base related""" + return self.select_related('page_type') \ + .prefetch_related('target_languages', 'sites', 'pages') + + def by_page_type(self, page_type: str): + """Filter Advertisement by page type.""" + return self.filter(page_type__name=page_type) + + def by_locale(self, locale): + """Filter by locale.""" + return self.filter(target_languages__locale=locale) + + +class Advertisement(ProjectBaseMixin): """Advertisement model.""" old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None) uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) url = models.URLField(verbose_name=_('Ad URL')) - width = models.PositiveIntegerField(verbose_name=_('Block width')) # 300 - height = models.PositiveIntegerField(verbose_name=_('Block height')) # 250 block_level = models.CharField(verbose_name=_('Block level'), max_length=10, blank=True, null=True) target_languages = models.ManyToManyField(Language) + start = models.DateTimeField(null=True, + verbose_name=_('start')) + end = models.DateTimeField(blank=True, null=True, default=None, + verbose_name=_('end')) + sites = models.ManyToManyField('main.SiteSettings', + related_name='advertisements', + verbose_name=_('site')) + page_type = models.ForeignKey('main.PageType', on_delete=models.PROTECT, + null=True, + related_name='advertisements', + verbose_name=_('page type')) + + objects = AdvertisementQuerySet.as_manager() class Meta: verbose_name = _('Advertisement') @@ -25,3 +54,13 @@ class Advertisement(URLImageMixin, ProjectBaseMixin, PlatformMixin): def __str__(self): return str(self.url) + + @property + def mobile_page(self): + """Return mobile page""" + return self.pages.by_platform(Page.MOBILE).first() + + @property + def web_page(self): + """Return web page""" + return self.pages.by_platform(Page.WEB).first() diff --git a/apps/advertisement/serializers/__init__.py b/apps/advertisement/serializers/__init__.py index e69de29b..393379a8 100644 --- a/apps/advertisement/serializers/__init__.py +++ b/apps/advertisement/serializers/__init__.py @@ -0,0 +1,3 @@ +from .common import * +from .mobile import * +from .web import * diff --git a/apps/advertisement/serializers/common.py b/apps/advertisement/serializers/common.py new file mode 100644 index 00000000..8b87abad --- /dev/null +++ b/apps/advertisement/serializers/common.py @@ -0,0 +1,39 @@ +"""Serializers for app advertisements""" +from rest_framework import serializers + +from advertisement import models +from translation.serializers import LanguageSerializer +from main.serializers import SiteShortSerializer +from main.serializers import PageBaseSerializer + + +class AdvertisementBaseSerializer(serializers.ModelSerializer): + """Base serializer for model Advertisement.""" + + languages = LanguageSerializer(many=True, read_only=True) + sites = SiteShortSerializer(many=True, read_only=True) + + class Meta: + model = models.Advertisement + fields = [ + 'id', + 'uuid', + 'url', + 'block_level', + 'languages', + 'sites', + 'start', + 'end', + ] + + +class AdvertisementPageTypeCommonListSerializer(AdvertisementBaseSerializer): + """Serializer for AdvertisementPageTypeCommonView.""" + + page = PageBaseSerializer(source='common_page', read_only=True) + + class Meta(AdvertisementBaseSerializer.Meta): + """Meta class.""" + fields = AdvertisementBaseSerializer.Meta.fields + [ + 'page', + ] diff --git a/apps/advertisement/serializers/mobile.py b/apps/advertisement/serializers/mobile.py new file mode 100644 index 00000000..80a19b82 --- /dev/null +++ b/apps/advertisement/serializers/mobile.py @@ -0,0 +1,15 @@ +"""Serializers for mobile app advertisements""" +from advertisement.serializers import AdvertisementBaseSerializer +from main.serializers import PageBaseSerializer + + +class AdvertisementPageTypeMobileListSerializer(AdvertisementBaseSerializer): + """Serializer for AdvertisementPageTypeMobileView.""" + + page = PageBaseSerializer(source='mobile_page', read_only=True) + + class Meta(AdvertisementBaseSerializer.Meta): + """Meta class.""" + fields = AdvertisementBaseSerializer.Meta.fields + [ + 'page', + ] diff --git a/apps/advertisement/serializers/web.py b/apps/advertisement/serializers/web.py index 6d5ebfc0..175f1875 100644 --- a/apps/advertisement/serializers/web.py +++ b/apps/advertisement/serializers/web.py @@ -1,22 +1,15 @@ -"""Serializers for app advertisements""" -from rest_framework import serializers - -from advertisement import models -from translation.serializers import LanguageSerializer +"""Serializers for web app advertisements""" +from advertisement.serializers import AdvertisementBaseSerializer +from main.serializers import PageBaseSerializer -class AdvertisementSerializer(serializers.ModelSerializer): - """Serializer for model Advertisement.""" +class AdvertisementPageTypeWebListSerializer(AdvertisementBaseSerializer): + """Serializer for AdvertisementPageTypeWebView.""" - class Meta: - model = models.Advertisement - fields = ( - 'id', - 'uuid', - 'url', - 'image_url', - 'width', - 'height', - 'block_level', - 'source' - ) + page = PageBaseSerializer(source='web_page', read_only=True) + + class Meta(AdvertisementBaseSerializer.Meta): + """Meta class.""" + fields = AdvertisementBaseSerializer.Meta.fields + [ + 'page', + ] diff --git a/apps/advertisement/urls/common.py b/apps/advertisement/urls/common.py new file mode 100644 index 00000000..323a3b48 --- /dev/null +++ b/apps/advertisement/urls/common.py @@ -0,0 +1,8 @@ +"""Advertisement common urlpaths.""" +from django.urls import path + + +app_name = 'advertisements' + +common_urlpatterns = [ +] diff --git a/apps/advertisement/urls/mobile.py b/apps/advertisement/urls/mobile.py new file mode 100644 index 00000000..f61003da --- /dev/null +++ b/apps/advertisement/urls/mobile.py @@ -0,0 +1,14 @@ +"""Advertisement common urlpaths.""" +from django.urls import path + +from advertisement.views import mobile as views +from .common import common_urlpatterns + + +app_name = 'advertisements' + +urlpatterns = [ + path('/', views.AdvertisementPageTypeMobileListView.as_view(), name='list'), +] + +urlpatterns += common_urlpatterns diff --git a/apps/advertisement/urls/web.py b/apps/advertisement/urls/web.py index 48f9d71a..4d17c831 100644 --- a/apps/advertisement/urls/web.py +++ b/apps/advertisement/urls/web.py @@ -2,9 +2,12 @@ from django.urls import path from advertisement.views import web as views +from .common import common_urlpatterns app_name = 'advertisements' urlpatterns = [ - path('/', views.AdvertisementListView.as_view(), name='list') + path('/', views.AdvertisementPageTypeWebListView.as_view(), name='list'), ] + +urlpatterns += common_urlpatterns diff --git a/apps/advertisement/views/__init__.py b/apps/advertisement/views/__init__.py index e69de29b..393379a8 100644 --- a/apps/advertisement/views/__init__.py +++ b/apps/advertisement/views/__init__.py @@ -0,0 +1,3 @@ +from .common import * +from .mobile import * +from .web import * diff --git a/apps/advertisement/views/common.py b/apps/advertisement/views/common.py new file mode 100644 index 00000000..43c6e965 --- /dev/null +++ b/apps/advertisement/views/common.py @@ -0,0 +1,32 @@ +"""Views for app advertisement""" +from rest_framework import generics +from rest_framework import permissions + +from advertisement.models import Advertisement +from advertisement.serializers import AdvertisementBaseSerializer, \ + AdvertisementPageTypeCommonListSerializer + + +class AdvertisementBaseView(generics.GenericAPIView): + """Advertisement list view.""" + + pagination_class = None + permission_classes = (permissions.AllowAny, ) + serializer_class = AdvertisementBaseSerializer + + def get_queryset(self): + """Overridden get queryset method.""" + return Advertisement.objects.with_base_related() \ + .by_locale(self.request.locale) + + +class AdvertisementPageTypeListView(AdvertisementBaseView, generics.ListAPIView): + """Advertisement list view by page type.""" + + def get_queryset(self): + """Overridden get queryset method.""" + product_type = self.kwargs.get('page_type') + qs = super(AdvertisementPageTypeListView, self).get_queryset() + if product_type: + return qs.by_page_type(product_type) + return qs.none() diff --git a/apps/advertisement/views/mobile.py b/apps/advertisement/views/mobile.py new file mode 100644 index 00000000..bac5a81d --- /dev/null +++ b/apps/advertisement/views/mobile.py @@ -0,0 +1,9 @@ +"""Mobile views for app advertisement""" +from advertisement.serializers import AdvertisementPageTypeMobileListSerializer +from .common import AdvertisementPageTypeListView + + +class AdvertisementPageTypeMobileListView(AdvertisementPageTypeListView): + """Advertisement mobile list view.""" + + serializer_class = AdvertisementPageTypeMobileListSerializer diff --git a/apps/advertisement/views/web.py b/apps/advertisement/views/web.py index 1740022c..db1cfde8 100644 --- a/apps/advertisement/views/web.py +++ b/apps/advertisement/views/web.py @@ -1,19 +1,9 @@ -"""Views for app advertisement""" -from rest_framework import generics -from rest_framework import permissions - -from advertisement import models -from advertisement.serializers import web as serializers +"""Web views for app advertisement""" +from advertisement.serializers import AdvertisementPageTypeWebListSerializer +from .common import AdvertisementPageTypeListView -class AdvertisementListView(generics.ListAPIView): - """List view for model Advertisement""" - pagination_class = None - model = models.Advertisement - permission_classes = (permissions.AllowAny,) - serializer_class = serializers.AdvertisementSerializer +class AdvertisementPageTypeWebListView(AdvertisementPageTypeListView): + """Advertisement mobile list view.""" - def get_queryset(self): - return models.Advertisement.objects\ - .filter(page__page_name__contains=self.kwargs['page'])\ - .filter(target_languages__locale=self.request.locale) + serializer_class = AdvertisementPageTypeWebListSerializer diff --git a/apps/collection/serializers/back.py b/apps/collection/serializers/back.py index 01b94c6a..4cc76b2f 100644 --- a/apps/collection/serializers/back.py +++ b/apps/collection/serializers/back.py @@ -1,9 +1,33 @@ from rest_framework import serializers +from location.models import Country +from location.serializers import CountrySimpleSerializer +from collection.serializers.common import CollectionBaseSerializer from collection import models -class CollectionBackOfficeSerializer(serializers.ModelSerializer): - """Collection serializer.""" +class CollectionBackOfficeSerializer(CollectionBaseSerializer): + """Collection back serializer.""" + country_id = serializers.PrimaryKeyRelatedField( + source='country', write_only=True, + queryset=Country.objects.all()) + collection_type_display = serializers.CharField( + source='get_collection_type_display', read_only=True) + country = CountrySimpleSerializer(read_only=True) + class Meta: model = models.Collection - fields = '__all__' + fields = CollectionBaseSerializer.Meta.fields + [ + 'id', + 'name', + 'collection_type', + 'collection_type_display', + 'is_publish', + 'on_top', + 'country', + 'country_id', + 'block_size', + 'description', + 'slug', + 'start', + 'end', + ] diff --git a/apps/collection/urls/back.py b/apps/collection/urls/back.py index df03df26..eee40327 100644 --- a/apps/collection/urls/back.py +++ b/apps/collection/urls/back.py @@ -7,4 +7,5 @@ app_name = 'collection' urlpatterns = [ path('', views.CollectionListCreateView.as_view(), name='list-create'), + path('/', views.CollectionRUDView.as_view(), name='rud-collection'), ] diff --git a/apps/collection/views/back.py b/apps/collection/views/back.py index 22dbb2b0..78a2dcb0 100644 --- a/apps/collection/views/back.py +++ b/apps/collection/views/back.py @@ -1,6 +1,6 @@ from rest_framework import generics, permissions from collection import models -from collection.serializers import common, back +from collection.serializers import back class CollectionListCreateView(generics.ListCreateAPIView): @@ -16,4 +16,4 @@ class CollectionRUDView(generics.RetrieveUpdateDestroyAPIView): queryset = models.Collection.objects.all() serializer_class = back.CollectionBackOfficeSerializer # todo: conf. permissions by TT - permission_classes = (permissions.IsAuthenticated, ) \ No newline at end of file + permission_classes = (permissions.IsAuthenticated, ) diff --git a/apps/establishment/admin.py b/apps/establishment/admin.py index e5b48822..366d54cf 100644 --- a/apps/establishment/admin.py +++ b/apps/establishment/admin.py @@ -57,6 +57,8 @@ class ProductInline(admin.TabularInline): 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 = [ # AwardInline, ContactPhoneInline, ContactEmailInline, @@ -94,3 +96,14 @@ class MenuAdmin(BaseModelAdminMixin, admin.ModelAdmin): @admin.register(models.RatingStrategy) class RatingStrategyAdmin(BaseModelAdminMixin, admin.ModelAdmin): """Admin conf for Rating Strategy model.""" + + +@admin.register(models.SocialChoice) +class SocialChoiceAdmin(BaseModelAdminMixin, admin.ModelAdmin): + """Admin conf for SocialChoice model.""" + + +@admin.register(models.SocialNetwork) +class SocialNetworkAdmin(BaseModelAdminMixin, admin.ModelAdmin): + """Admin conf for SocialNetwork model.""" + raw_id_fields = ('establishment',) diff --git a/apps/establishment/filters.py b/apps/establishment/filters.py index 91670031..adbcae76 100644 --- a/apps/establishment/filters.py +++ b/apps/establishment/filters.py @@ -11,10 +11,8 @@ class EstablishmentFilter(filters.FilterSet): tag_id = filters.NumberFilter(field_name='tags__metadata__id',) award_id = filters.NumberFilter(field_name='awards__id',) search = filters.CharFilter(method='search_text') - type = filters.ChoiceFilter(choices=models.EstablishmentType.INDEX_NAME_TYPES, - method='by_type') - subtype = filters.ChoiceFilter(choices=models.EstablishmentSubType.INDEX_NAME_TYPES, - method='by_subtype') + type = filters.CharFilter(method='by_type') + subtype = filters.CharFilter(method='by_subtype') class Meta: """Meta class.""" diff --git a/apps/establishment/management/commands/add_establishment_social.py b/apps/establishment/management/commands/add_establishment_social.py index 647d8764..22cd648a 100644 --- a/apps/establishment/management/commands/add_establishment_social.py +++ b/apps/establishment/management/commands/add_establishment_social.py @@ -1,6 +1,6 @@ from django.core.management.base import BaseCommand -from establishment.models import Establishment, SocialNetwork +from establishment.models import Establishment, SocialChoice, SocialNetwork from transfer.models import EstablishmentInfos @@ -10,47 +10,48 @@ class Command(BaseCommand): def handle(self, *args, **kwargs): count = 0 + facebook, _ = SocialChoice.objects.get_or_create(title='facebook') + twitter, _ = SocialChoice.objects.get_or_create(title='twitter') + instagram, _ = SocialChoice.objects.get_or_create(title='instagram') + queryset = EstablishmentInfos.objects.exclude( establishment_id__isnull=True ).values_list('id', 'establishment_id', 'facebook', 'twitter', 'instagram') - for id, es_id, facebook, twitter, instagram in queryset: - try: - establishment = Establishment.objects.get(old_id=es_id) - except Establishment.DoesNotExist: + for id, es_id, facebook_url, twitter_url, instagram_url in queryset: + establishment = Establishment.objects.filter(old_id=es_id).first() + if not establishment: continue - except Establishment.MultipleObjectsReturned: - establishment = Establishment.objects.filter(old_id=es_id).first() - else: - if facebook: - if 'facebook.com/' not in facebook: - facebook = 'https://www.facebook.com/' + facebook - obj, _ = SocialNetwork.objects.get_or_create( - old_id=id, - establishment=establishment, - title='facebook', - url=facebook, - ) - count += 1 - if twitter: - if 'twitter.com/' not in twitter: - twitter = 'https://www.twitter.com/' + twitter - obj, _ = SocialNetwork.objects.get_or_create( - old_id=id, - establishment=establishment, - title='twitter', - url=twitter, - ) - count += 1 - if instagram: - if 'instagram.com/' not in instagram: - instagram = 'https://www.instagram.com/' + instagram - obj, _ = SocialNetwork.objects.get_or_create( - old_id=id, - establishment=establishment, - title='instagram', - url=instagram, - ) - count += 1 + + if facebook_url: + if 'facebook.com/' not in facebook_url: + facebook_url = 'https://www.facebook.com/' + facebook_url + obj, _ = SocialNetwork.objects.get_or_create( + old_id=id, + establishment=establishment, + network=facebook, + url=facebook_url, + ) + count += 1 + if twitter_url: + if 'twitter.com/' not in twitter_url: + twitter_url = 'https://www.twitter.com/' + twitter_url + obj, _ = SocialNetwork.objects.get_or_create( + old_id=id, + establishment=establishment, + network=twitter, + url=twitter_url, + ) + count += 1 + if instagram_url: + if 'instagram.com/' not in instagram_url: + instagram = 'https://www.instagram.com/' + instagram_url + obj, _ = SocialNetwork.objects.get_or_create( + old_id=id, + establishment=establishment, + network=instagram, + url=instagram_url, + ) + count += 1 self.stdout.write(self.style.WARNING(f'Created/updated {count} objects.')) diff --git a/apps/establishment/management/commands/update_employee.py b/apps/establishment/management/commands/update_employee.py index 41831f0d..e6d00cc8 100644 --- a/apps/establishment/management/commands/update_employee.py +++ b/apps/establishment/management/commands/update_employee.py @@ -1,9 +1,12 @@ from django.core.management.base import BaseCommand from django.db import connections from establishment.management.commands.add_position import namedtuplefetchall -from establishment.models import Employee +from establishment.models import Employee, EstablishmentEmployee +from django.utils import timezone +from django.db.models import Q from tqdm import tqdm + class Command(BaseCommand): help = 'Add employee from old db to new db.' @@ -30,4 +33,7 @@ class Command(BaseCommand): for e in tqdm(self.employees_sql()): empl = Employee.objects.filter(old_id=e.profile_id).update(name=e.name) + EstablishmentEmployee.objects.filter(from_date__isnull=True, old_id__isnull=False)\ + .update(from_date=timezone.now()-timezone.timedelta(days=1)) + self.stdout.write(self.style.WARNING(f'Update employee name objects.')) diff --git a/apps/establishment/migrations/0060_auto_20191113_1512.py b/apps/establishment/migrations/0060_auto_20191113_1512.py new file mode 100644 index 00000000..e8e46ce3 --- /dev/null +++ b/apps/establishment/migrations/0060_auto_20191113_1512.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.7 on 2019-11-13 15:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('establishment', '0059_establishmentnote'), + ] + + operations = [ + migrations.AlterField( + model_name='establishmentsubtype', + name='index_name', + field=models.CharField(db_index=True, max_length=50, unique=True, verbose_name='Index name'), + ), + migrations.AlterField( + model_name='establishmenttype', + name='index_name', + field=models.CharField(db_index=True, max_length=50, unique=True, verbose_name='Index name'), + ), + ] diff --git a/apps/establishment/migrations/0061_auto_20191114_0550.py b/apps/establishment/migrations/0061_auto_20191114_0550.py new file mode 100644 index 00000000..c0529b28 --- /dev/null +++ b/apps/establishment/migrations/0061_auto_20191114_0550.py @@ -0,0 +1,35 @@ +# Generated by Django 2.2.7 on 2019-11-14 05:50 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('establishment', '0060_auto_20191113_1512'), + ] + + operations = [ + migrations.CreateModel( + name='SocialChoice', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255, unique=True, verbose_name='title')), + ], + options={ + 'verbose_name': 'social choice', + 'verbose_name_plural': 'social choices', + }, + ), + migrations.RemoveField( + model_name='socialnetwork', + name='title', + ), + migrations.AddField( + model_name='socialnetwork', + name='network', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='social_links', to='establishment.SocialChoice', verbose_name='social network'), + preserve_default=False, + ), + ] diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 092fa502..2c85890f 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -31,21 +31,14 @@ class EstablishmentType(TranslatedFieldsMixin, ProjectBaseMixin): STR_FIELD_NAME = 'name' - # INDEX NAME CHOICES + # EXAMPLE OF INDEX NAME CHOICES RESTAURANT = 'restaurant' ARTISAN = 'artisan' PRODUCER = 'producer' - INDEX_NAME_TYPES = ( - (RESTAURANT, _('Restaurant')), - (ARTISAN, _('Artisan')), - (PRODUCER, _('Producer')), - ) - name = TJSONField(blank=True, null=True, default=None, verbose_name=_('Description'), help_text='{"en-GB":"some text"}') - index_name = models.CharField(max_length=50, choices=INDEX_NAME_TYPES, - unique=True, db_index=True, + index_name = models.CharField(max_length=50, unique=True, db_index=True, verbose_name=_('Index name')) use_subtypes = models.BooleanField(_('Use subtypes'), default=True) tag_categories = models.ManyToManyField('tag.TagCategory', @@ -72,17 +65,12 @@ class EstablishmentSubTypeManager(models.Manager): class EstablishmentSubType(ProjectBaseMixin, TranslatedFieldsMixin): """Establishment type model.""" - # INDEX NAME CHOICES + # EXAMPLE OF INDEX NAME CHOICES WINERY = 'winery' - INDEX_NAME_TYPES = ( - (WINERY, _('Winery')), - ) - name = TJSONField(blank=True, null=True, default=None, verbose_name=_('Description'), help_text='{"en-GB":"some text"}') - index_name = models.CharField(max_length=50, choices=INDEX_NAME_TYPES, - unique=True, db_index=True, + index_name = models.CharField(max_length=50, unique=True, db_index=True, verbose_name=_('Index name')) establishment_type = models.ForeignKey(EstablishmentType, on_delete=models.CASCADE, @@ -109,7 +97,7 @@ class EstablishmentQuerySet(models.QuerySet): def with_base_related(self): """Return qs with related objects.""" - return self.select_related('address', 'establishment_type').\ + return self.select_related('address', 'establishment_type'). \ prefetch_related('tags') def with_schedule(self): @@ -126,9 +114,9 @@ class EstablishmentQuerySet(models.QuerySet): 'address__city__country') def with_extended_related(self): - return self.select_related('establishment_type').\ + return self.select_related('establishment_type'). \ prefetch_related('establishment_subtypes', 'awards', 'schedule', - 'phones').\ + 'phones'). \ prefetch_actual_employees() def with_type_related(self): @@ -136,7 +124,7 @@ class EstablishmentQuerySet(models.QuerySet): def with_es_related(self): """Return qs with related for ES indexing objects.""" - return self.select_related('address', 'establishment_type', 'address__city', 'address__city__country').\ + return self.select_related('address', 'establishment_type', 'address__city', 'address__city__country'). \ prefetch_related('tags', 'schedule') def search(self, value, locale=None): @@ -178,7 +166,7 @@ class EstablishmentQuerySet(models.QuerySet): """ Return QuerySet establishments with published reviews. """ - return self.filter(reviews__status=Review.READY,) + return self.filter(reviews__status=Review.READY, ) def annotate_distance(self, point: Point = None): """ @@ -234,10 +222,10 @@ class EstablishmentQuerySet(models.QuerySet): .values('id') ) return self.filter(id__in=subquery_filter_by_distance) \ - .annotate_intermediate_public_mark() \ - .annotate_mark_similarity(mark=establishment.public_mark) \ - .order_by('mark_similarity') \ - .distinct('mark_similarity', 'id') + .annotate_intermediate_public_mark() \ + .annotate_mark_similarity(mark=establishment.public_mark) \ + .order_by('mark_similarity') \ + .distinct('mark_similarity', 'id') else: return self.none() @@ -254,7 +242,7 @@ class EstablishmentQuerySet(models.QuerySet): .values('id') ) return self.filter(id__in=subquery_filter_by_distance) \ - .order_by('-reviews__published_at') + .order_by('-reviews__published_at') def prefetch_actual_employees(self): """Prefetch actual employees.""" @@ -290,28 +278,28 @@ class EstablishmentQuerySet(models.QuerySet): def artisans(self): """Return artisans.""" - return self.filter(establishment_type__index_name=EstablishmentType.ARTISAN) + return self.filter(establishment_type__index_name__icontains=EstablishmentType.ARTISAN) def producers(self): """Return producers.""" - return self.filter(establishment_type__index_name=EstablishmentType.PRODUCER) + return self.filter(establishment_type__index_name__icontains=EstablishmentType.PRODUCER) def restaurants(self): """Return restaurants.""" - return self.filter(establishment_type__index_name=EstablishmentType.RESTAURANT) + return self.filter(establishment_type__index_name__icontains=EstablishmentType.RESTAURANT) def wineries(self): """Return wineries.""" return self.producers().filter( - establishment_subtypes__index_name=EstablishmentSubType.WINERY) + establishment_subtypes__index_name__icontains=EstablishmentSubType.WINERY) def by_type(self, value): """Return QuerySet with type by value.""" - return self.filter(establishment_type__index_name=value) + return self.filter(establishment_type__index_name__icontains=value) def by_subtype(self, value): """Return QuerySet with subtype by value.""" - return self.filter(establishment_subtypes__index_name=value) + return self.filter(establishment_subtypes__index_name__icontains=value) def by_public_mark_range(self, min_value, max_value): """Filter by public mark range.""" @@ -341,11 +329,11 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin): help_text='{"en-GB":"some text"}') public_mark = models.PositiveIntegerField(blank=True, null=True, default=None, - verbose_name=_('public mark'),) + verbose_name=_('public mark'), ) # todo: set default 0 toque_number = models.PositiveIntegerField(blank=True, null=True, default=None, - verbose_name=_('toque number'),) + verbose_name=_('toque number'), ) establishment_type = models.ForeignKey(EstablishmentType, related_name='establishment', on_delete=models.PROTECT, @@ -369,9 +357,9 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin): lafourchette = models.URLField(blank=True, null=True, default=None, max_length=255, verbose_name=_('Lafourchette URL')) guestonline_id = models.PositiveIntegerField(blank=True, verbose_name=_('guestonline id'), - null=True, default=None,) + null=True, default=None, ) lastable_id = models.TextField(blank=True, verbose_name=_('lastable id'), unique=True, - null=True, default=None,) + null=True, default=None, ) booking = models.URLField(blank=True, null=True, default=None, max_length=255, verbose_name=_('Booking URL')) is_publish = models.BooleanField(default=False, verbose_name=_('Publish status')) @@ -517,13 +505,13 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin): @property def tags_indexing(self): return [{'id': tag.metadata.id, - 'label': tag.metadata.label} for tag in self.tags.all()] + 'label': tag.metadata.label} for tag in self.tags.all()] @property def last_published_review(self): """Return last published review""" - return self.reviews.published()\ - .order_by('-published_at').first() + return self.reviews.published() \ + .order_by('-published_at').first() @property def location(self): @@ -535,7 +523,7 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin): @property def the_most_recent_award(self): return Award.objects.filter(Q(establishment=self) | Q(employees__establishments=self)) \ - .latest(field_name='vintage_year') + .latest(field_name='vintage_year') @property def country_id(self): @@ -602,7 +590,7 @@ class Position(BaseAttributes, TranslatedFieldsMixin): class EstablishmentEmployeeQuerySet(models.QuerySet): - """Extended queryset for EstablishmEntemployee model.""" + """Extended queryset for EstablishmentEmployee model.""" def actual(self): """Actual objects..""" @@ -620,7 +608,7 @@ class EstablishmentEmployee(BaseAttributes): employee = models.ForeignKey('establishment.Employee', on_delete=models.PROTECT, verbose_name=_('Employee')) from_date = models.DateTimeField(default=timezone.now, verbose_name=_('From date'), - null=True, blank=True) + null=True, blank=True) to_date = models.DateTimeField(blank=True, null=True, default=None, verbose_name=_('To date')) position = models.ForeignKey(Position, on_delete=models.PROTECT, @@ -639,7 +627,7 @@ class Employee(BaseAttributes): verbose_name=_('User')) name = models.CharField(max_length=255, verbose_name=_('Last name')) establishments = models.ManyToManyField(Establishment, related_name='employees', - through=EstablishmentEmployee,) + through=EstablishmentEmployee, ) awards = generic.GenericRelation(to='main.Award', related_query_name='employees') tags = models.ManyToManyField('tag.Tag', related_name='employees', verbose_name=_('Tags')) @@ -752,12 +740,31 @@ class Menu(TranslatedFieldsMixin, BaseAttributes): verbose_name_plural = _('menu') +class SocialChoice(models.Model): + title = models.CharField(_('title'), max_length=255, unique=True) + + class Meta: + verbose_name = _('social choice') + verbose_name_plural = _('social choices') + + def __str__(self): + return self.title + + class SocialNetwork(models.Model): old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None) establishment = models.ForeignKey( - 'Establishment', verbose_name=_('establishment'), - related_name='socials', on_delete=models.CASCADE) - title = models.CharField(_('title'), max_length=255) + 'Establishment', + verbose_name=_('establishment'), + related_name='socials', + on_delete=models.CASCADE, + ) + network = models.ForeignKey( + SocialChoice, + verbose_name=_('social network'), + related_name='social_links', + on_delete=models.CASCADE, + ) url = models.URLField(_('URL'), max_length=255) class Meta: @@ -765,7 +772,7 @@ class SocialNetwork(models.Model): verbose_name_plural = _('social networks') def __str__(self): - return self.title + return f'{self.network.title}: {self.url}' class RatingStrategyManager(models.Manager): @@ -829,4 +836,4 @@ class RatingStrategy(ProjectBaseMixin): def __str__(self): return f'{self.country.code if self.country else "Other country"}. ' \ f'"{self.toque_number}": {self.public_mark_min_value}-' \ - f'{self.public_mark_max_value}' \ No newline at end of file + f'{self.public_mark_max_value}' diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index 36b3df99..5957cb16 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -84,20 +84,29 @@ class EstablishmentRUDSerializer(EstablishmentBaseSerializer): ] +class SocialChoiceSerializers(serializers.ModelSerializer): + """SocialChoice serializers.""" + + class Meta: + model = models.SocialChoice + fields = '__all__' + + class SocialNetworkSerializers(serializers.ModelSerializer): """Social network serializers.""" + class Meta: model = models.SocialNetwork fields = [ 'id', 'establishment', - 'title', + 'network', 'url', ] class PlatesSerializers(PlateSerializer): - """Social network serializers.""" + """Plates serializers.""" currency_id = serializers.PrimaryKeyRelatedField( source='currency', @@ -116,7 +125,8 @@ class PlatesSerializers(PlateSerializer): class ContactPhoneBackSerializers(PlateSerializer): - """Social network serializers.""" + """ContactPhone serializers.""" + class Meta: model = models.ContactPhone fields = [ @@ -127,7 +137,8 @@ class ContactPhoneBackSerializers(PlateSerializer): class ContactEmailBackSerializers(PlateSerializer): - """Social network serializers.""" + """ContactEmail serializers.""" + class Meta: model = models.ContactEmail fields = [ @@ -140,8 +151,8 @@ class ContactEmailBackSerializers(PlateSerializer): # TODO: test decorator @with_base_attributes class EmployeeBackSerializers(serializers.ModelSerializer): + """Employee serializers.""" - """Social network serializers.""" class Meta: model = models.Employee fields = [ diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 8784638a..1d405be7 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -39,7 +39,7 @@ class SocialNetworkRelatedSerializers(serializers.ModelSerializer): model = models.SocialNetwork fields = [ 'id', - 'title', + 'network', 'url', ] @@ -98,7 +98,8 @@ class EstablishmentTypeBaseSerializer(serializers.ModelSerializer): 'id', 'name', 'name_translated', - 'use_subtypes' + 'use_subtypes', + 'index_name', ] extra_kwargs = { 'name': {'write_only': True}, @@ -131,7 +132,8 @@ class EstablishmentSubTypeBaseSerializer(serializers.ModelSerializer): 'id', 'name', 'name_translated', - 'establishment_type' + 'establishment_type', + 'index_name', ] extra_kwargs = { 'name': {'write_only': True}, @@ -172,6 +174,8 @@ class EstablishmentEmployeeSerializer(serializers.ModelSerializer): class EstablishmentShortSerializer(serializers.ModelSerializer): """Short serializer for establishment.""" city = CitySerializer(source='address.city', allow_null=True) + establishment_type = EstablishmentTypeGeoSerializer() + establishment_subtypes = EstablishmentSubTypeBaseSerializer(many=True) class Meta: """Meta class.""" @@ -179,8 +183,11 @@ class EstablishmentShortSerializer(serializers.ModelSerializer): fields = [ 'id', 'name', + 'index_name', 'slug', 'city', + 'establishment_type', + 'establishment_subtypes', ] @@ -204,6 +211,8 @@ class EstablishmentBaseSerializer(ProjectModelSerializer): in_favorites = serializers.BooleanField(allow_null=True) tags = TagBaseSerializer(read_only=True, many=True) currency = CurrencySerializer() + type = EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True) + subtypes = EstablishmentSubTypeBaseSerializer(many=True, source='establishment_subtypes') class Meta: """Meta class.""" @@ -222,9 +231,12 @@ class EstablishmentBaseSerializer(ProjectModelSerializer): 'in_favorites', 'address', 'tags', - 'currency' + 'currency', + 'type', + 'subtypes', ] + class EstablishmentListRetrieveSerializer(EstablishmentBaseSerializer): """Establishment with city serializer.""" @@ -248,10 +260,7 @@ class EstablishmentGeoSerializer(EstablishmentBaseSerializer): class Meta(EstablishmentBaseSerializer.Meta): """Meta class.""" - fields = EstablishmentBaseSerializer.Meta.fields + [ - 'type', - 'subtypes', - ] + fields = EstablishmentBaseSerializer.Meta.fields class RangePriceSerializer(serializers.Serializer): @@ -264,8 +273,6 @@ class EstablishmentDetailSerializer(EstablishmentBaseSerializer): description_translated = TranslatedField() image = serializers.URLField(source='image_url') - type = EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True) - subtypes = EstablishmentSubTypeBaseSerializer(many=True, source='establishment_subtypes') awards = AwardSerializer(many=True) schedule = ScheduleRUDSerializer(many=True, allow_null=True) phones = ContactPhonesSerializer(read_only=True, many=True) @@ -288,8 +295,6 @@ class EstablishmentDetailSerializer(EstablishmentBaseSerializer): fields = EstablishmentBaseSerializer.Meta.fields + [ 'description_translated', 'image', - 'subtypes', - 'type', 'awards', 'schedule', 'website', diff --git a/apps/establishment/tasks.py b/apps/establishment/tasks.py index fdc10933..67572612 100644 --- a/apps/establishment/tasks.py +++ b/apps/establishment/tasks.py @@ -4,11 +4,14 @@ import logging from celery import shared_task from celery.schedules import crontab from celery.task import periodic_task + from django.core import management from django_elasticsearch_dsl.management.commands import search_index +from django_elasticsearch_dsl.registries import registry from establishment import models from location.models import Country +from search_indexes.documents.establishment import EstablishmentDocument logger = logging.getLogger(__name__) @@ -25,7 +28,18 @@ def recalculate_price_levels_by_country(country_id): establishment.recalculate_price_level(low_price=country.low_price, high_price=country.high_price) +# @periodic_task(run_every=crontab(minute=59)) +# def rebuild_establishment_indices(): +# management.call_command(search_index.Command(), action='populate', models=[models.Establishment.__name__], +# force=True) + + @periodic_task(run_every=crontab(minute=59)) -def rebuild_establishment_indices(): - management.call_command(search_index.Command(), action='rebuild', models=[models.Establishment.__name__], - force=True) +def update_establishment_indices(): + try: + doc = registry.get_documents([models.Establishment]).pop() + except KeyError: + pass + else: + qs = doc().get_indexing_queryset() + doc().update(qs) diff --git a/apps/establishment/tests.py b/apps/establishment/tests.py index cc9ed152..3534608c 100644 --- a/apps/establishment/tests.py +++ b/apps/establishment/tests.py @@ -4,7 +4,7 @@ from account.models import User from rest_framework import status from http.cookies import SimpleCookie from main.models import Currency -from establishment.models import Establishment, EstablishmentType, Menu +from establishment.models import Establishment, EstablishmentType, Menu, SocialChoice, SocialNetwork # Create your tests here. from translation.models import Language from account.models import Role, UserRole @@ -19,7 +19,11 @@ class BaseTestCase(APITestCase): self.email = 'sedragurda@desoz.com' self.newsletter = True self.user = User.objects.create_user( - username=self.username, email=self.email, password=self.password) + username=self.username, + email=self.email, + password=self.password, + is_staff=True, + ) # get tokens tokens = User.create_jwt_tokens(self.user) self.client.cookies = SimpleCookie( @@ -30,13 +34,14 @@ class BaseTestCase(APITestCase): name="Test establishment type") # Create lang object - self.lang = Language.objects.get( + self.lang = Language.objects.create( title='Russia', locale='ru-RU' ) - self.country_ru = Country.objects.get( - name={"en-GB": "Russian"} + self.country_ru = Country.objects.create( + name={'en-GB': 'Russian'}, + code='RU', ) self.region = Region.objects.create(name='Moscow area', code='01', @@ -72,10 +77,17 @@ class BaseTestCase(APITestCase): establishment=self.establishment) self.user_role.save() + self.social_choice = SocialChoice.objects.create(title='facebook') + self.social_network = SocialNetwork.objects.create( + network=self.social_choice, + url='https://testsocial.de', + establishment=self.establishment, + ) + class EstablishmentBTests(BaseTestCase): def test_establishment_CRUD(self): - params = {'page': 1, 'page_size': 1,} + params = {'page': 1, 'page_size': 1, } response = self.client.get('/api/back/establishments/', params, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -108,7 +120,7 @@ class EstablishmentBTests(BaseTestCase): class EmployeeTests(BaseTestCase): def test_employee_CRUD(self): - response = self.client.get('/api/back/establishments/employees/', format='json') + response = self.client.get('/api/back/establishments/employees/', format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) data = { @@ -205,13 +217,40 @@ class PhoneTests(ChildTestCase): self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) +class SocialChoicesTests(ChildTestCase): + def test_social_choices_CRUD(self): + response = self.client.get('/api/back/establishments/social_choice/', format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + data = { + 'title': 'twitter', + } + + response = self.client.post('/api/back/establishments/social_choice/', data=data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + response = self.client.get(f'/api/back/establishments/social_choice/{self.social_choice.id}/', format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + update_data = { + 'title': 'vk' + } + + response = self.client.patch(f'/api/back/establishments/social_choice/{self.social_choice.id}/', + data=update_data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + response = self.client.delete(f'/api/back/establishments/social_choice/{self.social_choice.id}/') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + class SocialTests(ChildTestCase): def test_social_CRUD(self): response = self.client.get('/api/back/establishments/socials/', format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) data = { - 'title': "Test social", + 'network': self.social_choice.id, 'url': 'https://testsocial.com', 'establishment': self.establishment.id } @@ -219,17 +258,17 @@ class SocialTests(ChildTestCase): response = self.client.post('/api/back/establishments/socials/', data=data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - response = self.client.get('/api/back/establishments/socials/1/', format='json') + response = self.client.get(f'/api/back/establishments/socials/{self.social_network.id}/', format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) update_data = { - 'title': 'Test new social' + 'url': 'https://newtestsocial.com' } - response = self.client.patch('/api/back/establishments/socials/1/', data=update_data) + response = self.client.patch(f'/api/back/establishments/socials/{self.social_network.id}/', data=update_data) self.assertEqual(response.status_code, status.HTTP_200_OK) - response = self.client.delete('/api/back/establishments/socials/1/') + response = self.client.delete(f'/api/back/establishments/socials/{self.social_network.id}/') self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) @@ -322,7 +361,7 @@ class EstablishmentShedulerTests(ChildTestCase): class EstablishmentWebTests(BaseTestCase): def test_establishment_Read(self): - params = {'page': 1, 'page_size': 1,} + params = {'page': 1, 'page_size': 1, } response = self.client.get('/api/web/establishments/', params, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -351,7 +390,6 @@ class EstablishmentWebSimilarTests(ChildTestCase): class EstablishmentWebCommentsTests(ChildTestCase): def test_comments_CRUD(self): - response = self.client.get(f'/api/web/establishments/slug/{self.establishment.slug}/comments/', format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -375,8 +413,9 @@ class EstablishmentWebCommentsTests(ChildTestCase): 'text': 'Test new establishment' } - response = self.client.patch(f'/api/web/establishments/slug/{self.establishment.slug}/comments/{comment["id"]}/', - data=update_data) + response = self.client.patch( + f'/api/web/establishments/slug/{self.establishment.slug}/comments/{comment["id"]}/', + data=update_data) self.assertEqual(response.status_code, status.HTTP_200_OK) response = self.client.delete( diff --git a/apps/establishment/urls/back.py b/apps/establishment/urls/back.py index 6a12e792..14575c46 100644 --- a/apps/establishment/urls/back.py +++ b/apps/establishment/urls/back.py @@ -6,7 +6,6 @@ from establishment import views app_name = 'establishment' - urlpatterns = [ path('', views.EstablishmentListCreateView.as_view(), name='list'), path('/', views.EstablishmentRUDView.as_view(), name='detail'), @@ -18,6 +17,8 @@ urlpatterns = [ path('menus//', views.MenuRUDView.as_view(), name='menu-rud'), path('plates/', views.PlateListCreateView.as_view(), name='plates'), path('plates//', views.PlateRUDView.as_view(), name='plate-rud'), + path('social_choice/', views.SocialChoiceListCreateView.as_view(), name='socials_choice'), + path('social_choice//', views.SocialChoiceRUDView.as_view(), name='socials_choice-rud'), path('socials/', views.SocialListCreateView.as_view(), name='socials'), path('socials//', views.SocialRUDView.as_view(), name='social-rud'), path('phones/', views.PhonesListCreateView.as_view(), name='phones'), diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index f6d4d63a..1a547032 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -1,6 +1,6 @@ """Establishment app views.""" from django.shortcuts import get_object_or_404 -from rest_framework import generics +from rest_framework import generics, permissions from utils.permissions import IsCountryAdmin, IsEstablishmentManager from establishment import filters, models, serializers @@ -27,7 +27,7 @@ class EstablishmentListCreateView(EstablishmentMixinViews, generics.ListCreateAP class EstablishmentRUDView(generics.RetrieveUpdateDestroyAPIView): queryset = models.Establishment.objects.all() serializer_class = serializers.EstablishmentRUDSerializer - permission_classes = [IsCountryAdmin|IsEstablishmentManager] + permission_classes = [IsCountryAdmin | IsEstablishmentManager] class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView): @@ -72,12 +72,27 @@ class MenuRUDView(generics.RetrieveUpdateDestroyAPIView): permission_classes = [IsEstablishmentManager] +class SocialChoiceListCreateView(generics.ListCreateAPIView): + """SocialChoice list create view.""" + serializer_class = serializers.SocialChoiceSerializers + queryset = models.SocialChoice.objects.all() + pagination_class = None + permission_classes = [permissions.IsAdminUser] + + +class SocialChoiceRUDView(generics.RetrieveUpdateDestroyAPIView): + """SocialChoice RUD view.""" + serializer_class = serializers.SocialChoiceSerializers + queryset = models.SocialChoice.objects.all() + permission_classes = [permissions.IsAdminUser] + + class SocialListCreateView(generics.ListCreateAPIView): """Social list create view.""" serializer_class = serializers.SocialNetworkSerializers queryset = models.SocialNetwork.objects.all() pagination_class = None - permission_classes = [IsEstablishmentManager] + permission_classes = [permissions.IsAdminUser] class SocialRUDView(generics.RetrieveUpdateDestroyAPIView): @@ -96,14 +111,14 @@ class PlateListCreateView(generics.ListCreateAPIView): class PlateRUDView(generics.RetrieveUpdateDestroyAPIView): - """Social RUD view.""" + """Plate RUD view.""" serializer_class = serializers.PlatesSerializers queryset = models.Plate.objects.all() permission_classes = [IsEstablishmentManager] class PhonesListCreateView(generics.ListCreateAPIView): - """Plate list create view.""" + """Phones list create view.""" serializer_class = serializers.ContactPhoneBackSerializers queryset = models.ContactPhone.objects.all() pagination_class = None @@ -111,14 +126,14 @@ class PhonesListCreateView(generics.ListCreateAPIView): class PhonesRUDView(generics.RetrieveUpdateDestroyAPIView): - """Social RUD view.""" + """Phones RUD view.""" serializer_class = serializers.ContactPhoneBackSerializers queryset = models.ContactPhone.objects.all() permission_classes = [IsEstablishmentManager] class EmailListCreateView(generics.ListCreateAPIView): - """Plate list create view.""" + """Email list create view.""" serializer_class = serializers.ContactEmailBackSerializers queryset = models.ContactEmail.objects.all() pagination_class = None @@ -126,7 +141,7 @@ class EmailListCreateView(generics.ListCreateAPIView): class EmailRUDView(generics.RetrieveUpdateDestroyAPIView): - """Social RUD view.""" + """Email RUD view.""" serializer_class = serializers.ContactEmailBackSerializers queryset = models.ContactEmail.objects.all() permission_classes = [IsEstablishmentManager] @@ -140,7 +155,7 @@ class EmployeeListCreateView(generics.ListCreateAPIView): class EmployeeRUDView(generics.RetrieveUpdateDestroyAPIView): - """Social RUD view.""" + """Employee RUD view.""" serializer_class = serializers.EmployeeBackSerializers queryset = models.Employee.objects.all() diff --git a/apps/favorites/views.py b/apps/favorites/views.py index ce06863d..3cf97246 100644 --- a/apps/favorites/views.py +++ b/apps/favorites/views.py @@ -5,7 +5,7 @@ from establishment.filters import EstablishmentFilter from establishment.serializers import EstablishmentBaseSerializer from news.filters import NewsListFilterSet from news.models import News -from news.serializers import NewsBaseSerializer +from news.serializers import NewsBaseSerializer, NewsListSerializer from product.models import Product from product.serializers import ProductBaseSerializer from product.filters import ProductFilterSet @@ -47,7 +47,7 @@ class FavoritesProductListView(generics.ListAPIView): class FavoritesNewsListView(generics.ListAPIView): """List views for news in favorites.""" - serializer_class = NewsBaseSerializer + serializer_class = NewsListSerializer filter_class = NewsListFilterSet def get_queryset(self): diff --git a/apps/location/management/__init__.py b/apps/location/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/location/management/commands/__init__.py b/apps/location/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/location/migrations/0024_wineregion_tag_categories.py b/apps/location/migrations/0024_wineregion_tag_categories.py new file mode 100644 index 00000000..63c456fe --- /dev/null +++ b/apps/location/migrations/0024_wineregion_tag_categories.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.7 on 2019-11-13 12:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tag', '0014_tag_old_id_meta_product'), + ('location', '0023_auto_20191112_0104'), + ] + + operations = [ + migrations.AddField( + model_name='wineregion', + name='tag_categories', + field=models.ManyToManyField(blank=True, help_text='attribute from legacy db', related_name='wine_regions', to='tag.TagCategory'), + ), + ] diff --git a/apps/location/migrations/0025_auto_20191114_0809.py b/apps/location/migrations/0025_auto_20191114_0809.py new file mode 100644 index 00000000..cecff5a2 --- /dev/null +++ b/apps/location/migrations/0025_auto_20191114_0809.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.7 on 2019-11-14 08:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tag', '0014_tag_old_id_meta_product'), + ('location', '0024_wineregion_tag_categories'), + ] + + operations = [ + migrations.RemoveField( + model_name='wineregion', + name='tag_categories', + ), + migrations.AddField( + model_name='wineregion', + name='tags', + field=models.ManyToManyField(blank=True, help_text='attribute from legacy db', related_name='wine_regions', to='tag.Tag'), + ), + ] diff --git a/apps/location/migrations/0026_country_is_active.py b/apps/location/migrations/0026_country_is_active.py new file mode 100644 index 00000000..9a477bbc --- /dev/null +++ b/apps/location/migrations/0026_country_is_active.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.4 on 2019-11-14 19:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('location', '0025_auto_20191114_0809'), + ] + + operations = [ + migrations.AddField( + model_name='country', + name='is_active', + field=models.BooleanField(default=True, verbose_name='is active'), + ), + ] diff --git a/apps/location/models.py b/apps/location/models.py index 0cb0f457..d5d4ef3d 100644 --- a/apps/location/models.py +++ b/apps/location/models.py @@ -11,6 +11,13 @@ from utils.models import (ProjectBaseMixin, SVGImageMixin, TJSONField, TranslatedFieldsMixin, get_current_locale) +class CountryQuerySet(models.QuerySet): + """Country queryset.""" + def active(self, switcher=True): + """Filter only active users.""" + return self.filter(is_active=switcher) + + class Country(TranslatedFieldsMixin, SVGImageMixin, ProjectBaseMixin): """Country model.""" @@ -29,8 +36,11 @@ class Country(TranslatedFieldsMixin, SVGImageMixin, ProjectBaseMixin): low_price = models.IntegerField(default=25, verbose_name=_('Low price')) high_price = models.IntegerField(default=50, verbose_name=_('High price')) languages = models.ManyToManyField(Language, verbose_name=_('Languages')) + is_active = models.BooleanField(_('is active'), default=True) old_id = models.IntegerField(null=True, blank=True, default=None) + objects = CountryQuerySet.as_manager() + @property def time_format(self): if self.code.lower() not in self.TWELVE_HOURS_FORMAT_COUNTRIES: @@ -200,7 +210,7 @@ class WineRegionQuerySet(models.QuerySet): """Wine region queryset.""" -class WineRegion(models.Model): +class WineRegion(models.Model, TranslatedFieldsMixin): """Wine region model.""" name = models.CharField(_('name'), max_length=255) country = models.ForeignKey(Country, on_delete=models.PROTECT, @@ -213,6 +223,9 @@ class WineRegion(models.Model): description = TJSONField(blank=True, null=True, default=None, verbose_name=_('description'), help_text='{"en-GB":"some text"}') + tags = models.ManyToManyField('tag.Tag', blank=True, + related_name='wine_regions', + help_text='attribute from legacy db') objects = WineRegionQuerySet.as_manager() @@ -221,6 +234,10 @@ class WineRegion(models.Model): verbose_name_plural = _('wine regions') verbose_name = _('wine region') + def __str__(self): + """Override dunder method.""" + return self.name + class WineSubRegionQuerySet(models.QuerySet): """Wine sub region QuerySet.""" @@ -241,6 +258,10 @@ class WineSubRegion(models.Model): verbose_name_plural = _('wine sub regions') verbose_name = _('wine sub region') + def __str__(self): + """Override dunder method.""" + return self.name + class WineVillageQuerySet(models.QuerySet): """Wine village QuerySet.""" @@ -264,6 +285,10 @@ class WineVillage(models.Model): verbose_name = _('wine village') verbose_name_plural = _('wine villages') + def __str__(self): + """Override str dunder.""" + return self.name + # todo: Make recalculate price levels @receiver(post_save, sender=Country) diff --git a/apps/location/transfer_data.py b/apps/location/transfer_data.py index 136a79d8..d7ebfd29 100644 --- a/apps/location/transfer_data.py +++ b/apps/location/transfer_data.py @@ -357,10 +357,10 @@ def update_child_regions(): data_types = { "dictionaries": [ - # transfer_countries, - # transfer_regions, - # transfer_cities, - # transfer_addresses, + transfer_countries, + transfer_regions, + transfer_cities, + transfer_addresses, transfer_wine_region, transfer_wine_sub_region, transfer_wine_village, diff --git a/apps/location/views/common.py b/apps/location/views/common.py index 5b17d31d..fab7662c 100644 --- a/apps/location/views/common.py +++ b/apps/location/views/common.py @@ -15,7 +15,7 @@ class CountryViewMixin(generics.GenericAPIView): serializer_class = serializers.CountrySerializer permission_classes = (permissions.AllowAny,) - queryset = models.Country.objects.all() + queryset = models.Country.objects.active() class RegionViewMixin(generics.GenericAPIView): diff --git a/apps/main/admin.py b/apps/main/admin.py index 77ef20ab..057515e8 100644 --- a/apps/main/admin.py +++ b/apps/main/admin.py @@ -35,3 +35,8 @@ class CurrencyContentAdmin(admin.ModelAdmin): @admin.register(models.Carousel) class CarouselAdmin(admin.ModelAdmin): """Carousel admin.""" + + +@admin.register(models.PageType) +class PageTypeAdmin(admin.ModelAdmin): + """PageType admin.""" diff --git a/apps/main/management/commands/add_award.py b/apps/main/management/commands/add_award.py index 85a613cf..a41edf3d 100644 --- a/apps/main/management/commands/add_award.py +++ b/apps/main/management/commands/add_award.py @@ -3,6 +3,7 @@ from django.db import connections from establishment.management.commands.add_position import namedtuplefetchall from main.models import Award, AwardType from establishment.models import Employee +from tqdm import tqdm class Command(BaseCommand): @@ -25,15 +26,18 @@ class Command(BaseCommand): def handle(self, *args, **kwargs): objects =[] - for a in self.award_sql(): - profile = Employee.objects.filter(old_id=a.profile_id).first() - type = AwardType.objects.filter(old_id=a.award_type).first() + Award.objects.all().delete() + for a in tqdm(self.award_sql(), desc='Add award to profile'): + profiles = Employee.objects.filter(old_id=a.profile_id) + type = AwardType.objects.filter(old_id=a.award_type) state = Award.PUBLISHED if a.state == 'published' else Award.WAITING - if profile and type: - award = Award(award_type=type, vintage_year=a.vintage_year, - title={"en-GB": a.title}, state=state, - content_object=profile, old_id=a.id) - objects.append(award) + if profiles.exists() and type.exists(): + for profile in profiles: + award = Award(award_type=type.first(), vintage_year=a.vintage_year, + title={"en-GB": a.title}, state=state, + content_object=profile, old_id=a.id) + + objects.append(award) awards = Award.objects.bulk_create(objects) self.stdout.write(self.style.WARNING(f'Created awards objects.')) diff --git a/apps/main/management/commands/add_award_type.py b/apps/main/management/commands/add_award_type.py index 63aa6546..eb693ad2 100644 --- a/apps/main/management/commands/add_award_type.py +++ b/apps/main/management/commands/add_award_type.py @@ -3,7 +3,7 @@ from django.db import connections from establishment.management.commands.add_position import namedtuplefetchall from main.models import AwardType from location.models import Country - +from tqdm import tqdm class Command(BaseCommand): help = '''Add award types from old db to new db. @@ -24,7 +24,7 @@ class Command(BaseCommand): def handle(self, *args, **kwargs): objects =[] - for a in self.award_types_sql(): + for a in tqdm(self.award_types_sql(), desc='Add award types: '): country = Country.objects.filter(code=a.country_code).first() if country: type = AwardType(name=a.name, old_id=a.id) diff --git a/apps/main/migrations/0036_auto_20191115_0750.py b/apps/main/migrations/0036_auto_20191115_0750.py new file mode 100644 index 00000000..e429d1e2 --- /dev/null +++ b/apps/main/migrations/0036_auto_20191115_0750.py @@ -0,0 +1,85 @@ +# Generated by Django 2.2.7 on 2019-11-15 07:50 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('advertisement', '0006_auto_20191115_0750'), + ('main', '0035_merge_20191112_1218'), + ] + + operations = [ + migrations.CreateModel( + name='PageType', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')), + ('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('name', models.CharField(max_length=255, unique=True, verbose_name='name')), + ], + options={ + 'verbose_name': 'page type', + 'verbose_name_plural': 'page types', + }, + ), + migrations.AlterModelOptions( + name='page', + options={'verbose_name': 'page', 'verbose_name_plural': 'pages'}, + ), + migrations.RemoveField( + model_name='page', + name='advertisements', + ), + migrations.RemoveField( + model_name='page', + name='page_name', + ), + migrations.AddField( + model_name='page', + name='advertisement', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='pages', to='advertisement.Advertisement', verbose_name='advertisement'), + ), + migrations.AddField( + model_name='page', + name='created', + field=models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created'), + ), + migrations.AddField( + model_name='page', + name='height', + field=models.PositiveIntegerField(null=True, verbose_name='Block height'), + ), + migrations.AddField( + model_name='page', + name='image_url', + field=models.URLField(blank=True, default=None, null=True, verbose_name='Image URL path'), + ), + migrations.AddField( + model_name='page', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='Date updated'), + ), + migrations.AddField( + model_name='page', + name='source', + field=models.PositiveSmallIntegerField(choices=[(0, 'Mobile'), (1, 'Web'), (2, 'All')], default=0, verbose_name='Source'), + ), + migrations.AddField( + model_name='page', + name='width', + field=models.PositiveIntegerField(null=True, verbose_name='Block width'), + ), + migrations.RemoveField( + model_name='feature', + name='route', + ), + migrations.AddField( + model_name='feature', + name='route', + field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='main.PageType'), + ), + ] diff --git a/apps/main/models.py b/apps/main/models.py index 310257f3..1bd39a6d 100644 --- a/apps/main/models.py +++ b/apps/main/models.py @@ -10,7 +10,6 @@ from django.db import models from django.db.models import Q from django.utils.translation import gettext_lazy as _ -from advertisement.models import Advertisement from configuration.models import TranslationSettings from location.models import Country from main import methods @@ -99,27 +98,12 @@ class SiteSettings(ProjectBaseMixin): domain=settings.SITE_DOMAIN_URI) -class Page(models.Model): - """Page model.""" - - page_name = models.CharField(max_length=255, unique=True) - advertisements = models.ManyToManyField(Advertisement) - - class Meta: - """Meta class.""" - verbose_name = _('Page') - verbose_name_plural = _('Pages') - - def __str__(self): - return f'{self.page_name}' - - class Feature(ProjectBaseMixin, PlatformMixin): """Feature model.""" slug = models.SlugField(max_length=255, unique=True) priority = models.IntegerField(unique=True, null=True, default=None) - route = models.ForeignKey(Page, on_delete=models.PROTECT, null=True, default=None) + route = models.ForeignKey('PageType', on_delete=models.PROTECT, null=True, default=None) site_settings = models.ManyToManyField(SiteSettings, through='SiteFeature') class Meta: @@ -310,3 +294,56 @@ class Carousel(models.Model): elif self.link not in EMPTY_VALUES: return 'external' return None + + +class PageQuerySet(models.QuerySet): + """QuerySet for model Page.""" + + def by_platform(self, platform: int): + """Filter by platform.""" + return self.filter(source=platform) + + +class Page(URLImageMixin, PlatformMixin, ProjectBaseMixin): + """Page model.""" + advertisement = models.ForeignKey('advertisement.Advertisement', + on_delete=models.PROTECT, null=True, + related_name='pages', + verbose_name=_('advertisement')) + width = models.PositiveIntegerField(null=True, + verbose_name=_('Block width')) # 300 + height = models.PositiveIntegerField(null=True, + verbose_name=_('Block height')) # 250 + + objects = PageQuerySet.as_manager() + + class Meta: + """Meta class.""" + verbose_name = _('page') + verbose_name_plural = _('pages') + + def __str__(self): + """Overridden dunder method.""" + return self.get_source_display() + + +class PageTypeQuerySet(models.QuerySet): + """QuerySet for model PageType.""" + + +class PageType(ProjectBaseMixin): + """Page type model.""" + + name = models.CharField(max_length=255, unique=True, + verbose_name=_('name')) + + objects = PageTypeQuerySet.as_manager() + + class Meta: + """Meta class.""" + verbose_name = _('page type') + verbose_name_plural = _('page types') + + def __str__(self): + """Overridden dunder method.""" + return self.name diff --git a/apps/main/serializers.py b/apps/main/serializers.py index 71bbd589..0ed2f026 100644 --- a/apps/main/serializers.py +++ b/apps/main/serializers.py @@ -1,7 +1,6 @@ """Main app serializers.""" from rest_framework import serializers -from advertisement.serializers.web import AdvertisementSerializer from location.serializers import CountrySerializer from main import models from utils.serializers import ProjectModelSerializer, TranslatedField, RecursiveFieldSerializer @@ -25,7 +24,7 @@ class SiteFeatureSerializer(serializers.ModelSerializer): id = serializers.IntegerField(source='feature.id') slug = serializers.CharField(source='feature.slug') priority = serializers.IntegerField(source='feature.priority') - route = serializers.CharField(source='feature.route.page_name') + route = serializers.CharField(source='feature.route.name') source = serializers.IntegerField(source='feature.source') nested = RecursiveFieldSerializer(many=True, allow_null=True) @@ -94,11 +93,20 @@ class SiteSerializer(serializers.ModelSerializer): class Meta: """Meta class.""" - model = models.SiteSettings fields = ('subdomain', 'site_url', 'country') +class SiteShortSerializer(serializers.ModelSerializer): + """Short serializer for model SiteSettings.""" + + class Meta(SiteSerializer.Meta): + """Meta class.""" + fields = [ + 'subdomain', + ] + + # class SiteFeatureSerializer(serializers.ModelSerializer): # """Site feature serializer.""" # @@ -167,15 +175,27 @@ class CarouselListSerializer(serializers.ModelSerializer): ] -class PageSerializer(serializers.ModelSerializer): - page_name = serializers.CharField() - advertisements = AdvertisementSerializer(source='advertisements', many=True) +class PageBaseSerializer(serializers.ModelSerializer): + """Serializer for model Page""" class Meta: """Meta class.""" - model = models.Carousel + model = models.Page fields = [ 'id', - 'page_name', - 'advertisements' + 'image_url', + 'width', + 'height', ] + + +class PageTypeBaseSerializer(serializers.ModelSerializer): + """Serializer fro model PageType.""" + + class Meta: + """Meta class.""" + model = models.PageType + fields = [ + 'id', + 'name', + ] \ No newline at end of file diff --git a/apps/main/views/common.py b/apps/main/views/common.py index 0c2ef6d4..15f89510 100644 --- a/apps/main/views/common.py +++ b/apps/main/views/common.py @@ -70,6 +70,9 @@ 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']: + qs = models.Carousel.objects.filter(id__in=settings.CAROUSEL_ITEMS) + return qs qs = models.Carousel.objects.is_parsed().active() if country_code: qs = qs.by_country_code(country_code) diff --git a/apps/news/migrations/0035_news_views_count.py b/apps/news/migrations/0035_news_views_count.py new file mode 100644 index 00000000..6b94f737 --- /dev/null +++ b/apps/news/migrations/0035_news_views_count.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.4 on 2019-11-14 20:43 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('rating', '0004_auto_20191114_2041'), + ('news', '0034_merge_20191030_1714'), + ] + + operations = [ + migrations.AddField( + model_name='news', + name='views_count', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='rating.ViewCount'), + ), + ] diff --git a/apps/news/models.py b/apps/news/models.py index 9cf385d2..550d5baa 100644 --- a/apps/news/models.py +++ b/apps/news/models.py @@ -1,15 +1,40 @@ """News app models.""" from django.contrib.contenttypes import fields as generic from django.db import models +from django.db.models import Case, When from django.utils import timezone from django.utils.translation import gettext_lazy as _ from rest_framework.reverse import reverse -from rating.models import Rating +from rating.models import Rating, ViewCount from utils.models import BaseAttributes, TJSONField, TranslatedFieldsMixin, ProjectBaseMixin from utils.querysets import TranslationQuerysetMixin +class Agenda(ProjectBaseMixin, TranslatedFieldsMixin): + """News agenda model""" + + event_datetime = models.DateTimeField(default=timezone.now, editable=False, + verbose_name=_('Event datetime')) + address = models.ForeignKey('location.Address', blank=True, null=True, + default=None, verbose_name=_('address'), + on_delete=models.SET_NULL) + content = TJSONField(blank=True, null=True, default=None, + verbose_name=_('content'), + help_text='{"en-GB":"some text"}') + + +class NewsBanner(ProjectBaseMixin, TranslatedFieldsMixin): + """News banner model""" + title = TJSONField(blank=True, null=True, default=None, + verbose_name=_('title'), + help_text='{"en-GB":"some text"}') + image_url = models.URLField(verbose_name=_('Image URL path'), + blank=True, null=True, default=None) + content_url = models.URLField(verbose_name=_('Content URL path'), + blank=True, null=True, default=None) + + class NewsType(models.Model): """NewsType model.""" @@ -31,6 +56,10 @@ class NewsType(models.Model): class NewsQuerySet(TranslationQuerysetMixin): """QuerySet for model News""" + def sort_by_start(self): + """Return qs sorted by start DESC""" + return self.order_by('-start') + def rating_value(self): return self.annotate(rating=models.Count('ratings__ip', distinct=True)) @@ -66,38 +95,29 @@ class NewsQuerySet(TranslationQuerysetMixin): # todo: filter by best score # todo: filter by country? - def should_read(self, news): + def should_read(self, news, user): return self.model.objects.exclude(pk=news.pk).published(). \ + annotate_in_favorites(user). \ with_base_related().by_type(news.news_type).distinct().order_by('?') - def same_theme(self, news): + def same_theme(self, news, user): return self.model.objects.exclude(pk=news.pk).published(). \ + annotate_in_favorites(user). \ with_base_related().by_type(news.news_type). \ by_tags(news.tags.all()).distinct().order_by('-start') - -class Agenda(ProjectBaseMixin, TranslatedFieldsMixin): - """News agenda model""" - - event_datetime = models.DateTimeField(default=timezone.now, editable=False, - verbose_name=_('Event datetime')) - address = models.ForeignKey('location.Address', blank=True, null=True, - default=None, verbose_name=_('address'), - on_delete=models.SET_NULL) - content = TJSONField(blank=True, null=True, default=None, - verbose_name=_('content'), - help_text='{"en-GB":"some text"}') - - -class NewsBanner(ProjectBaseMixin, TranslatedFieldsMixin): - """News banner model""" - title = TJSONField(blank=True, null=True, default=None, - verbose_name=_('title'), - help_text='{"en-GB":"some text"}') - image_url = models.URLField(verbose_name=_('Image URL path'), - blank=True, null=True, default=None) - content_url = models.URLField(verbose_name=_('Content URL path'), - blank=True, null=True, default=None) + def annotate_in_favorites(self, user): + """Annotate flag in_favorites""" + favorite_news_ids = [] + if user.is_authenticated: + favorite_news_ids = user.favorite_news_ids + return self.annotate( + in_favorites=Case( + When(id__in=favorite_news_ids, then=True), + default=False, + output_field=models.BooleanField(default=False) + ) + ) class News(BaseAttributes, TranslatedFieldsMixin): @@ -164,6 +184,7 @@ class News(BaseAttributes, TranslatedFieldsMixin): tags = models.ManyToManyField('tag.Tag', related_name='news', verbose_name=_('Tags')) gallery = models.ManyToManyField('gallery.Image', through='news.NewsGallery') + views_count = models.OneToOneField('rating.ViewCount', blank=True, null=True, on_delete=models.SET_NULL) ratings = generic.GenericRelation(Rating) favorites = generic.GenericRelation(to='favorites.Favorites') agenda = models.ForeignKey('news.Agenda', blank=True, null=True, @@ -193,13 +214,11 @@ class News(BaseAttributes, TranslatedFieldsMixin): def web_url(self): return reverse('web:news:rud', kwargs={'slug': self.slug}) - @property - def should_read(self): - return self.__class__.objects.should_read(self)[:3] + def should_read(self, user): + return self.__class__.objects.should_read(self, user)[:3] - @property - def same_theme(self): - return self.__class__.objects.same_theme(self)[:3] + def same_theme(self, user): + return self.__class__.objects.same_theme(self, user)[:3] @property def main_image(self): @@ -216,6 +235,13 @@ class News(BaseAttributes, TranslatedFieldsMixin): if self.main_image: return self.main_image.get_image_url(thumbnail_key='news_preview') + @property + def view_counter(self): + count_value = 0 + if self.views_count: + count_value = self.views_count.count + return count_value + class NewsGalleryQuerySet(models.QuerySet): """QuerySet for model News""" diff --git a/apps/news/serializers.py b/apps/news/serializers.py index 95ec21b8..7638847f 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -1,6 +1,7 @@ """News app common serializers.""" from django.utils.translation import gettext_lazy as _ from rest_framework import serializers +from rest_framework.fields import SerializerMethodField from account.serializers.common import UserBaseSerializer from gallery.models import Image @@ -135,6 +136,8 @@ class NewsBaseSerializer(ProjectModelSerializer): subtitle_translated = TranslatedField() news_type = NewsTypeSerializer(read_only=True) tags = TagBaseSerializer(read_only=True, many=True) + in_favorites = serializers.BooleanField(allow_null=True) + view_counter = serializers.IntegerField(read_only=True) class Meta: """Meta class.""" @@ -148,6 +151,8 @@ class NewsBaseSerializer(ProjectModelSerializer): 'news_type', 'tags', 'slug', + 'in_favorites', + 'view_counter', ) @@ -204,8 +209,8 @@ class NewsDetailSerializer(NewsBaseSerializer): class NewsDetailWebSerializer(NewsDetailSerializer): """News detail serializer for web users..""" - same_theme = NewsSimilarListSerializer(many=True, read_only=True) - should_read = NewsSimilarListSerializer(many=True, read_only=True) + same_theme = SerializerMethodField() + should_read = SerializerMethodField() agenda = AgendaSerializer() banner = NewsBannerSerializer() @@ -219,6 +224,12 @@ class NewsDetailWebSerializer(NewsDetailSerializer): 'banner', ) + def get_same_theme(self, obj): + return NewsSimilarListSerializer(obj.same_theme(self.context['request'].user), many=True, read_only=True).data + + def get_should_read(self, obj): + return NewsSimilarListSerializer(obj.should_read(self.context['request'].user), many=True, read_only=True).data + class NewsBackOfficeBaseSerializer(NewsBaseSerializer): """News back office base serializer.""" @@ -290,6 +301,9 @@ class NewsBackOfficeGallerySerializer(serializers.ModelSerializer): news = news_qs.first() image = image_qs.first() + if image in news.gallery.all(): + raise serializers.ValidationError({'detail': _('Image is already added.')}) + attrs['news'] = news attrs['image'] = image diff --git a/apps/news/views.py b/apps/news/views.py index 6c2374bc..fbc0e455 100644 --- a/apps/news/views.py +++ b/apps/news/views.py @@ -9,6 +9,7 @@ from gallery.tasks import delete_image from news import filters, models, serializers from rating.tasks import add_rating from utils.permissions import IsCountryAdmin, IsContentPageManager +from utils.views import CreateDestroyGalleryViewMixin class NewsMixinView: @@ -17,11 +18,13 @@ class NewsMixinView: permission_classes = (permissions.AllowAny,) serializer_class = serializers.NewsBaseSerializer - def get_queryset(self, *args, **kwargs): + def get_queryset(self): """Override get_queryset method.""" qs = models.News.objects.published() \ .with_base_related() \ + .annotate_in_favorites(self.request.user) \ .order_by('-is_highlighted', '-created') + country_code = self.request.country_code if country_code: qs = qs.by_country_code(country_code) @@ -41,10 +44,10 @@ class NewsDetailView(NewsMixinView, generics.RetrieveAPIView): lookup_field = 'slug' serializer_class = serializers.NewsDetailWebSerializer - queryset = models.News.objects.all() - def get_queryset(self): - return self.queryset + """Override get_queryset method.""" + qs = models.News.objects.all().annotate_in_favorites(self.request.user) + return qs class NewsTypeListView(generics.ListAPIView): @@ -60,8 +63,13 @@ class NewsBackOfficeMixinView: """News back office mixin view.""" permission_classes = (permissions.IsAuthenticated,) - queryset = models.News.objects.with_base_related() \ - .order_by('-is_highlighted', '-created') + + def get_queryset(self): + """Override get_queryset method.""" + qs = models.News.objects.with_base_related() \ + .annotate_in_favorites(self.request.user) \ + .order_by('-is_highlighted', '-created') + return qs class NewsBackOfficeLCView(NewsBackOfficeMixinView, @@ -84,8 +92,7 @@ class NewsBackOfficeLCView(NewsBackOfficeMixinView, class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView, - generics.CreateAPIView, - generics.DestroyAPIView): + CreateDestroyGalleryViewMixin): """Resource for a create gallery for news for back-office users.""" serializer_class = serializers.NewsBackOfficeGallerySerializer @@ -103,24 +110,6 @@ class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView, return gallery - def create(self, request, *args, **kwargs): - """Overridden create method""" - super().create(request, *args, **kwargs) - return Response(status=status.HTTP_201_CREATED) - - def destroy(self, request, *args, **kwargs): - """Override destroy method.""" - gallery_obj = self.get_object() - if settings.USE_CELERY: - on_commit(lambda: delete_image.delay(image_id=gallery_obj.image.id, - completely=False)) - else: - on_commit(lambda: delete_image(image_id=gallery_obj.image.id, - completely=False)) - # Delete an instances of NewsGallery model - gallery_obj.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - class NewsBackOfficeGalleryListView(NewsBackOfficeMixinView, generics.ListAPIView): """Resource for returning gallery for news for back-office users.""" diff --git a/apps/notification/migrations/0002_subscriber_old_id.py b/apps/notification/migrations/0002_subscriber_old_id.py new file mode 100644 index 00000000..bf92831d --- /dev/null +++ b/apps/notification/migrations/0002_subscriber_old_id.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.7 on 2019-11-15 07:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('notification', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='subscriber', + name='old_id', + field=models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='old id'), + ), + ] diff --git a/apps/notification/models.py b/apps/notification/models.py index 85176d24..3e6f7f3a 100644 --- a/apps/notification/models.py +++ b/apps/notification/models.py @@ -74,21 +74,29 @@ class Subscriber(ProjectBaseMixin): (USABLE, _('Usable')), ) - user = models.OneToOneField(User, blank=True, null=True, default=None, - on_delete=models.SET_NULL, related_name='subscriber', - verbose_name=_('User')) - email = models.EmailField(blank=True, null=True, default=None, unique=True, - verbose_name=_('Email')) - ip_address = models.GenericIPAddressField(blank=True, null=True, default=None, - verbose_name=_('IP address')) - country_code = models.CharField(max_length=10, blank=True, null=True, default=None, - verbose_name=_('Country code')) - locale = models.CharField(blank=True, null=True, default=None, - max_length=10, verbose_name=_('Locale identifier')) - state = models.PositiveIntegerField(choices=STATE_CHOICES, default=USABLE, - verbose_name=_('State')) - update_code = models.CharField(max_length=254, blank=True, null=True, default=None, - db_index=True, verbose_name=_('Token')) + user = models.OneToOneField( + User, + blank=True, + null=True, + default=None, + on_delete=models.SET_NULL, + related_name='subscriber', + verbose_name=_('User'), + ) + email = models.EmailField(blank=True, null=True, default=None, unique=True, verbose_name=_('Email')) + ip_address = models.GenericIPAddressField(blank=True, null=True, default=None, verbose_name=_('IP address')) + country_code = models.CharField(max_length=10, blank=True, null=True, default=None, verbose_name=_('Country code')) + locale = models.CharField(blank=True, null=True, default=None, max_length=10, verbose_name=_('Locale identifier')) + state = models.PositiveIntegerField(choices=STATE_CHOICES, default=USABLE, verbose_name=_('State')) + update_code = models.CharField( + max_length=254, + blank=True, + null=True, + default=None, + db_index=True, + verbose_name=_('Token'), + ) + old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None) objects = SubscriberManager.from_queryset(SubscriberQuerySet)() diff --git a/apps/notification/transfer_data.py b/apps/notification/transfer_data.py index 3dd69f56..487501c3 100644 --- a/apps/notification/transfer_data.py +++ b/apps/notification/transfer_data.py @@ -1,21 +1,41 @@ -from transfer.serializers.notification import SubscriberSerializer -from notification.models import Subscriber -from transfer.models import EmailAddresses -from django.db.models import Value, IntegerField, F from pprint import pprint +from django.db.models import Count + +from transfer.models import EmailAddresses, NewsletterSubscriber +from transfer.serializers.notification import SubscriberSerializer, NewsletterSubscriberSerializer + def transfer_subscriber(): - queryset = EmailAddresses.objects.filter(state="usable") + queryset = EmailAddresses.objects.filter(state='usable') serialized_data = SubscriberSerializer(data=list(queryset.values()), many=True) if serialized_data.is_valid(): serialized_data.save() else: - pprint(f"News serializer errors: {serialized_data.errors}") + pprint(f'News serializer errors: {serialized_data.errors}') + + +def transfer_newsletter_subscriber(): + queryset = NewsletterSubscriber.objects.all().values( + 'id', + 'email_address__email', + 'email_address__account_id', + 'email_address__ip', + 'email_address__country_code', + 'email_address__locale', + 'created_at', + ) + + # serialized_data = NewsletterSubscriberSerializer(data=list(queryset.values()), many=True) + # if serialized_data.is_valid(): + # serialized_data.save() + # else: + # pprint(f'NewsletterSubscriber serializer errors: {serialized_data.errors}') data_types = { - "subscriber": [transfer_subscriber] + 'subscriber': [transfer_subscriber], + 'newsletter_subscriber': [transfer_newsletter_subscriber], } diff --git a/apps/product/admin.py b/apps/product/admin.py index 9620bb5f..dafbdbcb 100644 --- a/apps/product/admin.py +++ b/apps/product/admin.py @@ -11,7 +11,7 @@ class ProductAdmin(BaseModelAdminMixin, admin.ModelAdmin): list_filter = ('available', 'product_type') list_display = ('id', '__str__', 'get_category_display', 'product_type') raw_id_fields = ('subtypes', 'classifications', 'standards', - 'tags', 'gallery') + 'tags', 'gallery', 'establishment',) @admin.register(ProductGallery) diff --git a/apps/product/filters.py b/apps/product/filters.py index a30147eb..c7e87dda 100644 --- a/apps/product/filters.py +++ b/apps/product/filters.py @@ -9,10 +9,8 @@ class ProductFilterSet(filters.FilterSet): """Product filter set.""" establishment_id = filters.NumberFilter() - product_type = filters.ChoiceFilter(method='by_product_type', - choices=models.ProductType.INDEX_NAME_TYPES) - product_subtype = filters.ChoiceFilter(method='by_product_subtype', - choices=models.ProductSubType.INDEX_NAME_TYPES) + product_type = filters.CharFilter(method='by_product_type') + product_subtype = filters.CharFilter(method='by_product_subtype') class Meta: """Meta class.""" diff --git a/apps/product/management/commands/add_assemblage_tag.py b/apps/product/management/commands/add_assemblage_to_product.py similarity index 83% rename from apps/product/management/commands/add_assemblage_tag.py rename to apps/product/management/commands/add_assemblage_to_product.py index faf07c39..617d0f32 100644 --- a/apps/product/management/commands/add_assemblage_tag.py +++ b/apps/product/management/commands/add_assemblage_to_product.py @@ -1,7 +1,7 @@ from django.core.management.base import BaseCommand from transfer.models import Assemblages -from transfer.serializers.product import AssemblageTagSerializer +from transfer.serializers.product import AssemblageProductTagSerializer class Command(BaseCommand): @@ -10,7 +10,7 @@ class Command(BaseCommand): def handle(self, *args, **kwarg): errors = [] legacy_products = Assemblages.objects.filter(product_id__isnull=False) - serialized_data = AssemblageTagSerializer( + serialized_data = AssemblageProductTagSerializer( data=list(legacy_products.values()), many=True) if serialized_data.is_valid(): diff --git a/apps/product/management/commands/add_product_tag.py b/apps/product/management/commands/add_product_tag.py index 5633230c..ee4829f5 100644 --- a/apps/product/management/commands/add_product_tag.py +++ b/apps/product/management/commands/add_product_tag.py @@ -4,7 +4,6 @@ from establishment.management.commands.add_position import namedtuplefetchall from tag.models import Tag, TagCategory from product.models import Product from tqdm import tqdm -from django.db.models.functions import Lower class Command(BaseCommand): @@ -27,8 +26,7 @@ class Command(BaseCommand): def add_category_tag(self): objects = [] for c in tqdm(self.category_sql(), desc='Add category tags'): - categories = TagCategory.objects.filter(index_name=c.category, - value_type=c.value_type + categories = TagCategory.objects.filter(index_name=c.category ) if not categories.exists(): objects.append( @@ -44,7 +42,8 @@ class Command(BaseCommand): with connections['legacy'].cursor() as cursor: cursor.execute(''' select - DISTINCT + DISTINCT + m.id as old_id, trim(CONVERT(m.value USING utf8)) as tag_value, trim(CONVERT(v.key_name USING utf8)) as tag_category FROM product_metadata m @@ -57,16 +56,21 @@ class Command(BaseCommand): for t in tqdm(self.tag_sql(), desc='Add tags'): category = TagCategory.objects.get(index_name=t.tag_category) - tag = Tag.objects.filter( + tags = Tag.objects.filter( category=category, value=t.tag_value ) - if not tag.exists(): + if not tags.exists(): objects.append(Tag(label={"en-GB": t.tag_value}, category=category, - value=t.tag_value + value=t.tag_value, + old_id_meta_product=t.old_id )) + else: + qs = tags.filter(old_id_meta_product__isnull=True)\ + .update(old_id_meta_product=t.old_id) + Tag.objects.bulk_create(objects) self.stdout.write(self.style.WARNING(f'Add or get tag objects.')) @@ -75,6 +79,7 @@ class Command(BaseCommand): cursor.execute(''' select DISTINCT + m.id as old_id_tag, m.product_id, lower(trim(CONVERT(m.value USING utf8))) as tag_value, trim(CONVERT(v.key_name USING utf8)) as tag_category @@ -84,15 +89,12 @@ class Command(BaseCommand): return namedtuplefetchall(cursor) def add_product_tag(self): - objects = [] for t in tqdm(self.product_sql(), desc='Add product tag'): - category = TagCategory.objects.get(index_name=t.tag_category) - tag = Tag.objects.annotate(lower_value=Lower('value')) - tag.filter(lower_value=t.tag_value, category=category) - products = Product.objects.filter(old_id=t.product_id) - if products.exists(): - products.tags.add(tag) - products.save() + tags = Tag.objects.filter(old_id_meta_product=t.old_id_tag) + product = Product.objects.get(old_id=t.product_id) + for tag in tags: + if product not in tag.products.all(): + product.tags.add(tag) self.stdout.write(self.style.WARNING(f'Add or get tag objects.')) @@ -108,9 +110,8 @@ class Command(BaseCommand): tag.label = new_label tag.save() - def handle(self, *args, **kwargs): self.add_category_tag() self.add_tag() + self.check_tag() self.add_product_tag() - self.check_tag() \ No newline at end of file diff --git a/apps/product/migrations/0013_auto_20191113_1512.py b/apps/product/migrations/0013_auto_20191113_1512.py new file mode 100644 index 00000000..36c12fb0 --- /dev/null +++ b/apps/product/migrations/0013_auto_20191113_1512.py @@ -0,0 +1,29 @@ +# Generated by Django 2.2.7 on 2019-11-13 15:12 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0012_auto_20191112_1007'), + ] + + operations = [ + migrations.AlterField( + model_name='product', + name='wine_village', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='location.WineVillage', verbose_name='wine village'), + ), + migrations.AlterField( + model_name='productsubtype', + name='index_name', + field=models.CharField(db_index=True, max_length=50, unique=True, verbose_name='Index name'), + ), + migrations.AlterField( + model_name='producttype', + name='index_name', + field=models.CharField(db_index=True, max_length=50, unique=True, verbose_name='Index name'), + ), + ] diff --git a/apps/product/models.py b/apps/product/models.py index 2f964627..ba9ccd38 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -3,6 +3,7 @@ from django.contrib.contenttypes import fields as generic from django.contrib.gis.db import models as gis_models from django.core.exceptions import ValidationError from django.db import models +from django.db.models import Case, When from django.utils.translation import gettext_lazy as _ from django.core.validators import MaxValueValidator, MinValueValidator @@ -15,25 +16,16 @@ class ProductType(TranslatedFieldsMixin, ProjectBaseMixin): STR_FIELD_NAME = 'name' - # INDEX NAME CHOICES + # EXAMPLE OF INDEX NAME CHOICES FOOD = 'food' WINE = 'wine' LIQUOR = 'liquor' SOUVENIR = 'souvenir' BOOK = 'book' - INDEX_NAME_TYPES = ( - (FOOD, _('Food')), - (WINE, _('Wine')), - (LIQUOR, _('Liquor')), - (SOUVENIR, _('Souvenir')), - (BOOK, _('Book')), - ) - name = TJSONField(blank=True, null=True, default=None, verbose_name=_('Name'), help_text='{"en-GB":"some text"}') - index_name = models.CharField(max_length=50, choices=INDEX_NAME_TYPES, - unique=True, db_index=True, + index_name = models.CharField(max_length=50, unique=True, db_index=True, verbose_name=_('Index name')) use_subtypes = models.BooleanField(_('Use subtypes'), default=True) tag_categories = models.ManyToManyField('tag.TagCategory', @@ -52,24 +44,17 @@ class ProductSubType(TranslatedFieldsMixin, ProjectBaseMixin): STR_FIELD_NAME = 'name' - # INDEX NAME CHOICES + # EXAMPLE OF INDEX NAME CHOICES RUM = 'rum' PLATE = 'plate' OTHER = 'other' - INDEX_NAME_TYPES = ( - (RUM, _('Rum')), - (PLATE, _('Plate')), - (OTHER, _('Other')), - ) - product_type = models.ForeignKey(ProductType, on_delete=models.CASCADE, related_name='subtypes', verbose_name=_('Product type')) name = TJSONField(blank=True, null=True, default=None, verbose_name=_('Name'), help_text='{"en-GB":"some text"}') - index_name = models.CharField(max_length=50, choices=INDEX_NAME_TYPES, - unique=True, db_index=True, + index_name = models.CharField(max_length=50, unique=True, db_index=True, verbose_name=_('Index name')) class Meta: @@ -92,7 +77,14 @@ class ProductQuerySet(models.QuerySet): def with_base_related(self): return self.select_related('product_type', 'establishment') \ - .prefetch_related('product_type__subtypes') + .prefetch_related('product_type__subtypes') + + def with_extended_related(self): + """Returns qs with almost all related objects.""" + return self.with_base_related() \ + .prefetch_related('tags', 'standards', 'classifications', 'classifications__standard', + 'classifications__classification_type', 'classifications__tags') \ + .select_related('wine_region', 'wine_sub_region') def common(self): return self.filter(category=self.model.COMMON) @@ -101,15 +93,36 @@ class ProductQuerySet(models.QuerySet): return self.filter(category=self.model.ONLINE) def wines(self): - return self.filter(type__index_name=ProductType.WINE) + return self.filter(type__index_name__icontains=ProductType.WINE) def by_product_type(self, product_type: str): """Filter by type.""" - return self.filter(product_type__index_name=product_type) + return self.filter(product_type__index_name__icontains=product_type) def by_product_subtype(self, product_subtype: str): """Filter by subtype.""" - return self.filter(subtypes__index_name=product_subtype) + return self.filter(subtypes__index_name__icontains=product_subtype) + + def by_country_code(self, country_code): + """Filter by country of produce.""" + return self.filter(establishment__address__city__country__code=country_code) + + def published(self): + """Filter products by published state.""" + return self.filter(state=self.model.PUBLISHED) + + def annotate_in_favorites(self, user): + """Annotate flag in_favorites""" + favorite_product_ids = [] + if user.is_authenticated: + favorite_product_ids = user.favorite_product_ids + return self.annotate( + in_favorites=Case( + When(id__in=favorite_product_ids, then=True), + default=False, + output_field=models.BooleanField(default=False) + ) + ) class Product(TranslatedFieldsMixin, BaseAttributes): @@ -155,7 +168,7 @@ class Product(TranslatedFieldsMixin, BaseAttributes): related_name='products', verbose_name=_('establishment')) public_mark = models.PositiveIntegerField(blank=True, null=True, default=None, - verbose_name=_('public mark'),) + verbose_name=_('public mark'), ) wine_region = models.ForeignKey('location.WineRegion', on_delete=models.PROTECT, related_name='wines', blank=True, null=True, default=None, @@ -173,7 +186,7 @@ class Product(TranslatedFieldsMixin, BaseAttributes): help_text=_('attribute from legacy db')) wine_village = models.ForeignKey('location.WineVillage', on_delete=models.PROTECT, blank=True, null=True, - verbose_name=_('wine appellation')) + verbose_name=_('wine village')) slug = models.SlugField(unique=True, max_length=255, null=True, verbose_name=_('Slug')) favorites = generic.GenericRelation(to='favorites.Favorites') @@ -258,7 +271,16 @@ class Product(TranslatedFieldsMixin, BaseAttributes): @property def related_tags(self): return self.tags.exclude( - category__index_name__in=['sugar-content', 'wine-color', 'bottles-produced']) + category__index_name__in=['sugar-content', 'wine-color', 'bottles-produced', + 'serial-number', 'grape-variety']) + + @property + def display_name(self): + name = f'{self.name} ' \ + f'({self.vintage if self.vintage else "BSA"})' + if self.establishment.name: + name = f'{self.establishment.name} - ' + name + return name class OnlineProductManager(ProductManager): diff --git a/apps/product/serializers/back.py b/apps/product/serializers/back.py index b217db47..630a815a 100644 --- a/apps/product/serializers/back.py +++ b/apps/product/serializers/back.py @@ -2,9 +2,11 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers -from product import models -from product.serializers import ProductDetailSerializer from gallery.models import Image +from product import models +from product.serializers import ProductDetailSerializer, ProductTypeBaseSerializer, \ + ProductSubTypeBaseSerializer +from tag.models import TagCategory class ProductBackOfficeGallerySerializer(serializers.ModelSerializer): @@ -33,12 +35,16 @@ class ProductBackOfficeGallerySerializer(serializers.ModelSerializer): if not product_qs.exists(): raise serializers.ValidationError({'detail': _('Product not found')}) + if not image_qs.exists(): raise serializers.ValidationError({'detail': _('Image not found')}) product = product_qs.first() image = image_qs.first() + if image in product.gallery.all(): + raise serializers.ValidationError({'detail': _('Image is already added.')}) + attrs['product'] = product attrs['image'] = image @@ -46,8 +52,10 @@ class ProductBackOfficeGallerySerializer(serializers.ModelSerializer): class ProductBackOfficeDetailSerializer(ProductDetailSerializer): + """Product back-office detail serializer.""" class Meta(ProductDetailSerializer.Meta): + """Meta class.""" fields = ProductDetailSerializer.Meta.fields + [ 'description', 'available', @@ -58,13 +66,64 @@ class ProductBackOfficeDetailSerializer(ProductDetailSerializer): 'wine_village', 'state', ] - extra_kwargs = { - 'description': {'write_only': True}, - 'available': {'write_only': True}, - 'product_type': {'write_only': True}, - 'establishment': {'write_only': True}, - 'wine_region': {'write_only': True}, - 'wine_sub_region': {'write_only': True}, - 'wine_village': {'write_only': True}, - 'state': {'write_only': True}, - } + + +class ProductTypeBackOfficeDetailSerializer(ProductTypeBaseSerializer): + """Product type back-office detail serializer.""" + + class Meta(ProductTypeBaseSerializer.Meta): + """Meta class.""" + fields = ProductTypeBaseSerializer.Meta.fields + [ + 'name', + 'use_subtypes', + ] + + +class ProductTypeTagCategorySerializer(serializers.ModelSerializer): + """Serializer for attaching tag category to product type.""" + product_type_id = serializers.PrimaryKeyRelatedField( + queryset=models.ProductType.objects.all(), + write_only=True) + tag_category_id = serializers.PrimaryKeyRelatedField( + queryset=TagCategory.objects.all(), + write_only=True) + + class Meta(ProductTypeBaseSerializer.Meta): + """Meta class.""" + fields = [ + 'product_type_id', + 'tag_category_id', + ] + + def validate(self, attrs): + """Validation method.""" + product_type = attrs.pop('product_type_id') + tag_category = attrs.get('tag_category_id') + + if tag_category in product_type.tag_categories.all(): + raise serializers.ValidationError({ + 'detail': _('Tag category is already attached.')}) + + attrs['product_type'] = product_type + attrs['tag_category'] = tag_category + return attrs + + def create(self, validated_data): + """Overridden create method.""" + product_type = validated_data.get('product_type') + tag_category = validated_data.get('tag_category') + + product_type.tag_categories.add(tag_category) + return product_type + + +class ProductSubTypeBackOfficeDetailSerializer(ProductSubTypeBaseSerializer): + """Product sub type back-office detail serializer.""" + + class Meta(ProductSubTypeBaseSerializer.Meta): + """Meta class.""" + fields = ProductSubTypeBaseSerializer.Meta.fields + [ + 'product_type', + 'name', + 'index_name', + ] diff --git a/apps/product/serializers/common.py b/apps/product/serializers/common.py index af1203ff..da2a2344 100644 --- a/apps/product/serializers/common.py +++ b/apps/product/serializers/common.py @@ -12,13 +12,27 @@ from utils import exceptions as utils_exceptions from utils.serializers import TranslatedField, FavoritesCreateSerializer from main.serializers import AwardSerializer from location.serializers import WineRegionBaseSerializer, WineSubRegionBaseSerializer -from tag.serializers import TagBaseSerializer +from tag.serializers import TagBaseSerializer, TagCategoryShortSerializer + + +class ProductTagSerializer(TagBaseSerializer): + """Serializer for model Tag.""" + + category = TagCategoryShortSerializer(read_only=True) + + class Meta(TagBaseSerializer.Meta): + """Meta class.""" + + fields = TagBaseSerializer.Meta.fields + ( + 'category', + ) class ProductSubTypeBaseSerializer(serializers.ModelSerializer): """ProductSubType base serializer""" name_translated = TranslatedField() - index_name_display = serializers.CharField(source='get_index_name_display') + index_name_display = serializers.CharField(source='get_index_name_display', + read_only=True) class Meta: model = models.ProductSubType @@ -32,14 +46,13 @@ class ProductSubTypeBaseSerializer(serializers.ModelSerializer): class ProductTypeBaseSerializer(serializers.ModelSerializer): """ProductType base serializer""" name_translated = TranslatedField() - index_name_display = serializers.CharField(source='get_index_name_display') class Meta: model = models.ProductType fields = [ 'id', 'name_translated', - 'index_name_display', + 'index_name', ] @@ -72,13 +85,17 @@ class ProductStandardBaseSerializer(serializers.ModelSerializer): class ProductBaseSerializer(serializers.ModelSerializer): """Product base serializer.""" - product_type = serializers.CharField(source='product_type_translated_name', read_only=True) + name = serializers.CharField(source='display_name', read_only=True) + product_type = ProductTypeBaseSerializer(read_only=True) subtypes = ProductSubTypeBaseSerializer(many=True, read_only=True) establishment_detail = EstablishmentShortSerializer(source='establishment', read_only=True) - tags = TagBaseSerializer(source='related_tags', many=True, read_only=True) + tags = ProductTagSerializer(source='related_tags', many=True, read_only=True) + wine_region = WineRegionBaseSerializer(read_only=True) + wine_colors = TagBaseSerializer(many=True, read_only=True) preview_image_url = serializers.URLField(source='preview_main_image_url', allow_null=True, read_only=True) + in_favorites = serializers.BooleanField(allow_null=True) class Meta: """Meta class.""" @@ -94,6 +111,9 @@ class ProductBaseSerializer(serializers.ModelSerializer): 'vintage', 'tags', 'preview_image_url', + 'wine_region', + 'wine_colors', + 'in_favorites', ] @@ -104,9 +124,7 @@ class ProductDetailSerializer(ProductBaseSerializer): awards = AwardSerializer(many=True, read_only=True) classifications = ProductClassificationBaseSerializer(many=True, read_only=True) standards = ProductStandardBaseSerializer(many=True, read_only=True) - wine_region = WineRegionBaseSerializer(read_only=True) wine_sub_region = WineSubRegionBaseSerializer(read_only=True) - wine_colors = TagBaseSerializer(many=True, read_only=True) bottles_produced = TagBaseSerializer(many=True, read_only=True) sugar_contents = TagBaseSerializer(many=True, read_only=True) image_url = serializers.ImageField(source='main_image_url', @@ -120,9 +138,7 @@ class ProductDetailSerializer(ProductBaseSerializer): 'awards', 'classifications', 'standards', - 'wine_region', 'wine_sub_region', - 'wine_colors', 'bottles_produced', 'sugar_contents', 'image_url', diff --git a/apps/product/urls/back.py b/apps/product/urls/back.py index 5ddc932c..7d3b1611 100644 --- a/apps/product/urls/back.py +++ b/apps/product/urls/back.py @@ -1,14 +1,24 @@ """Product backoffice url patterns.""" from django.urls import path -from product.urls.common import urlpatterns as common_urlpatterns + from product import views urlpatterns = [ + path('', views.ProductListCreateBackOfficeView.as_view(), name='list-create'), path('/', views.ProductDetailBackOfficeView.as_view(), name='rud'), path('/gallery/', views.ProductBackOfficeGalleryListView.as_view(), name='gallery-list'), path('/gallery//', views.ProductBackOfficeGalleryCreateDestroyView.as_view(), name='gallery-create-destroy'), + # product types + path('types/', views.ProductTypeListCreateBackOfficeView.as_view(), name='type-list-create'), + path('types//', views.ProductTypeRUDBackOfficeView.as_view(), + name='type-retrieve-update-destroy'), + path('types/attach-tag-category/', views.ProductTypeTagCategoryCreateBackOfficeView.as_view(), + name='type-tag-category-create'), + # product sub types + path('subtypes/', views.ProductSubTypeListCreateBackOfficeView.as_view(), + name='subtype-list-create'), + path('subtypes//', views.ProductSubTypeRUDBackOfficeView.as_view(), + name='subtype-retrieve-update-destroy'), ] - -urlpatterns.extend(common_urlpatterns) diff --git a/apps/product/urls/common.py b/apps/product/urls/common.py index f4eaaaac..bd6c331d 100644 --- a/apps/product/urls/common.py +++ b/apps/product/urls/common.py @@ -16,5 +16,4 @@ urlpatterns = [ name='create-comment'), path('slug//comments//', views.ProductCommentRUDView.as_view(), name='rud-comment'), - ] diff --git a/apps/product/views/__init__.py b/apps/product/views/__init__.py index 6f4a8001..d1a35297 100644 --- a/apps/product/views/__init__.py +++ b/apps/product/views/__init__.py @@ -1,4 +1,4 @@ -from .back import * from .common import * +from .back import * from .mobile import * from .web import * diff --git a/apps/product/views/back.py b/apps/product/views/back.py index 0298ba18..70ac9c57 100644 --- a/apps/product/views/back.py +++ b/apps/product/views/back.py @@ -1,25 +1,52 @@ """Product app back-office views.""" -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, permissions +from rest_framework import generics, status, permissions, views from rest_framework.response import Response -from gallery.tasks import delete_image from product import serializers, models +from product.views import ProductBaseView +from utils.views import CreateDestroyGalleryViewMixin -class ProductBackOfficeMixinView: +class ProductBackOfficeMixinView(ProductBaseView): """Product back-office mixin view.""" permission_classes = (permissions.IsAuthenticated,) - queryset = models.Product.objects.with_base_related() \ - .order_by('-created', ) + + def get_queryset(self): + """Override get_queryset method.""" + qs = models.Product.objects.annotate_in_favorites(self.request.user) + return qs + + +class ProductTypeBackOfficeMixinView: + """Product type back-office mixin view.""" + + permission_classes = (permissions.IsAuthenticated,) + queryset = models.ProductType.objects.all() + + +class ProductSubTypeBackOfficeMixinView: + """Product sub type back-office mixin view.""" + + permission_classes = (permissions.IsAuthenticated,) + queryset = models.ProductSubType.objects.all() + + +class BackOfficeListCreateMixin(views.APIView): + """Back-office list-create mixin view.""" + + def check_permissions(self, request): + """ + Check if the request should be permitted. + Raises an appropriate exception if the request is not permitted. + """ + if self.request.method != 'GET': + super().check_permissions(request) class ProductBackOfficeGalleryCreateDestroyView(ProductBackOfficeMixinView, - generics.CreateAPIView, - generics.DestroyAPIView): + CreateDestroyGalleryViewMixin): """Resource for a create gallery for product for back-office users.""" serializer_class = serializers.ProductBackOfficeGallerySerializer @@ -37,24 +64,6 @@ class ProductBackOfficeGalleryCreateDestroyView(ProductBackOfficeMixinView, return gallery - def create(self, request, *args, **kwargs): - """Overridden create method""" - super().create(request, *args, **kwargs) - return Response(status=status.HTTP_201_CREATED) - - def destroy(self, request, *args, **kwargs): - """Override destroy method.""" - gallery_obj = self.get_object() - if settings.USE_CELERY: - on_commit(lambda: delete_image.delay(image_id=gallery_obj.image.id, - completely=False)) - else: - on_commit(lambda: delete_image(image_id=gallery_obj.image.id, - completely=False)) - # Delete an instances of ProductGallery model - gallery_obj.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - class ProductBackOfficeGalleryListView(ProductBackOfficeMixinView, generics.ListAPIView): """Resource for returning gallery for product for back-office users.""" @@ -78,3 +87,47 @@ class ProductBackOfficeGalleryListView(ProductBackOfficeMixinView, generics.List class ProductDetailBackOfficeView(ProductBackOfficeMixinView, generics.RetrieveUpdateDestroyAPIView): """Product back-office R/U/D view.""" serializer_class = serializers.ProductBackOfficeDetailSerializer + + +class ProductListCreateBackOfficeView(BackOfficeListCreateMixin, ProductBackOfficeMixinView, + generics.ListCreateAPIView): + """Product back-office list-create view.""" + serializer_class = serializers.ProductBackOfficeDetailSerializer + + +class ProductTypeListCreateBackOfficeView(BackOfficeListCreateMixin, + ProductTypeBackOfficeMixinView, + generics.ListCreateAPIView): + """Product type back-office list-create view.""" + serializer_class = serializers.ProductTypeBackOfficeDetailSerializer + + +class ProductTypeRUDBackOfficeView(BackOfficeListCreateMixin, + ProductTypeBackOfficeMixinView, + generics.RetrieveUpdateDestroyAPIView): + """Product type back-office retrieve-update-destroy view.""" + serializer_class = serializers.ProductTypeBackOfficeDetailSerializer + + +class ProductTypeTagCategoryCreateBackOfficeView(ProductTypeBackOfficeMixinView, + generics.CreateAPIView): + """View for attaching tag category to product type.""" + serializer_class = serializers.ProductTypeTagCategorySerializer + + def create(self, request, *args, **kwargs): + super().create(request, *args, **kwargs) + return Response(status=status.HTTP_201_CREATED) + + +class ProductSubTypeListCreateBackOfficeView(BackOfficeListCreateMixin, + ProductSubTypeBackOfficeMixinView, + generics.ListCreateAPIView): + """Product sub type back-office list-create view.""" + serializer_class = serializers.ProductSubTypeBackOfficeDetailSerializer + + +class ProductSubTypeRUDBackOfficeView(BackOfficeListCreateMixin, + ProductSubTypeBackOfficeMixinView, + generics.RetrieveUpdateDestroyAPIView): + """Product sub type back-office retrieve-update-destroy view.""" + serializer_class = serializers.ProductSubTypeBackOfficeDetailSerializer diff --git a/apps/product/views/common.py b/apps/product/views/common.py index 7a537f6e..daa46fd7 100644 --- a/apps/product/views/common.py +++ b/apps/product/views/common.py @@ -10,11 +10,15 @@ from comment.serializers import CommentRUDSerializer class ProductBaseView(generics.GenericAPIView): """Product base view""" - permission_classes = (permissions.AllowAny, ) + permission_classes = (permissions.AllowAny,) def get_queryset(self): """Override get_queryset method.""" - return Product.objects.with_base_related() + return Product.objects.published() \ + .with_base_related() \ + .annotate_in_favorites(self.request.user) \ + .by_country_code(self.request.country_code) \ + .order_by('-created') class ProductListView(ProductBaseView, generics.ListAPIView): @@ -64,9 +68,9 @@ class ProductCommentListView(generics.ListAPIView): """Override get_queryset method""" product = get_object_or_404(Product, slug=self.kwargs['slug']) return Comment.objects.by_content_type(app_label='product', - model='product')\ - .by_object_id(object_id=product.pk)\ - .order_by('-created') + model='product') \ + .by_object_id(object_id=product.pk) \ + .order_by('-created') class ProductCommentRUDView(generics.RetrieveUpdateDestroyAPIView): diff --git a/apps/rating/migrations/0003_viewcount.py b/apps/rating/migrations/0003_viewcount.py new file mode 100644 index 00000000..2a371371 --- /dev/null +++ b/apps/rating/migrations/0003_viewcount.py @@ -0,0 +1,27 @@ +# Generated by Django 2.2.4 on 2019-11-14 17:15 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('rating', '0002_auto_20191004_0928'), + ] + + operations = [ + migrations.CreateModel( + name='ViewCount', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('object_id', models.PositiveIntegerField()), + ('count', models.IntegerField()), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), + ], + options={ + 'unique_together': {('object_id', 'content_type')}, + }, + ), + ] diff --git a/apps/rating/migrations/0004_auto_20191114_2041.py b/apps/rating/migrations/0004_auto_20191114_2041.py new file mode 100644 index 00000000..c8af8c71 --- /dev/null +++ b/apps/rating/migrations/0004_auto_20191114_2041.py @@ -0,0 +1,25 @@ +# Generated by Django 2.2.4 on 2019-11-14 20:41 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('rating', '0003_viewcount'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='viewcount', + unique_together=set(), + ), + migrations.RemoveField( + model_name='viewcount', + name='content_type', + ), + migrations.RemoveField( + model_name='viewcount', + name='object_id', + ), + ] diff --git a/apps/rating/models.py b/apps/rating/models.py index e1dcec86..5db8332e 100644 --- a/apps/rating/models.py +++ b/apps/rating/models.py @@ -20,3 +20,7 @@ class Rating(models.Model): return self.content_object.name if hasattr(self.content_object, 'title'): return self.content_object.title_translated + + +class ViewCount(models.Model): + count = models.IntegerField() diff --git a/apps/rating/transfer_data.py b/apps/rating/transfer_data.py new file mode 100644 index 00000000..f7875bfe --- /dev/null +++ b/apps/rating/transfer_data.py @@ -0,0 +1,29 @@ +from transfer.models import PageTexts, PageCounters +from news.models import News +from rating.models import ViewCount + + +def transfer_news_view_count(): + news_list = News.objects.filter(old_id__isnull=False) + for news_object in news_list: + try: + mysql_page_text = PageTexts.objects.get(id=news_object.old_id) + except PageTexts.DoesNotExist: + continue + + try: + mysql_views_count = PageCounters.objects.get(page_id=mysql_page_text.page_id) + except PageCounters.DoesNotExist: + continue + + view_count = ViewCount.objects.create( + count=mysql_views_count.count + ) + + news_object.views_count = view_count + news_object.save() + + +data_types = { + "rating_count": [transfer_news_view_count] +} diff --git a/apps/review/admin.py b/apps/review/admin.py index 4d50ceda..b1ac0636 100644 --- a/apps/review/admin.py +++ b/apps/review/admin.py @@ -8,4 +8,4 @@ from utils.admin import BaseModelAdminMixin class ReviewAdminModel(BaseModelAdminMixin, admin.ModelAdmin): """Admin model for model Review.""" - raw_id_fields = ('reviewer', 'language', 'child', 'country') + raw_id_fields = ('reviewer', 'child', 'country') diff --git a/apps/review/migrations/0015_review_mark.py b/apps/review/migrations/0015_review_mark.py new file mode 100644 index 00000000..5dc161ef --- /dev/null +++ b/apps/review/migrations/0015_review_mark.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.7 on 2019-11-13 09:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('review', '0014_auto_20191112_0538'), + ] + + operations = [ + migrations.AddField( + model_name='review', + name='mark', + field=models.FloatField(blank=True, default=None, null=True, verbose_name='mark'), + ), + ] diff --git a/apps/review/migrations/0016_remove_review_language.py b/apps/review/migrations/0016_remove_review_language.py new file mode 100644 index 00000000..dda4ffd2 --- /dev/null +++ b/apps/review/migrations/0016_remove_review_language.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.7 on 2019-11-14 07:21 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('review', '0015_review_mark'), + ] + + operations = [ + migrations.RemoveField( + model_name='review', + name='language', + ), + ] diff --git a/apps/review/migrations/0017_auto_20191115_0737.py b/apps/review/migrations/0017_auto_20191115_0737.py new file mode 100644 index 00000000..1dcea1ee --- /dev/null +++ b/apps/review/migrations/0017_auto_20191115_0737.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.4 on 2019-11-15 07:37 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('review', '0016_remove_review_language'), + ] + + operations = [ + migrations.AlterField( + model_name='review', + name='reviewer', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='reviews', to=settings.AUTH_USER_MODEL, verbose_name='Reviewer'), + ), + ] diff --git a/apps/review/models.py b/apps/review/models.py index 8f9dc0db..1dadc104 100644 --- a/apps/review/models.py +++ b/apps/review/models.py @@ -39,36 +39,49 @@ class Review(BaseAttributes, TranslatedFieldsMixin): (READY, _('Ready')), ) - reviewer = models.ForeignKey('account.User', - related_name='reviews', - on_delete=models.CASCADE, - verbose_name=_('Reviewer')) + reviewer = models.ForeignKey( + 'account.User', + related_name='reviews', + on_delete=models.CASCADE, + verbose_name=_('Reviewer'), + null=True, default=None, blank=True + ) + country = models.ForeignKey( + 'location.Country', + on_delete=models.CASCADE, + related_name='country', + verbose_name=_('Country'), + null=True, + ) + child = models.ForeignKey( + 'self', + blank=True, + default=None, + null=True, + on_delete=models.CASCADE, + verbose_name=_('Child review'), + ) text = TJSONField( - _('text'), null=True, blank=True, - default=None, help_text='{"en-GB":"Text review"}') + _('text'), + null=True, + blank=True, + default=None, + help_text='{"en-GB":"Text review"}', + ) content_type = models.ForeignKey(generic.ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField() content_object = generic.GenericForeignKey('content_type', 'object_id') - language = models.ForeignKey('translation.Language', - on_delete=models.CASCADE, - related_name='reviews', - verbose_name=_('Review language')) + status = models.PositiveSmallIntegerField(choices=REVIEW_STATUSES, default=TO_INVESTIGATE) - child = models.ForeignKey('self', - blank=True, default=None, null=True, - on_delete=models.CASCADE, - verbose_name=_('Child review')) - published_at = models.DateTimeField(verbose_name=_('Publish datetime'), - blank=True, default=None, null=True, - help_text=_('Review published datetime')) - vintage = models.IntegerField(verbose_name=_('Year of review'), - validators=[MinValueValidator(1900), - MaxValueValidator(2100)]) - - country = models.ForeignKey('location.Country', on_delete=models.CASCADE, - related_name='country', verbose_name=_('Country'), - null=True) - + published_at = models.DateTimeField( + _('Publish datetime'), + blank=True, + default=None, + null=True, + help_text=_('Review published datetime'), + ) + vintage = models.IntegerField(_('Year of review'), validators=[MinValueValidator(1900), MaxValueValidator(2100)]) + mark = models.FloatField(verbose_name=_('mark'), blank=True, null=True, default=None) old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None) objects = ReviewQuerySet.as_manager() diff --git a/apps/review/serializers/common.py b/apps/review/serializers/common.py index f4acc4f4..2889db6e 100644 --- a/apps/review/serializers/common.py +++ b/apps/review/serializers/common.py @@ -1,5 +1,6 @@ from rest_framework import serializers -from review.models import Review + +from review.models import Review, Inquiries, GridItems class ReviewBaseSerializer(serializers.ModelSerializer): @@ -9,7 +10,6 @@ class ReviewBaseSerializer(serializers.ModelSerializer): 'id', 'reviewer', 'text', - 'language', 'status', 'child', 'published_at', @@ -27,3 +27,41 @@ class ReviewShortSerializer(ReviewBaseSerializer): fields = ( 'text_translated', ) + + +class InquiriesBaseSerializer(serializers.ModelSerializer): + """Serializer for model Inquiries.""" + class Meta: + model = Inquiries + fields = ( + 'id', + 'review', + 'comment', + 'final_comment', + 'mark', + 'attachment_file', + 'author', + 'bill_file', + 'price', + 'moment', + 'gallery', + 'decibels', + 'nomination', + 'nominee', + 'published', + ) + + +class GridItemsBaseSerializer(serializers.ModelSerializer): + """Serializer for model GridItems.""" + class Meta: + model = GridItems + fields = ( + 'id', + 'inquiry', + 'sub_name', + 'name', + 'value', + 'desc', + 'dish_title', + ) diff --git a/apps/review/tests.py b/apps/review/tests.py index 7ce503c2..e04f5281 100644 --- a/apps/review/tests.py +++ b/apps/review/tests.py @@ -1,3 +1,133 @@ -from django.test import TestCase +from http.cookies import SimpleCookie -# Create your tests here. +from rest_framework import status +from rest_framework.test import APITestCase + +from account.models import User +from location.models import Country +from review.models import Review, Inquiries, GridItems +from translation.models import Language + + +class BaseTestCase(APITestCase): + + def setUp(self): + self.username = 'test_user' + self.password = 'test_user_password' + self.email = 'test_user@mail.com' + self.user = User.objects.create_user( + username=self.username, + email=self.email, + password=self.password, + ) + + tokens = User.create_jwt_tokens(self.user) + self.client.cookies = SimpleCookie({ + 'access_token': tokens.get('access_token'), + 'refresh_token': tokens.get('refresh_token'), + }) + + self.lang = Language.objects.create( + title='Russia', + locale='ru-RU' + ) + + self.country_ru = Country.objects.create( + name={'en-GB': 'Russian'}, + code='RU', + ) + + self.test_review = Review.objects.create( + reviewer=self.user, + status=Review.READY, + vintage=2020, + country=self.country_ru, + text={'en-GB': 'Text review'}, + created_by=self.user, + modified_by=self.user, + object_id=1, + content_type_id=1, + ) + + self.test_inquiry = Inquiries.objects.create( + review=self.test_review, + author=self.user, + comment='Test comment', + ) + + self.test_grid = GridItems.objects.create( + inquiry=self.test_inquiry, + name='Test name', + ) + + +class InquiriesTestCase(BaseTestCase): + def setUp(self): + super().setUp() + + def test_inquiry_list(self): + response = self.client.get('/api/back/review/inquiries/') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_inquiry_list_by_review_id(self): + response = self.client.get(f'/api/back/review/{self.test_review.id}/inquiries/') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_inquiry_post(self): + test_inquiry = { + 'review': self.test_review.pk, + 'author': self.user.pk, + 'comment': 'New test comment', + } + response = self.client.post('/api/back/review/inquiries/', data=test_inquiry) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_inquiry_detail(self): + response = self.client.get(f'/api/back/review/inquiries/{self.test_inquiry.id}/') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_inquiry_detail_put(self): + data = { + 'id': self.test_inquiry.id, + 'review': self.test_review.pk, + 'author': self.user.pk, + 'comment': 'New test comment 2', + } + + response = self.client.put(f'/api/back/review/inquiries/{self.test_inquiry.id}/', data=data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + +class GridItemsTestCase(BaseTestCase): + def setUp(self): + super().setUp() + + def test_grid_list(self): + response = self.client.get('/api/back/review/inquiries/grid/') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_grid_list_by_inquiry_id(self): + response = self.client.get(f'/api/back/review/inquiries/{self.test_inquiry.id}/grid/') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_grid_post(self): + test_grid = { + 'inquiry': self.test_inquiry.pk, + 'name': 'New test name', + } + response = self.client.post('/api/back/review/inquiries/grid/', data=test_grid) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_grid_detail(self): + response = self.client.get(f'/api/back/review/inquiries/grid/{self.test_grid.id}/') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_grid_detail_put(self): + data = { + 'id': self.test_grid.id, + 'inquiry': self.test_inquiry.pk, + 'name': 'New test name 2', + } + + response = self.client.put(f'/api/back/review/inquiries/grid/{self.test_inquiry.id}/', data=data) + self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/apps/review/transfer_data.py b/apps/review/transfer_data.py index cde55efe..9ccb6c80 100644 --- a/apps/review/transfer_data.py +++ b/apps/review/transfer_data.py @@ -1,15 +1,20 @@ -import json from pprint import pprint from django.db.models import Q +from product.models import Product +from account.models import User from account.transfer_data import STOP_LIST +from establishment.models import Establishment from review.models import Inquiries as NewInquiries, Review from transfer.models import Reviews, ReviewTexts, Inquiries, GridItems, InquiryPhotos from transfer.serializers.grid import GridItemsSerializer from transfer.serializers.inquiries import InquiriesSerializer from transfer.serializers.inquiry_gallery import InquiryGallerySerializer -from transfer.serializers.reviews import LanguageSerializer, ReviewSerializer, Establishment +from transfer.serializers.reviews import ( + LanguageSerializer, ReviewSerializer, ReviewTextSerializer, ProductReviewSerializer + +) def transfer_languages(): @@ -28,77 +33,40 @@ def transfer_languages(): def transfer_reviews(): - # TODO: убрать LIKE UPPER("%%paris%%"), accounts.email IN - queryset = Reviews.objects.raw("""SELECT reviews.id, reviews.vintage, reviews.establishment_id, - reviews.reviewer_id, review_texts.text AS text, reviews.mark, reviews.published_at, - review_texts.created_at AS published, review_texts.locale AS locale, - reviews.aasm_state - FROM reviews - LEFT OUTER JOIN review_texts - ON (reviews.id = review_texts.review_id) - WHERE reviews.reviewer_id > 0 - AND reviews.reviewer_id IS NOT NULL - AND review_texts.text IS NOT NULL - AND review_texts.locale IS NOT NULL - AND reviews.mark IS NOT NULL - AND reviews.reviewer_id IN ( - SELECT accounts.id - FROM accounts - WHERE accounts.confirmed_at IS NOT NULL - AND NOT accounts.email IN ( - "cyril@tomatic.net", - "cyril2@tomatic.net", - "cyril2@tomatic.net", - "d.sadykova@id-east.ru", - "d.sadykova@octopod.ru", - "n.yurchenko@id-east.ru" - )) - AND reviews.establishment_id IN ( - SELECT establishments.id - FROM establishments - INNER JOIN locations - ON (establishments.location_id = locations.id) - INNER JOIN cities - ON (locations.city_id = cities.id) - WHERE establishments.type IS NOT NULL AND locations.timezone IS NOT NULL - AND NOT establishments.type = "Wineyard" - ) - ORDER BY review_texts.created_at DESC - """) - - queryset_result = [] - establishments_mark_list = {} - - for query in queryset: - query = vars(query) - if query['establishment_id'] not in establishments_mark_list.keys(): - if "aasm_state" in query and query['aasm_state'] is not None and query['aasm_state'] == "published": - establishments_mark_list[query['establishment_id']] = [ - int(query['mark']), - json.dumps({query['locale']: query['text']}) - ] - else: - establishments_mark_list[query['establishment_id']] = int(query['mark']) - del (query['mark']) - queryset_result.append(query) - - serialized_data = ReviewSerializer(data=queryset_result, many=True) + establishments = Establishment.objects.filter(old_id__isnull=False).values_list('old_id', flat=True) + queryset = Reviews.objects.filter( + establishment_id__in=list(establishments), + ).values('id', 'reviewer_id', 'aasm_state', 'created_at', 'establishment_id', 'mark', 'vintage') + serialized_data = ReviewSerializer(data=list(queryset.values()), many=True) if serialized_data.is_valid(): serialized_data.save() - - for establishment_id, mark in establishments_mark_list.items(): - try: - establishment = Establishment.objects.get(old_id=establishment_id) - except Establishment.DoesNotExist: - continue - if isinstance(mark, list): - mark, review_text = mark - - establishment.public_mark = mark - establishment.save() else: - pprint(serialized_data.errors) + pprint(f"ReviewSerializer serializer errors: {serialized_data.errors}") + + +def transfer_text_review(): + reviews = Review.objects.filter(old_id__isnull=False).values_list('old_id', flat=True) + queryset = ReviewTexts.objects.filter( + review_id__in=list(reviews), + ).exclude( + Q(text__isnull=True) | Q(text='') + ).values('review_id', 'locale', 'text') + + serialized_data = ReviewTextSerializer(data=list(queryset.values()), many=True) + if serialized_data.is_valid(): + serialized_data.save() + else: + pprint(f"ReviewTextSerializer serializer errors: {serialized_data.errors}") + + for review in Review.objects.filter(old_id__isnull=False): + text = review.text + if text and 'en-GB' not in text: + text.update({ + 'en-GB': next(iter(text.values())) + }) + review.text = text + review.save() def transfer_inquiries(): @@ -137,14 +105,38 @@ def transfer_inquiry_photos(): pprint(f"InquiryGallery serializer errors: {serialized_data.errors}") +def transfer_product_reviews(): + + products = Product.objects.filter( + old_id__isnull=False).values_list('old_id', flat=True) + + users = User.objects.filter( + old_id__isnull=False).values_list('old_id', flat=True) + + queryset = Reviews.objects.filter( + product_id__in=list(products), + reviewer_id__in=list(users), + ).values('id', 'reviewer_id', 'aasm_state', 'created_at', 'product_id', 'mark', 'vintage') + + serialized_data = ProductReviewSerializer(data=list(queryset.values()), many=True) + if serialized_data.is_valid(): + serialized_data.save() + else: + pprint(f"ProductReviewSerializer serializer errors: {serialized_data.errors}") + + data_types = { "overlook": [ - transfer_languages, - transfer_reviews + # transfer_languages, + transfer_reviews, + transfer_text_review, ], 'inquiries': [ transfer_inquiries, transfer_grid, transfer_inquiry_photos, + ], + "product_review": [ + transfer_product_reviews, ] } diff --git a/apps/review/urls/back.py b/apps/review/urls/back.py index 84ca49f3..a59aafa8 100644 --- a/apps/review/urls/back.py +++ b/apps/review/urls/back.py @@ -8,4 +8,10 @@ app_name = 'review' urlpatterns = [ path('', views.ReviewLstView.as_view(), name='review-list-create'), path('/', views.ReviewRUDView.as_view(), name='review-crud'), + path('/inquiries/', views.InquiriesLstView.as_view(), name='inquiries-list'), + path('inquiries/', views.InquiriesLstView.as_view(), name='inquiries-list-create'), + path('inquiries//', views.InquiriesRUDView.as_view(), name='inquiries-crud'), + path('inquiries//grid/', views.GridItemsLstView.as_view(), name='grid-list-create'), + path('inquiries/grid/', views.GridItemsLstView.as_view(), name='grid-list-create'), + path('inquiries/grid//', views.GridItemsRUDView.as_view(), name='grid-crud'), ] diff --git a/apps/review/views/back.py b/apps/review/views/back.py index 31e8725a..c6ec6b67 100644 --- a/apps/review/views/back.py +++ b/apps/review/views/back.py @@ -1,6 +1,7 @@ from rest_framework import generics, permissions -from review import serializers + from review import models +from review import serializers from utils.permissions import IsReviewerManager, IsRestaurantReviewer @@ -8,12 +9,55 @@ class ReviewLstView(generics.ListCreateAPIView): """Comment list create view.""" serializer_class = serializers.ReviewBaseSerializer queryset = models.Review.objects.all() - permission_classes = [permissions.IsAuthenticatedOrReadOnly,] + permission_classes = [permissions.IsAuthenticatedOrReadOnly, ] class ReviewRUDView(generics.RetrieveUpdateDestroyAPIView): """Comment RUD view.""" serializer_class = serializers.ReviewBaseSerializer queryset = models.Review.objects.all() - permission_classes = [IsReviewerManager|IsRestaurantReviewer] + permission_classes = [IsReviewerManager | IsRestaurantReviewer] + lookup_field = 'id' + + +class InquiriesLstView(generics.ListCreateAPIView): + """Inquiries list create view.""" + + serializer_class = serializers.InquiriesBaseSerializer + queryset = models.Inquiries.objects.all() + permission_classes = (permissions.IsAuthenticatedOrReadOnly,) + + def get_queryset(self): + review_id = self.kwargs.get('review_id') + if review_id: + return super().get_queryset().filter(review_id=review_id) + return super().get_queryset() + + +class InquiriesRUDView(generics.RetrieveUpdateDestroyAPIView): + """Inquiries RUD view.""" + serializer_class = serializers.InquiriesBaseSerializer + queryset = models.Inquiries.objects.all() + permission_classes = (permissions.IsAuthenticatedOrReadOnly,) + lookup_field = 'id' + + +class GridItemsLstView(generics.ListCreateAPIView): + """GridItems list create view.""" + serializer_class = serializers.GridItemsBaseSerializer + queryset = models.GridItems.objects.all() + permission_classes = (permissions.IsAuthenticatedOrReadOnly,) + + def get_queryset(self): + inquiry_id = self.kwargs.get('inquiry_id') + if inquiry_id: + return super().get_queryset().filter(inquiry_id=inquiry_id) + return super().get_queryset() + + +class GridItemsRUDView(generics.RetrieveUpdateDestroyAPIView): + """GridItems RUD view.""" + serializer_class = serializers.GridItemsBaseSerializer + queryset = models.GridItems.objects.all() + permission_classes = (permissions.IsAuthenticatedOrReadOnly,) lookup_field = 'id' diff --git a/apps/search_indexes/documents/__init__.py b/apps/search_indexes/documents/__init__.py index 796bf289..70d17330 100644 --- a/apps/search_indexes/documents/__init__.py +++ b/apps/search_indexes/documents/__init__.py @@ -1,9 +1,11 @@ from search_indexes.documents.establishment import EstablishmentDocument from search_indexes.documents.news import NewsDocument +from search_indexes.documents.product import ProductDocument # todo: make signal to update documents on related fields __all__ = [ 'EstablishmentDocument', 'NewsDocument', + 'ProductDocument', ] \ No newline at end of file diff --git a/apps/search_indexes/documents/news.py b/apps/search_indexes/documents/news.py index 86b117e5..ff659416 100644 --- a/apps/search_indexes/documents/news.py +++ b/apps/search_indexes/documents/news.py @@ -31,6 +31,7 @@ class NewsDocument(Document): 'id': fields.IntegerField(attr='id'), 'label': fields.ObjectField(attr='label_indexing', properties=OBJECT_FIELD_PROPERTIES), + 'value': fields.KeywordField() }, multi=True) @@ -49,7 +50,7 @@ class NewsDocument(Document): related_models = [models.NewsType] def get_queryset(self): - return super().get_queryset().published().with_base_related() + return super().get_queryset().published().with_base_related().sort_by_start() def get_instances_from_related(self, related_instance): """If related_models is set, define how to retrieve the Car instance(s) from the related model. diff --git a/apps/search_indexes/documents/product.py b/apps/search_indexes/documents/product.py new file mode 100644 index 00000000..b00f3f50 --- /dev/null +++ b/apps/search_indexes/documents/product.py @@ -0,0 +1,116 @@ +"""Product app documents.""" +from django.conf import settings +from django_elasticsearch_dsl import Document, Index, fields +from search_indexes.utils import OBJECT_FIELD_PROPERTIES +from product import models + +ProductIndex = Index(settings.ELASTICSEARCH_INDEX_NAMES.get(__name__, 'product')) +ProductIndex.settings(number_of_shards=5, number_of_replicas=2, mapping={'total_fields':{'limit': 3000}}) + + +@ProductIndex.doc_type +class ProductDocument(Document): + """Product document.""" + + description = fields.ObjectField(attr='description_indexing', + properties=OBJECT_FIELD_PROPERTIES) + product_type = fields.ObjectField(properties={ + 'id': fields.IntegerField(), + 'name': fields.ObjectField(attr='name_indexing', properties=OBJECT_FIELD_PROPERTIES), + 'index_name': fields.KeywordField(), + 'use_subtypes': fields.BooleanField(), + }) + subtypes = fields.ObjectField( + properties={ + 'id': fields.IntegerField(), + 'name': fields.ObjectField(attr='name_indexing', properties=OBJECT_FIELD_PROPERTIES), + 'index_name': fields.KeywordField(), + }, + multi=True + ) + establishment = fields.ObjectField( + properties={ + 'id': fields.IntegerField(), + 'name': fields.KeywordField(), + 'slug': fields.KeywordField(), + # 'city' TODO: city indexing + } + ) + wine_colors = fields.ObjectField( + properties={ + 'id': fields.IntegerField(), + 'label': fields.ObjectField(attr='label_indexing', properties=OBJECT_FIELD_PROPERTIES), + 'value': fields.KeywordField(), + }, + multi=True, + ) + wine_region = fields.ObjectField(properties={ + 'id': fields.IntegerField(), + 'name': fields.KeywordField(), + 'country': fields.ObjectField(properties={ + 'id': fields.IntegerField(), + 'name': fields.ObjectField(attr='name_indexing', + properties=OBJECT_FIELD_PROPERTIES), + 'code': fields.KeywordField(), + }), + # 'coordinates': fields.GeoPointField(), + 'description': fields.ObjectField(attr='description_indexing', properties=OBJECT_FIELD_PROPERTIES), + }) + wine_sub_region = fields.ObjectField(properties={'name': fields.KeywordField()}) + classifications = fields.ObjectField( # TODO + properties={ + 'classification_type': fields.ObjectField(properties={}), + 'standard': fields.ObjectField(properties={ + 'name': fields.KeywordField(), + 'standard_type': fields.IntegerField(), + # 'coordinates': fields.GeoPointField(), + }), + 'tags': fields.ObjectField( + properties={ + 'label': fields.ObjectField(attr='label_indexing', properties=OBJECT_FIELD_PROPERTIES), + 'value': fields.KeywordField(), + }, + multi=True + ), + }, + multi=True + ) + standards = fields.ObjectField( + properties={ + 'id': fields.IntegerField(), + 'name': fields.KeywordField(), + 'standard_type': fields.IntegerField(), + # 'coordinates': fields.GeoPointField(), + }, + multi=True + ) + wine_village = fields.ObjectField(properties={ + 'name': fields.KeywordField(), + }) + tags = fields.ObjectField( + properties={ + 'id': fields.IntegerField(), + 'label': fields.ObjectField(attr='label_indexing', properties=OBJECT_FIELD_PROPERTIES), + 'value': fields.KeywordField(), + }, + multi=True + ) + + class Django: + model = models.Product + fields = ( + 'id', + 'category', + 'name', + 'available', + 'public_mark', + 'slug', + 'old_id', + 'state', + 'old_unique_key', + 'vintage', + ) + related_models = [models.ProductType] + + def get_queryset(self): + return super().get_queryset().published().with_base_related() diff --git a/apps/search_indexes/serializers.py b/apps/search_indexes/serializers.py index fb1b769a..dffd6e77 100644 --- a/apps/search_indexes/serializers.py +++ b/apps/search_indexes/serializers.py @@ -4,6 +4,7 @@ from elasticsearch_dsl import AttrDict from django_elasticsearch_dsl_drf.serializers import DocumentSerializer from news.serializers import NewsTypeSerializer from search_indexes.documents import EstablishmentDocument, NewsDocument +from search_indexes.documents.product import ProductDocument from search_indexes.utils import get_translated_value @@ -19,6 +20,73 @@ class TagsDocumentSerializer(serializers.Serializer): return get_translated_value(obj.label) +class EstablishmentTypeSerializer(serializers.Serializer): + """Establishment type serializer for ES Document""" + + id = serializers.IntegerField() + name_translated = serializers.SerializerMethodField() + index_name = serializers.CharField() + + def get_name_translated(self, obj): + if isinstance(obj, dict): + return get_translated_value(obj.get('name')) + return get_translated_value(obj.name) + + +class ProductSubtypeDocumentSerializer(serializers.Serializer): + """Product subtype serializer for ES Document.""" + + id = serializers.IntegerField() + name_translated = serializers.SerializerMethodField() + + get_name_translated = lambda obj: get_translated_value(obj.name) + + +class WineRegionCountryDocumentSerialzer(serializers.Serializer): + """Wine region country ES document serializer.""" + + id = serializers.IntegerField() + code = serializers.CharField() + name_translated = serializers.SerializerMethodField() + + @staticmethod + def get_name_translated(obj): + return get_translated_value(obj.name) + + def get_attribute(self, instance): + return instance.country if instance and instance.country else None + + +class WineRegionDocumentSerializer(serializers.Serializer): + """Wine region ES document serializer.""" + + id = serializers.IntegerField() + name = serializers.CharField() + country = WineRegionCountryDocumentSerialzer(allow_null=True) + + +class WineColorDocumentSerializer(serializers.Serializer): + """Wine color ES document serializer,""" + + id = serializers.IntegerField() + label_translated = serializers.SerializerMethodField() + index_name = serializers.CharField(source='value') + + @staticmethod + def get_label_translated(obj): + if isinstance(obj, dict): + return get_translated_value(obj.get('label')) + return get_translated_value(obj.label) + + +class ProductEstablishmentDocumentSerializer(serializers.Serializer): + """Related to Product Establishment ES document serializer.""" + + id = serializers.IntegerField() + name = serializers.CharField() + slug = serializers.CharField() + + class CityDocumentShortSerializer(serializers.Serializer): """City serializer for ES Document,""" @@ -94,6 +162,8 @@ class NewsDocumentSerializer(DocumentSerializer): class EstablishmentDocumentSerializer(DocumentSerializer): """Establishment document serializer.""" + establishment_type = EstablishmentTypeSerializer() + establishment_subtypes = EstablishmentTypeSerializer(many=True) address = AddressDocumentSerializer(allow_null=True) tags = TagsDocumentSerializer(many=True) schedule = ScheduleDocumentSerializer(many=True, allow_null=True) @@ -123,3 +193,41 @@ class EstablishmentDocumentSerializer(DocumentSerializer): # 'establishment_type', # 'establishment_subtypes', ) + + +class ProductDocumentSerializer(DocumentSerializer): + """Product document serializer""" + + tags = TagsDocumentSerializer(many=True) + subtypes = ProductSubtypeDocumentSerializer(many=True) + wine_region = WineRegionDocumentSerializer(allow_null=True) + wine_colors = WineColorDocumentSerializer(many=True) + product_type = serializers.SerializerMethodField() + establishment_detail = ProductEstablishmentDocumentSerializer(source='establishment', allow_null=True) + + @staticmethod + def get_product_type(obj): + return get_translated_value(obj.product_type.name if obj.product_type else {}) + + class Meta: + """Meta class.""" + + document = ProductDocument + fields = ( + 'id', + 'category', + 'name', + 'available', + 'public_mark', + 'slug', + 'old_id', + 'state', + 'old_unique_key', + 'vintage', + 'tags', + 'product_type', + 'subtypes', + 'wine_region', + 'wine_colors', + 'establishment_detail', + ) diff --git a/apps/search_indexes/signals.py b/apps/search_indexes/signals.py index 9284b20a..dfd795dd 100644 --- a/apps/search_indexes/signals.py +++ b/apps/search_indexes/signals.py @@ -13,44 +13,20 @@ def update_document(sender, **kwargs): model_name = sender._meta.model_name instance = kwargs['instance'] - if app_label == 'location': - if model_name == 'country': - establishments = Establishment.objects.filter( - address__city__country=instance) - for establishment in establishments: - registry.update(establishment) - if model_name == 'city': - establishments = Establishment.objects.filter( - address__city=instance) - for establishment in establishments: - registry.update(establishment) - if model_name == 'address': - establishments = Establishment.objects.filter( - address=instance) - for establishment in establishments: - registry.update(establishment) - - if app_label == 'establishment': + app_label_model_name_to_filter = { + ('location','country'): 'address__city__country', + ('location','city'): 'address__city', + ('location', 'address'): 'address', # todo: remove after migration - from establishment import models as establishment_models - if model_name == 'establishmenttype': - if isinstance(instance, establishment_models.EstablishmentType): - establishments = Establishment.objects.filter( - establishment_type=instance) - for establishment in establishments: - registry.update(establishment) - if model_name == 'establishmentsubtype': - if isinstance(instance, establishment_models.EstablishmentSubType): - establishments = Establishment.objects.filter( - establishment_subtypes=instance) - for establishment in establishments: - registry.update(establishment) - - if app_label == 'tag': - if model_name == 'tag': - establishments = Establishment.objects.filter(tags=instance) - for establishment in establishments: - registry.update(establishment) + ('establishment', 'establishmenttype'): 'establishment_type', + ('establishment', 'establishmentsubtype'): 'establishment_subtypes', + ('tag', 'tag'): 'tags', + } + filter_name = app_label_model_name_to_filter.get((app_label, model_name)) + if filter_name: + qs = Establishment.objects.filter(**{filter_name: instance}) + for product in qs: + registry.update(product) @receiver(post_save) @@ -59,21 +35,35 @@ def update_news(sender, **kwargs): app_label = sender._meta.app_label model_name = sender._meta.model_name instance = kwargs['instance'] + app_label_model_name_to_filter = { + ('location','country'): 'country', + ('news','newstype'): 'news_type', + ('tag', 'tag'): 'tags', + } + filter_name = app_label_model_name_to_filter.get((app_label, model_name)) + if filter_name: + qs = News.objects.filter(**{filter_name: instance}) + for product in qs: + registry.update(product) - if app_label == 'location': - if model_name == 'country': - qs = News.objects.filter(country=instance) - for news in qs: - registry.update(news) - if app_label == 'news': - if model_name == 'newstype': - qs = News.objects.filter(news_type=instance) - for news in qs: - registry.update(news) - - if app_label == 'tag': - if model_name == 'tag': - qs = News.objects.filter(tags=instance) - for news in qs: - registry.update(news) +@receiver(post_save) +def update_product(sender, **kwargs): + from product.models import Product + app_label = sender._meta.app_label + model_name = sender._meta.model_name + instance = kwargs['instance'] + app_label_model_name_to_filter = { + ('product','productstandard'): 'standards', + ('product', 'producttype'): 'product_type', + ('tag','tag'): 'tags', + ('location', 'wineregion'): 'wine_region', + ('location', 'winesubregion'): 'wine_sub_region', + ('location', 'winevillage'): 'wine_village', + ('establishment', 'establishment'): 'establishment', + } + filter_name = app_label_model_name_to_filter.get((app_label, model_name)) + if filter_name: + qs = Product.objects.filter(**{filter_name: instance}) + for product in qs: + registry.update(product) diff --git a/apps/search_indexes/urls.py b/apps/search_indexes/urls.py index 549e569d..70e21369 100644 --- a/apps/search_indexes/urls.py +++ b/apps/search_indexes/urls.py @@ -8,6 +8,7 @@ router = routers.SimpleRouter() router.register(r'establishments', views.EstablishmentDocumentViewSet, basename='establishment') router.register(r'mobile/establishments', views.EstablishmentDocumentViewSet, basename='establishment-mobile') router.register(r'news', views.NewsDocumentViewSet, basename='news') +router.register(r'products', views.ProductDocumentViewSet, basename='product') urlpatterns = router.urls diff --git a/apps/search_indexes/utils.py b/apps/search_indexes/utils.py index b88a1da3..d30e7de3 100644 --- a/apps/search_indexes/utils.py +++ b/apps/search_indexes/utils.py @@ -3,224 +3,35 @@ from django_elasticsearch_dsl import fields from utils.models import get_current_locale, get_default_locale ALL_LOCALES_LIST = [ - 'af-ZA', - 'am-ET', - 'ar-AE', - 'ar-BH', - 'ar-DZ', - 'ar-EG', - 'ar-IQ', - 'ar-JO', - 'ar-KW', - 'ar-LB', - 'ar-LY', - 'ar-MA', - 'arn-CL', - 'ar-OM', - 'ar-QA', - 'ar-SA', - 'ar-SY', - 'ar-TN', - 'ar-YE', - 'as-IN', - 'az-Cyrl-AZ', - 'az-Latn-AZ', - 'ba-RU', - 'be-BY', - 'bg-BG', - 'bn-BD', - 'bn-IN', - 'bo-CN', - 'br-FR', - 'bs-Cyrl-BA', - 'bs-Latn-BA', - 'ca-ES', - 'co-FR', - 'cs-CZ', - 'cy-GB', - 'da-DK', - 'de-AT', - 'de-CH', - 'de-DE', - 'de-LI', - 'de-LU', - 'dsb-DE', - 'dv-MV', - 'el-GR', - 'en-029', - 'en-AU', - 'en-BZ', - 'en-CA', - 'en-GB', - 'en-IE', - 'en-IN', - 'en-JM', - 'en-MY', - 'en-NZ', - 'en-PH', - 'en-SG', - 'en-TT', - 'en-US', - 'en-ZA', - 'en-ZW', - 'es-AR', - 'es-BO', - 'es-CL', - 'es-CO', - 'es-CR', - 'es-DO', - 'es-EC', - 'es-ES', - 'es-GT', - 'es-HN', - 'es-MX', - 'es-NI', - 'es-PA', - 'es-PE', - 'es-PR', - 'es-PY', - 'es-SV', - 'es-US', - 'es-UY', - 'es-VE', - 'et-EE', - 'eu-ES', - 'fa-IR', - 'fi-FI', - 'fil-PH', - 'fo-FO', - 'fr-BE', - 'fr-CA', - 'fr-CH', - 'fr-FR', - 'fr-LU', - 'fr-MC', - 'fy-NL', - 'ga-IE', - 'gd-GB', - 'gl-ES', - 'gsw-FR', - 'gu-IN', - 'ha-Latn-NG', - 'he-IL', - 'hi-IN', - 'hr-BA', 'hr-HR', - 'hsb-DE', - 'hu-HU', - 'hy-AM', - 'id-ID', - 'ig-NG', - 'ii-CN', - 'is-IS', - 'it-CH', - 'it-IT', - 'iu-Cans-CA', - 'iu-Latn-CA', - 'ja-JP', - 'ka-GE', - 'kk-KZ', - 'kl-GL', - 'km-KH', - 'kn-IN', - 'kok-IN', - 'ko-KR', - 'ky-KG', - 'lb-LU', - 'lo-LA', - 'lt-LT', - 'lv-LV', - 'mi-NZ', - 'mk-MK', - 'ml-IN', - 'mn-MN', - 'mn-Mong-CN', - 'moh-CA', - 'mr-IN', - 'ms-BN', - 'ms-MY', - 'mt-MT', - 'nb-NO', - 'ne-NP', - 'nl-BE', - 'nl-NL', - 'nn-NO', - 'nso-ZA', - 'oc-FR', - 'or-IN', - 'pa-IN', - 'pl-PL', - 'prs-AF', - 'ps-AF', - 'pt-BR', - 'pt-PT', - 'qut-GT', - 'quz-BO', - 'quz-EC', - 'quz-PE', - 'rm-CH', 'ro-RO', - 'ru-RU', - 'rw-RW', - 'sah-RU', - 'sa-IN', - 'se-FI', - 'se-NO', - 'se-SE', - 'si-LK', - 'sk-SK', 'sl-SI', - 'sma-NO', - 'sma-SE', - 'smj-NO', - 'smj-SE', - 'smn-FI', - 'sms-FI', - 'sq-AL', - 'sr-Cyrl-BA', - 'sr-Cyrl-CS', - 'sr-Cyrl-ME', - 'sr-Cyrl-RS', - 'sr-Latn-BA', - 'sr-Latn-CS', - 'sr-Latn-ME', - 'sr-Latn-RS', - 'sv-FI', - 'sv-SE', - 'sw-KE', - 'syr-SY', - 'ta-IN', - 'te-IN', - 'tg-Cyrl-TJ', - 'th-TH', - 'tk-TM', - 'tn-ZA', - 'tr-TR', - 'tt-RU', - 'tzm-Latn-DZ', - 'ug-CN', - 'uk-UA', - 'ur-PK', - 'uz-Cyrl-UZ', - 'uz-Latn-UZ', - 'vi-VN', - 'wo-SN', - 'xh-ZA', - 'yo-NG', - 'zh-CN', - 'zh-HK', - 'zh-MO', - 'zh-SG', - 'zh-TW', - 'zu-ZA', + 'ka-GE', + 'de-AT', + 'de-DE', + 'el-GR', + 'hu-HU', + 'nl-BE', + 'ja-JP', + 'it-IT', + 'pl-PL', + 'he-IL', + 'pt-BR', + 'hu_HU', ] # object field properties OBJECT_FIELD_PROPERTIES = {locale: fields.TextField() for locale in ALL_LOCALES_LIST} OBJECT_FIELD_PROPERTIES.update({ + 'en-AU': fields.TextField(analyzer='english'), + 'en-US': fields.TextField(analyzer='english'), 'en-GB': fields.TextField(analyzer='english'), + 'en-CA': fields.TextField(analyzer='english'), 'ru-RU': fields.TextField(analyzer='russian'), - 'fr-FR': fields.TextField(analyzer='french') + 'fr-FR': fields.TextField(analyzer='french'), + 'fr-BE': fields.TextField(analyzer='french'), + 'fr-MA': fields.TextField(analyzer='french'), + 'fr-CA': fields.TextField(analyzer='french'), }) diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index 92d96a64..21b9b675 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -9,6 +9,7 @@ from django_elasticsearch_dsl_drf.filter_backends import ( from django_elasticsearch_dsl_drf.viewsets import BaseDocumentViewSet from search_indexes import serializers, filters from search_indexes.documents import EstablishmentDocument, NewsDocument +from search_indexes.documents.product import ProductDocument from utils.pagination import ProjectMobilePagination @@ -20,7 +21,6 @@ class NewsDocumentViewSet(BaseDocumentViewSet): pagination_class = ProjectMobilePagination permission_classes = (permissions.AllowAny,) serializer_class = serializers.NewsDocumentSerializer - ordering = ('id',) filter_backends = [ filters.CustomSearchFilterBackend, @@ -28,9 +28,9 @@ class NewsDocumentViewSet(BaseDocumentViewSet): ] search_fields = { - 'title': {'fuzziness': 'auto:3,4'}, - 'subtitle': {'fuzziness': 'auto'}, - 'description': {'fuzziness': 'auto'}, + 'title': {'fuzziness': 'auto:2,5'}, + 'subtitle': {'fuzziness': 'auto:2,5'}, + 'description': {'fuzziness': 'auto:2,5'}, } translated_search_fields = ( 'title', @@ -43,6 +43,14 @@ class NewsDocumentViewSet(BaseDocumentViewSet): 'field': 'tags.id', 'lookups': [ constants.LOOKUP_QUERY_IN, + constants.LOOKUP_QUERY_EXCLUDE + ] + }, + 'tag_value': { + 'field': 'tags.value', + 'lookups': [ + constants.LOOKUP_QUERY_IN, + constants.LOOKUP_QUERY_EXCLUDE ] }, 'slug': 'slug', @@ -73,22 +81,21 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): FilteringFilterBackend, filters.CustomSearchFilterBackend, GeoSpatialFilteringFilterBackend, - DefaultOrderingFilterBackend, + # DefaultOrderingFilterBackend, ] search_fields = { - 'name': {'fuzziness': 'auto:3,4', + 'name': {'fuzziness': 'auto:2,5', 'boost': '2'}, - 'transliterated_name': {'fuzziness': 'auto:3,4', + 'transliterated_name': {'fuzziness': 'auto:2,5', 'boost': '2'}, - 'index_name': {'fuzziness': 'auto:3,4', + 'index_name': {'fuzziness': 'auto:2,5', 'boost': '2'}, - 'description': {'fuzziness': 'auto'}, + 'description': {'fuzziness': 'auto:2,5'}, } translated_search_fields = ( 'description', ) - ordering = 'id' filter_fields = { 'slug': 'slug', 'tag': { @@ -184,3 +191,71 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): ] } } + + +class ProductDocumentViewSet(BaseDocumentViewSet): + """Product document ViewSet.""" + + document = ProductDocument + lookup_field = 'slug' + pagination_class = ProjectMobilePagination + permission_classes = (permissions.AllowAny,) + serializer_class = serializers.ProductDocumentSerializer + + # def get_queryset(self): + # qs = super(ProductDocumentViewSet, self).get_queryset() + # qs = qs.filter('match', is_publish=True) + # return qs + + filter_backends = [ + FilteringFilterBackend, + filters.CustomSearchFilterBackend, + GeoSpatialFilteringFilterBackend, + DefaultOrderingFilterBackend, + ] + + search_fields = { + 'name': {'fuzziness': 'auto:2,5', + 'boost': '2'}, + 'transliterated_name': {'fuzziness': 'auto:2,5', + 'boost': '2'}, + 'index_name': {'fuzziness': 'auto:2,5', + 'boost': '2'}, + 'description': {'fuzziness': 'auto:2,5'}, + } + translated_search_fields = ( + 'description', + ) + + filter_fields = { + 'slug': 'slug', + 'tags_id': { + 'field': 'tags.id', + 'lookups': [constants.LOOKUP_QUERY_IN] + }, + 'wine_colors_id': { + 'field': 'wine_colors.id', + 'lookups': [ + constants.LOOKUP_QUERY_IN, + constants.LOOKUP_QUERY_EXCLUDE, + ] + }, + 'wine_from_country_code': { + 'field': 'wine_region.country.code', + }, + 'for_establishment': { + 'field': 'establishment.slug', + }, + 'type': { + 'field': 'product_type.index_name', + }, + 'subtype': { + 'field': 'subtypes.index_name', + 'lookups': [ + constants.LOOKUP_QUERY_IN, + constants.LOOKUP_QUERY_EXCLUDE, + ] + } + } + geo_spatial_filter_fields = { + } \ No newline at end of file diff --git a/apps/tag/filters.py b/apps/tag/filters.py index e8263e0d..0b1fb829 100644 --- a/apps/tag/filters.py +++ b/apps/tag/filters.py @@ -30,9 +30,7 @@ class TagsBaseFilterSet(filters.FilterSet): class TagCategoryFilterSet(TagsBaseFilterSet): """TagCategory filterset.""" - establishment_type = filters.ChoiceFilter( - choices=EstablishmentType.INDEX_NAME_TYPES, - method='by_establishment_type') + establishment_type = filters.CharFilter(method='by_establishment_type') class Meta: """Meta class.""" @@ -54,3 +52,17 @@ class TagsFilterSet(TagsBaseFilterSet): model = models.Tag fields = ('type',) + + # TMP TODO remove it later + # Временный хардкод для демонстрации 4 ноября, потом удалить! + def filter_by_type(self, queryset, name, value): + """ Overrides base filter. Temporary decision""" + if not (settings.NEWS_CHOSEN_TAGS and settings.ESTABLISHMENT_CHOSEN_TAGS): + return super().filter_by_type(queryset, name, value) + queryset = models.Tag.objects + if self.NEWS in value: + queryset = queryset.for_news().filter(value__in=settings.NEWS_CHOSEN_TAGS).distinct('value') + if self.ESTABLISHMENT in value: + queryset = queryset.for_establishments().filter(value__in=settings.ESTABLISHMENT_CHOSEN_TAGS).distinct( + 'value') + return queryset diff --git a/apps/tag/management/commands/add_cepage_tag.py b/apps/tag/management/commands/add_cepage_tag.py new file mode 100644 index 00000000..b1d333b7 --- /dev/null +++ b/apps/tag/management/commands/add_cepage_tag.py @@ -0,0 +1,22 @@ +from django.core.management.base import BaseCommand + +from transfer.models import Cepages +from transfer.serializers.tag import CepageTagSerializer + + +class Command(BaseCommand): + help = 'Add cepage tags' + + def handle(self, *args, **kwarg): + + errors = [] + queryset = Cepages.objects.all() + serialized_data = CepageTagSerializer( + data=list(queryset.values()), + many=True) + if serialized_data.is_valid(): + serialized_data.save() + else: + for d in serialized_data.errors: errors.append(d) if d else None + + self.stdout.write(self.style.WARNING(f'Error count: {len(errors)}\nErrors: {errors}')) diff --git a/apps/tag/management/commands/add_cepage_tag_to_wine_region.py b/apps/tag/management/commands/add_cepage_tag_to_wine_region.py new file mode 100644 index 00000000..d4f98dfd --- /dev/null +++ b/apps/tag/management/commands/add_cepage_tag_to_wine_region.py @@ -0,0 +1,22 @@ +from django.core.management.base import BaseCommand + +from transfer.models import CepageRegions +from transfer.serializers.location import CepageWineRegionSerializer + + +class Command(BaseCommand): + help = 'Add cepage tag to wine region' + + def handle(self, *args, **kwarg): + errors = [] + legacy_products = CepageRegions.objects.exclude(cepage_id__isnull=True) \ + .exclude(wine_region_id__isnull=True) + serialized_data = CepageWineRegionSerializer( + data=list(legacy_products.values()), + many=True) + if serialized_data.is_valid(): + serialized_data.save() + else: + for d in serialized_data.errors: errors.append(d) if d else None + + self.stdout.write(self.style.WARNING(f'Error count: {len(errors)}\nErrors: {errors}')) diff --git a/apps/product/management/commands/fix_wine_color_tag.py b/apps/tag/management/commands/add_wine_color_tag.py similarity index 100% rename from apps/product/management/commands/fix_wine_color_tag.py rename to apps/tag/management/commands/add_wine_color_tag.py diff --git a/apps/tag/management/commands/fix_cepage_tag_categories.py b/apps/tag/management/commands/fix_cepage_tag_categories.py new file mode 100644 index 00000000..a8217e46 --- /dev/null +++ b/apps/tag/management/commands/fix_cepage_tag_categories.py @@ -0,0 +1,17 @@ +from django.core.management.base import BaseCommand +from django.utils.text import slugify + +from tag.models import TagCategory +from transfer import models as transfer_models + + +class Command(BaseCommand): + help = 'Fix wine color tag' + + def handle(self, *args, **kwarg): + queryset = transfer_models.Cepages.objects.all() + cepage_list = [slugify(i) for i in queryset.values_list('name', flat=True)] + tag_categories = TagCategory.objects.filter(index_name__in=cepage_list) + deleted_tag_categories = tag_categories.count() + tag_categories.delete() + self.stdout.write(self.style.WARNING(f"Deleted tag categories: {deleted_tag_categories}")) diff --git a/apps/tag/management/commands/revert_wine_region_cepage_tags.py b/apps/tag/management/commands/revert_wine_region_cepage_tags.py new file mode 100644 index 00000000..3d157955 --- /dev/null +++ b/apps/tag/management/commands/revert_wine_region_cepage_tags.py @@ -0,0 +1,18 @@ +from django.core.management.base import BaseCommand + +from location.models import WineRegion +from transfer.models import CepageRegions + + +class Command(BaseCommand): + help = 'Cleared wine region tag categories (cleared M2M relation with tag categories)' + + def handle(self, *args, **kwarg): + cepage_wine_regions = CepageRegions.objects.exclude(cepage_id__isnull=True) \ + .exclude(wine_region_id__isnull=True)\ + .distinct() \ + .values_list('wine_region_id', flat=True) + wine_regions = WineRegion.objects.filter(id__in=tuple(cepage_wine_regions)) + for region in wine_regions: + region.tags.clear() + self.stdout.write(self.style.WARNING(f'Cleared wine region tag categories')) diff --git a/apps/tag/migrations/0013_auto_20191113_0930.py b/apps/tag/migrations/0013_auto_20191113_0930.py new file mode 100644 index 00000000..0cea4780 --- /dev/null +++ b/apps/tag/migrations/0013_auto_20191113_0930.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.7 on 2019-11-13 09:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tag', '0012_merge_20191112_1552'), + ] + + operations = [ + migrations.AlterField( + model_name='tagcategory', + name='value_type', + field=models.CharField(choices=[('string', 'string'), ('list', 'list'), ('integer', 'integer'), ('float', 'float'), ('percentage', 'percentage')], default='list', max_length=255, verbose_name='value type'), + ), + ] diff --git a/apps/tag/migrations/0014_tag_old_id_meta_product.py b/apps/tag/migrations/0014_tag_old_id_meta_product.py new file mode 100644 index 00000000..603871d5 --- /dev/null +++ b/apps/tag/migrations/0014_tag_old_id_meta_product.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.7 on 2019-11-13 11:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tag', '0013_auto_20191113_0930'), + ] + + operations = [ + migrations.AddField( + model_name='tag', + name='old_id_meta_product', + field=models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='old id metadata product'), + ), + ] diff --git a/apps/tag/models.py b/apps/tag/models.py index e5e1fdbe..a35425e8 100644 --- a/apps/tag/models.py +++ b/apps/tag/models.py @@ -39,6 +39,9 @@ class Tag(TranslatedFieldsMixin, models.Model): old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None) + old_id_meta_product = models.PositiveIntegerField(_('old id metadata product'), + blank=True, null=True, default=None) + objects = TagQuerySet.as_manager() class Meta: diff --git a/apps/tag/serializers.py b/apps/tag/serializers.py index d6b851ab..02f2e4d3 100644 --- a/apps/tag/serializers.py +++ b/apps/tag/serializers.py @@ -13,7 +13,6 @@ class TagBaseSerializer(serializers.ModelSerializer): """Serializer for model Tag.""" label_translated = TranslatedField() - # label_translated = serializers.CharField(source='value', read_only=True, allow_null=True) index_name = serializers.CharField(source='value', read_only=True, allow_null=True) class Meta: @@ -57,6 +56,21 @@ class TagCategoryBaseSerializer(serializers.ModelSerializer): ) +class TagCategoryShortSerializer(serializers.ModelSerializer): + """Serializer for model TagCategory.""" + + label_translated = TranslatedField() + value_type_display = serializers.CharField(source='get_value_type_display', + read_only=True) + + class Meta(TagCategoryBaseSerializer.Meta): + """Meta class.""" + fields = [ + 'label_translated', + 'value_type_display', + ] + + class TagCategoryBackOfficeDetailSerializer(TagCategoryBaseSerializer): """Tag Category detail serializer for back-office users.""" diff --git a/apps/tag/transfer_data.py b/apps/tag/transfer_data.py index b94fd0a0..725f164c 100644 --- a/apps/tag/transfer_data.py +++ b/apps/tag/transfer_data.py @@ -1,8 +1,7 @@ from pprint import pprint from transfer import models as transfer_models -from transfer.serializers.tag import AssemblageTagSerializer, \ - CepagesTagCategorySerializer +from transfer.serializers.tag import AssemblageTagSerializer def transfer_assemblage(): @@ -13,24 +12,10 @@ def transfer_assemblage(): if serialized_data.is_valid(): serialized_data.save() else: - pprint(f"transfer_wine_color errors: {serialized_data.errors}") - - -def transfer_cepages(): - queryset = transfer_models.Cepages.objects.all() - serialized_data = CepagesTagCategorySerializer( - data=list(queryset.values()), - many=True) - if serialized_data.is_valid(): - serialized_data.save() - else: - pprint(f"transfer_cepages errors: {serialized_data.errors}") + pprint(f"transfer_assemblage errors: {serialized_data.errors}") data_types = { - "cepage": [ - transfer_cepages - ], "assemblage": [ transfer_assemblage, ] diff --git a/apps/tag/views.py b/apps/tag/views.py index c55834e0..4a2f2613 100644 --- a/apps/tag/views.py +++ b/apps/tag/views.py @@ -23,6 +23,23 @@ class ChosenTagsView(generics.ListAPIView, viewsets.GenericViewSet): .filter(id__in=result_tags_ids) \ .order_by_priority() + def list(self, request, *args, **kwargs): + # TMP TODO remove it later + # Временный хардкод для демонстрации > 15 ноября, потом удалить! + queryset = self.filter_queryset(self.get_queryset()) + + page = self.paginate_queryset(queryset) + if page is not None: + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + + serializer = self.get_serializer(queryset, many=True) + result_list = serializer.data + if request.query_params.get('type') and (settings.ESTABLISHMENT_CHOSEN_TAGS or settings.NEWS_CHOSEN_TAGS): + ordered_list = settings.ESTABLISHMENT_CHOSEN_TAGS if request.query_params.get('type') == 'establishment' else settings.NEWS_CHOSEN_TAGS + result_list = sorted(result_list, key=lambda x: ordered_list.index(x['index_name'])) + return Response(result_list) + # User`s views & viewsets class TagCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): diff --git a/apps/transfer/management/commands/transfer.py b/apps/transfer/management/commands/transfer.py index b905a8f2..088deb51 100644 --- a/apps/transfer/management/commands/transfer.py +++ b/apps/transfer/management/commands/transfer.py @@ -35,14 +35,17 @@ class Command(BaseCommand): 'product_note', 'souvenir', 'establishment_note', - 'cepage', 'assemblage', + 'rating_count', + 'product_review', + 'newsletter_subscriber', # подписчики на рассылку - переносить после переноса пользователей №1 'update_city_info', 'migrate_city_gallery' ] def handle(self, *args, **options): - """Находим включённую опцию путём пересечения множества типов данных и множества включённых опций""" + """ + Находим включённую опцию путём пересечения множества типов данных и множества включённых опций""" data_type = list(set(option for option in options.keys() if options[option]) & set(self.LONG_DATA_TYPES)) if len(data_type) != 1: data_type = list(set(option for option in options.keys() if options[option]) & set(self.SHORT_DATA_TYPES)) diff --git a/apps/transfer/mixins.py b/apps/transfer/mixins.py index 31d2bc2d..7b43b210 100644 --- a/apps/transfer/mixins.py +++ b/apps/transfer/mixins.py @@ -4,6 +4,7 @@ from rest_framework import serializers from tag import models as tag_models from django.conf import settings from product.models import ProductType, ProductSubType, Product +from location.models import WineRegion from django.utils.text import slugify @@ -109,3 +110,8 @@ class TransferSerializerMixin(serializers.ModelSerializer): product_qs = Product.objects.filter(old_id=old_id) if product_qs.exists(): return product_qs.first() + + def get_wine_region(self, parent_id): + qs = WineRegion.objects.filter(old_id=parent_id) + if qs.exists(): + return qs.first() diff --git a/apps/transfer/models.py b/apps/transfer/models.py index 079e788a..29ccb4bd 100644 --- a/apps/transfer/models.py +++ b/apps/transfer/models.py @@ -708,10 +708,10 @@ class Reviews(MigrateMixin): published_at = models.DateTimeField(blank=True, null=True) updated_at = models.DateTimeField() aasm_state = models.CharField(max_length=255, blank=True, null=True) - reviewer_id = models.IntegerField() + reviewer = models.ForeignKey(Accounts, models.DO_NOTHING, blank=True, null=True) priority = models.IntegerField(blank=True, null=True) # TODO: модель Products в postgres закомментирована - # product = models.ForeignKey("Products", models.DO_NOTHING, blank=True, null=True) + product = models.ForeignKey("Products", models.DO_NOTHING, blank=True, null=True) received_at = models.DateTimeField(blank=True, null=True) reviewer_name = models.CharField(max_length=255, blank=True, null=True) type = models.CharField(max_length=255, blank=True, null=True) @@ -1156,7 +1156,6 @@ class GridItems(MigrateMixin): class Assemblages(MigrateMixin): - using = 'legacy' percent = models.FloatField() @@ -1168,8 +1167,18 @@ class Assemblages(MigrateMixin): db_table = 'assemblages' -class Cepages(MigrateMixin): +class CepageRegions(MigrateMixin): + using = 'legacy' + cepage = models.ForeignKey('Cepages', on_delete=models.DO_NOTHING) + wine_region_id = models.IntegerField() + + class Meta: + managed = False + db_table = 'cepage_regions' + + +class Cepages(MigrateMixin): using = 'legacy' name = models.CharField(max_length=255) @@ -1177,3 +1186,19 @@ class Cepages(MigrateMixin): class Meta: managed = False db_table = 'cepages' + + +class NewsletterSubscriber(MigrateMixin): + using = 'legacy' + + site = models.ForeignKey(Sites, models.DO_NOTHING, blank=True, null=True) + email_address = models.ForeignKey(EmailAddresses, models.DO_NOTHING, blank=True, null=True) + state = models.CharField(max_length=255, blank=True, null=True) + consent_purpose = models.CharField(max_length=255, blank=True, null=True) + consent_at = models.DateTimeField() + created_at = models.DateTimeField() + updated_at = models.DateTimeField() + + class Meta: + managed = False + db_table = 'newsletter_subscriptions' diff --git a/apps/transfer/serializers/comments.py b/apps/transfer/serializers/comments.py index 3b099239..73c90802 100644 --- a/apps/transfer/serializers/comments.py +++ b/apps/transfer/serializers/comments.py @@ -10,7 +10,6 @@ class CommentSerializer(serializers.Serializer): mark = serializers.DecimalField(max_digits=4, decimal_places=2, allow_null=True) account_id = serializers.IntegerField() establishment_id = serializers.CharField() - created_at = serializers.DateTimeField(format='%m-%d-%Y %H:%M:%S') def validate(self, data): data.update({ diff --git a/apps/transfer/serializers/location.py b/apps/transfer/serializers/location.py index def9aaa0..f1a1cea3 100644 --- a/apps/transfer/serializers/location.py +++ b/apps/transfer/serializers/location.py @@ -5,6 +5,9 @@ from rest_framework import serializers from location import models from transfer.mixins import TransferSerializerMixin from utils.methods import get_point_from_coordinates +from transfer.models import Cepages +from tag.models import TagCategory +from django.utils.text import slugify from django.contrib.gis.geos import Point from django.core.exceptions import MultipleObjectsReturned @@ -451,3 +454,28 @@ class CityGallerySerializer(serializers.ModelSerializer): raise ValueError(f"Multiple cities find with {data}: {e}") return data + + +class CepageWineRegionSerializer(TransferSerializerMixin): + + CATEGORY_LABEL = 'Cepage' + CATEGORY_INDEX_NAME = slugify(CATEGORY_LABEL) + + cepage_id = serializers.PrimaryKeyRelatedField( + queryset=Cepages.objects.all()) + wine_region_id = serializers.IntegerField() + + class Meta(WineRegion.Meta): + fields = [ + 'cepage_id', + 'wine_region_id', + ] + + def create(self, validated_data): + obj = self.get_wine_region(validated_data['wine_region_id']) + cepage = validated_data.get('cepage_id') + tag = self.get_tag(cepage.name, self.CATEGORY_INDEX_NAME) + + if obj and tag not in obj.tags.all(): + obj.tags.add(tag) + return obj diff --git a/apps/transfer/serializers/notification.py b/apps/transfer/serializers/notification.py index c179dd2a..7eb7bdac 100644 --- a/apps/transfer/serializers/notification.py +++ b/apps/transfer/serializers/notification.py @@ -1,4 +1,6 @@ from rest_framework import serializers + +from account.models import User from notification.models import Subscriber @@ -33,3 +35,44 @@ class SubscriberSerializer(serializers.ModelSerializer): def get_country_code(self, obj): return obj["country_code"] + + +class NewsletterSubscriberSerializer(serializers.Serializer): + id = serializers.IntegerField() + email_address__email = serializers.CharField() + email_address__account_id = serializers.IntegerField(allow_null=True) + email_address__ip = serializers.CharField(allow_null=True) + email_address__country_code = serializers.CharField(allow_null=True) + email_address__locale = serializers.CharField(allow_null=True) + created_at = serializers.DateTimeField(format='%m-%d-%Y %H:%M:%S') + + def validate(self, data): + data.update({ + 'old_id': data.pop('id'), + 'email': data.pop('email_address__email'), + 'ip_address': data.pop('email_address__ip'), + 'country_code': data.pop('email_address__country_code'), + 'locale': data.pop('email_address__locale'), + 'created': data.pop('created_at'), + 'user_id': self.get_user(data), + }) + data.pop('email_address__account_id') + return data + + # def create(self, validated_data): + # obj, _ = Review.objects.update_or_create( + # old_id=validated_data['old_id'], + # defaults=validated_data, + # ) + # return obj + + @staticmethod + def get_user(data): + + if not data['email_address__account_id']: + return None + + user = User.objects.filter(old_id=data['email_address__account_id']).first() + if not user: + raise ValueError(f"User account not found with old_id {data['email_address__account_id']}") + return user.id diff --git a/apps/transfer/serializers/product.py b/apps/transfer/serializers/product.py index dc5b088f..f0871c41 100644 --- a/apps/transfer/serializers/product.py +++ b/apps/transfer/serializers/product.py @@ -571,7 +571,10 @@ class ProductNoteSerializer(TransferSerializerMixin): return qs.first() -class AssemblageTagSerializer(TransferSerializerMixin): +class AssemblageProductTagSerializer(TransferSerializerMixin): + + CATEGORY_LABEL = 'Grape variety' + CATEGORY_INDEX_NAME = slugify(CATEGORY_LABEL) percent = serializers.FloatField() cepage_id = serializers.PrimaryKeyRelatedField( @@ -586,11 +589,11 @@ class AssemblageTagSerializer(TransferSerializerMixin): ) def validate(self, attrs): - tag_category = attrs.pop('cepage_id') - tag = str(attrs.pop('percent')) + cepage = attrs.pop('cepage_id') + legacy_tag = str(attrs.pop('percent')) attrs['product'] = self.get_product(attrs.pop('product_id')) - attrs['tag'] = self.get_tag(tag, self.get_tag_category(tag_category)) + attrs['tag'] = self.get_tag_object(legacy_tag, self.tag_category, cepage) return attrs def create(self, validated_data): @@ -602,16 +605,10 @@ class AssemblageTagSerializer(TransferSerializerMixin): obj.tags.add(tag) return obj - def get_tag(self, tag, tag_category): - if tag and tag_category: + def get_tag_object(self, tag, tag_category, cepage): + cepage = cepage.name if isinstance(cepage, transfer_models.Cepages) else cepage + if tag and tag_category and cepage: qs = tag_models.Tag.objects.filter(category=tag_category, - value=tag) + value=slugify(f'{cepage} - {tag}')) if qs.exists(): return qs.first() - - def get_tag_category(self, tag_category): - if isinstance(tag_category, transfer_models.Cepages): - tag_category = tag_category.name - qs = tag_models.TagCategory.objects.filter(index_name=slugify(tag_category)) - if qs.exists(): - return qs.first() diff --git a/apps/transfer/serializers/reviews.py b/apps/transfer/serializers/reviews.py index 7bfe2004..a6cb1124 100644 --- a/apps/transfer/serializers/reviews.py +++ b/apps/transfer/serializers/reviews.py @@ -3,84 +3,131 @@ from review.models import Review from account.models import User from translation.models import Language from establishment.models import Establishment +from product.models import Product -class ReviewSerializer(serializers.ModelSerializer): - id = serializers.IntegerField() - reviewer_id = serializers.IntegerField() +class ReviewSerializer(serializers.Serializer): vintage = serializers.IntegerField() - published = serializers.DateTimeField() - published_at = serializers.DateTimeField(allow_null=True) + mark = serializers.FloatField(allow_null=True) establishment_id = serializers.IntegerField() - text = serializers.CharField(allow_null=True, allow_blank=True) - locale = serializers.CharField() + created_at = serializers.DateTimeField(format='%m-%d-%Y %H:%M:%S') aasm_state = serializers.CharField(allow_null=True) - - class Meta: - model = Review - fields = ( - "id", "reviewer_id", "published", "vintage", - "establishment_id", "text", "locale", - "published_at", "aasm_state" - ) + reviewer_id = serializers.IntegerField() + id = serializers.IntegerField() def validate(self, data): - data = self.set_old_id(data) - data = self.set_published_at(data) - data = self.set_reviewer(data) - data = self.set_establishment(data) - data = self.set_language(data) - data = self.set_published(data) + data.update({ + 'reviewer': self.get_reviewer(data), + 'status': Review.READY if data['aasm_state'] == 'published' else Review.TO_INVESTIGATE, + 'published_at': data.pop('created_at'), + 'old_id': data.pop('id'), + 'content_object': self.get_establishment(data), + }) + data.pop('reviewer_id') + data.pop('establishment_id') + data.pop('aasm_state') return data def create(self, validated_data): - try: - return Review.objects.create(**validated_data) - except Exception as e: - raise ValueError(f"Error creating review with {validated_data}: {e}") + obj, _ = Review.objects.update_or_create( + old_id=validated_data['old_id'], + defaults=validated_data, + ) + return obj - def set_old_id(self, data): - data['old_id'] = data.pop("id") + @staticmethod + def get_reviewer(data): + if data['reviewer_id'] and not data['reviewer_id'] == -1: + user = User.objects.filter(old_id=data['reviewer_id']).first() + return user + + @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 + + +class ProductReviewSerializer(ReviewSerializer): + vintage = serializers.IntegerField() + mark = serializers.FloatField(allow_null=True) + product_id = serializers.IntegerField() + created_at = serializers.DateTimeField(format='%m-%d-%Y %H:%M:%S') + aasm_state = serializers.CharField(allow_null=True) + reviewer_id = serializers.IntegerField() + id = serializers.IntegerField() + + def validate(self, data): + data.update({ + 'reviewer': self.get_reviewer(data), + 'status': Review.READY if data['aasm_state'] == 'published' else Review.TO_INVESTIGATE, + 'published_at': data.pop('created_at'), + 'old_id': data.pop('id'), + 'content_object': self.get_product(data), + }) + data.pop('reviewer_id') + data.pop('product_id') + data.pop('aasm_state') return data - def set_reviewer(self, data): - try: - data['reviewer'] = User.objects.get(old_id=data['reviewer_id']) - except User.DoesNotExist as e: - raise ValueError(f"Cannot find reviewer with {data}: {e}") - del(data['reviewer_id']) + def create(self, validated_data): + obj, _ = Review.objects.update_or_create( + old_id=validated_data['old_id'], + defaults=validated_data, + ) + return obj + + @staticmethod + def get_reviewer(data): + user = User.objects.filter(old_id=data['reviewer_id']).first() + if not user: + raise ValueError(f"User account not found with old_id {data['reviewer_id']}") + return user + + @staticmethod + def get_product(data): + establishment = Product.objects.filter(old_id=data['product_id']).first() + if not establishment: + raise ValueError(f"Product not found with old_id {data['product_id']}: ") + return establishment + + +class ReviewTextSerializer(serializers.Serializer): + review_id = serializers.IntegerField() + locale = serializers.CharField(allow_null=True) + text = serializers.CharField() + + def validate(self, data): + data.update({ + 'new_text': self.get_text(data), + 'review': self.get_review(data), + }) return data - def set_establishment(self, data): - try: - data['content_object'] = Establishment.objects.get(old_id=data.pop('establishment_id')) - except Establishment.DoesNotExist as e: - raise ValueError(f"Cannot find review establishment with {data}: {e}") - return data + def create(self, validated_data): + review = validated_data['review'] + if review.text: + review.text.update(validated_data['new_text']) + else: + review.text = validated_data['new_text'] + review.save() + return review - def set_language(self, data): - try: - data['language'] = Language.objects.get(locale=data['locale']) - except Language.DoesNotExist as e: - raise ValueError(f"Cannot find language with {data}: {e}") + @staticmethod + def get_text(data): + locale = data['locale'] or 'en-GB' + return {locale: data['text']} - del(data['locale']) - - return data - - def set_published(self, data): - data['published_at'] = data.pop("published") - return data - - def set_published_at(self, data): - if "aasm_state" in data and data['aasm_state'] is not None and data['aasm_state'] == "published": - data['status'] = Review.READY - del(data['aasm_state']) - return data + @staticmethod + def get_review(data): + review = Review.objects.filter(old_id=data['review_id']).first() + if not review: + raise ValueError(f"Review not found with old_id {data['review_id']}: ") + return review class LanguageSerializer(serializers.ModelSerializer): - LANGUAGES = { "be-BE": "Belarusian - Belarusia", "el-GR": "Greek - Greek", diff --git a/apps/transfer/serializers/tag.py b/apps/transfer/serializers/tag.py index db4a882c..c47ffafc 100644 --- a/apps/transfer/serializers/tag.py +++ b/apps/transfer/serializers/tag.py @@ -1,13 +1,17 @@ -from transfer.mixins import TransferSerializerMixin +from django.conf import settings from django.utils.text import slugify from rest_framework import serializers -from tag.models import Tag, TagCategory + +from tag.models import Tag +from transfer.mixins import TransferSerializerMixin from transfer.models import Cepages -from django.conf import settings class AssemblageTagSerializer(TransferSerializerMixin): + CATEGORY_LABEL = 'Grape variety' + CATEGORY_INDEX_NAME = slugify(CATEGORY_LABEL) + percent = serializers.FloatField() cepage_id = serializers.PrimaryKeyRelatedField( queryset=Cepages.objects.all()) @@ -20,12 +24,13 @@ class AssemblageTagSerializer(TransferSerializerMixin): ) def validate(self, attrs): - name = attrs.pop('percent') + percent = attrs.pop('percent') cepage = attrs.pop('cepage_id') + value = self.get_tag_value(cepage, percent) - attrs['label'] = {settings.FALLBACK_LOCALE: name} - attrs['value'] = name - attrs['category'] = self.get_tag_category(cepage) + attrs['label'] = {settings.FALLBACK_LOCALE: value} + attrs['value'] = slugify(value) + attrs['category'] = self.tag_category return attrs def create(self, validated_data): @@ -34,28 +39,29 @@ class AssemblageTagSerializer(TransferSerializerMixin): if not qs.exists() and category: return super().create(validated_data) - def get_tag_category(self, cepage): - cepage_name = cepage.name if isinstance(cepage, Cepages) else cepage - qs = TagCategory.objects.filter(index_name=slugify(cepage_name)) - if qs.exists(): - return qs.first() + def get_tag_value(self, cepage, percent): + if cepage and percent: + return f'{cepage.name} - {percent}%' -class CepagesTagCategorySerializer(TransferSerializerMixin): +class CepageTagSerializer(TransferSerializerMixin): + + CATEGORY_LABEL = 'Cepage' + CATEGORY_INDEX_NAME = slugify(CATEGORY_LABEL) name = serializers.CharField() class Meta: - model = TagCategory + model = Tag fields = ( 'name', ) def validate(self, attrs): + name = attrs.pop('name') attrs['label'] = {settings.FALLBACK_LOCALE: name} - attrs['public'] = True - attrs['index_name'] = slugify(name) - attrs['value_type'] = TagCategory.PERCENTAGE + attrs['value'] = slugify(name) + attrs['category'] = self.tag_category return attrs diff --git a/apps/translation/migrations/0007_language_is_active.py b/apps/translation/migrations/0007_language_is_active.py new file mode 100644 index 00000000..c44c6dbc --- /dev/null +++ b/apps/translation/migrations/0007_language_is_active.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.4 on 2019-11-14 19:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('translation', '0006_language_old_id'), + ] + + operations = [ + migrations.AddField( + model_name='language', + name='is_active', + field=models.BooleanField(default=True, verbose_name='is active'), + ), + ] diff --git a/apps/translation/models.py b/apps/translation/models.py index 20b00233..1d9695fe 100644 --- a/apps/translation/models.py +++ b/apps/translation/models.py @@ -8,6 +8,10 @@ from utils.models import ProjectBaseMixin, LocaleManagerMixin class LanguageQuerySet(models.QuerySet): """QuerySet for model Language""" + def active(self, switcher=True): + """Filter only active users.""" + return self.filter(is_active=switcher) + def by_locale(self, locale: str) -> models.QuerySet: """Filter by locale""" return self.filter(locale=locale) @@ -27,6 +31,8 @@ class Language(models.Model): old_id = models.IntegerField(null=True, blank=True, default=None) + is_active = models.BooleanField(_('is active'), default=True) + objects = LanguageQuerySet.as_manager() class Meta: diff --git a/apps/translation/views.py b/apps/translation/views.py index 974e6ff5..1b8c2736 100644 --- a/apps/translation/views.py +++ b/apps/translation/views.py @@ -9,7 +9,7 @@ from utils.views import JWTGenericViewMixin class LanguageViewMixin(generics.GenericAPIView): """Mixin for Language views""" - queryset = models.Language.objects.all() + queryset = models.Language.objects.active() serializer_class = serializers.LanguageSerializer diff --git a/apps/utils/views.py b/apps/utils/views.py index 8333b34a..d3d09079 100644 --- a/apps/utils/views.py +++ b/apps/utils/views.py @@ -1,10 +1,13 @@ from collections import namedtuple from django.conf import settings +from django.db.transaction import on_commit from rest_framework import generics from rest_framework import status from rest_framework.response import Response +from gallery.tasks import delete_image + # JWT # Login base view mixins @@ -95,3 +98,26 @@ class JWTGenericViewMixin(generics.GenericAPIView): http_only=self.REFRESH_TOKEN_HTTP_ONLY, secure=self.REFRESH_TOKEN_SECURE, max_age=_cookies.get('max_age'))] + + +class CreateDestroyGalleryViewMixin(generics.CreateAPIView, + generics.DestroyAPIView): + """Mixin for creating and destroying entity linked with gallery.""" + + def create(self, request, *args, **kwargs): + """Overridden create method""" + super().create(request, *args, **kwargs) + return Response(status=status.HTTP_201_CREATED) + + def destroy(self, request, *args, **kwargs): + """Override destroy method.""" + gallery_obj = self.get_object() + if settings.USE_CELERY: + on_commit(lambda: delete_image.delay(image_id=gallery_obj.image.id, + completely=False)) + else: + on_commit(lambda: delete_image(image_id=gallery_obj.image.id, + completely=False)) + # Delete an instances of Gallery model + gallery_obj.delete() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/docker-compose.yml b/docker-compose.yml index c518a3ad..48fea8eb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,7 +24,7 @@ services: - 9200:9200 - 9300:9300 environment: - - "ES_JAVA_OPTS=-Xms512m -Xmx512m" + - "ES_JAVA_OPTS=-Xms1g -Xmx1g" - discovery.type=single-node - xpack.security.enabled=false diff --git a/project/settings/base.py b/project/settings/base.py index ecddc9c2..c7ec76c7 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -495,3 +495,9 @@ PHONENUMBER_DB_FORMAT = 'NATIONAL' PHONENUMBER_DEFAULT_REGION = "FR" FALLBACK_LOCALE = 'en-GB' + +# TMP TODO remove it later +# Временный хардкод для демонстрации > 15 ноября, потом удалить! +CAROUSEL_ITEMS = [230, 231, 232] +ESTABLISHMENT_CHOSEN_TAGS = ['gastronomic', 'en_vogue', 'terrace', 'streetfood', 'business', 'bar_cocktail', 'brunch', 'pop'] +NEWS_CHOSEN_TAGS = ['eat', 'drink', 'cook', 'style', 'international', 'event', 'partnership'] diff --git a/project/settings/development.py b/project/settings/development.py index b9203259..3bc258a1 100644 --- a/project/settings/development.py +++ b/project/settings/development.py @@ -28,8 +28,9 @@ ELASTICSEARCH_DSL = { ELASTICSEARCH_INDEX_NAMES = { - 'search_indexes.documents.news': 'development_news', # temporarily disabled + 'search_indexes.documents.news': 'development_news', 'search_indexes.documents.establishment': 'development_establishment', + 'search_indexes.documents.product': 'development_product', } diff --git a/project/settings/local.py b/project/settings/local.py index 62298faa..f9a096fe 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -43,7 +43,8 @@ INSTALLED_APPS.append('transfer.apps.TransferConfig') DATABASES.update({ 'legacy': { 'ENGINE': 'django.db.backends.mysql', - 'HOST': 'mysql_db', + 'HOST': '172.22.0.1', + # 'HOST': 'mysql_db', 'PORT': 3306, 'NAME': 'dev', 'USER': 'dev', @@ -98,6 +99,7 @@ ELASTICSEARCH_DSL = { ELASTICSEARCH_INDEX_NAMES = { # 'search_indexes.documents.news': 'local_news', 'search_indexes.documents.establishment': 'local_establishment', + 'search_indexes.documents.product': 'local_product', } diff --git a/project/settings/production.py b/project/settings/production.py index fe8618dd..3192acea 100644 --- a/project/settings/production.py +++ b/project/settings/production.py @@ -32,6 +32,7 @@ ELASTICSEARCH_DSL = { ELASTICSEARCH_INDEX_NAMES = { 'search_indexes.documents.news': 'development_news', # temporarily disabled 'search_indexes.documents.establishment': 'development_establishment', + 'search_indexes.documents.product': 'development_product', } diff --git a/project/urls/mobile.py b/project/urls/mobile.py index 8a1558bc..4fa53ad9 100644 --- a/project/urls/mobile.py +++ b/project/urls/mobile.py @@ -9,7 +9,7 @@ urlpatterns = [ path('tags/', include('tag.urls.mobile')), path('timetables/', include('timetable.urls.mobile')), # path('account/', include('account.urls.web')), - # path('advertisement/', include('advertisement.urls.web')), + path('re_blocks/', include('advertisement.urls.mobile')), # path('collection/', include('collection.urls.web')), # path('establishments/', include('establishment.urls.web')), path('news/', include('news.urls.mobile')), diff --git a/project/urls/web.py b/project/urls/web.py index 86f7eac2..c5a609e2 100644 --- a/project/urls/web.py +++ b/project/urls/web.py @@ -36,4 +36,5 @@ urlpatterns = [ path('favorites/', include('favorites.urls')), path('timetables/', include('timetable.urls.web')), path('products/', include('product.urls.web')), + ] diff --git a/requirements/base.txt b/requirements/base.txt index d7050a29..8ce99c84 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -35,8 +35,12 @@ django-cors-headers==3.0.2 # JWT djangorestframework-simplejwt==4.3.0 -django-elasticsearch-dsl>=7.0.0,<8.0.0 +# Elasticsearch +django-elasticsearch-dsl==7.1.0 django-elasticsearch-dsl-drf==0.20.2 +elasticsearch==7.1.0 +elasticsearch-dsl==7.1.0 + sentry-sdk==0.11.2 # AMAZON S3 @@ -50,7 +54,5 @@ PyYAML==5.1.2 # temp solution redis==3.2.0 -amqp>=2.4.0 - -kombu==4.5.0 -celery==4.3.0rc2 +kombu==4.6.6 +celery==4.3.0