Merge branch 'develop' into migration_fix

This commit is contained in:
evgeniy-st 2019-10-30 11:50:30 +03:00
commit e371fdceea
48 changed files with 1043 additions and 160 deletions

View File

@ -243,6 +243,20 @@ class User(AbstractUser):
template_name=settings.CHANGE_EMAIL_TEMPLATE, template_name=settings.CHANGE_EMAIL_TEMPLATE,
context=context) context=context)
@property
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)
@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)
class UserRole(ProjectBaseMixin): class UserRole(ProjectBaseMixin):
"""UserRole model.""" """UserRole model."""

View File

@ -6,4 +6,4 @@ from rest_framework import serializers
class CommentBaseSerializer(serializers.ModelSerializer): class CommentBaseSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = models.Comment model = models.Comment
fields = ('id', 'text', 'mark', 'user') fields = ('id', 'text', 'mark', 'user', 'object_id', 'content_type')

View File

@ -30,18 +30,48 @@ class CommentModeratorPermissionTests(BasePermissionTests):
) )
self.userRole.save() self.userRole.save()
content_type = ContentType.objects.get(app_label='location', model='country') self.content_type = ContentType.objects.get(app_label='location', model='country')
self.user_test = get_tokens_for_user() self.user_test = get_tokens_for_user()
self.comment = Comment.objects.create(text='Test comment', mark=1, self.comment = Comment.objects.create(text='Test comment', mark=1,
user=self.user_test["user"], user=self.user_test["user"],
object_id= self.country_ru.pk, object_id=self.country_ru.pk,
content_type_id=content_type.id, content_type_id=self.content_type.id,
country=self.country_ru country=self.country_ru
) )
self.comment.save() self.comment.save()
self.url = reverse('back:comment:comment-crud', kwargs={"id": self.comment.id}) self.url = reverse('back:comment:comment-crud', kwargs={"id": self.comment.id})
def test_post(self):
self.url = reverse('back:comment:comment-list-create')
comment = {
"text": "Test comment POST",
"user": self.user_test["user"].id,
"object_id": self.country_ru.pk,
"content_type": self.content_type.id,
"country_id": self.country_ru.id
}
response = self.client.post(self.url, format='json', data=comment)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
comment = {
"text": "Test comment POST moder",
"user": self.moderator.id,
"object_id": self.country_ru.id,
"content_type": self.content_type.id,
"country_id": self.country_ru.id
}
tokens = User.create_jwt_tokens(self.moderator)
self.client.cookies = SimpleCookie(
{'access_token': tokens.get('access_token'),
'refresh_token': tokens.get('access_token')})
response = self.client.post(self.url, format='json', data=comment)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
def test_put_moderator(self): def test_put_moderator(self):
tokens = User.create_jwt_tokens(self.moderator) tokens = User.create_jwt_tokens(self.moderator)
self.client.cookies = SimpleCookie( self.client.cookies = SimpleCookie(
@ -52,7 +82,9 @@ class CommentModeratorPermissionTests(BasePermissionTests):
"id": self.comment.id, "id": self.comment.id,
"text": "test text moderator", "text": "test text moderator",
"mark": 1, "mark": 1,
"user": self.moderator.id "user": self.moderator.id,
"object_id": self.comment.country_id,
"content_type": self.content_type.id
} }
response = self.client.put(self.url, data=data, format='json') response = self.client.put(self.url, data=data, format='json')
@ -60,7 +92,7 @@ class CommentModeratorPermissionTests(BasePermissionTests):
def test_get(self): def test_get(self):
response = self.client.get(self.url, format='json') response = self.client.get(self.url, format='json')
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_put_other_user(self): def test_put_other_user(self):
other_user = User.objects.create_user(username='test', other_user = User.objects.create_user(username='test',
@ -99,9 +131,10 @@ class CommentModeratorPermissionTests(BasePermissionTests):
"id": self.comment.id, "id": self.comment.id,
"text": "test text moderator", "text": "test text moderator",
"mark": 1, "mark": 1,
"user": super_user.id "user": super_user.id,
"object_id": self.country_ru.id,
"content_type": self.content_type.id,
} }
response = self.client.put(self.url, data=data, format='json') response = self.client.put(self.url, data=data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)

View File

@ -8,12 +8,13 @@ class CommentLstView(generics.ListCreateAPIView):
"""Comment list create view.""" """Comment list create view."""
serializer_class = serializers.CommentBaseSerializer serializer_class = serializers.CommentBaseSerializer
queryset = models.Comment.objects.all() queryset = models.Comment.objects.all()
permission_classes = [permissions.IsAuthenticatedOrReadOnly,] permission_classes = [permissions.IsAuthenticatedOrReadOnly| IsCommentModerator|IsCountryAdmin]
class CommentRUDView(generics.RetrieveUpdateDestroyAPIView): class CommentRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Comment RUD view.""" """Comment RUD view."""
serializer_class = serializers.CommentBaseSerializer serializer_class = serializers.CommentBaseSerializer
queryset = models.Comment.objects.all() queryset = models.Comment.objects.all()
permission_classes = [IsCountryAdmin|IsCommentModerator]
permission_classes = [IsCountryAdmin | IsCommentModerator]
lookup_field = 'id' lookup_field = 'id'

View File

@ -7,6 +7,7 @@ from comment.models import Comment
from utils.admin import BaseModelAdminMixin from utils.admin import BaseModelAdminMixin
from establishment import models from establishment import models
from main.models import Award from main.models import Award
from product.models import Product
from review import models as review_models from review import models as review_models
@ -47,13 +48,19 @@ class CommentInline(GenericTabularInline):
extra = 0 extra = 0
class ProductInline(admin.TabularInline):
model = Product
extra = 0
@admin.register(models.Establishment) @admin.register(models.Establishment)
class EstablishmentAdmin(BaseModelAdminMixin, admin.ModelAdmin): class EstablishmentAdmin(BaseModelAdminMixin, admin.ModelAdmin):
"""Establishment admin.""" """Establishment admin."""
list_display = ['id', '__str__', 'image_tag', ] list_display = ['id', '__str__', 'image_tag', ]
inlines = [ inlines = [
AwardInline, ContactPhoneInline, ContactEmailInline, AwardInline, ContactPhoneInline, ContactEmailInline,
ReviewInline, CommentInline] ReviewInline, CommentInline, ProductInline]
raw_id_fields = ('address',) raw_id_fields = ('address',)
fields = ['old_id', 'name'] fields = ['old_id', 'name']

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-10-24 14:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('establishment', '0043_establishment_currency'),
]
operations = [
migrations.AddField(
model_name='position',
name='index_name',
field=models.CharField(db_index=True, max_length=255, null=True, unique=True, verbose_name='Index name'),
),
]

View File

@ -121,6 +121,11 @@ class EstablishmentQuerySet(models.QuerySet):
def with_type_related(self): def with_type_related(self):
return self.prefetch_related('establishment_subtypes') return self.prefetch_related('establishment_subtypes')
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').\
prefetch_related('tags', 'schedule')
def search(self, value, locale=None): def search(self, value, locale=None):
"""Search text in JSON fields.""" """Search text in JSON fields."""
if locale is not None: if locale is not None:
@ -244,14 +249,12 @@ class EstablishmentQuerySet(models.QuerySet):
def annotate_in_favorites(self, user): def annotate_in_favorites(self, user):
"""Annotate flag in_favorites""" """Annotate flag in_favorites"""
favorite_establishments = [] favorite_establishment_ids = []
if user.is_authenticated: if user.is_authenticated:
favorite_establishments = user.favorites.by_content_type(app_label='establishment', favorite_establishment_ids = user.favorite_establishment_ids
model='establishment') \
.values_list('object_id', flat=True)
return self.annotate(in_favorites=Case( return self.annotate(in_favorites=Case(
When( When(
id__in=favorite_establishments, id__in=favorite_establishment_ids,
then=True), then=True),
default=False, default=False,
output_field=models.BooleanField(default=False))) output_field=models.BooleanField(default=False)))
@ -483,6 +486,11 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
""" """
return self.id return self.id
@property
def wines(self):
"""Return list products with type wine"""
return self.products.wines()
class Position(BaseAttributes, TranslatedFieldsMixin): class Position(BaseAttributes, TranslatedFieldsMixin):
"""Position model.""" """Position model."""
@ -494,6 +502,9 @@ class Position(BaseAttributes, TranslatedFieldsMixin):
priority = models.IntegerField(unique=True, null=True, default=None) priority = models.IntegerField(unique=True, null=True, default=None)
index_name = models.CharField(max_length=255, db_index=True, unique=True,
null=True, verbose_name=_('Index name'))
class Meta: class Meta:
"""Meta class.""" """Meta class."""

View File

@ -5,15 +5,14 @@ from rest_framework import serializers
from comment import models as comment_models from comment import models as comment_models
from comment.serializers import common as comment_serializers from comment.serializers import common as comment_serializers
from establishment import models from establishment import models
from favorites.models import Favorites
from location.serializers import AddressBaseSerializer from location.serializers import AddressBaseSerializer
from main.serializers import AwardSerializer, CurrencySerializer from main.serializers import AwardSerializer, CurrencySerializer
from review import models as review_models from review import models as review_models
from tag.serializers import TagBaseSerializer from tag.serializers import TagBaseSerializer
from timetable.serialziers import ScheduleRUDSerializer from timetable.serialziers import ScheduleRUDSerializer
from utils import exceptions as utils_exceptions from utils import exceptions as utils_exceptions
from utils.serializers import ProjectModelSerializer from utils.serializers import (ProjectModelSerializer, TranslatedField,
from utils.serializers import TranslatedField FavoritesCreateSerializer)
class ContactPhonesSerializer(serializers.ModelSerializer): class ContactPhonesSerializer(serializers.ModelSerializer):
@ -118,6 +117,18 @@ class EstablishmentTypeBaseSerializer(serializers.ModelSerializer):
'use_subtypes': {'write_only': True}, 'use_subtypes': {'write_only': True},
} }
class EstablishmentTypeGeoSerializer(EstablishmentTypeBaseSerializer):
"""Serializer for EstablishmentType model w/ index_name."""
class Meta(EstablishmentTypeBaseSerializer.Meta):
fields = EstablishmentTypeBaseSerializer.Meta.fields + [
'index_name'
]
extra_kwargs = {
**EstablishmentTypeBaseSerializer.Meta.extra_kwargs,
'index_name': {'read_only': True},
}
class EstablishmentSubTypeBaseSerializer(serializers.ModelSerializer): class EstablishmentSubTypeBaseSerializer(serializers.ModelSerializer):
"""Serializer for EstablishmentSubType models.""" """Serializer for EstablishmentSubType models."""
@ -147,12 +158,13 @@ class EstablishmentEmployeeSerializer(serializers.ModelSerializer):
position_translated = serializers.CharField(source='position.name_translated') position_translated = serializers.CharField(source='position.name_translated')
awards = AwardSerializer(source='employee.awards', many=True) awards = AwardSerializer(source='employee.awards', many=True)
priority = serializers.IntegerField(source='position.priority') priority = serializers.IntegerField(source='position.priority')
position_index_name = serializers.CharField(source='position.index_name')
class Meta: class Meta:
"""Meta class.""" """Meta class."""
model = models.Employee model = models.Employee
fields = ('id', 'name', 'position_translated', 'awards', 'priority') fields = ('id', 'name', 'position_translated', 'awards', 'priority', 'position_index_name')
class EstablishmentBaseSerializer(ProjectModelSerializer): class EstablishmentBaseSerializer(ProjectModelSerializer):
@ -184,6 +196,19 @@ class EstablishmentBaseSerializer(ProjectModelSerializer):
] ]
class EstablishmentGeoSerializer(EstablishmentBaseSerializer):
"""Serializer for Geo view."""
type = EstablishmentTypeGeoSerializer(source='establishment_type', read_only=True)
class Meta(EstablishmentBaseSerializer.Meta):
"""Meta class."""
fields = EstablishmentBaseSerializer.Meta.fields + [
'type'
]
class EstablishmentDetailSerializer(EstablishmentBaseSerializer): class EstablishmentDetailSerializer(EstablishmentBaseSerializer):
"""Serializer for Establishment model.""" """Serializer for Establishment model."""
@ -280,26 +305,13 @@ class EstablishmentCommentRUDSerializer(comment_serializers.CommentSerializer):
] ]
class EstablishmentFavoritesCreateSerializer(serializers.ModelSerializer): class EstablishmentFavoritesCreateSerializer(FavoritesCreateSerializer):
"""Create comment serializer""" """Serializer to favorite object w/ model Establishment."""
class Meta:
"""Serializer for model Comment"""
model = Favorites
fields = [
'id',
'created',
]
def get_user(self):
"""Get user from request"""
return self.context.get('request').user
def validate(self, attrs): def validate(self, attrs):
"""Override validate method""" """Overridden validate method"""
# Check establishment object # Check establishment object
establishment_slug = self.context.get('request').parser_context.get('kwargs').get('slug') establishment_qs = models.Establishment.objects.filter(slug=self.slug)
establishment_qs = models.Establishment.objects.filter(slug=establishment_slug)
# Check establishment obj by slug from lookup_kwarg # Check establishment obj by slug from lookup_kwarg
if not establishment_qs.exists(): if not establishment_qs.exists():
@ -308,18 +320,16 @@ class EstablishmentFavoritesCreateSerializer(serializers.ModelSerializer):
establishment = establishment_qs.first() establishment = establishment_qs.first()
# Check existence in favorites # Check existence in favorites
if self.get_user().favorites.by_content_type(app_label='establishment', if establishment.favorites.filter(user=self.user).exists():
model='establishment')\
.by_object_id(object_id=establishment.id).exists():
raise utils_exceptions.FavoritesError() raise utils_exceptions.FavoritesError()
attrs['establishment'] = establishment attrs['establishment'] = establishment
return attrs return attrs
def create(self, validated_data, *args, **kwargs): def create(self, validated_data, *args, **kwargs):
"""Override create method""" """Overridden create method"""
validated_data.update({ validated_data.update({
'user': self.get_user(), 'user': self.user,
'content_object': validated_data.pop('establishment') 'content_object': validated_data.pop('establishment')
}) })
return super().create(validated_data) return super().create(validated_data)

View File

@ -1,10 +1,15 @@
"""Establishment app tasks.""" """Establishment app tasks."""
import logging import logging
from celery import shared_task 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 establishment import models from establishment import models
from location.models import Country from location.models import Country
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -12,10 +17,15 @@ logger = logging.getLogger(__name__)
def recalculate_price_levels_by_country(country_id): def recalculate_price_levels_by_country(country_id):
try: try:
country = Country.objects.get(pk=country_id) country = Country.objects.get(pk=country_id)
except Country.DoesNotExist as ex: except Country.DoesNotExist as _:
logger.error(f'ESTABLISHMENT. Country does not exist. ID {country_id}') logger.error(f'ESTABLISHMENT. Country does not exist. ID {country_id}')
else: else:
qs = models.Establishment.objects.filter(address__city__country=country) qs = models.Establishment.objects.filter(address__city__country=country)
for establishment in qs: for establishment in qs:
establishment.recalculate_price_level(low_price=country.low_price, establishment.recalculate_price_level(low_price=country.low_price,
high_price=country.high_price) high_price=country.high_price)
@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)

View File

@ -9,7 +9,6 @@ urlpatterns = [
path('', views.EstablishmentListView.as_view(), name='list'), path('', views.EstablishmentListView.as_view(), name='list'),
path('recent-reviews/', views.EstablishmentRecentReviewListView.as_view(), path('recent-reviews/', views.EstablishmentRecentReviewListView.as_view(),
name='recent-reviews'), 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>/', views.EstablishmentRetrieveView.as_view(), name='detail'),
path('slug/<slug:slug>/similar/', views.EstablishmentSimilarListView.as_view(), name='similar'), path('slug/<slug:slug>/similar/', views.EstablishmentSimilarListView.as_view(), name='similar'),
path('slug/<slug:slug>/comments/', views.EstablishmentCommentListView.as_view(), name='list-comments'), path('slug/<slug:slug>/comments/', views.EstablishmentCommentListView.as_view(), name='list-comments'),
@ -18,5 +17,5 @@ urlpatterns = [
path('slug/<slug:slug>/comments/<int:comment_id>/', views.EstablishmentCommentRUDView.as_view(), path('slug/<slug:slug>/comments/<int:comment_id>/', views.EstablishmentCommentRUDView.as_view(),
name='rud-comment'), name='rud-comment'),
path('slug/<slug:slug>/favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(), path('slug/<slug:slug>/favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(),
name='add-to-favorites') name='create-destroy-favorites')
] ]

View File

@ -138,21 +138,18 @@ class EstablishmentFavoritesCreateDestroyView(generics.CreateAPIView, generics.D
""" """
Returns the object the view is displaying. Returns the object the view is displaying.
""" """
establishment_obj = get_object_or_404(models.Establishment, establishment = get_object_or_404(models.Establishment,
slug=self.kwargs['slug']) slug=self.kwargs['slug'])
obj = get_object_or_404( favorites = get_object_or_404(establishment.favorites.filter(user=self.request.user))
self.request.user.favorites.by_content_type(app_label='establishment',
model='establishment')
.by_object_id(object_id=establishment_obj.pk))
# May raise a permission denied # May raise a permission denied
self.check_object_permissions(self.request, obj) self.check_object_permissions(self.request, favorites)
return obj return favorites
class EstablishmentNearestRetrieveView(EstablishmentListView, generics.ListAPIView): class EstablishmentNearestRetrieveView(EstablishmentListView, generics.ListAPIView):
"""Resource for getting list of nearest establishments.""" """Resource for getting list of nearest establishments."""
serializer_class = serializers.EstablishmentBaseSerializer serializer_class = serializers.EstablishmentGeoSerializer
filter_class = filters.EstablishmentFilter filter_class = filters.EstablishmentFilter
def get_queryset(self): def get_queryset(self):
@ -170,14 +167,3 @@ class EstablishmentNearestRetrieveView(EstablishmentListView, generics.ListAPIVi
return qs.by_distance_from_point(**{k: v for k, v in filter_kwargs.items() return qs.by_distance_from_point(**{k: v for k, v in filter_kwargs.items()
if v is not None}) if v is not None})
return qs 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

@ -8,4 +8,6 @@ app_name = 'favorites'
urlpatterns = [ urlpatterns = [
path('establishments/', views.FavoritesEstablishmentListView.as_view(), path('establishments/', views.FavoritesEstablishmentListView.as_view(),
name='establishment-list'), name='establishment-list'),
path('products/', views.FavoritesProductListView.as_view(),
name='product-list'),
] ]

View File

@ -1,7 +1,11 @@
"""Views for app favorites.""" """Views for app favorites."""
from rest_framework import generics from rest_framework import generics
from establishment.models import Establishment from establishment.models import Establishment
from establishment.filters import EstablishmentFilter
from establishment.serializers import EstablishmentBaseSerializer from establishment.serializers import EstablishmentBaseSerializer
from product.models import Product
from product.serializers import ProductBaseSerializer
from product.filters import ProductFilterSet
from .models import Favorites from .models import Favorites
@ -14,11 +18,24 @@ class FavoritesBaseView(generics.GenericAPIView):
class FavoritesEstablishmentListView(generics.ListAPIView): class FavoritesEstablishmentListView(generics.ListAPIView):
"""List views for favorites""" """List views for establishments in favorites."""
serializer_class = EstablishmentBaseSerializer serializer_class = EstablishmentBaseSerializer
filter_class = EstablishmentFilter
def get_queryset(self): def get_queryset(self):
"""Override get_queryset method""" """Override get_queryset method"""
return Establishment.objects.filter(favorites__user=self.request.user)\ return Establishment.objects.filter(favorites__user=self.request.user)\
.order_by('-favorites') .order_by('-favorites')
class FavoritesProductListView(generics.ListAPIView):
"""List views for products in favorites."""
serializer_class = ProductBaseSerializer
filter_class = ProductFilterSet
def get_queryset(self):
"""Override get_queryset method"""
return Product.objects.filter(favorites__user=self.request.user)\
.order_by('-favorites')

View File

@ -19,6 +19,22 @@ class CityAdmin(admin.ModelAdmin):
"""City admin.""" """City admin."""
class WineAppellationInline(admin.TabularInline):
model = models.WineAppellation
extra = 0
@admin.register(models.WineRegion)
class WineRegionAdmin(admin.ModelAdmin):
"""WineRegion admin."""
inlines = [WineAppellationInline, ]
@admin.register(models.WineAppellation)
class WineAppellationAdmin(admin.ModelAdmin):
"""WineAppellation admin."""
@admin.register(models.Address) @admin.register(models.Address)
class AddressAdmin(admin.OSMGeoAdmin): class AddressAdmin(admin.OSMGeoAdmin):
"""Address admin.""" """Address admin."""

View File

@ -0,0 +1,41 @@
# Generated by Django 2.2.4 on 2019-10-28 07:27
from django.db import migrations, models
import django.db.models.deletion
import utils.models
class Migration(migrations.Migration):
dependencies = [
('location', '0012_data_migrate'),
]
operations = [
migrations.CreateModel(
name='WineRegion',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', utils.models.TJSONField(help_text='{"en-GB":"some text"}', verbose_name='Name')),
('country', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='location.Country', verbose_name='country')),
],
options={
'verbose_name': 'wine region',
'verbose_name_plural': 'wine regions',
},
bases=(utils.models.TranslatedFieldsMixin, models.Model),
),
migrations.CreateModel(
name='WineAppellation',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', utils.models.TJSONField(help_text='{"en-GB":"some text"}', verbose_name='Name')),
('wine_region', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='appellations', to='location.WineRegion', verbose_name='wine region')),
],
options={
'verbose_name': 'wine appellation',
'verbose_name_plural': 'wine appellations',
},
bases=(utils.models.TranslatedFieldsMixin, models.Model),
),
]

View File

@ -134,6 +134,49 @@ class Address(models.Model):
return self.city.country_id return self.city.country_id
class WineRegionQuerySet(models.QuerySet):
"""Wine region queryset."""
class WineRegion(TranslatedFieldsMixin, models.Model):
"""Wine region model."""
STR_FIELD_NAME = 'name'
name = TJSONField(verbose_name=_('Name'),
help_text='{"en-GB":"some text"}')
country = models.ForeignKey(Country, on_delete=models.PROTECT,
verbose_name=_('country'))
objects = WineRegionQuerySet.as_manager()
class Meta:
"""Meta class."""
verbose_name_plural = _('wine regions')
verbose_name = _('wine region')
class WineAppellationQuerySet(models.QuerySet):
"""Wine appellation queryset."""
class WineAppellation(TranslatedFieldsMixin, models.Model):
"""Wine appellation model."""
STR_FIELD_NAME = 'name'
name = TJSONField(verbose_name=_('Name'),
help_text='{"en-GB":"some text"}')
wine_region = models.ForeignKey(WineRegion, on_delete=models.PROTECT,
related_name='appellations',
verbose_name=_('wine region'))
objects = WineAppellationQuerySet.as_manager()
class Meta:
"""Meta class."""
verbose_name_plural = _('wine appellations')
verbose_name = _('wine appellation')
# todo: Make recalculate price levels # todo: Make recalculate price levels
@receiver(post_save, sender=Country) @receiver(post_save, sender=Country)
def run_recalculate_price_levels(sender, instance, **kwargs): def run_recalculate_price_levels(sender, instance, **kwargs):

View File

@ -16,4 +16,5 @@ class CountryBackSerializer(common.CountrySerializer):
'code', 'code',
'svg_image', 'svg_image',
'name', 'name',
'country_id'
] ]

View File

@ -148,3 +148,31 @@ class AddressDetailSerializer(AddressBaseSerializer):
'city_id', 'city_id',
'city', 'city',
) )
class WineAppellationBaseSerializer(serializers.ModelSerializer):
"""Wine appellations."""
name_translated = TranslatedField()
class Meta:
"""Meta class."""
model = models.WineAppellation
fields = [
'id',
'name_translated',
]
class WineRegionBaseSerializer(serializers.ModelSerializer):
"""Wine region serializer."""
name_translated = TranslatedField()
country = CountrySerializer()
class Meta:
"""Meta class."""
model = models.WineRegion
fields = [
'id',
'name_translated',
'country',
]

View File

@ -20,6 +20,7 @@ class BaseTestCase(APITestCase):
username=self.username, email=self.email, password=self.password) username=self.username, email=self.email, password=self.password)
tokens = User.create_jwt_tokens(self.user) tokens = User.create_jwt_tokens(self.user)
self.client.cookies = SimpleCookie( self.client.cookies = SimpleCookie(
{'access_token': tokens.get('access_token'), {'access_token': tokens.get('access_token'),
'refresh_token': tokens.get('refresh_token')}) 'refresh_token': tokens.get('refresh_token')})

View File

@ -4,44 +4,48 @@ from rest_framework import generics
from location import models, serializers from location import models, serializers
from location.views import common from location.views import common
from utils.permissions import IsCountryAdmin from utils.permissions import IsCountryAdmin
from rest_framework.permissions import IsAuthenticatedOrReadOnly
# Address # Address
class AddressListCreateView(common.AddressViewMixin, generics.ListCreateAPIView): class AddressListCreateView(common.AddressViewMixin, generics.ListCreateAPIView):
"""Create view for model Address.""" """Create view for model Address."""
serializer_class = serializers.AddressDetailSerializer serializer_class = serializers.AddressDetailSerializer
queryset = models.Address.objects.all() queryset = models.Address.objects.all()
permission_classes = [IsCountryAdmin] permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
class AddressRUDView(common.AddressViewMixin, generics.RetrieveUpdateDestroyAPIView): class AddressRUDView(common.AddressViewMixin, generics.RetrieveUpdateDestroyAPIView):
"""RUD view for model Address.""" """RUD view for model Address."""
serializer_class = serializers.AddressDetailSerializer serializer_class = serializers.AddressDetailSerializer
queryset = models.Address.objects.all() queryset = models.Address.objects.all()
permission_classes = [IsCountryAdmin] permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
# City # City
class CityListCreateView(common.CityViewMixin, generics.ListCreateAPIView): class CityListCreateView(common.CityViewMixin, generics.ListCreateAPIView):
"""Create view for model City.""" """Create view for model City."""
serializer_class = serializers.CitySerializer serializer_class = serializers.CitySerializer
permission_classes = [IsCountryAdmin] permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
class CityRUDView(common.CityViewMixin, generics.RetrieveUpdateDestroyAPIView): class CityRUDView(common.CityViewMixin, generics.RetrieveUpdateDestroyAPIView):
"""RUD view for model City.""" """RUD view for model City."""
serializer_class = serializers.CitySerializer serializer_class = serializers.CitySerializer
permission_classes = [IsCountryAdmin] permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
# Region # Region
class RegionListCreateView(common.RegionViewMixin, generics.ListCreateAPIView): class RegionListCreateView(common.RegionViewMixin, generics.ListCreateAPIView):
"""Create view for model Region""" """Create view for model Region"""
serializer_class = serializers.RegionSerializer serializer_class = serializers.RegionSerializer
permission_classes = [IsCountryAdmin] permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
class RegionRUDView(common.RegionViewMixin, generics.RetrieveUpdateDestroyAPIView): class RegionRUDView(common.RegionViewMixin, generics.RetrieveUpdateDestroyAPIView):
"""Retrieve view for model Region""" """Retrieve view for model Region"""
serializer_class = serializers.RegionSerializer serializer_class = serializers.RegionSerializer
permission_classes = [IsCountryAdmin] permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
# Country # Country
@ -50,10 +54,11 @@ class CountryListCreateView(generics.ListCreateAPIView):
queryset = models.Country.objects.all() queryset = models.Country.objects.all()
serializer_class = serializers.CountryBackSerializer serializer_class = serializers.CountryBackSerializer
pagination_class = None pagination_class = None
permission_classes = [IsCountryAdmin] permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
class CountryRUDView(generics.RetrieveUpdateDestroyAPIView): class CountryRUDView(generics.RetrieveUpdateDestroyAPIView):
"""RUD view for model Country.""" """RUD view for model Country."""
serializer_class = serializers.CountryBackSerializer serializer_class = serializers.CountryBackSerializer
permission_classes = [IsCountryAdmin] permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
queryset = models.Country.objects.all() queryset = models.Country.objects.all()

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-10-25 12:41
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('gallery', '0003_auto_20191003_1228'),
('news', '0028_auto_20191024_1649'),
]
operations = [
migrations.AlterUniqueTogether(
name='newsgallery',
unique_together={('news', 'image'), ('news', 'is_main')},
),
]

View File

@ -236,4 +236,4 @@ class NewsGallery(models.Model):
"""NewsGallery meta class.""" """NewsGallery meta class."""
verbose_name = _('news gallery') verbose_name = _('news gallery')
verbose_name_plural = _('news galleries') verbose_name_plural = _('news galleries')
unique_together = ('news', 'is_main') unique_together = (('news', 'is_main'), ('news', 'image'))

View File

@ -97,6 +97,7 @@ class CropImageSerializer(serializers.Serializer):
class NewsImageSerializer(serializers.ModelSerializer): class NewsImageSerializer(serializers.ModelSerializer):
"""Serializer for returning crop images of news image.""" """Serializer for returning crop images of news image."""
orientation_display = serializers.CharField(source='get_orientation_display', orientation_display = serializers.CharField(source='get_orientation_display',
read_only=True) read_only=True)
original_url = serializers.URLField(source='image.url') original_url = serializers.URLField(source='image.url')
@ -149,6 +150,17 @@ class NewsBaseSerializer(ProjectModelSerializer):
) )
class NewsSimilarListSerializer(NewsBaseSerializer):
"""List serializer for News model."""
preview_image_url = serializers.URLField()
class Meta(NewsBaseSerializer.Meta):
"""Meta class."""
fields = NewsBaseSerializer.Meta.fields + (
'preview_image_url',
)
class NewsListSerializer(NewsBaseSerializer): class NewsListSerializer(NewsBaseSerializer):
"""List serializer for News model.""" """List serializer for News model."""
@ -191,8 +203,8 @@ class NewsDetailSerializer(NewsBaseSerializer):
class NewsDetailWebSerializer(NewsDetailSerializer): class NewsDetailWebSerializer(NewsDetailSerializer):
"""News detail serializer for web users..""" """News detail serializer for web users.."""
same_theme = NewsBaseSerializer(many=True, read_only=True) same_theme = NewsSimilarListSerializer(many=True, read_only=True)
should_read = NewsBaseSerializer(many=True, read_only=True) should_read = NewsSimilarListSerializer(many=True, read_only=True)
agenda = AgendaSerializer() agenda = AgendaSerializer()
banner = NewsBannerSerializer() banner = NewsBannerSerializer()
@ -265,7 +277,6 @@ class NewsBackOfficeGallerySerializer(serializers.ModelSerializer):
"""Override validate method.""" """Override validate method."""
news_pk = self.get_request_kwargs().get('pk') news_pk = self.get_request_kwargs().get('pk')
image_id = self.get_request_kwargs().get('image_id') image_id = self.get_request_kwargs().get('image_id')
is_main = attrs.get('is_main')
news_qs = models.News.objects.filter(pk=news_pk) news_qs = models.News.objects.filter(pk=news_pk)
image_qs = Image.objects.filter(id=image_id) image_qs = Image.objects.filter(id=image_id)
@ -278,12 +289,6 @@ class NewsBackOfficeGallerySerializer(serializers.ModelSerializer):
news = news_qs.first() news = news_qs.first()
image = image_qs.first() image = image_qs.first()
if news.news_gallery.filter(image=image).exists():
raise serializers.ValidationError({'detail': _('Image is already added')})
if is_main and news.news_gallery.main_image().exists():
raise serializers.ValidationError({'detail': _('Main image is already added')})
attrs['news'] = news attrs['news'] = news
attrs['image'] = image attrs['image'] = image

View File

@ -66,6 +66,22 @@ class NewsTestCase(BaseTestCase):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
def test_news_post(self):
test_news ={
"title": {"en-GB": "Test news POST"},
"news_type_id": self.test_news_type.id,
"description": {"en-GB": "Description test news"},
"start": datetime.now() + timedelta(hours=-2),
"end": datetime.now() + timedelta(hours=2),
"state": News.PUBLISHED,
"slug": 'test-news-slug_post',
"country_id": self.country_ru.id,
}
url = reverse("back:news:list-create")
response = self.client.post(url, data=test_news, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
def test_web_news(self): def test_web_news(self):
response = self.client.get(reverse('web:news:list')) response = self.client.get(reverse('web:news:list'))
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)

View File

@ -119,7 +119,7 @@ class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView,
return gallery return gallery
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
"""Override create method""" """Overridden create method"""
super().create(request, *args, **kwargs) super().create(request, *args, **kwargs)
return Response(status=status.HTTP_201_CREATED) return Response(status=status.HTTP_201_CREATED)

18
apps/product/admin.py Normal file
View File

@ -0,0 +1,18 @@
"""Product admin conf."""
from django.contrib import admin
from .models import Product, ProductType, ProductSubType
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
"""Admin page for model Product."""
@admin.register(ProductType)
class ProductTypeAdmin(admin.ModelAdmin):
"""Admin page for model ProductType."""
@admin.register(ProductSubType)
class ProductSubTypeAdmin(admin.ModelAdmin):
"""Admin page for model ProductSubType."""

34
apps/product/filters.py Normal file
View File

@ -0,0 +1,34 @@
"""Filters for app Product."""
from django.core.validators import EMPTY_VALUES
from django_filters import rest_framework as filters
from product import models
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)
class Meta:
"""Meta class."""
model = models.Product
fields = [
'establishment_id',
'product_type',
'product_subtype',
]
def by_product_type(self, queryset, name, value):
if value not in EMPTY_VALUES:
return queryset.by_product_type(value)
return queryset
def by_product_subtype(self, queryset, name, value):
if value not in EMPTY_VALUES:
return queryset.by_product_subtype(value)
return queryset

View File

@ -0,0 +1,94 @@
# Generated by Django 2.2.4 on 2019-10-28 07:27
from django.conf import settings
import django.contrib.postgres.fields.jsonb
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import utils.models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('location', '0013_wineappellation_wineregion'),
('establishment', '0044_position_index_name'),
]
operations = [
migrations.CreateModel(
name='ProductType',
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', utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='Name')),
('index_name', models.CharField(choices=[('food', 'Food'), ('wine', 'Wine'), ('liquor', 'Liquor')], db_index=True, max_length=50, unique=True, verbose_name='Index name')),
('use_subtypes', models.BooleanField(default=True, verbose_name='Use subtypes')),
],
options={
'verbose_name': 'Product type',
'verbose_name_plural': 'Product types',
},
bases=(utils.models.TranslatedFieldsMixin, models.Model),
),
migrations.CreateModel(
name='ProductSubType',
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', utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='Name')),
('index_name', models.CharField(choices=[('rum', 'Rum'), ('other', 'Other')], db_index=True, max_length=50, unique=True, verbose_name='Index name')),
('product_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subtypes', to='product.ProductType', verbose_name='Product type')),
],
options={
'verbose_name': 'Product subtype',
'verbose_name_plural': 'Product subtypes',
},
bases=(utils.models.TranslatedFieldsMixin, models.Model),
),
migrations.CreateModel(
name='Product',
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')),
('category', models.PositiveIntegerField(choices=[(0, 'Common'), (1, 'Online')], default=0)),
('name', utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='Name')),
('description', utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='Description')),
('characteristics', django.contrib.postgres.fields.jsonb.JSONField(verbose_name='Characteristics')),
('available', models.BooleanField(default=True, verbose_name='Available')),
('public_mark', models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='public mark')),
('country', models.ManyToManyField(to='location.Country', verbose_name='Country')),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='product_records_created', to=settings.AUTH_USER_MODEL, verbose_name='created by')),
('establishment', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='products', to='establishment.Establishment', verbose_name='establishment')),
('modified_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='product_records_modified', to=settings.AUTH_USER_MODEL, verbose_name='modified by')),
('product_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='products', to='product.ProductType', verbose_name='Type')),
('subtypes', models.ManyToManyField(blank=True, related_name='products', to='product.ProductSubType', verbose_name='Subtypes')),
('wine_appellation', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='location.WineAppellation', verbose_name='wine appellation')),
('wine_region', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='wines', to='location.WineRegion', verbose_name='wine region')),
],
options={
'verbose_name': 'Product',
'verbose_name_plural': 'Products',
},
bases=(utils.models.TranslatedFieldsMixin, models.Model),
),
migrations.CreateModel(
name='OnlineProduct',
fields=[
],
options={
'verbose_name': 'Online product',
'verbose_name_plural': 'Online products',
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('product.product',),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-10-29 14:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='product',
name='slug',
field=models.SlugField(max_length=255, null=True, unique=True, verbose_name='Establishment slug'),
),
]

View File

@ -1,5 +1,7 @@
"""Product app models.""" """Product app models."""
from django.db import models from django.db import models
from django.contrib.contenttypes import fields as generic
from django.core.exceptions import ValidationError
from django.contrib.postgres.fields import JSONField from django.contrib.postgres.fields import JSONField
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from utils.models import (BaseAttributes, ProjectBaseMixin, from utils.models import (BaseAttributes, ProjectBaseMixin,
@ -9,9 +11,23 @@ from utils.models import (BaseAttributes, ProjectBaseMixin,
class ProductType(TranslatedFieldsMixin, ProjectBaseMixin): class ProductType(TranslatedFieldsMixin, ProjectBaseMixin):
"""ProductType model.""" """ProductType model."""
STR_FIELD_NAME = 'name'
# INDEX NAME CHOICES
FOOD = 'food'
WINE = 'wine'
LIQUOR = 'liquor'
INDEX_NAME_TYPES = (
(FOOD, _('Food')),
(WINE, _('Wine')),
(LIQUOR, _('Liquor')),
)
name = TJSONField(blank=True, null=True, default=None, name = TJSONField(blank=True, null=True, default=None,
verbose_name=_('Name'), help_text='{"en-GB":"some text"}') verbose_name=_('Name'), help_text='{"en-GB":"some text"}')
index_name = models.CharField(max_length=50, unique=True, db_index=True, index_name = models.CharField(max_length=50, choices=INDEX_NAME_TYPES,
unique=True, db_index=True,
verbose_name=_('Index name')) verbose_name=_('Index name'))
use_subtypes = models.BooleanField(_('Use subtypes'), default=True) use_subtypes = models.BooleanField(_('Use subtypes'), default=True)
@ -25,19 +41,35 @@ class ProductType(TranslatedFieldsMixin, ProjectBaseMixin):
class ProductSubType(TranslatedFieldsMixin, ProjectBaseMixin): class ProductSubType(TranslatedFieldsMixin, ProjectBaseMixin):
"""ProductSubtype model.""" """ProductSubtype model."""
STR_FIELD_NAME = 'name'
# INDEX NAME CHOICES
RUM = 'rum'
OTHER = 'other'
INDEX_NAME_TYPES = (
(RUM, _('Rum')),
(OTHER, _('Other')),
)
product_type = models.ForeignKey(ProductType, on_delete=models.CASCADE, product_type = models.ForeignKey(ProductType, on_delete=models.CASCADE,
related_name='subtypes', related_name='subtypes',
verbose_name=_('Product type')) verbose_name=_('Product type'))
name = TJSONField(blank=True, null=True, default=None, name = TJSONField(blank=True, null=True, default=None,
verbose_name=_('Name'), help_text='{"en-GB":"some text"}') verbose_name=_('Name'), help_text='{"en-GB":"some text"}')
index_name = models.CharField(max_length=50, unique=True, db_index=True, index_name = models.CharField(max_length=50, choices=INDEX_NAME_TYPES,
unique=True, db_index=True,
verbose_name=_('Index name')) verbose_name=_('Index name'))
class Meta: class Meta:
"""Meta class.""" """Meta class."""
verbose_name = _('Product type') verbose_name = _('Product subtype')
verbose_name_plural = _('Product types') verbose_name_plural = _('Product subtypes')
def clean_fields(self, exclude=None):
if not self.product_type.use_subtypes:
raise ValidationError(_('Product type is not use subtypes.'))
class ProductManager(models.Manager): class ProductManager(models.Manager):
@ -47,16 +79,33 @@ class ProductManager(models.Manager):
class ProductQuerySet(models.QuerySet): class ProductQuerySet(models.QuerySet):
"""Product queryset.""" """Product queryset."""
def with_base_related(self):
return self.select_related('product_type', 'establishment') \
.prefetch_related('product_type__subtypes', 'country')
def common(self): def common(self):
return self.filter(category=self.model.COMMON) return self.filter(category=self.model.COMMON)
def online(self): def online(self):
return self.filter(category=self.model.ONLINE) return self.filter(category=self.model.ONLINE)
def wines(self):
return self.filter(type__index_name=ProductType.WINE)
def by_product_type(self, product_type: str):
"""Filter by type."""
return self.filter(product_type__index_name=product_type)
def by_product_subtype(self, product_subtype: str):
"""Filter by subtype."""
return self.filter(subtypes__index_name=product_subtype)
class Product(TranslatedFieldsMixin, BaseAttributes): class Product(TranslatedFieldsMixin, BaseAttributes):
"""Product models.""" """Product models."""
STR_FIELD_NAME = 'name'
COMMON = 0 COMMON = 0
ONLINE = 1 ONLINE = 1
@ -72,13 +121,30 @@ class Product(TranslatedFieldsMixin, BaseAttributes):
description = TJSONField(_('Description'), null=True, blank=True, description = TJSONField(_('Description'), null=True, blank=True,
default=None, help_text='{"en-GB":"some text"}') default=None, help_text='{"en-GB":"some text"}')
characteristics = JSONField(_('Characteristics')) characteristics = JSONField(_('Characteristics'))
country = models.ForeignKey('location.Country', on_delete=models.PROTECT, country = models.ManyToManyField('location.Country',
verbose_name=_('Country')) verbose_name=_('Country'))
available = models.BooleanField(_('Available'), default=True) available = models.BooleanField(_('Available'), default=True)
type = models.ForeignKey(ProductType, on_delete=models.PROTECT, product_type = models.ForeignKey(ProductType, on_delete=models.PROTECT,
related_name='products', verbose_name=_('Type')) related_name='products', verbose_name=_('Type'))
subtypes = models.ManyToManyField(ProductSubType, related_name='products', subtypes = models.ManyToManyField(ProductSubType, blank=True,
related_name='products',
verbose_name=_('Subtypes')) verbose_name=_('Subtypes'))
establishment = models.ForeignKey('establishment.Establishment',
on_delete=models.PROTECT,
related_name='products',
verbose_name=_('establishment'))
public_mark = models.PositiveIntegerField(blank=True, null=True, default=None,
verbose_name=_('public mark'),)
wine_region = models.ForeignKey('location.WineRegion', on_delete=models.PROTECT,
related_name='wines',
blank=True, null=True,
verbose_name=_('wine region'))
wine_appellation = models.ForeignKey('location.WineAppellation', on_delete=models.PROTECT,
blank=True, null=True,
verbose_name=_('wine appellation'))
slug = models.SlugField(unique=True, max_length=255, null=True,
verbose_name=_('Establishment slug'))
favorites = generic.GenericRelation(to='favorites.Favorites')
objects = ProductManager.from_queryset(ProductQuerySet)() objects = ProductManager.from_queryset(ProductQuerySet)()
@ -88,12 +154,22 @@ class Product(TranslatedFieldsMixin, BaseAttributes):
verbose_name = _('Product') verbose_name = _('Product')
verbose_name_plural = _('Products') verbose_name_plural = _('Products')
def clean_fields(self, exclude=None):
super().clean_fields(exclude=exclude)
if self.product_type.index_name == ProductType.WINE and not self.wine_region:
raise ValidationError(_('wine_region field must be specified.'))
if not self.product_type.index_name == ProductType.WINE and self.wine_region:
raise ValidationError(_('wine_region field must not be specified.'))
if (self.wine_region and self.wine_appellation) and \
self.wine_appellation not in self.wine_region.appellations.all():
raise ValidationError(_('Wine appellation not exists in wine region.'))
class OnlineProductManager(ProductManager): class OnlineProductManager(ProductManager):
"""Extended manger for OnlineProduct model.""" """Extended manger for OnlineProduct model."""
def get_queryset(self): def get_queryset(self):
"""Overrided get_queryset method.""" """Overridden get_queryset method."""
return super().get_queryset().online() return super().get_queryset().online()

View File

@ -0,0 +1,3 @@
from .common import *
from .web import *
from .mobile import *

View File

@ -1 +1,96 @@
"""Product app serializers."""
from rest_framework import serializers from rest_framework import serializers
from utils.serializers import TranslatedField, FavoritesCreateSerializer
from product.models import Product, ProductSubType, ProductType
from utils import exceptions as utils_exceptions
from django.utils.translation import gettext_lazy as _
from location.serializers import (WineRegionBaseSerializer, WineAppellationBaseSerializer,
CountrySimpleSerializer)
class ProductSubTypeBaseSerializer(serializers.ModelSerializer):
"""ProductSubType base serializer"""
name_translated = TranslatedField()
index_name_display = serializers.CharField(source='get_index_name_display')
class Meta:
model = ProductSubType
fields = [
'id',
'name_translated',
'index_name_display',
]
class ProductTypeBaseSerializer(serializers.ModelSerializer):
"""ProductType base serializer"""
name_translated = TranslatedField()
index_name_display = serializers.CharField(source='get_index_name_display')
class Meta:
model = ProductType
fields = [
'id',
'name_translated',
'index_name_display',
]
class ProductBaseSerializer(serializers.ModelSerializer):
"""Product base serializer."""
name_translated = TranslatedField()
description_translated = TranslatedField()
category_display = serializers.CharField(source='get_category_display')
product_type = ProductTypeBaseSerializer()
subtypes = ProductSubTypeBaseSerializer(many=True)
wine_region = WineRegionBaseSerializer(allow_null=True)
wine_appellation = WineAppellationBaseSerializer(allow_null=True)
available_countries = CountrySimpleSerializer(source='country', many=True)
class Meta:
"""Meta class."""
model = Product
fields = [
'id',
'slug',
'name_translated',
'category_display',
'description_translated',
'available',
'product_type',
'subtypes',
'public_mark',
'wine_region',
'wine_appellation',
'available_countries',
]
class ProductFavoritesCreateSerializer(FavoritesCreateSerializer):
"""Serializer to create favorite object w/ model Product."""
def validate(self, attrs):
"""Overridden validate method"""
# Check establishment object
product_qs = Product.objects.filter(slug=self.slug)
# Check establishment obj by slug from lookup_kwarg
if not product_qs.exists():
raise serializers.ValidationError({'detail': _('Object not found.')})
else:
product = product_qs.first()
# Check existence in favorites
if product.favorites.filter(user=self.user).exists():
raise utils_exceptions.FavoritesError()
attrs['product'] = product
return attrs
def create(self, validated_data, *args, **kwargs):
"""Overridden create method"""
validated_data.update({
'user': self.user,
'content_object': validated_data.pop('product')
})
return super().create(validated_data)

View File

@ -0,0 +1,12 @@
"""Product url patterns."""
from django.urls import path
from product import views
app_name = 'product'
urlpatterns = [
path('', views.ProductListView.as_view(), name='list'),
path('slug/<slug:slug>/favorites/', views.CreateFavoriteProductView.as_view(),
name='create-destroy-favorites')
]

View File

@ -0,0 +1,7 @@
"""Product web url patterns."""
from product.urls.common import urlpatterns as common_urlpatterns
urlpatterns = [
]
urlpatterns.extend(common_urlpatterns)

View File

@ -0,0 +1,4 @@
from .back import *
from .common import *
from .mobile import *
from .web import *

View File

@ -0,0 +1,39 @@
"""Product app views."""
from rest_framework import generics, permissions
from django.shortcuts import get_object_or_404
from product.models import Product
from product import serializers
from product import filters
class ProductBaseView(generics.GenericAPIView):
"""Product base view"""
def get_queryset(self):
"""Override get_queryset method."""
return Product.objects.with_base_related()
class ProductListView(ProductBaseView, generics.ListAPIView):
"""List view for model Product."""
permission_classes = (permissions.AllowAny, )
serializer_class = serializers.ProductBaseSerializer
filter_class = filters.ProductFilterSet
class CreateFavoriteProductView(generics.CreateAPIView,
generics.DestroyAPIView):
"""View for create/destroy product in favorites."""
serializer_class = serializers.ProductFavoritesCreateSerializer
lookup_field = 'slug'
def get_object(self):
"""
Returns the object the view is displaying.
"""
product = get_object_or_404(Product, slug=self.kwargs['slug'])
favorites = get_object_or_404(product.favorites.filter(user=self.request.user))
# May raise a permission denied
self.check_object_permissions(self.request, favorites)
return favorites

View File

@ -15,14 +15,12 @@ class RecipeQuerySet(models.QuerySet):
def annotate_in_favorites(self, user): def annotate_in_favorites(self, user):
"""Annotate flag in_favorites""" """Annotate flag in_favorites"""
favorite_establishments = [] favorite_recipe_ids = []
if user.is_authenticated: if user.is_authenticated:
favorite_establishments = user.favorites.by_content_type(app_label='recipe', favorite_recipe_ids = user.favorite_recipe_ids
model='recipe') \
.values_list('object_id', flat=True)
return self.annotate(in_favorites=models.Case( return self.annotate(in_favorites=models.Case(
models.When( models.When(
id__in=favorite_establishments, id__in=favorite_recipe_ids,
then=True), then=True),
default=False, default=False,
output_field=models.BooleanField(default=False))) output_field=models.BooleanField(default=False)))

View File

@ -22,14 +22,13 @@ class EstablishmentDocument(Document):
'id': fields.IntegerField(), 'id': fields.IntegerField(),
'name': fields.ObjectField(attr='name_indexing', 'name': fields.ObjectField(attr='name_indexing',
properties=OBJECT_FIELD_PROPERTIES), properties=OBJECT_FIELD_PROPERTIES),
'index_name': fields.KeywordField(attr='index_name'),
}) })
establishment_subtypes = fields.ObjectField( establishment_subtypes = fields.ObjectField(
properties={ properties={
'id': fields.IntegerField(), 'id': fields.IntegerField(),
'name': fields.ObjectField(attr='name_indexing', 'name': fields.ObjectField(attr='name_indexing'),
properties={ 'index_name': fields.KeywordField(attr='index_name'),
'id': fields.IntegerField(),
}),
}, },
multi=True) multi=True)
works_evening = fields.ListField(fields.IntegerField( works_evening = fields.ListField(fields.IntegerField(
@ -82,15 +81,8 @@ class EstablishmentDocument(Document):
), ),
} }
), ),
} },
) )
# todo: need to fix
# collections = fields.ObjectField(
# properties={
# 'id': fields.IntegerField(attr='collection.id'),
# 'collection_type': fields.IntegerField(attr='collection.collection_type'),
# },
# multi=True)
class Django: class Django:
@ -99,6 +91,7 @@ class EstablishmentDocument(Document):
'id', 'id',
'name', 'name',
'name_translated', 'name_translated',
'is_publish',
'price_level', 'price_level',
'toque_number', 'toque_number',
'public_mark', 'public_mark',
@ -106,4 +99,4 @@ class EstablishmentDocument(Document):
) )
def get_queryset(self): def get_queryset(self):
return super().get_queryset().published() return super().get_queryset().with_es_related()

View File

@ -1,5 +1,6 @@
"""Search indexes serializers.""" """Search indexes serializers."""
from rest_framework import serializers from rest_framework import serializers
from elasticsearch_dsl import AttrDict
from django_elasticsearch_dsl_drf.serializers import DocumentSerializer from django_elasticsearch_dsl_drf.serializers import DocumentSerializer
from news.serializers import NewsTypeSerializer from news.serializers import NewsTypeSerializer
from search_indexes.documents import EstablishmentDocument, NewsDocument from search_indexes.documents import EstablishmentDocument, NewsDocument
@ -13,6 +14,8 @@ class TagsDocumentSerializer(serializers.Serializer):
label_translated = serializers.SerializerMethodField() label_translated = serializers.SerializerMethodField()
def get_label_translated(self, obj): def get_label_translated(self, obj):
if isinstance(obj, dict):
return get_translated_value(obj.get('label'))
return get_translated_value(obj.label) return get_translated_value(obj.label)
@ -29,6 +32,13 @@ class AddressDocumentSerializer(serializers.Serializer):
geo_lon = serializers.FloatField(allow_null=True, source='coordinates.lon') geo_lon = serializers.FloatField(allow_null=True, source='coordinates.lon')
geo_lat = serializers.FloatField(allow_null=True, source='coordinates.lat') geo_lat = serializers.FloatField(allow_null=True, source='coordinates.lat')
# todo: refator
def to_representation(self, instance):
if instance != AttrDict(d={}) or \
(isinstance(instance, dict) and len(instance) != 0):
return super().to_representation(instance)
return None
class ScheduleDocumentSerializer(serializers.Serializer): class ScheduleDocumentSerializer(serializers.Serializer):
"""Schedule serializer for ES Document""" """Schedule serializer for ES Document"""
@ -75,7 +85,7 @@ class NewsDocumentSerializer(DocumentSerializer):
class EstablishmentDocumentSerializer(DocumentSerializer): class EstablishmentDocumentSerializer(DocumentSerializer):
"""Establishment document serializer.""" """Establishment document serializer."""
address = AddressDocumentSerializer() address = AddressDocumentSerializer(allow_null=True)
tags = TagsDocumentSerializer(many=True) tags = TagsDocumentSerializer(many=True)
schedule = ScheduleDocumentSerializer(many=True, allow_null=True) schedule = ScheduleDocumentSerializer(many=True, allow_null=True)

View File

@ -3,12 +3,13 @@ from rest_framework import permissions
from django_elasticsearch_dsl_drf import constants from django_elasticsearch_dsl_drf import constants
from django_elasticsearch_dsl_drf.filter_backends import ( from django_elasticsearch_dsl_drf.filter_backends import (
FilteringFilterBackend, FilteringFilterBackend,
GeoSpatialFilteringFilterBackend GeoSpatialFilteringFilterBackend,
DefaultOrderingFilterBackend,
) )
from django_elasticsearch_dsl_drf.viewsets import BaseDocumentViewSet from django_elasticsearch_dsl_drf.viewsets import BaseDocumentViewSet
from search_indexes import serializers, filters from search_indexes import serializers, filters
from search_indexes.documents import EstablishmentDocument, NewsDocument from search_indexes.documents import EstablishmentDocument, NewsDocument
from utils.pagination import ProjectPageNumberPagination from utils.pagination import ProjectMobilePagination
class NewsDocumentViewSet(BaseDocumentViewSet): class NewsDocumentViewSet(BaseDocumentViewSet):
@ -16,7 +17,7 @@ class NewsDocumentViewSet(BaseDocumentViewSet):
document = NewsDocument document = NewsDocument
lookup_field = 'slug' lookup_field = 'slug'
pagination_class = ProjectPageNumberPagination pagination_class = ProjectMobilePagination
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
serializer_class = serializers.NewsDocumentSerializer serializer_class = serializers.NewsDocumentSerializer
ordering = ('id',) ordering = ('id',)
@ -53,15 +54,23 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
document = EstablishmentDocument document = EstablishmentDocument
lookup_field = 'slug' lookup_field = 'slug'
pagination_class = ProjectPageNumberPagination pagination_class = ProjectMobilePagination
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
serializer_class = serializers.EstablishmentDocumentSerializer serializer_class = serializers.EstablishmentDocumentSerializer
ordering = ('id',)
def get_queryset(self):
qs = super(EstablishmentDocumentViewSet, self).get_queryset()
qs = qs.filter('match', is_publish=True)
if self.request.country_code:
qs = qs.filter('term',
**{'address.city.country.code': self.request.country_code})
return qs
filter_backends = [ filter_backends = [
FilteringFilterBackend, FilteringFilterBackend,
filters.CustomSearchFilterBackend, filters.CustomSearchFilterBackend,
GeoSpatialFilteringFilterBackend, GeoSpatialFilteringFilterBackend,
DefaultOrderingFilterBackend,
] ]
search_fields = { search_fields = {
@ -72,6 +81,7 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
translated_search_fields = ( translated_search_fields = (
'description', 'description',
) )
ordering = 'id'
filter_fields = { filter_fields = {
'slug': 'slug', 'slug': 'slug',
'tag': { 'tag': {
@ -124,6 +134,15 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
'establishment_subtypes': { 'establishment_subtypes': {
'field': 'establishment_subtypes.id' 'field': 'establishment_subtypes.id'
}, },
'type': {
'field': 'establishment_type.index_name'
},
'subtype': {
'field': 'establishment_subtypes.index_name',
'lookups': [
constants.LOOKUP_QUERY_IN,
],
},
'works_noon': { 'works_noon': {
'field': 'works_noon', 'field': 'works_noon',
'lookups': [ 'lookups': [

View File

@ -44,18 +44,19 @@ class TJSONField(JSONField):
def to_locale(language): def to_locale(language):
"""Turn a language name (en-us) into a locale name (en_US).""" """Turn a language name (en-us) into a locale name (en_US)."""
language, _, country = language.lower().partition('-') if language:
if not country: language, _, country = language.lower().partition('-')
return language if not country:
# A language with > 2 characters after the dash only has its first return language
# character after the dash capitalized; e.g. sr-latn becomes sr-Latn. # A language with > 2 characters after the dash only has its first
# A language with 2 characters after the dash has both characters # character after the dash capitalized; e.g. sr-latn becomes sr-Latn.
# capitalized; e.g. en-us becomes en-US. # A language with 2 characters after the dash has both characters
country, _, tail = country.partition('-') # capitalized; e.g. en-us becomes en-US.
country = country.title() if len(country) > 2 else country.upper() country, _, tail = country.partition('-')
if tail: country = country.title() if len(country) > 2 else country.upper()
country += '-' + tail if tail:
return language + '-' + country country += '-' + tail
return language + '-' + country
def translate_field(self, field_name): def translate_field(self, field_name):

View File

@ -20,8 +20,8 @@ class IsAuthenticatedAndTokenIsValid(permissions.BasePermission):
access_token = request.COOKIES.get('access_token') access_token = request.COOKIES.get('access_token')
if user.is_authenticated and access_token: if user.is_authenticated and access_token:
access_token = AccessToken(access_token) access_token = AccessToken(access_token)
valid_tokens = user.access_tokens.valid()\ valid_tokens = user.access_tokens.valid() \
.by_jti(jti=access_token.payload.get('jti')) .by_jti(jti=access_token.payload.get('jti'))
return valid_tokens.exists() return valid_tokens.exists()
else: else:
return False return False
@ -31,13 +31,14 @@ class IsRefreshTokenValid(permissions.BasePermission):
""" """
Check if user has a valid refresh token and authenticated Check if user has a valid refresh token and authenticated
""" """
def has_permission(self, request, view): def has_permission(self, request, view):
"""Check permissions by refresh token and default REST permission IsAuthenticated""" """Check permissions by refresh token and default REST permission IsAuthenticated"""
refresh_token = request.COOKIES.get('refresh_token') refresh_token = request.COOKIES.get('refresh_token')
if refresh_token: if refresh_token:
refresh_token = GMRefreshToken(refresh_token) refresh_token = GMRefreshToken(refresh_token)
refresh_token_qs = JWTRefreshToken.objects.valid()\ refresh_token_qs = JWTRefreshToken.objects.valid() \
.by_jti(jti=refresh_token.payload.get('jti')) .by_jti(jti=refresh_token.payload.get('jti'))
return refresh_token_qs.exists() return refresh_token_qs.exists()
else: else:
return False return False
@ -55,11 +56,15 @@ class IsGuest(permissions.IsAuthenticatedOrReadOnly):
""" """
Object-level permission to only allow owners of an object to edit it. Object-level permission to only allow owners of an object to edit it.
""" """
def has_permission(self, request, view): def has_permission(self, request, view):
return request.user.is_authenticated rules = [
request.user.is_superuser,
request.method in permissions.SAFE_METHODS
]
return any(rules)
def has_object_permission(self, request, view, obj): def has_object_permission(self, request, view, obj):
rules = [ rules = [
request.user.is_superuser, request.user.is_superuser,
request.method in permissions.SAFE_METHODS request.method in permissions.SAFE_METHODS
@ -72,6 +77,21 @@ class IsStandardUser(IsGuest):
Object-level permission to only allow owners of an object to edit it. Object-level permission to only allow owners of an object to edit it.
Assumes the model instance has an `owner` attribute. Assumes the model instance has an `owner` attribute.
""" """
def has_permission(self, request, view):
rules = [
super().has_permission(request, view)
]
# and request.user.email_confirmed,
if hasattr(request, 'user'):
rules = [
request.user.is_authenticated,
super().has_permission(request, view)
]
return any(rules)
def has_object_permission(self, request, view, obj): def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request # Read permissions are allowed to any request
rules = [ rules = [
@ -92,11 +112,29 @@ class IsContentPageManager(IsStandardUser):
Object-level permission to only allow owners of an object to edit it. Object-level permission to only allow owners of an object to edit it.
Assumes the model instance has an `owner` attribute. Assumes the model instance has an `owner` attribute.
""" """
def has_permission(self, request, view):
rules = [
super().has_permission(request, view)
]
# and request.user.email_confirmed,
if hasattr(request, 'user'):
role = Role.objects.filter(role=Role.CONTENT_PAGE_MANAGER,
country_id=request.country_id) \
.first() # 'Comments moderator'
rules = [
UserRole.objects.filter(user=request.user, role=role).exists(),
# and obj.user != request.user,
super().has_permission(request, view)
]
return any(rules)
def has_object_permission(self, request, view, obj): def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request. # Read permissions are allowed to any request.
role = Role.objects.filter(role=Role.CONTENT_PAGE_MANAGER, role = Role.objects.filter(role=Role.CONTENT_PAGE_MANAGER,
country_id=obj.country_id)\ country_id=obj.country_id) \
.first() # 'Comments moderator' .first() # 'Comments moderator'
rules = [ rules = [
@ -112,6 +150,26 @@ class IsCountryAdmin(IsStandardUser):
Object-level permission to only allow owners of an object to edit it. Object-level permission to only allow owners of an object to edit it.
Assumes the model instance has an `owner` attribute. Assumes the model instance has an `owner` attribute.
""" """
def has_permission(self, request, view):
rules = [
super().has_permission(request, view)
]
# and request.user.email_confirmed,
if hasattr(request.data, 'user') and hasattr(request.data, 'country_id'):
# Read permissions are allowed to any request.
role = Role.objects.filter(role=Role.COUNTRY_ADMIN,
country_id=request.data.country_id) \
.first() # 'Comments moderator'
rules = [
UserRole.objects.filter(user=request.user, role=role).exists(),
super().has_permission(request, view)
]
return any(rules)
def has_object_permission(self, request, view, obj): def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request. # Read permissions are allowed to any request.
role = Role.objects.filter(role=Role.COUNTRY_ADMIN, role = Role.objects.filter(role=Role.COUNTRY_ADMIN,
@ -119,9 +177,20 @@ class IsCountryAdmin(IsStandardUser):
.first() # 'Comments moderator' .first() # 'Comments moderator'
rules = [ rules = [
UserRole.objects.filter(user=request.user, role=role).exists(), super().has_object_permission(request, view, obj)
super().has_object_permission(request, view, obj),
] ]
# and request.user.email_confirmed,
if hasattr(request, 'user') and request.user.is_authenticated:
rules = [
UserRole.objects.filter(user=request.user, role=role).exists(),
super().has_object_permission(request, view, obj),
]
if hasattr(request.data, 'user'):
rules = [
UserRole.objects.filter(user=request.data.user, role=role).exists(),
super().has_object_permission(request, view, obj),
]
return any(rules) return any(rules)
@ -131,10 +200,31 @@ class IsCommentModerator(IsStandardUser):
Object-level permission to only allow owners of an object to edit it. Object-level permission to only allow owners of an object to edit it.
Assumes the model instance has an `owner` attribute. Assumes the model instance has an `owner` attribute.
""" """
def has_permission(self, request, view):
rules = [
super().has_permission(request, view)
]
# and request.user.email_confirmed,
if hasattr(request.data, 'user') and hasattr(request.data, 'country_id'):
# Read permissions are allowed to any request.
role = Role.objects.filter(role=Role.COMMENTS_MODERATOR,
country_id=request.data.country_id) \
.first() # 'Comments moderator'
rules = [
UserRole.objects.filter(user=request.user, role=role).exists(),
super().has_permission(request, view)
]
return any(rules)
def has_object_permission(self, request, view, obj): def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request. # Read permissions are allowed to any request.
role = Role.objects.filter(role=Role.COMMENTS_MODERATOR, role = Role.objects.filter(role=Role.COMMENTS_MODERATOR,
country_id=obj.country_id)\ country_id=obj.country_id) \
.first() # 'Comments moderator' .first() # 'Comments moderator'
rules = [ rules = [
@ -147,10 +237,28 @@ class IsCommentModerator(IsStandardUser):
class IsEstablishmentManager(IsStandardUser): class IsEstablishmentManager(IsStandardUser):
def has_object_permission(self, request, view, obj): def has_permission(self, request, view):
role = Role.objects.filter(role=Role.ESTABLISHMENT_MANAGER)\ rules = [
super().has_permission(request, view)
]
# and request.user.email_confirmed,
if hasattr(request.data, 'user') and hasattr(request.data, 'establishment_id'):
role = Role.objects.filter(role=Role.ESTABLISHMENT_MANAGER) \
.first() # 'Comments moderator' .first() # 'Comments moderator'
rules = [
UserRole.objects.filter(user=request.user, role=role,
establishment_id=request.data.establishment_id
).exists(),
super().has_permission(request, view)
]
return any(rules)
def has_object_permission(self, request, view, obj):
role = Role.objects.filter(role=Role.ESTABLISHMENT_MANAGER) \
.first() # 'Comments moderator'
rules = [ rules = [
UserRole.objects.filter(user=request.user, role=role, UserRole.objects.filter(user=request.user, role=role,
establishment_id=obj.establishment_id establishment_id=obj.establishment_id
@ -163,11 +271,28 @@ class IsEstablishmentManager(IsStandardUser):
class IsReviewerManager(IsStandardUser): class IsReviewerManager(IsStandardUser):
def has_object_permission(self, request, view, obj): def has_permission(self, request, view):
rules = [
super().has_permission(request, view)
]
# and request.user.email_confirmed,
if hasattr(request.data, 'user') and hasattr(request.data, 'country_id'):
role = Role.objects.filter(role=Role.REVIEWER_MANGER) \
.first() # 'Comments moderator'
rules = [
UserRole.objects.filter(user=request.user, role=role,
establishment_id=request.data.country_id
).exists(),
super().has_permission(request, view)
]
return any(rules)
def has_object_permission(self, request, view, obj):
role = Role.objects.filter(role=Role.REVIEWER_MANGER, role = Role.objects.filter(role=Role.REVIEWER_MANGER,
country_id=obj.country_id)\ country_id=obj.country_id) \
.first() .first()
rules = [ rules = [
UserRole.objects.filter(user=request.user, role=role).exists(), UserRole.objects.filter(user=request.user, role=role).exists(),
@ -179,8 +304,25 @@ class IsReviewerManager(IsStandardUser):
class IsRestaurantReviewer(IsStandardUser): class IsRestaurantReviewer(IsStandardUser):
def has_object_permission(self, request, view, obj): def has_permission(self, request, view):
rules = [
super().has_permission(request, view)
]
# and request.user.email_confirmed,
if hasattr(request.data, 'user') and hasattr(request.data, 'object_id'):
role = Role.objects.filter(role=Role.RESTAURANT_REVIEWER) \
.first() # 'Comments moderator'
rules = [
UserRole.objects.filter(user=request.user, role=role,
establishment_id=request.data.object_id
).exists(),
super().has_permission(request, view)
]
return any(rules)
def has_object_permission(self, request, view, obj):
content_type = ContentType.objects.get(app_lable='establishment', content_type = ContentType.objects.get(app_lable='establishment',
model='establishment') model='establishment')

View File

@ -4,6 +4,7 @@ from django.core import exceptions
from rest_framework import serializers from rest_framework import serializers
from utils import models from utils import models
from translation.models import Language from translation.models import Language
from favorites.models import Favorites
class EmptySerializer(serializers.Serializer): class EmptySerializer(serializers.Serializer):
@ -72,3 +73,28 @@ class ProjectModelSerializer(serializers.ModelSerializer):
"""Overrided ModelSerializer.""" """Overrided ModelSerializer."""
serializers.ModelSerializer.serializer_field_mapping[models.TJSONField] = TJSONField serializers.ModelSerializer.serializer_field_mapping[models.TJSONField] = TJSONField
class FavoritesCreateSerializer(serializers.ModelSerializer):
"""Serializer to favorite object."""
class Meta:
"""Serializer for model Comment."""
model = Favorites
fields = [
'id',
'created',
]
@property
def request(self):
return self.context.get('request')
@property
def user(self):
"""Get user from request"""
return self.request.user
@property
def slug(self):
return self.request.parser_context.get('kwargs').get('slug')

View File

@ -9,10 +9,11 @@ class BasePermissionTests(APITestCase):
title='Russia', title='Russia',
locale='ru-RU' locale='ru-RU'
) )
self.lang.save()
self.country_ru = Country.objects.get( self.country_ru = Country.objects.get(
name={"en-GB": "Russian"} name={"en-GB": "Russian"}
) )
self.country_ru.save()

View File

@ -9,6 +9,7 @@ from account.models import User
from news.models import News, NewsType from news.models import News, NewsType
from establishment.models import Establishment, EstablishmentType, Employee from establishment.models import Establishment, EstablishmentType, Employee
from location.models import Country
class BaseTestCase(APITestCase): class BaseTestCase(APITestCase):
@ -39,7 +40,13 @@ class TranslateFieldTests(BaseTestCase):
self.news_type = NewsType.objects.create(name="Test news type") self.news_type = NewsType.objects.create(name="Test news type")
self.news_type.save() self.news_type.save()
self.country_ru = Country.objects.get(
name={"en-GB": "Russian"}
)
self.news_item = News.objects.create( self.news_item = News.objects.create(
id=8,
created_by=self.user, created_by=self.user,
modified_by=self.user, modified_by=self.user,
title={ title={
@ -52,6 +59,7 @@ class TranslateFieldTests(BaseTestCase):
news_type=self.news_type, news_type=self.news_type,
slug='test', slug='test',
state=News.PUBLISHED, state=News.PUBLISHED,
country=self.country_ru,
) )
self.news_item.save() self.news_item.save()

14
fabfile.py vendored
View File

@ -53,12 +53,14 @@ def collectstatic():
def deploy(branch=None): def deploy(branch=None):
fetch() role = env.roles[0]
install_requirements() if env.roledefs[role]['branch'] != 'develop':
migrate() fetch()
collectstatic() install_requirements()
touch() migrate()
kill_celery() collectstatic()
touch()
kill_celery()
def rev(): def rev():

View File

@ -75,8 +75,8 @@ PROJECT_APPS = [
'favorites.apps.FavoritesConfig', 'favorites.apps.FavoritesConfig',
'rating.apps.RatingConfig', 'rating.apps.RatingConfig',
'transfer.apps.TransferConfig', 'transfer.apps.TransferConfig',
'tag.apps.TagConfig' 'tag.apps.TagConfig',
'product.apps.ProductConfig',
] ]
EXTERNAL_APPS = [ EXTERNAL_APPS = [
@ -100,7 +100,7 @@ EXTERNAL_APPS = [
'timezone_field', 'timezone_field',
'storages', 'storages',
'sorl.thumbnail', 'sorl.thumbnail',
'timezonefinder' 'timezonefinder',
] ]
@ -373,7 +373,7 @@ THUMBNAIL_DEFAULT_OPTIONS = {
THUMBNAIL_QUALITY = 85 THUMBNAIL_QUALITY = 85
THUMBNAIL_DEBUG = False THUMBNAIL_DEBUG = False
SORL_THUMBNAIL_ALIASES = { SORL_THUMBNAIL_ALIASES = {
'news_preview': {'geometry_string': '100x100', 'crop': 'center'}, 'news_preview': {'geometry_string': '300x260', 'crop': 'center'},
'news_promo_horizontal_web': {'geometry_string': '1900x600', 'crop': 'center'}, 'news_promo_horizontal_web': {'geometry_string': '1900x600', 'crop': 'center'},
'news_promo_horizontal_mobile': {'geometry_string': '375x260', 'crop': 'center'}, 'news_promo_horizontal_mobile': {'geometry_string': '375x260', 'crop': 'center'},
'news_tile_horizontal_web': {'geometry_string': '300x275', 'crop': 'center'}, 'news_tile_horizontal_web': {'geometry_string': '300x275', 'crop': 'center'},

View File

@ -35,4 +35,5 @@ urlpatterns = [
path('comments/', include('comment.urls.web')), path('comments/', include('comment.urls.web')),
path('favorites/', include('favorites.urls')), path('favorites/', include('favorites.urls')),
path('timetables/', include('timetable.urls.web')), path('timetables/', include('timetable.urls.web')),
path('products/', include('product.urls.web')),
] ]