Merge branch 'feature/gm-192' into feature/gm-148

This commit is contained in:
evgeniy-st 2019-10-21 10:31:06 +03:00
commit 2dbe65a169
17 changed files with 439 additions and 54 deletions

View File

@ -10,6 +10,10 @@ 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')
est_type = filters.ChoiceFilter(choices=models.EstablishmentType.INDEX_NAME_TYPES,
method='by_type')
est_subtype = filters.ChoiceFilter(choices=models.EstablishmentSubType.INDEX_NAME_TYPES,
method='by_subtype')
class Meta:
"""Meta class."""
@ -19,6 +23,8 @@ class EstablishmentFilter(filters.FilterSet):
'tag_id',
'award_id',
'search',
'est_type',
'est_subtype',
)
def search_text(self, queryset, name, value):
@ -27,6 +33,12 @@ class EstablishmentFilter(filters.FilterSet):
return queryset.search(value, locale=self.request.locale)
return queryset
def by_type(self, queryset, name, value):
return queryset.by_type(value)
def by_subtype(self, queryset, name, value):
return queryset.by_subtype(value)
class EstablishmentTypeTagFilter(filters.FilterSet):
"""Establishment tag filter set."""

View File

@ -0,0 +1,35 @@
# Generated by Django 2.2.4 on 2019-10-16 11:33
from django.db import migrations, models
def fill_establishment_type(apps, schema_editor):
# We can't import the Person model directly as it may be a newer
# version than this migration expects. We use the historical version.
EstablishmentType = apps.get_model('establishment', 'EstablishmentType')
for n, et in enumerate(EstablishmentType.objects.all()):
et.index_name = f'Type {n}'
et.save()
class Migration(migrations.Migration):
dependencies = [
('establishment', '0037_auto_20191015_1404'),
]
operations = [
migrations.AddField(
model_name='establishmenttype',
name='index_name',
field=models.CharField(blank=True, db_index=True, max_length=50, null=True, unique=True, default=None, verbose_name='Index name'),
),
migrations.RunPython(fill_establishment_type, migrations.RunPython.noop),
migrations.AlterField(
model_name='establishmenttype',
name='index_name',
field=models.CharField(choices=[('restaurant', 'Restaurant'), ('artisan', 'Artisan'),
('producer', 'Producer')], db_index=True, max_length=50,
unique=True, verbose_name='Index name'),
),
]

View File

@ -0,0 +1,35 @@
# Generated by Django 2.2.4 on 2019-10-18 13:47
from django.db import migrations, models
def fill_establishment_subtype(apps, schema_editor):
# We can't import the Person model directly as it may be a newer
# version than this migration expects. We use the historical version.
EstablishmentSubType = apps.get_model('establishment', 'EstablishmentSubType')
for n, et in enumerate(EstablishmentSubType.objects.all()):
et.index_name = f'Type {n}'
et.save()
class Migration(migrations.Migration):
dependencies = [
('establishment', '0038_establishmenttype_index_name'),
]
operations = [
migrations.AddField(
model_name='establishmentsubtype',
name='index_name',
field=models.CharField(blank=True, db_index=True, max_length=50, null=True, unique=True, default=None, verbose_name='Index name'),
),
migrations.RunPython(fill_establishment_subtype),
migrations.AlterField(
model_name='establishmentsubtype',
name='index_name',
field=models.CharField(choices=[('winery', 'Winery'), ], db_index=True, max_length=50,
unique=True, verbose_name='Index name'),
),
]

View File

@ -30,8 +30,22 @@ class EstablishmentType(TranslatedFieldsMixin, ProjectBaseMixin):
STR_FIELD_NAME = 'name'
# 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,
verbose_name=_('Index name'))
use_subtypes = models.BooleanField(_('Use subtypes'), default=True)
tag_categories = models.ManyToManyField('tag.TagCategory',
related_name='establishment_types',
@ -57,8 +71,18 @@ class EstablishmentSubTypeManager(models.Manager):
class EstablishmentSubType(ProjectBaseMixin, TranslatedFieldsMixin):
"""Establishment type model."""
# 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,
verbose_name=_('Index name'))
establishment_type = models.ForeignKey(EstablishmentType,
on_delete=models.CASCADE,
verbose_name=_('Type'))
@ -93,6 +117,9 @@ class EstablishmentQuerySet(models.QuerySet):
'phones').\
prefetch_actual_employees()
def with_type_related(self):
return self.prefetch_related('establishment_subtypes')
def search(self, value, locale=None):
"""Search text in JSON fields."""
if locale is not None:
@ -240,6 +267,31 @@ class EstablishmentQuerySet(models.QuerySet):
kwargs = {unit: radius}
return self.filter(address__coordinates__distance_lte=(center, DistanceMeasure(**kwargs)))
def artisans(self):
"""Return artisans."""
return self.filter(establishment_type__index_name=EstablishmentType.ARTISAN)
def producers(self):
"""Return producers."""
return self.filter(establishment_type__index_name=EstablishmentType.PRODUCER)
def restaurants(self):
"""Return restaurants."""
return self.filter(establishment_type__index_name=EstablishmentType.RESTAURANT)
def wineries(self):
"""Return wineries."""
return self.producers().filter(
establishment_subtypes__index_name=EstablishmentSubType.WINERY)
def by_type(self, value):
"""Return QuerySet with type by value."""
return self.filter(establishment_type__index_name=value)
def by_subtype(self, value):
"""Return QuerySet with subtype by value."""
return self.filter(establishment_subtypes__index_name=value)
class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
"""Establishment model."""

View File

@ -9,6 +9,7 @@ urlpatterns = [
path('', views.EstablishmentListView.as_view(), name='list'),
path('recent-reviews/', views.EstablishmentRecentReviewListView.as_view(),
name='recent-reviews'),
# path('wineries/', views.WineriesListView.as_view(), name='wineries-list'),
path('slug/<slug:slug>/', views.EstablishmentRetrieveView.as_view(), name='detail'),
path('slug/<slug:slug>/similar/', views.EstablishmentSimilarListView.as_view(), name='similar'),
path('slug/<slug:slug>/comments/', views.EstablishmentCommentListView.as_view(), name='list-comments'),

View File

@ -174,3 +174,14 @@ class EstablishmentNearestRetrieveView(EstablishmentListView, generics.ListAPIVi
return qs.by_distance_from_point(**{k: v for k, v in filter_kwargs.items()
if v is not None})
return qs
# Wineries
# todo: find out about difference between subtypes data
# class WineriesListView(EstablishmentListView):
# """Return list establishments with type Wineries"""
#
# def get_queryset(self):
# """Overridden get_queryset method."""
# qs = super(WineriesListView, self).get_queryset()
# return qs.with_type_related().wineries()

View File

@ -40,12 +40,8 @@ def update_document(sender, **kwargs):
for establishment in establishments:
registry.update(establishment)
if app_label == 'main':
if model_name == 'metadata':
establishments = Establishment.objects.filter(tags__metadata=instance)
for establishment in establishments:
registry.update(establishment)
if model_name == 'metadatacontent':
if app_label == 'tag':
if model_name == 'tag':
establishments = Establishment.objects.filter(tags=instance)
for establishment in establishments:
registry.update(establishment)
@ -70,12 +66,8 @@ def update_news(sender, **kwargs):
for news in qs:
registry.update(news)
if app_label == 'main':
if model_name == 'metadata':
qs = News.objects.filter(tags__metadata=instance)
for news in qs:
registry.update(news)
if model_name == 'metadatacontent':
if app_label == 'tag':
if model_name == 'tag':
qs = News.objects.filter(tags=instance)
for news in qs:
registry.update(news)

View File

@ -1,5 +1,6 @@
"""Tag app filters."""
from django_filters import rest_framework as filters
from establishment.models import EstablishmentType
from tag import models
@ -18,16 +19,9 @@ class TagCategoryFilterSet(filters.FilterSet):
type = filters.MultipleChoiceFilter(choices=TYPE_CHOICES,
method='filter_by_type')
# Establishment type choices
RESTAURANT = 'restaurant'
ESTABLISHMENT_TYPE_CHOICES = (
(RESTAURANT, 'restaurant'),
)
establishment_type = filters.MultipleChoiceFilter(
choices=ESTABLISHMENT_TYPE_CHOICES,
method='filter_by_establishment_type')
establishment_type = filters.ChoiceFilter(
choices=EstablishmentType.INDEX_NAME_TYPES,
method='by_establishment_type')
class Meta:
"""Meta class."""
@ -44,5 +38,5 @@ class TagCategoryFilterSet(filters.FilterSet):
return queryset
# todo: filter by establishment type
def filter_by_establishment_type(self, queryset, name, value):
return queryset.for_establishments()
def by_establishment_type(self, queryset, name, value):
return queryset.by_establishment_type(value)

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.4 on 2019-10-18 07:58
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('tag', '0002_auto_20191009_1408'),
]
operations = [
migrations.AlterField(
model_name='tag',
name='category',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tags', to='tag.TagCategory', verbose_name='Category'),
),
]

View File

@ -11,7 +11,7 @@ class Tag(TranslatedFieldsMixin, models.Model):
label = TJSONField(blank=True, null=True, default=None,
verbose_name=_('label'),
help_text='{"en-GB":"some text"}')
category = models.ForeignKey('TagCategory', on_delete=models.PROTECT,
category = models.ForeignKey('TagCategory', on_delete=models.CASCADE,
null=True, related_name='tags',
verbose_name=_('Category'))
# chosen tags should have non-null priority,other tags priority should be null
@ -38,6 +38,10 @@ class TagCategoryQuerySet(models.QuerySet):
"""Select related objects."""
return self.prefetch_related('tags')
def with_extended_related(self):
"""Select related objects."""
return self.select_related('country')
def for_news(self):
"""Select tag categories for news."""
return self.filter(news_types__isnull=True)
@ -47,6 +51,10 @@ class TagCategoryQuerySet(models.QuerySet):
return self.filter(models.Q(establishment_types__isnull=False) |
models.Q(establishment_subtypes__isnull=False))
def by_establishment_type(self, index_name):
"""Filter by establishment type index name."""
return self.filter(establishment_types__index_name=index_name)
def with_tags(self, switcher=True):
"""Filter by existing tags."""
return self.exclude(tags__isnull=switcher)

View File

@ -1,6 +1,11 @@
"""Tag serializers."""
from rest_framework import serializers
from establishment.models import (Establishment, EstablishmentType,
EstablishmentSubType)
from news.models import News, NewsType
from tag import models
from utils.exceptions import (ObjectAlreadyAdded, BindingObjectNotFound,
RemovedBindingObjectNotFound)
from utils.serializers import TranslatedField
@ -49,14 +54,114 @@ class TagCategoryBaseSerializer(serializers.ModelSerializer):
class TagCategoryBackOfficeDetailSerializer(TagCategoryBaseSerializer):
"""Tag Category detail serializer for back-office users."""
country_translated = TranslatedField(source='country.name_translated')
class Meta(TagBaseSerializer.Meta):
class Meta(TagCategoryBaseSerializer.Meta):
"""Meta class."""
fields = TagCategoryBaseSerializer.Meta.fields + (
'news_types',
'label',
'country',
'country_translated',
)
class TagBindObjectSerializer(serializers.Serializer):
"""Serializer for binding tag category and objects"""
ESTABLISHMENT = 'establishment'
NEWS = 'news'
TYPE_CHOICES = (
(ESTABLISHMENT, 'Establishment type'),
(NEWS, 'News type'),
)
type = serializers.ChoiceField(TYPE_CHOICES)
object_id = serializers.IntegerField()
def validate(self, attrs):
view = self.context.get('view')
request = self.context.get('request')
obj_type = attrs.get('type')
obj_id = attrs.get('object_id')
tag = view.get_object()
attrs['tag'] = tag
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:
raise BindingObjectNotFound()
if request.method == 'POST' and tag.news.filter(pk=news.pk).exists():
raise ObjectAlreadyAdded()
if request.method == 'DELETE' and not tag.news.filter(
pk=news.pk).exists():
raise RemovedBindingObjectNotFound()
attrs['related_object'] = news
return attrs
class TagCategoryBindObjectSerializer(serializers.Serializer):
"""Serializer for binding tag category and objects"""
ESTABLISHMENT_TYPE = 'establishment_type'
NEWS_TYPE = 'news_type'
TYPE_CHOICES = (
(ESTABLISHMENT_TYPE, 'Establishment type'),
(NEWS_TYPE, 'News type'),
)
type = serializers.ChoiceField(TYPE_CHOICES)
object_id = serializers.IntegerField()
def validate(self, attrs):
view = self.context.get('view')
request = self.context.get('request')
obj_type = attrs.get('type')
obj_id = attrs.get('object_id')
tag_category = view.get_object()
attrs['tag_category'] = tag_category
if obj_type == self.ESTABLISHMENT_TYPE:
establishment_type = EstablishmentType.objects.filter(pk=obj_id).\
first()
if not establishment_type:
raise BindingObjectNotFound()
if request.method == 'POST' and tag_category.establishment_types.\
filter(pk=establishment_type.pk).exists():
raise ObjectAlreadyAdded()
if request.method == 'DELETE' and not tag_category.\
establishment_types.filter(pk=establishment_type.pk).\
exists():
raise RemovedBindingObjectNotFound()
attrs['related_object'] = establishment_type
elif obj_type == self.NEWS_TYPE:
news_type = NewsType.objects.filter(pk=obj_id).first()
if not news_type:
raise BindingObjectNotFound()
if request.method == 'POST' and tag_category.news_types.\
filter(pk=news_type.pk).exists():
raise ObjectAlreadyAdded()
if request.method == 'DELETE' and not tag_category.news_types.\
filter(pk=news_type.pk).exists():
raise RemovedBindingObjectNotFound()
attrs['related_object'] = news_type
return attrs

View File

@ -1,16 +1,11 @@
"""Urlconf for app tag."""
from django.urls import path
from rest_framework.routers import SimpleRouter
from tag import views
app_name = 'tag'
router = SimpleRouter()
router.register(r'', views.TagViewSet)
router.register(r'categories', views.TagCategoryBackOfficeViewSet)
router.register(r'', views.TagBackOfficeViewSet)
urlpatterns = [
path('category/', views.TagCategoryListCreateView.as_view(), name='category-list-create'),
]
urlpatterns += router.urls
urlpatterns = router.urls

View File

@ -1,24 +1,111 @@
"""Tag views."""
from rest_framework import viewsets, mixins
from rest_framework import viewsets, mixins, status
from rest_framework.decorators import action
from rest_framework.response import Response
from tag import filters, models, serializers
from rest_framework import permissions
class TagViewSet(mixins.ListModelMixin, mixins.CreateModelMixin,
mixins.UpdateModelMixin, mixins.DestroyModelMixin,
viewsets.GenericViewSet):
"""List/create tag view."""
pagination_class = None
queryset = models.Tag.objects.all()
serializer_class = serializers.TagBackOfficeSerializer
# User`s views & viewsets
class TagCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
"""ViewSet for TagCategory model."""
filterset_class = filters.TagCategoryFilterSet
pagination_class = None
permission_classes = (permissions.AllowAny, )
queryset = models.TagCategory.objects.with_tags().with_base_related()
queryset = models.TagCategory.objects.with_tags().with_base_related().\
distinct()
serializer_class = serializers.TagCategoryBaseSerializer
# BackOffice user`s views & viewsets
class BindObjectMixin:
"""Bind object mixin."""
def get_serializer_class(self):
if self.action == 'bind_object':
return self.bind_object_serializer_class
return self.serializer_class
def perform_binding(self, serializer):
raise NotImplemented
def perform_unbinding(self, serializer):
raise NotImplemented
@action(methods=['post', 'delete'], detail=True, url_path='bind-object')
def bind_object(self, request, pk=None):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
if request.method == 'POST':
self.perform_binding(serializer)
return Response(serializer.data, status=status.HTTP_201_CREATED)
elif request.method == 'DELETE':
self.perform_unbinding(serializer)
return Response(status=status.HTTP_204_NO_CONTENT)
class TagBackOfficeViewSet(mixins.ListModelMixin, mixins.CreateModelMixin,
mixins.UpdateModelMixin, mixins.DestroyModelMixin,
BindObjectMixin, viewsets.GenericViewSet):
"""List/create tag view."""
pagination_class = None
permission_classes = (permissions.IsAuthenticated, )
queryset = models.Tag.objects.all()
serializer_class = serializers.TagBackOfficeSerializer
bind_object_serializer_class = serializers.TagBindObjectSerializer
def perform_binding(self, serializer):
data = serializer.validated_data
tag = data.pop('tag')
obj_type = data.get('type')
related_object = data.get('related_object')
if obj_type == self.bind_object_serializer_class.ESTABLISHMENT:
tag.establishments.add(related_object)
elif obj_type == self.bind_object_serializer_class.NEWS:
tag.news.add(related_object)
def perform_unbinding(self, serializer):
data = serializer.validated_data
tag = data.pop('tag')
obj_type = data.get('type')
related_object = data.get('related_object')
if obj_type == self.bind_object_serializer_class.ESTABLISHMENT:
tag.establishments.remove(related_object)
elif obj_type == self.bind_object_serializer_class.NEWS:
tag.news.remove(related_object)
class TagCategoryBackOfficeViewSet(mixins.CreateModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.RetrieveModelMixin,
BindObjectMixin,
TagCategoryViewSet):
"""ViewSet for TagCategory model for BackOffice users."""
permission_classes = (permissions.IsAuthenticated, )
queryset = TagCategoryViewSet.queryset.with_extended_related()
serializer_class = serializers.TagCategoryBackOfficeDetailSerializer
bind_object_serializer_class = serializers.TagCategoryBindObjectSerializer
def perform_binding(self, serializer):
data = serializer.validated_data
tag_category = data.pop('tag_category')
obj_type = data.get('type')
related_object = data.get('related_object')
if obj_type == self.bind_object_serializer_class.ESTABLISHMENT_TYPE:
tag_category.establishment_types.add(related_object)
elif obj_type == self.bind_object_serializer_class.NEWS_TYPE:
tag_category.news_types.add(related_object)
def perform_unbinding(self, serializer):
data = serializer.validated_data
tag_category = data.pop('tag_category')
obj_type = data.get('type')
related_object = data.get('related_object')
if obj_type == self.bind_object_serializer_class.ESTABLISHMENT_TYPE:
tag_category.establishment_types.remove(related_object)
elif obj_type == self.bind_object_serializer_class.NEWS_TYPE:
tag_category.news_types.remove(related_object)

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-10-18 08:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('translation', '0003_auto_20190901_1032'),
]
operations = [
migrations.AlterField(
model_name='language',
name='locale',
field=models.CharField(max_length=10, unique=True, verbose_name='Locale identifier'),
),
]

View File

@ -22,7 +22,7 @@ class Language(models.Model):
title = models.CharField(max_length=255,
verbose_name=_('Language title'))
locale = models.CharField(max_length=10,
locale = models.CharField(max_length=10, unique=True,
verbose_name=_('Locale identifier'))
objects = LanguageQuerySet.as_manager()

View File

@ -1,5 +1,5 @@
from django.utils.translation import gettext_lazy as _
from rest_framework import exceptions, status
from rest_framework import exceptions, serializers, status
class ProjectBaseException(exceptions.APIException):
@ -142,3 +142,24 @@ class PasswordResetRequestExistedError(exceptions.APIException):
"""
status_code = status.HTTP_400_BAD_REQUEST
default_detail = _('Password reset request is already exists and valid.')
class ObjectAlreadyAdded(serializers.ValidationError):
"""
The exception must be thrown if the object has already been added to the
list.
"""
default_detail = _('Object has already been added.')
class BindingObjectNotFound(serializers.ValidationError):
"""The exception must be thrown if the object not found."""
default_detail = _('Binding object not found.')
class RemovedBindingObjectNotFound(serializers.ValidationError):
"""The exception must be thrown if the object not found."""
default_detail = _('Removed binding object not found.')

View File

@ -3,11 +3,11 @@ from django.urls import path, include
app_name = 'back'
urlpatterns = [
path('gallery/', include(('gallery.urls', 'gallery'), namespace='gallery')),
path('establishments/', include('establishment.urls.back')),
path('location/', include('location.urls.back')),
path('news/', include('news.urls.back')),
# path('tags/', include(('tag.urls.back', 'tag'), namespace='tag')),
path('account/', include('account.urls.back')),
path('comment/', include('comment.urls.back')),
path('establishments/', include('establishment.urls.back')),
path('gallery/', include(('gallery.urls', 'gallery'), namespace='gallery')),
path('location/', include('location.urls.back')),
path('news/', include('news.urls.back')),
path('tags/', include(('tag.urls.back', 'tag'), namespace='tag')),
]