diff --git a/apps/account/models.py b/apps/account/models.py index 5e280919..5bf66321 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -147,8 +147,8 @@ class User(AbstractUser): ) EMAIL_FIELD = 'email' - USERNAME_FIELD = 'username' - REQUIRED_FIELDS = ['email'] + USERNAME_FIELD = 'email' + REQUIRED_FIELDS = ['username'] roles = models.ManyToManyField( Role, verbose_name=_('Roles'), symmetrical=False, diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index ada87016..d53fca1e 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -8,6 +8,8 @@ from comment.serializers import common as comment_serializers from establishment import models from location.serializers import AddressBaseSerializer, CitySerializer, AddressDetailSerializer, \ CityShortSerializer +from location.serializers import EstablishmentWineRegionBaseSerializer, \ + EstablishmentWineOriginBaseSerializer from main.serializers import AwardSerializer, CurrencySerializer from review.serializers import ReviewShortSerializer from tag.serializers import TagBaseSerializer @@ -16,8 +18,6 @@ from utils import exceptions as utils_exceptions from utils.serializers import ImageBaseSerializer, CarouselCreateSerializer from utils.serializers import (ProjectModelSerializer, TranslatedField, FavoritesCreateSerializer) -from location.serializers import EstablishmentWineRegionBaseSerializer, \ - EstablishmentWineOriginBaseSerializer class ContactPhonesSerializer(serializers.ModelSerializer): diff --git a/apps/main/models.py b/apps/main/models.py index 63a65062..ab06e4f9 100644 --- a/apps/main/models.py +++ b/apps/main/models.py @@ -6,7 +6,7 @@ from django.contrib.contenttypes import fields as generic from django.contrib.contenttypes.models import ContentType from django.contrib.postgres.fields import JSONField from django.core.validators import EMPTY_VALUES -from django.db import connections, connection +from django.db import connections from django.db import models from django.db.models import Q from django.utils.translation import gettext_lazy as _ @@ -16,6 +16,7 @@ from configuration.models import TranslationSettings from location.models import Country from main import methods from review.models import Review +from tag.models import Tag from utils.exceptions import UnprocessableEntityError from utils.methods import dictfetchall from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin, @@ -117,6 +118,8 @@ class Feature(ProjectBaseMixin, PlatformMixin): site_settings = models.ManyToManyField(SiteSettings, through='SiteFeature') old_id = models.IntegerField(null=True, blank=True) + chosen_tags = generic.GenericRelation(to='tag.ChosenTag') + class Meta: """Meta class.""" verbose_name = _('Feature') @@ -125,6 +128,10 @@ class Feature(ProjectBaseMixin, PlatformMixin): def __str__(self): return f'{self.slug}' + @property + def get_chosen_tags(self): + return Tag.objects.filter(chosen_tags__in=self.chosen_tags.all()).distinct() + class SiteFeatureQuerySet(models.QuerySet): """Extended queryset for SiteFeature model.""" diff --git a/apps/main/serializers.py b/apps/main/serializers.py index 4b41eae4..6def6300 100644 --- a/apps/main/serializers.py +++ b/apps/main/serializers.py @@ -2,11 +2,12 @@ from django.contrib.contenttypes.models import ContentType from rest_framework import serializers +from account.models import User +from account.serializers.back import BackUserSerializer from location.serializers import CountrySerializer from main import models +from tag.serializers import TagBackOfficeSerializer from utils.serializers import ProjectModelSerializer, TranslatedField, RecursiveFieldSerializer -from account.serializers.back import BackUserSerializer -from account.models import User class FeatureSerializer(serializers.ModelSerializer): @@ -90,6 +91,8 @@ class SiteFeatureSerializer(serializers.ModelSerializer): route = serializers.CharField(source='feature.route.name') source = serializers.IntegerField(source='feature.source') nested = RecursiveFieldSerializer(many=True, allow_null=True) + chosen_tags = TagBackOfficeSerializer( + source='feature.get_chosen_tags', many=True, read_only=True) class Meta: """Meta class.""" @@ -101,6 +104,7 @@ class SiteFeatureSerializer(serializers.ModelSerializer): 'route', 'source', 'nested', + 'chosen_tags', ) diff --git a/apps/search_indexes/documents/establishment.py b/apps/search_indexes/documents/establishment.py index 4a975615..20185b9c 100644 --- a/apps/search_indexes/documents/establishment.py +++ b/apps/search_indexes/documents/establishment.py @@ -85,6 +85,14 @@ class EstablishmentDocument(Document): 'value': fields.KeywordField(), }, multi=True) + distillery_types = fields.ObjectField( + properties={ + 'id': fields.IntegerField(attr='id'), + 'label': fields.ObjectField(attr='label_indexing', + properties=OBJECT_FIELD_PROPERTIES), + 'value': fields.KeywordField(), + }, + multi=True) products = fields.ObjectField( properties={ 'wine_origins': fields.ListField( diff --git a/apps/search_indexes/serializers.py b/apps/search_indexes/serializers.py index a668ccf1..b3c55ab1 100644 --- a/apps/search_indexes/serializers.py +++ b/apps/search_indexes/serializers.py @@ -277,6 +277,7 @@ class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer): tags = TagsDocumentSerializer(many=True, source='visible_tags') restaurant_category = TagsDocumentSerializer(many=True, allow_null=True) restaurant_cuisine = TagsDocumentSerializer(many=True, allow_null=True) + distillery_types = TagsDocumentSerializer(many=True, allow_null=True) artisan_category = TagsDocumentSerializer(many=True, allow_null=True) schedule = ScheduleDocumentSerializer(many=True, allow_null=True) wine_origins = WineOriginSerializer(many=True) @@ -310,6 +311,7 @@ class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer): # 'collections', 'type', 'subtypes', + 'distillery_types', ) diff --git a/apps/tag/filters.py b/apps/tag/filters.py index 28c3db33..7d82bf84 100644 --- a/apps/tag/filters.py +++ b/apps/tag/filters.py @@ -21,7 +21,6 @@ class TagsBaseFilterSet(filters.FilterSet): type = filters.MultipleChoiceFilter(choices=TYPE_CHOICES, method='filter_by_type') - def filter_by_type(self, queryset, name, value): if self.NEWS in value: queryset = queryset.for_news() diff --git a/apps/tag/migrations/0018_auto_20200113_1357.py b/apps/tag/migrations/0018_auto_20200113_1357.py new file mode 100644 index 00000000..35f4379b --- /dev/null +++ b/apps/tag/migrations/0018_auto_20200113_1357.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.7 on 2020-01-13 13:57 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('tag', '0017_auto_20191220_1623'), + ] + + operations = [ + migrations.AlterField( + model_name='tagcategory', + name='country', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='location.Country'), + ), + ] diff --git a/apps/tag/migrations/0018_chosentag.py b/apps/tag/migrations/0018_chosentag.py new file mode 100644 index 00000000..c394cc7c --- /dev/null +++ b/apps/tag/migrations/0018_chosentag.py @@ -0,0 +1,26 @@ +# Generated by Django 2.2.7 on 2020-01-13 08:36 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0045_carousel_is_international'), + ('contenttypes', '0002_remove_content_type_name'), + ('tag', '0017_auto_20191220_1623'), + ] + + operations = [ + migrations.CreateModel( + name='ChosenTag', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('object_id', models.PositiveIntegerField()), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), + ('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='main.SiteSettings', verbose_name='site')), + ('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chosen_tags', to='tag.Tag', verbose_name='tag')), + ], + ), + ] diff --git a/apps/tag/migrations/0019_merge_20200114_0756.py b/apps/tag/migrations/0019_merge_20200114_0756.py new file mode 100644 index 00000000..a786e116 --- /dev/null +++ b/apps/tag/migrations/0019_merge_20200114_0756.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.7 on 2020-01-14 07:56 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tag', '0018_auto_20200113_1357'), + ('tag', '0018_chosentag'), + ] + + operations = [ + ] diff --git a/apps/tag/models.py b/apps/tag/models.py index 1b735724..2a57a1c4 100644 --- a/apps/tag/models.py +++ b/apps/tag/models.py @@ -1,4 +1,5 @@ """Tag app models.""" +from django.contrib.contenttypes import fields as generic from django.contrib.contenttypes.models import ContentType from django.db import models from django.utils.translation import gettext_lazy as _ @@ -145,8 +146,8 @@ class TagCategory(models.Model): (BOOLEAN, _('boolean')), ) country = models.ForeignKey('location.Country', - on_delete=models.SET_NULL, null=True, - default=None) + on_delete=models.SET_NULL, + blank=True, null=True, default=None) public = models.BooleanField(default=False) index_name = models.CharField(max_length=255, blank=True, null=True, verbose_name=_('indexing name'), unique=True) @@ -154,8 +155,9 @@ class TagCategory(models.Model): value_type = models.CharField(_('value type'), max_length=255, choices=VALUE_TYPE_CHOICES, default=LIST, ) old_id = models.IntegerField(blank=True, null=True) - translation = models.OneToOneField('translation.SiteInterfaceDictionary', on_delete=models.SET_NULL, - null=True, related_name='tag_category', verbose_name=_('Translation')) + translation = models.OneToOneField( + 'translation.SiteInterfaceDictionary', on_delete=models.SET_NULL, + null=True, related_name='tag_category', verbose_name=_('Translation')) @property def label_indexing(self): @@ -175,3 +177,20 @@ class TagCategory(models.Model): def __str__(self): return self.index_name + + +class ChosenTag(models.Model): + """Chosen tag for type.""" + tag = models.ForeignKey( + 'Tag', verbose_name=_('tag'), related_name='chosen_tags', + on_delete=models.CASCADE) + site = models.ForeignKey( + 'main.SiteSettings', verbose_name=_('site'), on_delete=models.CASCADE) + + content_type = models.ForeignKey( + generic.ContentType, on_delete=models.CASCADE) + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey('content_type', 'object_id') + + def __str__(self): + return f'chosen_tag:{self.tag}' diff --git a/apps/tag/serializers.py b/apps/tag/serializers.py index 5ec87d17..9722e87a 100644 --- a/apps/tag/serializers.py +++ b/apps/tag/serializers.py @@ -9,6 +9,7 @@ from tag import models from utils.exceptions import BindingObjectNotFound, ObjectAlreadyAdded, RemovedBindingObjectNotFound from utils.serializers import TranslatedField from utils.models import get_default_locale, get_language, to_locale +from main.models import Feature def translate_obj(obj): @@ -56,7 +57,8 @@ class TagBackOfficeSerializer(TagBaseSerializer): fields = TagBaseSerializer.Meta.fields + ( 'label', - 'category' + 'category', + 'value', ) @@ -191,7 +193,7 @@ class TagCategoryBackOfficeDetailSerializer(TagCategoryBaseSerializer): class TagBindObjectSerializer(serializers.Serializer): - """Serializer for binding tag category and objects""" + """Serializer for binding tag category and objects.""" ESTABLISHMENT = 'establishment' NEWS = 'news' @@ -216,15 +218,20 @@ class TagBindObjectSerializer(serializers.Serializer): if obj_type == self.ESTABLISHMENT: establishment = Establishment.objects.filter(pk=obj_id).first() + if not establishment: raise BindingObjectNotFound() + if request.method == 'POST' and tag.establishments.filter( pk=establishment.pk).exists(): raise ObjectAlreadyAdded() + if request.method == 'DELETE' and not tag.establishments.filter( pk=establishment.pk).exists(): raise RemovedBindingObjectNotFound() + attrs['related_object'] = establishment + elif obj_type == self.NEWS: news = News.objects.filter(pk=obj_id).first() if not news: @@ -287,3 +294,41 @@ class TagCategoryBindObjectSerializer(serializers.Serializer): raise RemovedBindingObjectNotFound() attrs['related_object'] = news_type return attrs + + +class ChosenTagSerializer(serializers.ModelSerializer): + tag = TagBackOfficeSerializer(read_only=True) + + class Meta: + model = models.ChosenTag + fields = [ + 'id', + 'tag', + ] + + +class ChosenTagBindObjectSerializer(serializers.Serializer): + """Serializer for binding chosen tag and objects""" + + feature_id = serializers.IntegerField() + + def validate(self, attrs): + view = self.context.get('view') + request = self.context.get('request') + + obj_id = attrs.get('feature_id') + + tag = view.get_object() + attrs['tag'] = tag + + feature = Feature.objects.filter(pk=obj_id). \ + first() + if not feature: + raise BindingObjectNotFound() + if request.method == 'DELETE' and not feature. \ + chosen_tags.filter(tag=tag). \ + exists(): + raise RemovedBindingObjectNotFound() + attrs['related_object'] = feature + + return attrs diff --git a/apps/tag/views.py b/apps/tag/views.py index 2b8eb4ef..4f1cd9ba 100644 --- a/apps/tag/views.py +++ b/apps/tag/views.py @@ -1,14 +1,15 @@ """Tag views.""" from django.conf import settings +from django.contrib.contenttypes.models import ContentType +from django.utils.translation import gettext_lazy as _ from rest_framework import generics, mixins, permissions, status, viewsets from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.serializers import ValidationError -from django.utils.translation import gettext_lazy as _ -from search_indexes import views as search_views from location.models import WineRegion from product.models import ProductType +from search_indexes import views as search_views from tag import filters, models, serializers @@ -292,6 +293,8 @@ class BindObjectMixin: def get_serializer_class(self): if self.action == 'bind_object': return self.bind_object_serializer_class + elif self.action == 'chosen': + return self.chosen_serializer_class return self.serializer_class def perform_binding(self, serializer): @@ -311,6 +314,17 @@ class BindObjectMixin: self.perform_unbinding(serializer) return Response(status=status.HTTP_204_NO_CONTENT) + @action(methods=['post', 'delete'], detail=True, url_path='chosen') + def chosen(self, request, pk=None): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + if request.method == 'POST': + self.perform_binding(serializer) + return Response(serializer.data, status=status.HTTP_201_CREATED) + elif request.method == 'DELETE': + self.perform_unbinding(serializer) + return Response(status=status.HTTP_204_NO_CONTENT) + class TagBackOfficeViewSet(mixins.ListModelMixin, mixins.CreateModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, @@ -322,12 +336,27 @@ class TagBackOfficeViewSet(mixins.ListModelMixin, mixins.CreateModelMixin, queryset = models.Tag.objects.all() serializer_class = serializers.TagBackOfficeSerializer bind_object_serializer_class = serializers.TagBindObjectSerializer + chosen_serializer_class = serializers.ChosenTagBindObjectSerializer def perform_binding(self, serializer): data = serializer.validated_data tag = data.pop('tag') obj_type = data.get('type') related_object = data.get('related_object') + + # for compatible exist code + if self.action == 'chosen': + obj_type = ContentType.objects.get_for_model(models.ChosenTag) + models.ChosenTag.objects.update_or_create( + tag=tag, + content_type=obj_type, + object_id=related_object.id, + defaults={ + "content_object": related_object, + "site": self.request.user.last_country + }, + ) + if obj_type == self.bind_object_serializer_class.ESTABLISHMENT: tag.establishments.add(related_object) elif obj_type == self.bind_object_serializer_class.NEWS: @@ -338,6 +367,11 @@ class TagBackOfficeViewSet(mixins.ListModelMixin, mixins.CreateModelMixin, tag = data.pop('tag') obj_type = data.get('type') related_object = data.get('related_object') + + # for compatible exist code + if self.action == 'chosen': + related_object.chosen_tags.filter(tag=tag).delete() + if obj_type == self.bind_object_serializer_class.ESTABLISHMENT: tag.establishments.remove(related_object) elif obj_type == self.bind_object_serializer_class.NEWS: