Backoffice Tag&TagCategory

This commit is contained in:
evgeniy-st 2019-10-18 18:02:24 +03:00
parent 7be7315cef
commit 21e3f76f5a
9 changed files with 272 additions and 29 deletions

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, label = TJSONField(blank=True, null=True, default=None,
verbose_name=_('label'), verbose_name=_('label'),
help_text='{"en-GB":"some text"}') 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', null=True, related_name='tags',
verbose_name=_('Category')) verbose_name=_('Category'))
@ -36,6 +36,10 @@ class TagCategoryQuerySet(models.QuerySet):
"""Select related objects.""" """Select related objects."""
return self.prefetch_related('tags') return self.prefetch_related('tags')
def with_extended_related(self):
"""Select related objects."""
return self.select_related('country')
def for_news(self): def for_news(self):
"""Select tag categories for news.""" """Select tag categories for news."""
return self.filter(news_types__isnull=True) return self.filter(news_types__isnull=True)

View File

@ -1,6 +1,11 @@
"""Tag serializers.""" """Tag serializers."""
from rest_framework import serializers from rest_framework import serializers
from establishment.models import (Establishment, EstablishmentType,
EstablishmentSubType)
from news.models import News, NewsType
from tag import models from tag import models
from utils.exceptions import (ObjectAlreadyAdded, BindingObjectNotFound,
RemovedBindingObjectNotFound)
from utils.serializers import TranslatedField from utils.serializers import TranslatedField
@ -49,14 +54,109 @@ class TagCategoryBaseSerializer(serializers.ModelSerializer):
class TagCategoryBackOfficeDetailSerializer(TagCategoryBaseSerializer): class TagCategoryBackOfficeDetailSerializer(TagCategoryBaseSerializer):
"""Tag Category detail serializer for back-office users."""
country_translated = TranslatedField(source='country.name_translated') country_translated = TranslatedField(source='country.name_translated')
class Meta(TagBaseSerializer.Meta): class Meta(TagCategoryBaseSerializer.Meta):
"""Meta class.""" """Meta class."""
fields = TagCategoryBaseSerializer.Meta.fields + ( fields = TagCategoryBaseSerializer.Meta.fields + (
'news_types', 'label',
'country', 'country',
'country_translated', '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):
obj_type = attrs.get('type')
obj_id = attrs.get('object_id')
request = self.context.get('request')
view = self.context.get('view')
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):
obj_type = attrs.get('type')
obj_id = attrs.get('object_id')
view = self.context.get('view')
tag_category = view.get_object()
attrs['tag_category'] = tag_category
request = self.context.get('request')
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:
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.""" """Urlconf for app tag."""
from django.urls import path
from rest_framework.routers import SimpleRouter from rest_framework.routers import SimpleRouter
from tag import views from tag import views
app_name = 'tag' app_name = 'tag'
router = SimpleRouter() router = SimpleRouter()
router.register(r'', views.TagViewSet) router.register(r'categories', views.TagCategoryBackOfficeViewSet)
router.register(r'', views.TagBackOfficeViewSet)
urlpatterns = router.urls
urlpatterns = [
path('category/', views.TagCategoryListCreateView.as_view(), name='category-list-create'),
]
urlpatterns += router.urls

View File

@ -1,19 +1,12 @@
"""Tag views.""" """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 tag import filters, models, serializers
from rest_framework import permissions from rest_framework import permissions
class TagViewSet(mixins.ListModelMixin, mixins.CreateModelMixin, # User`s views & viewsets
mixins.UpdateModelMixin, mixins.DestroyModelMixin,
viewsets.GenericViewSet):
"""List/create tag view."""
pagination_class = None
queryset = models.Tag.objects.all()
serializer_class = serializers.TagBackOfficeSerializer
class TagCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): class TagCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
"""ViewSet for TagCategory model.""" """ViewSet for TagCategory model."""
@ -23,3 +16,96 @@ class TagCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
queryset = models.TagCategory.objects.with_tags().with_base_related().\ queryset = models.TagCategory.objects.with_tags().with_base_related().\
distinct() distinct()
serializer_class = serializers.TagCategoryBaseSerializer 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, title = models.CharField(max_length=255,
verbose_name=_('Language title')) verbose_name=_('Language title'))
locale = models.CharField(max_length=10, locale = models.CharField(max_length=10, unique=True,
verbose_name=_('Locale identifier')) verbose_name=_('Locale identifier'))
objects = LanguageQuerySet.as_manager() objects = LanguageQuerySet.as_manager()

View File

@ -1,5 +1,5 @@
from django.utils.translation import gettext_lazy as _ 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): class ProjectBaseException(exceptions.APIException):
@ -142,3 +142,24 @@ class PasswordResetRequestExistedError(exceptions.APIException):
""" """
status_code = status.HTTP_400_BAD_REQUEST status_code = status.HTTP_400_BAD_REQUEST
default_detail = _('Password reset request is already exists and valid.') 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' app_name = 'back'
urlpatterns = [ 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('account/', include('account.urls.back')),
path('comment/', include('comment.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')),
] ]