Merge remote-tracking branch 'origin/develop' into origin/migration_fix

# Conflicts:
#	apps/establishment/admin.py
#	project/settings/base.py
This commit is contained in:
Dmitriy Kuzmenko 2019-10-30 19:12:59 +03:00
commit 638bafe4a6
49 changed files with 1042 additions and 161 deletions

View File

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

View File

@ -6,4 +6,4 @@ from rest_framework import serializers
class CommentBaseSerializer(serializers.ModelSerializer):
class Meta:
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()
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.comment = Comment.objects.create(text='Test comment', mark=1,
user=self.user_test["user"],
object_id= self.country_ru.pk,
content_type_id=content_type.id,
object_id=self.country_ru.pk,
content_type_id=self.content_type.id,
country=self.country_ru
)
self.comment.save()
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):
tokens = User.create_jwt_tokens(self.moderator)
self.client.cookies = SimpleCookie(
@ -52,7 +82,9 @@ class CommentModeratorPermissionTests(BasePermissionTests):
"id": self.comment.id,
"text": "test text moderator",
"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')
@ -60,7 +92,7 @@ class CommentModeratorPermissionTests(BasePermissionTests):
def test_get(self):
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):
other_user = User.objects.create_user(username='test',
@ -99,9 +131,10 @@ class CommentModeratorPermissionTests(BasePermissionTests):
"id": self.comment.id,
"text": "test text moderator",
"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')
self.assertEqual(response.status_code, status.HTTP_200_OK)

View File

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

View File

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

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

View File

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

View File

@ -1,10 +1,15 @@
"""Establishment app tasks."""
import logging
from celery import shared_task
from celery.schedules import crontab
from celery.task import periodic_task
from django.core import management
from django_elasticsearch_dsl.management.commands import search_index
from establishment import models
from location.models import Country
logger = logging.getLogger(__name__)
@ -12,10 +17,15 @@ logger = logging.getLogger(__name__)
def recalculate_price_levels_by_country(country_id):
try:
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}')
else:
qs = models.Establishment.objects.filter(address__city__country=country)
for establishment in qs:
establishment.recalculate_price_level(low_price=country.low_price,
high_price=country.high_price)
@periodic_task(run_every=crontab(minute=59))
def rebuild_establishment_indices():
management.call_command(search_index.Command(), action='rebuild', models=[models.Establishment.__name__],
force=True)

View File

@ -9,7 +9,6 @@ urlpatterns = [
path('', views.EstablishmentListView.as_view(), name='list'),
path('recent-reviews/', views.EstablishmentRecentReviewListView.as_view(),
name='recent-reviews'),
# path('wineries/', views.WineriesListView.as_view(), name='wineries-list'),
path('slug/<slug:slug>/', views.EstablishmentRetrieveView.as_view(), name='detail'),
path('slug/<slug:slug>/similar/', views.EstablishmentSimilarListView.as_view(), name='similar'),
path('slug/<slug:slug>/comments/', views.EstablishmentCommentListView.as_view(), name='list-comments'),
@ -18,5 +17,5 @@ urlpatterns = [
path('slug/<slug:slug>/comments/<int:comment_id>/', views.EstablishmentCommentRUDView.as_view(),
name='rud-comment'),
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.
"""
establishment_obj = get_object_or_404(models.Establishment,
slug=self.kwargs['slug'])
obj = get_object_or_404(
self.request.user.favorites.by_content_type(app_label='establishment',
model='establishment')
.by_object_id(object_id=establishment_obj.pk))
establishment = get_object_or_404(models.Establishment,
slug=self.kwargs['slug'])
favorites = get_object_or_404(establishment.favorites.filter(user=self.request.user))
# May raise a permission denied
self.check_object_permissions(self.request, obj)
return obj
self.check_object_permissions(self.request, favorites)
return favorites
class EstablishmentNearestRetrieveView(EstablishmentListView, generics.ListAPIView):
"""Resource for getting list of nearest establishments."""
serializer_class = serializers.EstablishmentBaseSerializer
serializer_class = serializers.EstablishmentGeoSerializer
filter_class = filters.EstablishmentFilter
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()
if v is not None})
return qs
# Wineries
# todo: find out about difference between subtypes data
# class WineriesListView(EstablishmentListView):
# """Return list establishments with type Wineries"""
#
# def get_queryset(self):
# """Overridden get_queryset method."""
# qs = super(WineriesListView, self).get_queryset()
# return qs.with_type_related().wineries()

View File

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

View File

@ -1,7 +1,11 @@
"""Views for app favorites."""
from rest_framework import generics
from establishment.models import Establishment
from establishment.filters import EstablishmentFilter
from establishment.serializers import EstablishmentBaseSerializer
from product.models import Product
from product.serializers import ProductBaseSerializer
from product.filters import ProductFilterSet
from .models import Favorites
@ -14,11 +18,24 @@ class FavoritesBaseView(generics.GenericAPIView):
class FavoritesEstablishmentListView(generics.ListAPIView):
"""List views for favorites"""
"""List views for establishments in favorites."""
serializer_class = EstablishmentBaseSerializer
filter_class = EstablishmentFilter
def get_queryset(self):
"""Override get_queryset method"""
return Establishment.objects.filter(favorites__user=self.request.user)\
.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."""
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)
class AddressAdmin(admin.OSMGeoAdmin):
"""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
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
@receiver(post_save, sender=Country)
def run_recalculate_price_levels(sender, instance, **kwargs):

View File

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

View File

@ -148,3 +148,31 @@ class AddressDetailSerializer(AddressBaseSerializer):
'city_id',
'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)
tokens = User.create_jwt_tokens(self.user)
self.client.cookies = SimpleCookie(
{'access_token': tokens.get('access_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.views import common
from utils.permissions import IsCountryAdmin
from rest_framework.permissions import IsAuthenticatedOrReadOnly
# Address
class AddressListCreateView(common.AddressViewMixin, generics.ListCreateAPIView):
"""Create view for model Address."""
serializer_class = serializers.AddressDetailSerializer
queryset = models.Address.objects.all()
permission_classes = [IsCountryAdmin]
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
class AddressRUDView(common.AddressViewMixin, generics.RetrieveUpdateDestroyAPIView):
"""RUD view for model Address."""
serializer_class = serializers.AddressDetailSerializer
queryset = models.Address.objects.all()
permission_classes = [IsCountryAdmin]
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
# City
class CityListCreateView(common.CityViewMixin, generics.ListCreateAPIView):
"""Create view for model City."""
serializer_class = serializers.CitySerializer
permission_classes = [IsCountryAdmin]
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
class CityRUDView(common.CityViewMixin, generics.RetrieveUpdateDestroyAPIView):
"""RUD view for model City."""
serializer_class = serializers.CitySerializer
permission_classes = [IsCountryAdmin]
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
# Region
class RegionListCreateView(common.RegionViewMixin, generics.ListCreateAPIView):
"""Create view for model Region"""
serializer_class = serializers.RegionSerializer
permission_classes = [IsCountryAdmin]
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
class RegionRUDView(common.RegionViewMixin, generics.RetrieveUpdateDestroyAPIView):
"""Retrieve view for model Region"""
serializer_class = serializers.RegionSerializer
permission_classes = [IsCountryAdmin]
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
# Country
@ -50,10 +54,11 @@ class CountryListCreateView(generics.ListCreateAPIView):
queryset = models.Country.objects.all()
serializer_class = serializers.CountryBackSerializer
pagination_class = None
permission_classes = [IsCountryAdmin]
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
class CountryRUDView(generics.RetrieveUpdateDestroyAPIView):
"""RUD view for model Country."""
serializer_class = serializers.CountryBackSerializer
permission_classes = [IsCountryAdmin]
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
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."""
verbose_name = _('news gallery')
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):
"""Serializer for returning crop images of news image."""
orientation_display = serializers.CharField(source='get_orientation_display',
read_only=True)
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):
"""List serializer for News model."""
@ -191,8 +203,8 @@ class NewsDetailSerializer(NewsBaseSerializer):
class NewsDetailWebSerializer(NewsDetailSerializer):
"""News detail serializer for web users.."""
same_theme = NewsBaseSerializer(many=True, read_only=True)
should_read = NewsBaseSerializer(many=True, read_only=True)
same_theme = NewsSimilarListSerializer(many=True, read_only=True)
should_read = NewsSimilarListSerializer(many=True, read_only=True)
agenda = AgendaSerializer()
banner = NewsBannerSerializer()
@ -265,7 +277,6 @@ class NewsBackOfficeGallerySerializer(serializers.ModelSerializer):
"""Override validate method."""
news_pk = self.get_request_kwargs().get('pk')
image_id = self.get_request_kwargs().get('image_id')
is_main = attrs.get('is_main')
news_qs = models.News.objects.filter(pk=news_pk)
image_qs = Image.objects.filter(id=image_id)
@ -278,12 +289,6 @@ class NewsBackOfficeGallerySerializer(serializers.ModelSerializer):
news = news_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['image'] = image

View File

@ -66,6 +66,22 @@ class NewsTestCase(BaseTestCase):
def setUp(self):
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):
response = self.client.get(reverse('web:news:list'))
self.assertEqual(response.status_code, status.HTTP_200_OK)

View File

@ -119,7 +119,7 @@ class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView,
return gallery
def create(self, request, *args, **kwargs):
"""Override create method"""
"""Overridden create method"""
super().create(request, *args, **kwargs)
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."""
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.utils.translation import gettext_lazy as _
from utils.models import (BaseAttributes, ProjectBaseMixin,
@ -9,9 +11,23 @@ from utils.models import (BaseAttributes, ProjectBaseMixin,
class ProductType(TranslatedFieldsMixin, ProjectBaseMixin):
"""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,
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'))
use_subtypes = models.BooleanField(_('Use subtypes'), default=True)
@ -25,19 +41,35 @@ class ProductType(TranslatedFieldsMixin, ProjectBaseMixin):
class ProductSubType(TranslatedFieldsMixin, ProjectBaseMixin):
"""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,
related_name='subtypes',
verbose_name=_('Product type'))
name = TJSONField(blank=True, null=True, default=None,
verbose_name=_('Name'), help_text='{"en-GB":"some text"}')
index_name = models.CharField(max_length=50, 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'))
class Meta:
"""Meta class."""
verbose_name = _('Product type')
verbose_name_plural = _('Product types')
verbose_name = _('Product subtype')
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):
@ -47,16 +79,33 @@ class ProductManager(models.Manager):
class ProductQuerySet(models.QuerySet):
"""Product queryset."""
def with_base_related(self):
return self.select_related('product_type', 'establishment') \
.prefetch_related('product_type__subtypes', 'country')
def common(self):
return self.filter(category=self.model.COMMON)
def online(self):
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):
"""Product models."""
STR_FIELD_NAME = 'name'
COMMON = 0
ONLINE = 1
@ -72,13 +121,30 @@ class Product(TranslatedFieldsMixin, BaseAttributes):
description = TJSONField(_('Description'), null=True, blank=True,
default=None, help_text='{"en-GB":"some text"}')
characteristics = JSONField(_('Characteristics'))
country = models.ForeignKey('location.Country', on_delete=models.PROTECT,
verbose_name=_('Country'))
country = models.ManyToManyField('location.Country',
verbose_name=_('Country'))
available = models.BooleanField(_('Available'), default=True)
type = models.ForeignKey(ProductType, on_delete=models.PROTECT,
related_name='products', verbose_name=_('Type'))
subtypes = models.ManyToManyField(ProductSubType, related_name='products',
product_type = models.ForeignKey(ProductType, on_delete=models.PROTECT,
related_name='products', verbose_name=_('Type'))
subtypes = models.ManyToManyField(ProductSubType, blank=True,
related_name='products',
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)()
@ -88,12 +154,22 @@ class Product(TranslatedFieldsMixin, BaseAttributes):
verbose_name = _('Product')
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):
"""Extended manger for OnlineProduct model."""
def get_queryset(self):
"""Overrided get_queryset method."""
"""Overridden get_queryset method."""
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 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):
"""Annotate flag in_favorites"""
favorite_establishments = []
favorite_recipe_ids = []
if user.is_authenticated:
favorite_establishments = user.favorites.by_content_type(app_label='recipe',
model='recipe') \
.values_list('object_id', flat=True)
favorite_recipe_ids = user.favorite_recipe_ids
return self.annotate(in_favorites=models.Case(
models.When(
id__in=favorite_establishments,
id__in=favorite_recipe_ids,
then=True),
default=False,
output_field=models.BooleanField(default=False)))

View File

@ -22,14 +22,13 @@ class EstablishmentDocument(Document):
'id': fields.IntegerField(),
'name': fields.ObjectField(attr='name_indexing',
properties=OBJECT_FIELD_PROPERTIES),
'index_name': fields.KeywordField(attr='index_name'),
})
establishment_subtypes = fields.ObjectField(
properties={
'id': fields.IntegerField(),
'name': fields.ObjectField(attr='name_indexing',
properties={
'id': fields.IntegerField(),
}),
'name': fields.ObjectField(attr='name_indexing'),
'index_name': fields.KeywordField(attr='index_name'),
},
multi=True)
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:
@ -99,6 +91,7 @@ class EstablishmentDocument(Document):
'id',
'name',
'name_translated',
'is_publish',
'price_level',
'toque_number',
'public_mark',
@ -106,4 +99,4 @@ class EstablishmentDocument(Document):
)
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."""
from rest_framework import serializers
from elasticsearch_dsl import AttrDict
from django_elasticsearch_dsl_drf.serializers import DocumentSerializer
from news.serializers import NewsTypeSerializer
from search_indexes.documents import EstablishmentDocument, NewsDocument
@ -13,6 +14,8 @@ class TagsDocumentSerializer(serializers.Serializer):
label_translated = serializers.SerializerMethodField()
def get_label_translated(self, obj):
if isinstance(obj, dict):
return get_translated_value(obj.get('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_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):
"""Schedule serializer for ES Document"""
@ -75,7 +85,7 @@ class NewsDocumentSerializer(DocumentSerializer):
class EstablishmentDocumentSerializer(DocumentSerializer):
"""Establishment document serializer."""
address = AddressDocumentSerializer()
address = AddressDocumentSerializer(allow_null=True)
tags = TagsDocumentSerializer(many=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.filter_backends import (
FilteringFilterBackend,
GeoSpatialFilteringFilterBackend
GeoSpatialFilteringFilterBackend,
DefaultOrderingFilterBackend,
)
from django_elasticsearch_dsl_drf.viewsets import BaseDocumentViewSet
from search_indexes import serializers, filters
from search_indexes.documents import EstablishmentDocument, NewsDocument
from utils.pagination import ProjectPageNumberPagination
from utils.pagination import ProjectMobilePagination
class NewsDocumentViewSet(BaseDocumentViewSet):
@ -16,7 +17,7 @@ class NewsDocumentViewSet(BaseDocumentViewSet):
document = NewsDocument
lookup_field = 'slug'
pagination_class = ProjectPageNumberPagination
pagination_class = ProjectMobilePagination
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.NewsDocumentSerializer
ordering = ('id',)
@ -53,15 +54,23 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
document = EstablishmentDocument
lookup_field = 'slug'
pagination_class = ProjectPageNumberPagination
pagination_class = ProjectMobilePagination
permission_classes = (permissions.AllowAny,)
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 = [
FilteringFilterBackend,
filters.CustomSearchFilterBackend,
GeoSpatialFilteringFilterBackend,
DefaultOrderingFilterBackend,
]
search_fields = {
@ -72,6 +81,7 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
translated_search_fields = (
'description',
)
ordering = 'id'
filter_fields = {
'slug': 'slug',
'tag': {
@ -124,6 +134,15 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
'establishment_subtypes': {
'field': 'establishment_subtypes.id'
},
'type': {
'field': 'establishment_type.index_name'
},
'subtype': {
'field': 'establishment_subtypes.index_name',
'lookups': [
constants.LOOKUP_QUERY_IN,
],
},
'works_noon': {
'field': 'works_noon',
'lookups': [

View File

@ -27,6 +27,7 @@ class ScheduleRUDSerializer(serializers.ModelSerializer):
fields = [
'id',
'weekday_display',
'weekday',
'lunch_start',
'lunch_end',
'dinner_start',

View File

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

View File

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

View File

@ -4,6 +4,7 @@ from django.core import exceptions
from rest_framework import serializers
from utils import models
from translation.models import Language
from favorites.models import Favorites
class EmptySerializer(serializers.Serializer):
@ -72,3 +73,28 @@ class ProjectModelSerializer(serializers.ModelSerializer):
"""Overrided ModelSerializer."""
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',
locale='ru-RU'
)
self.lang.save()
self.country_ru = Country.objects.get(
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 establishment.models import Establishment, EstablishmentType, Employee
from location.models import Country
class BaseTestCase(APITestCase):
@ -39,7 +40,13 @@ class TranslateFieldTests(BaseTestCase):
self.news_type = NewsType.objects.create(name="Test news type")
self.news_type.save()
self.country_ru = Country.objects.get(
name={"en-GB": "Russian"}
)
self.news_item = News.objects.create(
id=8,
created_by=self.user,
modified_by=self.user,
title={
@ -52,6 +59,7 @@ class TranslateFieldTests(BaseTestCase):
news_type=self.news_type,
slug='test',
state=News.PUBLISHED,
country=self.country_ru,
)
self.news_item.save()

14
fabfile.py vendored
View File

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

View File

@ -76,7 +76,7 @@ PROJECT_APPS = [
'rating.apps.RatingConfig',
'transfer.apps.TransferConfig',
'tag.apps.TagConfig'
'product.apps.ProductConfig',
]
EXTERNAL_APPS = [
@ -100,7 +100,7 @@ EXTERNAL_APPS = [
'timezone_field',
'storages',
'sorl.thumbnail',
'timezonefinder'
'timezonefinder',
]
@ -373,7 +373,7 @@ THUMBNAIL_DEFAULT_OPTIONS = {
THUMBNAIL_QUALITY = 85
THUMBNAIL_DEBUG = False
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_mobile': {'geometry_string': '375x260', '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('favorites/', include('favorites.urls')),
path('timetables/', include('timetable.urls.web')),
path('products/', include('product.urls.web')),
]