Merge branch 'develop' into feature/fix-country-region-city-transfer

This commit is contained in:
littlewolf 2019-12-02 15:29:14 +03:00
commit 4c273a00be
111 changed files with 2220 additions and 453 deletions

3
.gitignore vendored
View File

@ -24,4 +24,5 @@ logs/
./docker-compose.override.yml ./docker-compose.override.yml
celerybeat-schedule celerybeat-schedule
local_files local_files
celerybeat.pid

View File

@ -0,0 +1,20 @@
# Generated by Django 2.2.7 on 2019-11-22 08:32
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('main', '0037_sitesettings_old_id'),
('account', '0019_auto_20191108_0827'),
]
operations = [
migrations.AddField(
model_name='role',
name='site',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='main.SiteSettings', verbose_name='Site settings'),
),
]

View File

@ -46,10 +46,8 @@ class Role(ProjectBaseMixin):
null=False, blank=False) null=False, blank=False)
country = models.ForeignKey(Country, verbose_name=_('Country'), country = models.ForeignKey(Country, verbose_name=_('Country'),
null=True, blank=True, on_delete=models.SET_NULL) null=True, blank=True, on_delete=models.SET_NULL)
# is_list = models.BooleanField(verbose_name=_('list'), default=True, null=False) site = models.ForeignKey(SiteSettings, verbose_name=_('Site settings'),
# is_create = models.BooleanField(verbose_name=_('create'), default=False, null=False) null=True, blank=True, on_delete=models.SET_NULL)
# is_update = models.BooleanField(verbose_name=_('update'), default=False, null=False)
# is_delete = models.BooleanField(verbose_name=_('delete'), default=False, null=False)
class UserManager(BaseUserManager): class UserManager(BaseUserManager):

View File

@ -34,7 +34,7 @@ class CheckWhetherBookingAvailable(generics.GenericAPIView):
periods = response['periods'] periods = response['periods']
periods_by_name = {period['period']: period for period in periods if 'period' in period} periods_by_name = {period['period']: period for period in periods if 'period' in period}
if not periods_by_name: if not periods_by_name:
raise ValueError('Empty guestonline response') return None
period_template = iter(periods_by_name.values()).__next__().copy() period_template = iter(periods_by_name.values()).__next__().copy()
period_template.pop('total_left_seats') period_template.pop('total_left_seats')
@ -84,8 +84,10 @@ class CheckWhetherBookingAvailable(generics.GenericAPIView):
service_response = self._preprocess_guestonline_response(service.response) \ service_response = self._preprocess_guestonline_response(service.response) \
if establishment.guestonline_id is not None \ if establishment.guestonline_id is not None \
else service.response else service.response if service else None
response.update({'details': service_response} if service and service.response else {}) response.update({'details': service_response})
if service_response is None:
response['available'] = False
return Response(data=response, status=200) return Response(data=response, status=200)

View File

@ -0,0 +1,32 @@
from django.conf import settings
from django.core.management.base import BaseCommand
from tqdm import tqdm
from collection.models import Collection
class Command(BaseCommand):
help = """Fix existed collections."""
def handle(self, *args, **kwarg):
update_collections = []
collections = Collection.objects.values_list('id', 'collection_type', 'description')
for id, collection_type, description in tqdm(collections):
collection = Collection.objects.get(id=id)
description = collection.description
collection_updated = False
if isinstance(description, str):
if description.lower().find('pop') != -1:
collection.collection_type = Collection.POP
collection_updated = True
if not isinstance(description, dict):
collection.description = {settings.FALLBACK_LOCALE: collection.description}
collection_updated = True
if collection_updated:
update_collections.append(collection)
Collection.objects.bulk_update(update_collections, ['collection_type', 'description', ])
self.stdout.write(self.style.WARNING(f'Updated products: {len(update_collections)}'))

View File

@ -3,6 +3,7 @@ from establishment.models import Establishment
from location.models import Country, Language from location.models import Country, Language
from transfer.models import Collections from transfer.models import Collections
from collection.models import Collection from collection.models import Collection
from django.conf import settings
from news.models import News from news.models import News
@ -93,9 +94,11 @@ class Command(BaseCommand):
country = Country.objects.filter(code=obj['country_code']).first() country = Country.objects.filter(code=obj['country_code']).first()
if country: if country:
objects.append( objects.append(
Collection(name={"en-GB": obj['title']}, collection_type=Collection.ORDINARY, Collection(name={settings.FALLBACK_LOCALE: obj['title']},
collection_type=Collection.POP if obj['description'].lower().find('pop') != -1
else Collection.ORDINARY,
country=country, country=country,
description=obj['description'], description={settings.FALLBACK_LOCALE: obj['description']},
slug=obj['slug'], old_id=obj['collection_id'], slug=obj['slug'], old_id=obj['collection_id'],
start=obj['start'], start=obj['start'],
image_url='https://s3.eu-central-1.amazonaws.com/gm-test.com/media/'+obj['attachment_suffix_url'] image_url='https://s3.eu-central-1.amazonaws.com/gm-test.com/media/'+obj['attachment_suffix_url']

View File

@ -0,0 +1,20 @@
# Generated by Django 2.2.7 on 2019-11-25 08:10
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('main', '0037_sitesettings_old_id'),
('comment', '0006_comment_is_publish'),
]
operations = [
migrations.AddField(
model_name='comment',
name='site',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='main.SiteSettings', verbose_name='site'),
),
]

View File

@ -35,7 +35,8 @@ class Comment(ProjectBaseMixin):
user = models.ForeignKey('account.User', related_name='comments', on_delete=models.CASCADE, verbose_name=_('User')) user = models.ForeignKey('account.User', related_name='comments', on_delete=models.CASCADE, verbose_name=_('User'))
old_id = models.IntegerField(null=True, blank=True, default=None) old_id = models.IntegerField(null=True, blank=True, default=None)
is_publish = models.BooleanField(default=False, verbose_name=_('Publish status')) is_publish = models.BooleanField(default=False, verbose_name=_('Publish status'))
site = models.ForeignKey('main.SiteSettings', blank=True, null=True,
on_delete=models.SET_NULL, verbose_name=_('site'))
content_type = models.ForeignKey(generic.ContentType, on_delete=models.CASCADE) content_type = models.ForeignKey(generic.ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField() object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey('content_type', 'object_id') content_object = generic.GenericForeignKey('content_type', 'object_id')

View File

@ -8,15 +8,20 @@ from account.models import Role, User, UserRole
from authorization.tests.tests_authorization import get_tokens_for_user from authorization.tests.tests_authorization import get_tokens_for_user
from comment.models import Comment from comment.models import Comment
from utils.tests.tests_permissions import BasePermissionTests from utils.tests.tests_permissions import BasePermissionTests
from main.models import SiteSettings
class CommentModeratorPermissionTests(BasePermissionTests): class CommentModeratorPermissionTests(BasePermissionTests):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.site_ru, created = SiteSettings.objects.get_or_create(
subdomain='ru'
)
self.role = Role.objects.create( self.role = Role.objects.create(
role=2, role=2,
country=self.country_ru site=self.site_ru
) )
self.role.save() self.role.save()
@ -33,11 +38,12 @@ class CommentModeratorPermissionTests(BasePermissionTests):
self.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=self.content_type.id, content_type_id=self.content_type.id,
country=self.country_ru site=self.site_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})
@ -50,7 +56,7 @@ class CommentModeratorPermissionTests(BasePermissionTests):
"user": self.user_test["user"].id, "user": self.user_test["user"].id,
"object_id": self.country_ru.pk, "object_id": self.country_ru.pk,
"content_type": self.content_type.id, "content_type": self.content_type.id,
"country_id": self.country_ru.id "site_id": self.site_ru.id
} }
response = self.client.post(self.url, format='json', data=comment) response = self.client.post(self.url, format='json', data=comment)
@ -61,7 +67,7 @@ class CommentModeratorPermissionTests(BasePermissionTests):
"user": self.moderator.id, "user": self.moderator.id,
"object_id": self.country_ru.id, "object_id": self.country_ru.id,
"content_type": self.content_type.id, "content_type": self.content_type.id,
"country_id": self.country_ru.id "site_id": self.site_ru.id
} }
tokens = User.create_jwt_tokens(self.moderator) tokens = User.create_jwt_tokens(self.moderator)
@ -83,8 +89,9 @@ class CommentModeratorPermissionTests(BasePermissionTests):
"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, "object_id": self.country_ru.id,
"content_type": self.content_type.id "content_type": self.content_type.id,
'site_id': self.site_ru.id
} }
response = self.client.put(self.url, data=data, format='json') response = self.client.put(self.url, data=data, format='json')

View File

@ -8,13 +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| IsCommentModerator|IsCountryAdmin] # 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 = [IsCommentModerator]
permission_classes = [IsCountryAdmin | IsCommentModerator] # permission_classes = [IsCountryAdmin | IsCommentModerator]
lookup_field = 'id' lookup_field = 'id'

View File

@ -55,3 +55,23 @@ class EstablishmentTypeTagFilter(filters.FilterSet):
fields = ( fields = (
'type_id', 'type_id',
) )
class EmployeeBackFilter(filters.FilterSet):
"""Employee filter set."""
search = filters.CharFilter(method='search_by_name_or_last_name')
class Meta:
"""Meta class."""
model = models.Employee
fields = (
'search',
)
def search_by_name_or_last_name(self, queryset, name, value):
"""Search by name or last name."""
if value not in EMPTY_VALUES:
return queryset.search_by_name_or_last_name(value)
return queryset

View File

@ -0,0 +1,49 @@
from django.core.management.base import BaseCommand
from tqdm import tqdm
from establishment.models import Establishment, EstablishmentSubType, EstablishmentType
from transfer.models import Metadata
class Command(BaseCommand):
help = 'Add subtype for establishment artisan'
def handle(self, *args, **options):
artisans = Establishment.objects.artisans().filter(
old_id__isnull=False,
).prefetch_related('tags')
old_tags = Metadata.objects.filter(
establishment__in=list(artisans.values_list('old_id', flat=True)),
key='shop_category',
)
tags = []
for tag in tqdm(old_tags):
tags.append(tag.value)
subtypes = set(tags)
es_type, _ = EstablishmentType.objects.get_or_create(
index_name='artisan',
defaults={
'index_name': 'artisan',
'name': {'en-GB': 'artisan'},
}
)
for artisan in tqdm(artisans):
artisan_tags = artisan.tags.all()
for t in artisan_tags:
if t.value in subtypes:
tag = 'coffee_shop' if t.value == 'coffe_shop' else t.value
subtype, _ = EstablishmentSubType.objects.get_or_create(
index_name=tag,
defaults={
'index_name': tag,
'name': {'en-GB': ' '.join(tag.split('_')).capitalize()},
'establishment_type': es_type,
}
)
artisan.establishment_subtypes.add(subtype)
artisan.save()
self.stdout.write(self.style.WARNING(f'Artisans subtype updated.'))

View File

@ -0,0 +1,28 @@
# Generated by Django 2.2.7 on 2019-11-22 11:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('establishment', '0065_establishment_purchased_products'),
]
operations = [
migrations.AddField(
model_name='employee',
name='last_name',
field=models.CharField(default=None, max_length=255, null=True, verbose_name='Last Name'),
),
migrations.AddField(
model_name='establishmentemployee',
name='status',
field=models.CharField(choices=[('I', 'Idle'), ('A', 'Accepted'), ('D', 'Declined')], default='I', max_length=1),
),
migrations.AlterField(
model_name='employee',
name='name',
field=models.CharField(max_length=255, verbose_name='Name'),
),
]

View File

@ -0,0 +1,39 @@
# Generated by Django 2.2.7 on 2019-11-22 12:44
from django.db import migrations, models
import phonenumber_field.modelfields
class Migration(migrations.Migration):
dependencies = [
('establishment', '0066_auto_20191122_1144'),
]
operations = [
migrations.AddField(
model_name='employee',
name='birth_date',
field=models.DateTimeField(default=None, null=True, verbose_name='Birth date'),
),
migrations.AddField(
model_name='employee',
name='email',
field=models.EmailField(blank=True, default=None, max_length=254, null=True, verbose_name='Email'),
),
migrations.AddField(
model_name='employee',
name='phone',
field=phonenumber_field.modelfields.PhoneNumberField(default=None, max_length=128, null=True),
),
migrations.AddField(
model_name='employee',
name='sex',
field=models.PositiveSmallIntegerField(choices=[(0, 'Male'), (1, 'Female')], default=None, null=True, verbose_name='Sex'),
),
migrations.AddField(
model_name='employee',
name='toque_number',
field=models.PositiveSmallIntegerField(default=None, null=True, verbose_name='Toque number'),
),
]

View File

@ -1,6 +1,7 @@
"""Establishment models.""" """Establishment models."""
from datetime import datetime from datetime import datetime
from functools import reduce from functools import reduce
from typing import List
from operator import or_ from operator import or_
import elasticsearch_dsl import elasticsearch_dsl
@ -25,7 +26,8 @@ from main.models import Award, Currency
from review.models import Review from review.models import Review
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin, from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
TranslatedFieldsMixin, BaseAttributes, GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes, GalleryModelMixin,
IntermediateGalleryModelMixin, HasTagsMixin) IntermediateGalleryModelMixin, HasTagsMixin,
FavoritesMixin)
# todo: establishment type&subtypes check # todo: establishment type&subtypes check
@ -117,9 +119,10 @@ class EstablishmentQuerySet(models.QuerySet):
'address__city__country') 'address__city__country')
def with_extended_related(self): def with_extended_related(self):
return self.select_related('establishment_type'). \ return self.with_extended_address_related().select_related('establishment_type'). \
prefetch_related('establishment_subtypes', 'awards', 'schedule', prefetch_related('establishment_subtypes', 'awards', 'schedule',
'phones'). \ 'phones', 'gallery', 'menu_set', 'menu_set__plate_set',
'menu_set__plate_set__currency', 'currency'). \
prefetch_actual_employees() prefetch_actual_employees()
def with_type_related(self): def with_type_related(self):
@ -319,7 +322,8 @@ class EstablishmentQuerySet(models.QuerySet):
return self.exclude(address__city__country__in=countries) return self.exclude(address__city__country__in=countries)
class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin, HasTagsMixin): class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin,
TranslatedFieldsMixin, HasTagsMixin, FavoritesMixin):
"""Establishment model.""" """Establishment model."""
# todo: delete image URL fields after moving on gallery # todo: delete image URL fields after moving on gallery
@ -393,6 +397,7 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, Translat
verbose_name=_('Tag')) verbose_name=_('Tag'))
reviews = generic.GenericRelation(to='review.Review') reviews = generic.GenericRelation(to='review.Review')
comments = generic.GenericRelation(to='comment.Comment') comments = generic.GenericRelation(to='comment.Comment')
carousels = generic.GenericRelation(to='main.Carousel')
favorites = generic.GenericRelation(to='favorites.Favorites') favorites = generic.GenericRelation(to='favorites.Favorites')
currency = models.ForeignKey(Currency, blank=True, null=True, default=None, currency = models.ForeignKey(Currency, blank=True, null=True, default=None,
on_delete=models.PROTECT, on_delete=models.PROTECT,
@ -432,11 +437,12 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, Translat
@property @property
def visible_tags(self): def visible_tags(self):
return super().visible_tags\ return super().visible_tags \
.exclude(category__index_name__in=['guide', 'collection', 'purchased_item', .exclude(category__index_name__in=['guide', 'collection', 'purchased_item',
'business_tag', 'business_tags_de'])\ 'business_tag', 'business_tags_de']) \
\
# todo: recalculate toque_number
# todo: recalculate toque_number
def recalculate_toque_number(self): def recalculate_toque_number(self):
toque_number = 0 toque_number = 0
if self.address and self.public_mark: if self.address and self.public_mark:
@ -609,7 +615,6 @@ class EstablishmentNote(ProjectBaseMixin):
class EstablishmentGallery(IntermediateGalleryModelMixin): class EstablishmentGallery(IntermediateGalleryModelMixin):
establishment = models.ForeignKey(Establishment, null=True, establishment = models.ForeignKey(Establishment, null=True,
related_name='establishment_gallery', related_name='establishment_gallery',
on_delete=models.CASCADE, on_delete=models.CASCADE,
@ -660,6 +665,16 @@ class EstablishmentEmployeeQuerySet(models.QuerySet):
class EstablishmentEmployee(BaseAttributes): class EstablishmentEmployee(BaseAttributes):
"""EstablishmentEmployee model.""" """EstablishmentEmployee model."""
IDLE = 'I'
ACCEPTED = 'A'
DECLINED = 'D'
STATUS_CHOICES = (
(IDLE, 'Idle'),
(ACCEPTED, 'Accepted'),
(DECLINED, 'Declined'),
)
establishment = models.ForeignKey(Establishment, on_delete=models.PROTECT, establishment = models.ForeignKey(Establishment, on_delete=models.PROTECT,
verbose_name=_('Establishment')) verbose_name=_('Establishment'))
employee = models.ForeignKey('establishment.Employee', on_delete=models.PROTECT, employee = models.ForeignKey('establishment.Employee', on_delete=models.PROTECT,
@ -670,19 +685,53 @@ class EstablishmentEmployee(BaseAttributes):
verbose_name=_('To date')) verbose_name=_('To date'))
position = models.ForeignKey(Position, on_delete=models.PROTECT, position = models.ForeignKey(Position, on_delete=models.PROTECT,
verbose_name=_('Position')) verbose_name=_('Position'))
status = models.CharField(max_length=1, choices=STATUS_CHOICES, default=IDLE)
# old_id = affiliations_id # old_id = affiliations_id
old_id = models.IntegerField(verbose_name=_('Old id'), null=True, blank=True) old_id = models.IntegerField(verbose_name=_('Old id'), null=True, blank=True)
objects = EstablishmentEmployeeQuerySet.as_manager() objects = EstablishmentEmployeeQuerySet.as_manager()
class EmployeeQuerySet(models.QuerySet):
def _generic_search(self, value, filter_fields_names: List[str]):
"""Generic method for searching value in specified fields"""
filters = [
{f'{field}__icontains': value}
for field in filter_fields_names
]
return self.filter(reduce(lambda x, y: x | y, [models.Q(**i) for i in filters]))
def search_by_name_or_last_name(self, value):
"""Search by name or last_name."""
return self._generic_search(value, ['name', 'last_name'])
class Employee(BaseAttributes): class Employee(BaseAttributes):
"""Employee model.""" """Employee model."""
user = models.OneToOneField('account.User', on_delete=models.PROTECT, user = models.OneToOneField('account.User', on_delete=models.PROTECT,
null=True, blank=True, default=None, null=True, blank=True, default=None,
verbose_name=_('User')) verbose_name=_('User'))
name = models.CharField(max_length=255, verbose_name=_('Last name')) name = models.CharField(max_length=255, verbose_name=_('Name'))
last_name = models.CharField(max_length=255, verbose_name=_('Last Name'), null=True, default=None)
# SEX CHOICES
MALE = 0
FEMALE = 1
SEX_CHOICES = (
(MALE, _('Male')),
(FEMALE, _('Female'))
)
sex = models.PositiveSmallIntegerField(choices=SEX_CHOICES, verbose_name=_('Sex'), null=True, default=None)
birth_date = models.DateTimeField(editable=True, verbose_name=_('Birth date'), null=True, default=None)
email = models.EmailField(blank=True, null=True, default=None, verbose_name=_('Email'))
phone = PhoneNumberField(null=True, default=None)
toque_number = models.PositiveSmallIntegerField(verbose_name=_('Toque number'), null=True, default=None)
establishments = models.ManyToManyField(Establishment, related_name='employees', establishments = models.ManyToManyField(Establishment, related_name='employees',
through=EstablishmentEmployee, ) through=EstablishmentEmployee, )
awards = generic.GenericRelation(to='main.Award', related_query_name='employees') awards = generic.GenericRelation(to='main.Award', related_query_name='employees')
@ -691,6 +740,8 @@ class Employee(BaseAttributes):
# old_id = profile_id # old_id = profile_id
old_id = models.IntegerField(verbose_name=_('Old id'), null=True, blank=True) old_id = models.IntegerField(verbose_name=_('Old id'), null=True, blank=True)
objects = EmployeeQuerySet.as_manager()
class Meta: class Meta:
"""Meta class.""" """Meta class."""

View File

@ -2,8 +2,9 @@ from rest_framework import serializers
from establishment import models from establishment import models
from establishment import serializers as model_serializers from establishment import serializers as model_serializers
from location.serializers import AddressDetailSerializer from location.serializers import AddressDetailSerializer, TranslatedField
from main.models import Currency from main.models import Currency
from main.serializers import AwardSerializer
from utils.decorators import with_base_attributes from utils.decorators import with_base_attributes
from utils.serializers import TimeZoneChoiceField from utils.serializers import TimeZoneChoiceField
from gallery.models import Image from gallery.models import Image
@ -161,12 +162,54 @@ class ContactEmailBackSerializers(model_serializers.PlateSerializer):
class EmployeeBackSerializers(serializers.ModelSerializer): class EmployeeBackSerializers(serializers.ModelSerializer):
"""Employee serializers.""" """Employee serializers."""
awards = AwardSerializer(many=True, read_only=True)
class Meta: class Meta:
model = models.Employee model = models.Employee
fields = [ fields = [
'id', 'id',
'user', 'user',
'name' 'name',
'last_name',
'sex',
'birth_date',
'email',
'phone',
'toque_number',
'awards',
]
class PositionBackSerializer(serializers.ModelSerializer):
"""Position Back serializer."""
name_translated = TranslatedField()
class Meta:
model = models.Position
fields = [
'id',
'name_translated',
'priority',
'index_name',
]
class EstablishmentEmployeeBackSerializer(serializers.ModelSerializer):
"""Establishment Employee serializer."""
employee = EmployeeBackSerializers()
position = PositionBackSerializer()
class Meta:
model = models.EstablishmentEmployee
fields = [
'id',
'employee',
'from_date',
'to_date',
'position',
'status',
] ]

View File

@ -13,7 +13,7 @@ from review.serializers import ReviewShortSerializer
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 ImageBaseSerializer from utils.serializers import ImageBaseSerializer, CarouselCreateSerializer
from utils.serializers import (ProjectModelSerializer, TranslatedField, from utils.serializers import (ProjectModelSerializer, TranslatedField,
FavoritesCreateSerializer) FavoritesCreateSerializer)
@ -168,12 +168,51 @@ class EstablishmentEmployeeSerializer(serializers.ModelSerializer):
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') position_index_name = serializers.CharField(source='position.index_name')
status = serializers.CharField()
class Meta: class Meta:
"""Meta class.""" """Meta class."""
model = models.Employee model = models.Employee
fields = ('id', 'name', 'position_translated', 'awards', 'priority', 'position_index_name') fields = ('id', 'name', 'position_translated', 'awards', 'priority', 'position_index_name', 'status')
class EstablishmentEmployeeCreateSerializer(serializers.ModelSerializer):
"""Serializer for establishment employee relation."""
class Meta:
"""Meta class."""
model = models.EstablishmentEmployee
fields = ('id',)
def _validate_entity(self, entity_id_param: str, entity_class):
entity_id = self.context.get('request').parser_context.get('kwargs').get(entity_id_param)
entity_qs = entity_class.objects.filter(id=entity_id)
if not entity_qs.exists():
raise serializers.ValidationError({'detail': _(f'{entity_class.__name__} not found.')})
return entity_qs.first()
def validate(self, attrs):
"""Override validate method"""
establishment = self._validate_entity("establishment_id", models.Establishment)
employee = self._validate_entity("employee_id", models.Employee)
position = self._validate_entity("position_id", models.Position)
attrs['establishment'] = establishment
attrs['employee'] = employee
attrs['position'] = position
return attrs
def create(self, validated_data, *args, **kwargs):
"""Override create method"""
validated_data.update({
'employee': validated_data.pop('employee'),
'establishment': validated_data.pop('establishment'),
'position': validated_data.pop("position")
})
return super().create(validated_data)
class EstablishmentShortSerializer(serializers.ModelSerializer): class EstablishmentShortSerializer(serializers.ModelSerializer):
@ -396,6 +435,22 @@ class EstablishmentCommentCreateSerializer(comment_serializers.CommentSerializer
return super().create(validated_data) return super().create(validated_data)
class EstablishmentCommentRUDSerializer(comment_serializers.CommentSerializer):
"""Retrieve/Update/Destroy comment serializer."""
class Meta:
"""Meta class."""
model = comment_models.Comment
fields = [
'id',
'created',
'text',
'mark',
'nickname',
'profile_pic',
]
class EstablishmentFavoritesCreateSerializer(FavoritesCreateSerializer): class EstablishmentFavoritesCreateSerializer(FavoritesCreateSerializer):
"""Serializer to favorite object w/ model Establishment.""" """Serializer to favorite object w/ model Establishment."""
@ -426,6 +481,27 @@ class EstablishmentFavoritesCreateSerializer(FavoritesCreateSerializer):
return super().create(validated_data) return super().create(validated_data)
class EstablishmentCarouselCreateSerializer(CarouselCreateSerializer):
"""Serializer to carousel object w/ model News."""
def validate(self, attrs):
establishment = models.Establishment.objects.filter(pk=self.pk).first()
if not establishment:
raise serializers.ValidationError({'detail': _('Object not found.')})
if establishment.carousels.exists():
raise utils_exceptions.CarouselError()
attrs['establishment'] = establishment
return attrs
def create(self, validated_data, *args, **kwargs):
validated_data.update({
'content_object': validated_data.pop('establishment')
})
return super().create(validated_data)
class CompanyBaseSerializer(serializers.ModelSerializer): class CompanyBaseSerializer(serializers.ModelSerializer):
"""Company base serializer""" """Company base serializer"""
phone_list = serializers.SerializerMethodField(source='phones', read_only=True) phone_list = serializers.SerializerMethodField(source='phones', read_only=True)

View File

@ -10,6 +10,8 @@ from translation.models import Language
from account.models import Role, UserRole from account.models import Role, UserRole
from location.models import Country, Address, City, Region from location.models import Country, Address, City, Region
from pytz import timezone as py_tz from pytz import timezone as py_tz
from main.models import SiteSettings
from timetable.models import Timetable
class BaseTestCase(APITestCase): class BaseTestCase(APITestCase):
@ -278,13 +280,13 @@ class PlateTests(ChildTestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
menu = Menu.objects.create( menu = Menu.objects.create(
category=json.dumps({"en-GB": "Test category"}), category=json.dumps({"ru-RU": "Test category"}),
establishment=self.establishment establishment=self.establishment
) )
currency = Currency.objects.create(name="Test currency") currency = Currency.objects.create(name="Test currency")
data = { data = {
'name': json.dumps({"en-GB": "Test plate"}), 'name': json.dumps({"ru-RU": "Test plate"}),
'establishment': self.establishment.id, 'establishment': self.establishment.id,
'price': 10, 'price': 10,
'menu': menu.id, 'menu': menu.id,
@ -298,7 +300,7 @@ class PlateTests(ChildTestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
update_data = { update_data = {
'name': json.dumps({"en-GB": "Test new plate"}) 'name': json.dumps({"ru-RU": "Test new plate"})
} }
response = self.client.patch('/api/back/establishments/plates/1/', data=update_data) response = self.client.patch('/api/back/establishments/plates/1/', data=update_data)
@ -314,7 +316,7 @@ class MenuTests(ChildTestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
data = { data = {
'category': json.dumps({"en-GB": "Test category"}), 'category': json.dumps({"ru-RU": "Test category"}),
'establishment': self.establishment.id 'establishment': self.establishment.id
} }
@ -325,7 +327,7 @@ class MenuTests(ChildTestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
update_data = { update_data = {
'category': json.dumps({"en-GB": "Test new category"}) 'category': json.dumps({"ru-RU": "Test new category"})
} }
response = self.client.patch('/api/back/establishments/menus/1/', data=update_data) response = self.client.patch('/api/back/establishments/menus/1/', data=update_data)
@ -336,24 +338,56 @@ class MenuTests(ChildTestCase):
class EstablishmentShedulerTests(ChildTestCase): class EstablishmentShedulerTests(ChildTestCase):
def test_shedule_CRUD(self): def setUp(self):
super().setUp()
self.lang, created = Language.objects.get_or_create(
title='Russia',
locale='ru-RU'
)
self.country_ru, created = Country.objects.get_or_create(
name={"en-GB": "Russian"}
)
self.site_ru, created = SiteSettings.objects.get_or_create(
subdomain='ru'
)
role, created = Role.objects.get_or_create(
role=Role.ESTABLISHMENT_MANAGER,
country_id=self.country_ru.id,
site_id=self.site_ru.id
)
user_role, created = UserRole.objects.get_or_create(
user=self.user,
role=role,
establishment_id=self.establishment.id
)
user_role.save()
def test_schedule_CRUD(self):
data = { data = {
'weekday': 1 'weekday': 1
} }
response = self.client.post(f'/api/back/establishments/{self.establishment.id}/schedule/', data=data) response = self.client.post(f'/api/back/establishments/{self.establishment.id}/schedule/', data=data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
schedule = response.data
response = self.client.get(f'/api/back/establishments/{self.establishment.id}/schedule/1/') response = self.client.get(f'/api/back/establishments/{self.establishment.id}/schedule/{schedule["id"]}/')
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
update_data = { update_data = {
'weekday': 2 'weekday': 2
} }
response = self.client.patch(f'/api/back/establishments/{self.establishment.id}/schedule/1/', data=update_data) response = self.client.patch(f'/api/back/establishments/{self.establishment.id}/schedule/{schedule["id"]}/',
data=update_data)
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
response = self.client.delete(f'/api/back/establishments/{self.establishment.id}/schedule/1/') response = self.client.delete(f'/api/back/establishments/{self.establishment.id}/schedule/{schedule["id"]}/')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
@ -441,3 +475,17 @@ class EstablishmentWebFavoriteTests(ChildTestCase):
f'/api/web/establishments/slug/{self.establishment.slug}/favorites/', f'/api/web/establishments/slug/{self.establishment.slug}/favorites/',
format='json') format='json')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
class EstablishmentCarouselTests(ChildTestCase):
def test_back_carousel_CR(self):
data = {
"object_id": self.establishment.id
}
response = self.client.post(f'/api/back/establishments/{self.establishment.id}/carousels/', data=data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
response = self.client.delete(f'/api/back/establishments/{self.establishment.id}/carousels/')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)

View File

@ -9,6 +9,8 @@ app_name = 'establishment'
urlpatterns = [ urlpatterns = [
path('', views.EstablishmentListCreateView.as_view(), name='list'), path('', views.EstablishmentListCreateView.as_view(), name='list'),
path('<int:pk>/', views.EstablishmentRUDView.as_view(), name='detail'), path('<int:pk>/', views.EstablishmentRUDView.as_view(), name='detail'),
path('<int:pk>/carousels/', views.EstablishmentCarouselCreateDestroyView.as_view(),
name='create-destroy-carousels'),
path('<int:pk>/schedule/<int:schedule_id>/', views.EstablishmentScheduleRUDView.as_view(), path('<int:pk>/schedule/<int:schedule_id>/', views.EstablishmentScheduleRUDView.as_view(),
name='schedule-rud'), name='schedule-rud'),
path('<int:pk>/schedule/', views.EstablishmentScheduleCreateView.as_view(), path('<int:pk>/schedule/', views.EstablishmentScheduleCreateView.as_view(),
@ -38,10 +40,19 @@ urlpatterns = [
path('phones/<int:pk>/', views.PhonesRUDView.as_view(), name='phones-rud'), path('phones/<int:pk>/', views.PhonesRUDView.as_view(), name='phones-rud'),
path('emails/', views.EmailListCreateView.as_view(), name='emails'), path('emails/', views.EmailListCreateView.as_view(), name='emails'),
path('emails/<int:pk>/', views.EmailRUDView.as_view(), name='emails-rud'), path('emails/<int:pk>/', views.EmailRUDView.as_view(), name='emails-rud'),
path('<int:establishment_id>/employees/', views.EstablishmentEmployeeListView.as_view(),
name='establishment-employees'),
path('employees/', views.EmployeeListCreateView.as_view(), name='employees'), path('employees/', views.EmployeeListCreateView.as_view(), name='employees'),
path('employees/<int:pk>/', views.EmployeeRUDView.as_view(), name='employees-rud'), path('employees/<int:pk>/', views.EmployeeRUDView.as_view(), name='employees-rud'),
path('<int:establishment_id>/employee/<int:employee_id>/position/<int:position_id>',
views.EstablishmentEmployeeCreateView.as_view(),
name='employees-establishment-create'),
path('<int:establishment_id>/employee/<int:employee_id>',
views.EstablishmentEmployeeDeleteView.as_view(),
name='employees-establishment-delete'),
path('types/', views.EstablishmentTypeListCreateView.as_view(), name='type-list'), path('types/', views.EstablishmentTypeListCreateView.as_view(), name='type-list'),
path('types/<int:pk>/', views.EstablishmentTypeRUDView.as_view(), name='type-rud'), path('types/<int:pk>/', views.EstablishmentTypeRUDView.as_view(), name='type-rud'),
path('subtypes/', views.EstablishmentSubtypeListCreateView.as_view(), name='subtype-list'), path('subtypes/', views.EstablishmentSubtypeListCreateView.as_view(), name='subtype-list'),
path('subtypes/<int:pk>/', views.EstablishmentSubtypeRUDView.as_view(), name='subtype-rud'), path('subtypes/<int:pk>/', views.EstablishmentSubtypeRUDView.as_view(), name='subtype-rud'),
path('positions/', views.EstablishmentPositionListView.as_view(), name='position-list'),
] ]

View File

@ -17,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='create-destroy-favorites') name='create-destroy-favorites'),
] ]

View File

@ -1,7 +1,9 @@
"""Establishment app views.""" """Establishment app views."""
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from rest_framework import generics, permissions from rest_framework import generics, permissions, status
from utils.permissions import IsCountryAdmin, IsEstablishmentManager
from establishment import filters, models, serializers from establishment import filters, models, serializers
from timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer from timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer
from utils.permissions import IsCountryAdmin, IsEstablishmentManager from utils.permissions import IsCountryAdmin, IsEstablishmentManager
@ -43,8 +45,8 @@ class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView):
""" """
Returns the object the view is displaying. Returns the object the view is displaying.
""" """
establishment_pk = self.kwargs.get('pk') establishment_pk = self.kwargs['pk']
schedule_id = self.kwargs.get('schedule_id') schedule_id = self.kwargs['schedule_id']
establishment = get_object_or_404(klass=models.Establishment.objects.all(), establishment = get_object_or_404(klass=models.Establishment.objects.all(),
pk=establishment_pk) pk=establishment_pk)
@ -156,11 +158,23 @@ class EmailRUDView(generics.RetrieveUpdateDestroyAPIView):
class EmployeeListCreateView(generics.ListCreateAPIView): class EmployeeListCreateView(generics.ListCreateAPIView):
"""Emplyoee list create view.""" """Emplyoee list create view."""
permission_classes = (permissions.AllowAny, )
filter_class = filters.EmployeeBackFilter
serializer_class = serializers.EmployeeBackSerializers serializer_class = serializers.EmployeeBackSerializers
queryset = models.Employee.objects.all() queryset = models.Employee.objects.all()
pagination_class = None pagination_class = None
class EstablishmentEmployeeListView(generics.ListAPIView):
"""Establishment emplyoees list view."""
permission_classes = (permissions.AllowAny, )
serializer_class = serializers.EstablishmentEmployeeBackSerializer
def get_queryset(self):
establishment_id = self.kwargs['establishment_id']
return models.EstablishmentEmployee.objects.filter(establishment__id=establishment_id)
class EmployeeRUDView(generics.RetrieveUpdateDestroyAPIView): class EmployeeRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Employee RUD view.""" """Employee RUD view."""
serializer_class = serializers.EmployeeBackSerializers serializer_class = serializers.EmployeeBackSerializers
@ -318,3 +332,36 @@ class EstablishmentNoteRUDView(EstablishmentMixinViews,
self.check_object_permissions(self.request, note) self.check_object_permissions(self.request, note)
return note return note
class EstablishmentEmployeeCreateView(generics.CreateAPIView):
serializer_class = serializers.EstablishmentEmployeeCreateSerializer
queryset = models.EstablishmentEmployee.objects.all()
# TODO send email to all admins and add endpoint for changing status
class EstablishmentEmployeeDeleteView(generics.DestroyAPIView):
def _get_object_to_delete(self, establishment_id, employee_id):
result_qs = models.EstablishmentEmployee\
.objects\
.filter(establishment_id=establishment_id, employee_id=employee_id)
if not result_qs.exists():
raise Http404
return result_qs.first()
def delete(self, request, *args, **kwargs):
establishment_id = self.kwargs["establishment_id"]
employee_id = self.kwargs["employee_id"]
object_to_delete = self._get_object_to_delete(establishment_id, employee_id)
object_to_delete.delete()
return HttpResponse(status=status.HTTP_204_NO_CONTENT)
class EstablishmentPositionListView(generics.ListAPIView):
"""Establishment positions list view."""
pagination_class = None
permission_classes = (permissions.AllowAny, )
queryset = models.Position.objects.all()
serializer_class = serializers.PositionBackSerializer

View File

@ -5,12 +5,11 @@ from django.shortcuts import get_object_or_404
from rest_framework import generics, permissions from rest_framework import generics, permissions
from comment import models as comment_models from comment import models as comment_models
from establishment import filters from comment.serializers import CommentRUDSerializer
from establishment import models, serializers from establishment import filters, models, serializers
from main import methods from main import methods
from utils.pagination import EstablishmentPortionPagination from utils.pagination import EstablishmentPortionPagination
from utils.permissions import IsCountryAdmin from utils.views import FavoritesCreateDestroyMixinView, CarouselCreateDestroyMixinView
from comment.serializers import CommentRUDSerializer
class EstablishmentMixinView: class EstablishmentMixinView:
@ -35,7 +34,7 @@ class EstablishmentListView(EstablishmentMixinView, generics.ListAPIView):
serializer_class = serializers.EstablishmentListRetrieveSerializer serializer_class = serializers.EstablishmentListRetrieveSerializer
def get_queryset(self): def get_queryset(self):
return super().get_queryset().with_schedule()\ return super().get_queryset().with_schedule() \
.with_extended_address_related().with_currency_related() .with_extended_address_related().with_currency_related()
@ -57,12 +56,11 @@ class EstablishmentRecentReviewListView(EstablishmentListView):
def get_queryset(self): def get_queryset(self):
"""Overridden method 'get_queryset'.""" """Overridden method 'get_queryset'."""
qs = super().get_queryset() qs = super().get_queryset()
user_ip = methods.get_user_ip(self.request)
query_params = self.request.query_params query_params = self.request.query_params
if 'longitude' in query_params and 'latitude' in query_params: if 'longitude' in query_params and 'latitude' in query_params:
longitude, latitude = query_params.get('longitude'), query_params.get('latitude') longitude, latitude = query_params.get('longitude'), query_params.get('latitude')
else: else:
longitude, latitude = methods.determine_coordinates(user_ip) longitude, latitude = methods.determine_coordinates(self.request)
if not longitude or not latitude: if not longitude or not latitude:
return qs.none() return qs.none()
point = Point(x=float(longitude), y=float(latitude), srid=settings.GEO_DEFAULT_SRID) point = Point(x=float(longitude), y=float(latitude), srid=settings.GEO_DEFAULT_SRID)
@ -107,9 +105,9 @@ class EstablishmentCommentListView(generics.ListAPIView):
establishment = get_object_or_404(models.Establishment, slug=self.kwargs['slug']) establishment = get_object_or_404(models.Establishment, slug=self.kwargs['slug'])
return comment_models.Comment.objects.by_content_type(app_label='establishment', return comment_models.Comment.objects.by_content_type(app_label='establishment',
model='establishment')\ model='establishment') \
.by_object_id(object_id=establishment.pk)\ .by_object_id(object_id=establishment.pk) \
.order_by('-created') .order_by('-created')
class EstablishmentCommentRUDView(generics.RetrieveUpdateDestroyAPIView): class EstablishmentCommentRUDView(generics.RetrieveUpdateDestroyAPIView):
@ -134,21 +132,18 @@ class EstablishmentCommentRUDView(generics.RetrieveUpdateDestroyAPIView):
return comment_obj return comment_obj
class EstablishmentFavoritesCreateDestroyView(generics.CreateAPIView, generics.DestroyAPIView): class EstablishmentFavoritesCreateDestroyView(FavoritesCreateDestroyMixinView):
"""View for create/destroy establishment from favorites.""" """View for create/destroy establishment from favorites."""
serializer_class = serializers.EstablishmentFavoritesCreateSerializer
lookup_field = 'slug'
def get_object(self): _model = models.Establishment
""" serializer_class = serializers.EstablishmentFavoritesCreateSerializer
Returns the object the view is displaying.
"""
establishment = get_object_or_404(models.Establishment, class EstablishmentCarouselCreateDestroyView(CarouselCreateDestroyMixinView):
slug=self.kwargs['slug']) """View for create/destroy establishment from carousel."""
favorites = get_object_or_404(establishment.favorites.filter(user=self.request.user))
# May raise a permission denied _model = models.Establishment
self.check_object_permissions(self.request, favorites) serializer_class = serializers.EstablishmentCarouselCreateSerializer
return favorites
class EstablishmentNearestRetrieveView(EstablishmentListView, generics.ListAPIView): class EstablishmentNearestRetrieveView(EstablishmentListView, generics.ListAPIView):

View File

@ -1,4 +1,9 @@
from django.conf import settings
from django.core.validators import MinValueValidator, MaxValueValidator
from rest_framework import serializers from rest_framework import serializers
from sorl.thumbnail.parsers import parse_crop
from sorl.thumbnail.parsers import ThumbnailParseError
from django.utils.translation import gettext_lazy as _
from . import models from . import models
@ -29,3 +34,86 @@ class ImageSerializer(serializers.ModelSerializer):
extra_kwargs = { extra_kwargs = {
'orientation': {'write_only': True} 'orientation': {'write_only': True}
} }
class CropImageSerializer(ImageSerializer):
"""Serializers for image crops."""
width = serializers.IntegerField(write_only=True, required=False)
height = serializers.IntegerField(write_only=True, required=False)
crop = serializers.CharField(write_only=True,
required=False,
default='center')
quality = serializers.IntegerField(write_only=True, required=False,
default=settings.THUMBNAIL_QUALITY,
validators=[
MinValueValidator(1),
MaxValueValidator(100)])
cropped_image = serializers.DictField(read_only=True, allow_null=True)
class Meta(ImageSerializer.Meta):
"""Meta class."""
fields = [
'id',
'url',
'orientation_display',
'width',
'height',
'crop',
'quality',
'cropped_image',
]
def validate(self, attrs):
"""Overridden validate method."""
file = self._image.image
crop_width = attrs.get('width')
crop_height = attrs.get('height')
crop = attrs.get('crop')
if (crop_height and crop_width) and (crop and crop != 'smart'):
xy_image = (file.width, file.width)
xy_window = (crop_width, crop_height)
try:
parse_crop(crop, xy_image, xy_window)
attrs['image'] = file
except ThumbnailParseError:
raise serializers.ValidationError({'margin': _('Unrecognized crop option: %s') % crop})
return attrs
def create(self, validated_data):
"""Overridden create method."""
width = validated_data.pop('width', None)
height = validated_data.pop('height', None)
quality = validated_data.pop('quality')
crop = validated_data.pop('crop')
image = self._image
if image and width and height:
setattr(image,
'cropped_image',
image.get_cropped_image(
geometry=f'{width}x{height}',
quality=quality,
crop=crop))
return image
@property
def view(self):
return self.context.get('view')
@property
def lookup_field(self):
lookup_field = 'pk'
if lookup_field in self.view.kwargs:
return self.view.kwargs.get(lookup_field)
@property
def _image(self):
"""Return image from url_kwargs."""
qs = models.Image.objects.filter(id=self.lookup_field)
if qs.exists():
return qs.first()
raise serializers.ValidationError({'detail': _('Image not found.')})

View File

@ -6,6 +6,7 @@ from . import views
app_name = 'gallery' app_name = 'gallery'
urlpatterns = [ urlpatterns = [
path('', views.ImageListCreateView.as_view(), name='list-create-image'), path('', views.ImageListCreateView.as_view(), name='list-create'),
path('<int:pk>/', views.ImageRetrieveDestroyView.as_view(), name='retrieve-destroy-image'), path('<int:pk>/', views.ImageRetrieveDestroyView.as_view(), name='retrieve-destroy'),
path('<int:pk>/crop/', views.CropImageCreateView.as_view(), name='create-crop'),
] ]

View File

@ -28,3 +28,8 @@ class ImageRetrieveDestroyView(ImageBaseView, generics.RetrieveDestroyAPIView):
else: else:
on_commit(lambda: tasks.delete_image(image_id=instance.id)) on_commit(lambda: tasks.delete_image(image_id=instance.id))
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
class CropImageCreateView(ImageBaseView, generics.CreateAPIView):
"""Create crop image."""
serializer_class = serializers.CropImageSerializer

View File

@ -25,12 +25,12 @@ class BaseTestCase(APITestCase):
{'access_token': tokens.get('access_token'), {'access_token': tokens.get('access_token'),
'refresh_token': tokens.get('refresh_token')}) 'refresh_token': tokens.get('refresh_token')})
self.lang = Language.objects.get( self.lang, created = Language.objects.get_or_create(
title='Russia', title='Russia',
locale='ru-RU' locale='ru-RU'
) )
self.country_ru = Country.objects.get( self.country_ru, created = Country.objects.get_or_create(
name={"en-GB": "Russian"} name={"en-GB": "Russian"}
) )
@ -72,7 +72,7 @@ class CountryTests(BaseTestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
update_data = { update_data = {
'name': json.dumps({"en-GB": "Test new country"}) 'name': json.dumps({"ru-RU": "Test new country"})
} }
response = self.client.patch(f'/api/back/location/countries/{response_data["id"]}/', data=update_data) response = self.client.patch(f'/api/back/location/countries/{response_data["id"]}/', data=update_data)

View File

@ -3,9 +3,15 @@ from django.contrib import admin
from main import models from main import models
class SiteSettingsInline(admin.TabularInline):
model = models.SiteFeature
extra = 1
@admin.register(models.SiteSettings) @admin.register(models.SiteSettings)
class SiteSettingsAdmin(admin.ModelAdmin): class SiteSettingsAdmin(admin.ModelAdmin):
"""Site settings admin conf.""" """Site settings admin conf."""
inlines = [SiteSettingsInline,]
@admin.register(models.Feature) @admin.register(models.Feature)

View File

@ -0,0 +1,94 @@
from django.core.management.base import BaseCommand
from django.db import connections
from django.utils.text import slugify
from establishment.management.commands.add_position import namedtuplefetchall
from main.models import SiteSettings, Feature, SiteFeature
from location.models import Country
from tqdm import tqdm
class Command(BaseCommand):
help = '''Add site_features for old db to new db.
Run after command add_site_settings!'''
def site_sql(self):
with connections['legacy'].cursor() as cursor:
cursor.execute('''
select s.id, s.country_code_2
from sites as s
''')
return namedtuplefetchall(cursor)
def update_site_old_id(self):
for a in tqdm(self.site_sql(), desc='Update old_id site: '):
country = Country.objects.filter(code=a.country_code_2).first()
SiteSettings.objects.filter(country=country, old_id__isnull=True)\
.update(old_id=a.id)
self.stdout.write(self.style.WARNING(f'Updated old_id site.'))
def feature_sql(self):
with connections['legacy'].cursor() as cursor:
cursor.execute('''
select f.id, slug
from features as f
''')
return namedtuplefetchall(cursor)
def add_feature(self):
objects = []
for a in tqdm(self.feature_sql(), desc='Add feature: '):
features = Feature.objects.filter(slug=slugify(a.slug)).update(old_id=a.id)
if features == 0:
objects.append(
Feature(slug=slugify(a.slug), old_id=a.id)
)
Feature.objects.bulk_create(objects)
self.stdout.write(self.style.WARNING(f'Created feature objects.'))
def site_features_sql(self):
with connections['legacy'].cursor() as cursor:
cursor.execute('''
select s.id as old_site_feature,
s.site_id,
case when s.state = 'published'
then True
else False
end as published,
s.feature_id,
c.country_code_2
from features as f
join site_features s on s.feature_id=f.id
join sites c on c.id = s.site_id
''')
return namedtuplefetchall(cursor)
def add_site_features(self):
objects = []
for a in tqdm(self.site_features_sql(), desc='Add site feature: '):
site = SiteSettings.objects.get(old_id=a.site_id,
subdomain=a.country_code_2)
feature = Feature.objects.get(old_id=a.feature_id)
site_features = SiteFeature.objects\
.filter(site_settings=site,
feature=feature
).update(old_id=a.old_site_feature, published=a.published)
if site_features == 0:
objects.append(
SiteFeature(site_settings=site,
feature=feature,
published=a.published,
main=False,
old_id=a.old_site_feature
)
)
SiteFeature.objects.bulk_create(objects)
self.stdout.write(self.style.WARNING(f'Site feature add objects.'))
def handle(self, *args, **kwargs):
self.update_site_old_id()
self.add_feature()
self.add_site_features()

View File

@ -0,0 +1,56 @@
from django.core.management.base import BaseCommand
from django.db import connections
from establishment.management.commands.add_position import namedtuplefetchall
from main.models import SiteSettings
from location.models import Country
from tqdm import tqdm
class Command(BaseCommand):
help = '''Add add site settings from old db to new db.
Run after country migrate!!!'''
def site_sql(self):
with connections['legacy'].cursor() as cursor:
cursor.execute('''
select
distinct
id,
country_code_2 as code,
pinterest_page_url,
twitter_page_url,
facebook_page_url,
contact_email,
config,
released,
instagram_page_url,
ad_config
from sites as s
''')
return namedtuplefetchall(cursor)
def add_site_settings(self):
objects=[]
for s in tqdm(self.site_sql(), desc='Add site settings'):
country = Country.objects.filter(code=s.code).first()
sites = SiteSettings.objects.filter(subdomain=s.code)
if not sites.exists():
objects.append(
SiteSettings(
subdomain=s.code,
country=country,
pinterest_page_url=s.pinterest_page_url,
twitter_page_url=s.twitter_page_url,
facebook_page_url=s.facebook_page_url,
instagram_page_url=s.instagram_page_url,
contact_email=s.contact_email,
config=s.config,
ad_config=s.ad_config,
old_id=s.id
)
)
SiteSettings.objects.bulk_create(objects)
self.stdout.write(self.style.WARNING(f'Add or get tag category objects.'))
def handle(self, *args, **kwargs):
self.add_site_settings()

View File

@ -28,31 +28,25 @@ def get_user_ip(request):
return ip return ip
def determine_country_code(ip_addr): def determine_country_code(request):
"""Determine country code.""" """Determine country code."""
country_code = None META = request.META
if ip_addr: country_code = META.get('X-GeoIP-Country-Code',
try: META.get('HTTP_X_GEOIP_COUNTRY_CODE'))
geoip = GeoIP2() if isinstance(country_code, str):
country_code = geoip.country_code(ip_addr) return country_code.lower()
country_code = country_code.lower()
except GeoIP2Exception as ex:
logger.info(f'GEOIP Exception: {ex}. ip: {ip_addr}')
except Exception as ex:
logger.error(f'GEOIP Base exception: {ex}')
return country_code
def determine_coordinates(ip_addr: str) -> Tuple[Optional[float], Optional[float]]: def determine_coordinates(request):
if ip_addr: META = request.META
try: longitude = META.get('X-GeoIP-Longitude',
geoip = GeoIP2() META.get('HTTP_X_GEOIP_LONGITUDE'))
return geoip.coords(ip_addr) latitude = META.get('X-GeoIP-Latitude',
except GeoIP2Exception as ex: META.get('HTTP_X_GEOIP_LATITUDE'))
logger.warning(f'GEOIP Exception: {ex}. ip: {ip_addr}') try:
except Exception as ex: return float(longitude), float(latitude)
logger.warning(f'GEOIP Base exception: {ex}') except (TypeError, ValueError):
return None, None return None, None
def determine_user_site_url(country_code): def determine_user_site_url(country_code):
@ -76,15 +70,11 @@ def determine_user_site_url(country_code):
return site.site_url return site.site_url
def determine_user_city(ip_addr: str) -> Optional[City]: def determine_user_city(request):
try: META = request.META
geoip = GeoIP2() city = META.get('X-GeoIP-City',
return geoip.city(ip_addr) META.get('HTTP_X_GEOIP_CITY'))
except GeoIP2Exception as ex: return city
logger.warning(f'GEOIP Exception: {ex}. ip: {ip_addr}')
except Exception as ex:
logger.warning(f'GEOIP Base exception: {ex}')
return None
def determine_subdivision( def determine_subdivision(

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2019-11-22 07:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0036_auto_20191115_0750'),
]
operations = [
migrations.AddField(
model_name='sitesettings',
name='old_id',
field=models.IntegerField(blank=True, null=True),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2019-11-26 11:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0037_sitesettings_old_id'),
]
operations = [
migrations.AddField(
model_name='feature',
name='old_id',
field=models.IntegerField(blank=True, null=True),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2019-11-26 12:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0038_feature_old_id'),
]
operations = [
migrations.AddField(
model_name='sitefeature',
name='old_id',
field=models.IntegerField(blank=True, null=True),
),
]

View File

@ -69,6 +69,8 @@ class SiteSettings(ProjectBaseMixin):
verbose_name=_('AD config')) verbose_name=_('AD config'))
currency = models.ForeignKey(Currency, on_delete=models.PROTECT, null=True, default=None) currency = models.ForeignKey(Currency, on_delete=models.PROTECT, null=True, default=None)
old_id = models.IntegerField(blank=True, null=True)
objects = SiteSettingsQuerySet.as_manager() objects = SiteSettingsQuerySet.as_manager()
class Meta: class Meta:
@ -105,6 +107,7 @@ class Feature(ProjectBaseMixin, PlatformMixin):
priority = models.IntegerField(unique=True, null=True, default=None) priority = models.IntegerField(unique=True, null=True, default=None)
route = models.ForeignKey('PageType', on_delete=models.PROTECT, null=True, default=None) route = models.ForeignKey('PageType', on_delete=models.PROTECT, null=True, default=None)
site_settings = models.ManyToManyField(SiteSettings, through='SiteFeature') site_settings = models.ManyToManyField(SiteSettings, through='SiteFeature')
old_id = models.IntegerField(null=True, blank=True)
class Meta: class Meta:
"""Meta class.""" """Meta class."""
@ -136,6 +139,7 @@ class SiteFeature(ProjectBaseMixin):
published = models.BooleanField(default=False, verbose_name=_('Published')) published = models.BooleanField(default=False, verbose_name=_('Published'))
main = models.BooleanField(default=False, verbose_name=_('Main')) main = models.BooleanField(default=False, verbose_name=_('Main'))
nested = models.ManyToManyField('self', symmetrical=False) nested = models.ManyToManyField('self', symmetrical=False)
old_id = models.IntegerField(null=True, blank=True)
objects = SiteFeatureQuerySet.as_manager() objects = SiteFeatureQuerySet.as_manager()

View File

@ -1,4 +1,5 @@
"""Main app serializers.""" """Main app serializers."""
from django.contrib.contenttypes.models import ContentType
from rest_framework import serializers from rest_framework import serializers
from location.serializers import CountrySerializer from location.serializers import CountrySerializer
@ -71,7 +72,7 @@ class SiteSettingsSerializer(serializers.ModelSerializer):
"""Meta class.""" """Meta class."""
model = models.SiteSettings model = models.SiteSettings
fields = ( fields = [
'country_code', 'country_code',
'time_format', 'time_format',
'subdomain', 'subdomain',
@ -85,7 +86,17 @@ class SiteSettingsSerializer(serializers.ModelSerializer):
'published_features', 'published_features',
'currency', 'currency',
'country_name', 'country_name',
) ]
class SiteSettingsBackOfficeSerializer(SiteSettingsSerializer):
"""Site settings serializer for back office."""
class Meta(SiteSettingsSerializer.Meta):
"""Meta class."""
fields = SiteSettingsSerializer.Meta.fields + [
'id',
]
class SiteSerializer(serializers.ModelSerializer): class SiteSerializer(serializers.ModelSerializer):
@ -94,7 +105,11 @@ class SiteSerializer(serializers.ModelSerializer):
class Meta: class Meta:
"""Meta class.""" """Meta class."""
model = models.SiteSettings model = models.SiteSettings
fields = ('subdomain', 'site_url', 'country') fields = [
'subdomain',
'site_url',
'country'
]
class SiteShortSerializer(serializers.ModelSerializer): class SiteShortSerializer(serializers.ModelSerializer):
@ -107,6 +122,16 @@ class SiteShortSerializer(serializers.ModelSerializer):
] ]
class SiteBackOfficeSerializer(SiteSerializer):
"""Serializer for back office."""
class Meta(SiteSerializer.Meta):
"""Meta class."""
fields = SiteSerializer.Meta.fields + [
'id',
]
# class SiteFeatureSerializer(serializers.ModelSerializer): # class SiteFeatureSerializer(serializers.ModelSerializer):
# """Site feature serializer.""" # """Site feature serializer."""
# #
@ -216,3 +241,11 @@ class PageTypeBaseSerializer(serializers.ModelSerializer):
'id', 'id',
'name', 'name',
] ]
class ContentTypeBackSerializer(serializers.ModelSerializer):
"""Serializer fro model ContentType."""
class Meta:
model = ContentType
fields = '__all__'

View File

@ -9,7 +9,7 @@ from location.models import Country
from main.models import Award, AwardType from main.models import Award, AwardType
class AwardTestCase(APITestCase): class BaseTestCase(APITestCase):
def setUp(self): def setUp(self):
self.user = User.objects.create_user( self.user = User.objects.create_user(
@ -25,6 +25,12 @@ class AwardTestCase(APITestCase):
{'access_token': tokens.get('access_token'), {'access_token': tokens.get('access_token'),
'refresh_token': tokens.get('refresh_token')}) 'refresh_token': tokens.get('refresh_token')})
class AwardTestCase(BaseTestCase):
def setUp(self):
super().setUp()
self.country_ru = Country.objects.create( self.country_ru = Country.objects.create(
name={'en-GB': 'Russian'}, name={'en-GB': 'Russian'},
code='RU', code='RU',
@ -71,3 +77,13 @@ class AwardTestCase(APITestCase):
response = self.client.delete(f'/api/back/main/awards/{self.award.id}/') response = self.client.delete(f'/api/back/main/awards/{self.award.id}/')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
class ContentTypeTestCase(BaseTestCase):
def setUp(self):
super().setUp()
def test_content_type_list(self):
response = self.client.get('/api/back/main/content_type/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)

View File

@ -1,11 +1,15 @@
"""Back main URLs""" """Back main URLs"""
from django.urls import path from django.urls import path
from main.views import back as views from main import views
app_name = 'main' app_name = 'main'
urlpatterns = [ urlpatterns = [
path('awards/', views.AwardLstView.as_view(), name='awards-list-create'), path('awards/', views.AwardLstView.as_view(), name='awards-list-create'),
path('awards/<int:id>/', views.AwardRUDView.as_view(), name='awards-rud'), path('awards/<int:id>/', views.AwardRUDView.as_view(), name='awards-rud'),
path('content_type/', views.ContentTypeView.as_view(), name='content_type-list'),
path('sites/', views.SiteListBackOfficeView.as_view(), name='site-list'),
path('site-settings/<subdomain>/', views.SiteSettingsBackOfficeView.as_view(),
name='site-settings'),
] ]

View File

@ -1,6 +1,6 @@
"""Main app urls.""" """Main app urls."""
from django.urls import path from django.urls import path
from main.views.common import * from main.views import *
app = 'main' app = 'main'
@ -8,5 +8,5 @@ common_urlpatterns = [
path('awards/', AwardView.as_view(), name='awards_list'), path('awards/', AwardView.as_view(), name='awards_list'),
path('awards/<int:pk>/', AwardRetrieveView.as_view(), name='awards_retrieve'), path('awards/<int:pk>/', AwardRetrieveView.as_view(), name='awards_retrieve'),
path('carousel/', CarouselListView.as_view(), name='carousel-list'), path('carousel/', CarouselListView.as_view(), name='carousel-list'),
path('determine-location/', DetermineLocation.as_view(), name='determine-location') path('determine-location/', DetermineLocation.as_view(), name='determine-location'),
] ]

View File

@ -1,11 +1,12 @@
from main.urls.common import common_urlpatterns
from django.urls import path from django.urls import path
from main.urls.common import common_urlpatterns
from main.views.web import DetermineSiteView, SiteListView, SiteSettingsView from main.views.web import DetermineSiteView, SiteListView, SiteSettingsView
urlpatterns = [ urlpatterns = [
path('determine-site/', DetermineSiteView.as_view(), name='determine-site'), path('determine-site/', DetermineSiteView.as_view(), name='determine-site'),
path('sites/', SiteListView.as_view(), name='site-list'), path('sites/', SiteListView.as_view(), name='site-list'),
path('site-settings/<subdomain>/', SiteSettingsView.as_view(), name='site-settings'), ] path('site-settings/<subdomain>/', SiteSettingsView.as_view(), name='site-settings'),
]
urlpatterns.extend(common_urlpatterns) urlpatterns.extend(common_urlpatterns)

View File

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

View File

@ -1,8 +1,11 @@
from django.contrib.contenttypes.models import ContentType
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import generics, permissions from rest_framework import generics, permissions
from main import serializers from main import serializers
from main.filters import AwardFilter from main.filters import AwardFilter
from main.models import Award from main.models import Award
from main.views import SiteSettingsView, SiteListView
class AwardLstView(generics.ListCreateAPIView): class AwardLstView(generics.ListCreateAPIView):
@ -19,3 +22,28 @@ class AwardRUDView(generics.RetrieveUpdateDestroyAPIView):
serializer_class = serializers.BackAwardSerializer serializer_class = serializers.BackAwardSerializer
permission_classes = (permissions.IsAdminUser,) permission_classes = (permissions.IsAdminUser,)
lookup_field = 'id' lookup_field = 'id'
class ContentTypeView(generics.ListAPIView):
"""ContentType list view"""
queryset = ContentType.objects.all()
serializer_class = serializers.ContentTypeBackSerializer
permission_classes = (permissions.IsAdminUser,)
filter_backends = (DjangoFilterBackend, )
ordering_fields = '__all__'
lookup_field = 'id'
filterset_fields = (
'id',
'model',
'app_label',
)
class SiteSettingsBackOfficeView(SiteSettingsView):
"""Site settings View."""
serializer_class = serializers.SiteSettingsBackOfficeSerializer
class SiteListBackOfficeView(SiteListView):
"""Site settings View."""
serializer_class = serializers.SiteBackOfficeSerializer

View File

@ -70,7 +70,7 @@ class CarouselListView(generics.ListAPIView):
def get_queryset(self): def get_queryset(self):
country_code = self.request.country_code country_code = self.request.country_code
if hasattr(settings, 'CAROUSEL_ITEMS') and country_code in ['www', 'main']: if hasattr(settings, 'CAROUSEL_ITEMS') and country_code in settings.INTERNATIONAL_COUNTRY_CODES:
qs = models.Carousel.objects.filter(id__in=settings.CAROUSEL_ITEMS) qs = models.Carousel.objects.filter(id__in=settings.CAROUSEL_ITEMS)
return qs return qs
qs = models.Carousel.objects.is_parsed().active() qs = models.Carousel.objects.is_parsed().active()
@ -86,9 +86,8 @@ class DetermineLocation(generics.GenericAPIView):
serializer_class = EmptySerializer serializer_class = EmptySerializer
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
user_ip = methods.get_user_ip(request) longitude, latitude = methods.determine_coordinates(request)
longitude, latitude = methods.determine_coordinates(user_ip) city = methods.determine_user_city(request)
city = methods.determine_user_city(user_ip)
if longitude and latitude and city: if longitude and latitude and city:
return Response(data={'latitude': latitude, 'longitude': longitude, 'city': city}) return Response(data={'latitude': latitude, 'longitude': longitude, 'city': city})
else: else:

View File

@ -14,8 +14,7 @@ class DetermineSiteView(generics.GenericAPIView):
serializer_class = EmptySerializer serializer_class = EmptySerializer
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
user_ip = methods.get_user_ip(request) country_code = methods.determine_country_code(request)
country_code = methods.determine_country_code(user_ip)
url = methods.determine_user_site_url(country_code) url = methods.determine_user_site_url(country_code)
return Response(data={'url': url}) return Response(data={'url': url})
@ -26,7 +25,7 @@ class SiteSettingsView(generics.RetrieveAPIView):
lookup_field = 'subdomain' lookup_field = 'subdomain'
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
queryset = models.SiteSettings.objects.all() queryset = models.SiteSettings.objects.all()
serializer_class = serializers.SiteSettingsSerializer serializer_class = serializers.SiteSettingsBackOfficeSerializer
class SiteListView(generics.ListAPIView): class SiteListView(generics.ListAPIView):

View File

@ -20,6 +20,16 @@ class NewsListFilterSet(filters.FilterSet):
tag_value__in = filters.CharFilter(method='in_tags') tag_value__in = filters.CharFilter(method='in_tags')
type = filters.CharFilter(method='by_type') type = filters.CharFilter(method='by_type')
state = filters.NumberFilter()
SORT_BY_CREATED_CHOICE = "created"
SORT_BY_START_CHOICE = "start"
SORT_BY_CHOICES = (
(SORT_BY_CREATED_CHOICE, "created"),
(SORT_BY_START_CHOICE, "start"),
)
sort_by = filters.ChoiceFilter(method='sort_by_field', choices=SORT_BY_CHOICES)
class Meta: class Meta:
"""Meta class""" """Meta class"""
model = models.News model = models.News
@ -29,6 +39,8 @@ class NewsListFilterSet(filters.FilterSet):
'tag_group', 'tag_group',
'tag_value__exclude', 'tag_value__exclude',
'tag_value__in', 'tag_value__in',
'state',
'sort_by',
) )
def in_tags(self, queryset, name, value): def in_tags(self, queryset, name, value):
@ -58,3 +70,6 @@ class NewsListFilterSet(filters.FilterSet):
return queryset.filter(news_type__name=value) return queryset.filter(news_type__name=value)
else: else:
return queryset return queryset
def sort_by_field(self, queryset, name, value):
return queryset.order_by(f'-{value}')

View File

@ -0,0 +1,20 @@
# Generated by Django 2.2.7 on 2019-11-22 09:12
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('main', '0037_sitesettings_old_id'),
('news', '0035_news_views_count'),
]
operations = [
migrations.AddField(
model_name='news',
name='site',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='main.SiteSettings', verbose_name='site settings'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2019-11-29 13:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('news', '0036_news_site'),
]
operations = [
migrations.AlterField(
model_name='news',
name='start',
field=models.DateTimeField(blank=True, default=None, null=True, verbose_name='Start'),
),
]

View File

@ -8,7 +8,8 @@ from rest_framework.reverse import reverse
from rating.models import Rating, ViewCount from rating.models import Rating, ViewCount
from utils.models import (BaseAttributes, TJSONField, TranslatedFieldsMixin, HasTagsMixin, from utils.models import (BaseAttributes, TJSONField, TranslatedFieldsMixin, HasTagsMixin,
ProjectBaseMixin, GalleryModelMixin, IntermediateGalleryModelMixin) ProjectBaseMixin, GalleryModelMixin, IntermediateGalleryModelMixin,
FavoritesMixin)
from utils.querysets import TranslationQuerysetMixin from utils.querysets import TranslationQuerysetMixin
from django.conf import settings from django.conf import settings
@ -126,7 +127,8 @@ class NewsQuerySet(TranslationQuerysetMixin):
) )
class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin): class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin,
FavoritesMixin):
"""News model.""" """News model."""
STR_FIELD_NAME = 'title' STR_FIELD_NAME = 'title'
@ -172,7 +174,8 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
description = TJSONField(blank=True, null=True, default=None, description = TJSONField(blank=True, null=True, default=None,
verbose_name=_('description'), verbose_name=_('description'),
help_text='{"en-GB":"some text"}') help_text='{"en-GB":"some text"}')
start = models.DateTimeField(verbose_name=_('Start')) start = models.DateTimeField(blank=True, null=True, default=None,
verbose_name=_('Start'))
end = models.DateTimeField(blank=True, null=True, default=None, end = models.DateTimeField(blank=True, null=True, default=None,
verbose_name=_('End')) verbose_name=_('End'))
slug = models.SlugField(unique=True, max_length=255, slug = models.SlugField(unique=True, max_length=255,
@ -194,6 +197,7 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
views_count = models.OneToOneField('rating.ViewCount', blank=True, null=True, on_delete=models.SET_NULL) views_count = models.OneToOneField('rating.ViewCount', blank=True, null=True, on_delete=models.SET_NULL)
ratings = generic.GenericRelation(Rating) ratings = generic.GenericRelation(Rating)
favorites = generic.GenericRelation(to='favorites.Favorites') favorites = generic.GenericRelation(to='favorites.Favorites')
carousels = generic.GenericRelation(to='main.Carousel')
agenda = models.ForeignKey('news.Agenda', blank=True, null=True, agenda = models.ForeignKey('news.Agenda', blank=True, null=True,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
verbose_name=_('agenda')) verbose_name=_('agenda'))
@ -201,7 +205,8 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
banner = models.ForeignKey('news.NewsBanner', blank=True, null=True, banner = models.ForeignKey('news.NewsBanner', blank=True, null=True,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
verbose_name=_('banner')) verbose_name=_('banner'))
site = models.ForeignKey('main.SiteSettings', blank=True, null=True,
on_delete=models.SET_NULL, verbose_name=_('site settings'))
objects = NewsQuerySet.as_manager() objects = NewsQuerySet.as_manager()
class Meta: class Meta:
@ -217,6 +222,10 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
def is_publish(self): def is_publish(self):
return self.state in self.PUBLISHED_STATES return self.state in self.PUBLISHED_STATES
@property
def is_international(self):
return self.INTERNATIONAL_TAG_VALUE in map(lambda tag: tag.value, self.tags.all())
@property @property
def web_url(self): def web_url(self):
return reverse('web:news:rud', kwargs={'slug': self.slug}) return reverse('web:news:rud', kwargs={'slug': self.slug})

View File

@ -5,13 +5,14 @@ from rest_framework.fields import SerializerMethodField
from account.serializers.common import UserBaseSerializer from account.serializers.common import UserBaseSerializer
from gallery.models import Image from gallery.models import Image
from main.models import SiteSettings
from location import models as location_models from location import models as location_models
from location.serializers import CountrySimpleSerializer, AddressBaseSerializer from location.serializers import CountrySimpleSerializer, AddressBaseSerializer
from news import models from news import models
from tag.serializers import TagBaseSerializer from tag.serializers import TagBaseSerializer
from utils import exceptions as utils_exceptions from utils import exceptions as utils_exceptions
from utils.serializers import (TranslatedField, ProjectModelSerializer, from utils.serializers import (TranslatedField, ProjectModelSerializer,
FavoritesCreateSerializer, ImageBaseSerializer) FavoritesCreateSerializer, ImageBaseSerializer, CarouselCreateSerializer)
class AgendaSerializer(ProjectModelSerializer): class AgendaSerializer(ProjectModelSerializer):
@ -65,7 +66,7 @@ class NewsBaseSerializer(ProjectModelSerializer):
subtitle_translated = TranslatedField() subtitle_translated = TranslatedField()
news_type = NewsTypeSerializer(read_only=True) news_type = NewsTypeSerializer(read_only=True)
tags = TagBaseSerializer(read_only=True, many=True, source='visible_tags') tags = TagBaseSerializer(read_only=True, many=True, source='visible_tags')
in_favorites = serializers.BooleanField(allow_null=True) in_favorites = serializers.BooleanField(allow_null=True, read_only=True)
view_counter = serializers.IntegerField(read_only=True) view_counter = serializers.IntegerField(read_only=True)
class Meta: class Meta:
@ -80,7 +81,6 @@ class NewsBaseSerializer(ProjectModelSerializer):
'news_type', 'news_type',
'tags', 'tags',
'slug', 'slug',
'in_favorites',
'view_counter', 'view_counter',
) )
@ -162,6 +162,7 @@ class NewsDetailWebSerializer(NewsDetailSerializer):
class NewsBackOfficeBaseSerializer(NewsBaseSerializer): class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
"""News back office base serializer.""" """News back office base serializer."""
is_published = serializers.BooleanField(source='is_publish', read_only=True)
class Meta(NewsBaseSerializer.Meta): class Meta(NewsBaseSerializer.Meta):
"""Meta class.""" """Meta class."""
@ -169,6 +170,7 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
fields = NewsBaseSerializer.Meta.fields + ( fields = NewsBaseSerializer.Meta.fields + (
'title', 'title',
'subtitle', 'subtitle',
'is_published',
) )
@ -182,6 +184,9 @@ class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer,
country_id = serializers.PrimaryKeyRelatedField( country_id = serializers.PrimaryKeyRelatedField(
source='country', write_only=True, source='country', write_only=True,
queryset=location_models.Country.objects.all()) queryset=location_models.Country.objects.all())
site_id = serializers.PrimaryKeyRelatedField(
source='site', write_only=True,
queryset=SiteSettings.objects.all())
template_display = serializers.CharField(source='get_template_display', template_display = serializers.CharField(source='get_template_display',
read_only=True) read_only=True)
@ -193,8 +198,10 @@ class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer,
'description', 'description',
'news_type_id', 'news_type_id',
'country_id', 'country_id',
'site_id',
'template', 'template',
'template_display', 'template_display',
'is_international',
) )
@ -267,3 +274,24 @@ class NewsFavoritesCreateSerializer(FavoritesCreateSerializer):
'content_object': validated_data.pop('news') 'content_object': validated_data.pop('news')
}) })
return super().create(validated_data) return super().create(validated_data)
class NewsCarouselCreateSerializer(CarouselCreateSerializer):
"""Serializer to carousel object w/ model News."""
def validate(self, attrs):
news = models.News.objects.filter(pk=self.pk).first()
if not news:
raise serializers.ValidationError({'detail': _('Object not found.')})
if news.carousels.exists():
raise utils_exceptions.CarouselError()
attrs['news'] = news
return attrs
def create(self, validated_data, *args, **kwargs):
validated_data.update({
'content_object': validated_data.pop('news')
})
return super().create(validated_data)

View File

@ -5,6 +5,7 @@ from rest_framework.test import APITestCase
from rest_framework import status from rest_framework import status
from datetime import datetime, timedelta from datetime import datetime, timedelta
from main.models import SiteSettings
from news.models import NewsType, News from news.models import NewsType, News
from account.models import User, Role, UserRole from account.models import User, Role, UserRole
from translation.models import Language from translation.models import Language
@ -30,18 +31,23 @@ class BaseTestCase(APITestCase):
'refresh_token': tokens.get('refresh_token')}) 'refresh_token': tokens.get('refresh_token')})
self.test_news_type = NewsType.objects.create(name="Test news type") self.test_news_type = NewsType.objects.create(name="Test news type")
self.lang = Language.objects.get(
self.lang, created = Language.objects.get_or_create(
title='Russia', title='Russia',
locale='ru-RU' locale='ru-RU'
) )
self.country_ru = Country.objects.get( self.country_ru, created = Country.objects.get_or_create(
name={"en-GB": "Russian"} name={"en-GB": "Russian"}
) )
self.site_ru, created = SiteSettings.objects.get_or_create(
subdomain='ru'
)
role = Role.objects.create( role = Role.objects.create(
role=Role.CONTENT_PAGE_MANAGER, role=Role.CONTENT_PAGE_MANAGER,
country=self.country_ru site_id=self.site_ru.id
) )
role.save() role.save()
@ -51,16 +57,18 @@ class BaseTestCase(APITestCase):
) )
user_role.save() user_role.save()
self.test_news = News.objects.create( self.test_news = News.objects.create(
created_by=self.user, modified_by=self.user, created_by=self.user, modified_by=self.user,
title={"en-GB": "Test news"}, title={"ru-RU": "Test news"},
news_type=self.test_news_type, news_type=self.test_news_type,
description={"en-GB": "Description test news"}, description={"ru-RU": "Description test news"},
start=datetime.now() + timedelta(hours=-2), start=datetime.now() + timedelta(hours=-2),
end=datetime.now() + timedelta(hours=2), end=datetime.now() + timedelta(hours=2),
state=News.PUBLISHED, state=News.PUBLISHED,
slug='test-news-slug', slug='test-news-slug',
country=self.country_ru, country=self.country_ru,
site=self.site_ru
) )
@ -70,14 +78,15 @@ class NewsTestCase(BaseTestCase):
def test_news_post(self): def test_news_post(self):
test_news = { test_news = {
"title": {"en-GB": "Test news POST"}, "title": {"ru-RU": "Test news POST"},
"news_type_id": self.test_news_type.id, "news_type_id": self.test_news_type.id,
"description": {"en-GB": "Description test news"}, "description": {"ru-RU": "Description test news"},
"start": datetime.now() + timedelta(hours=-2), "start": datetime.now() + timedelta(hours=-2),
"end": datetime.now() + timedelta(hours=2), "end": datetime.now() + timedelta(hours=2),
"state": News.PUBLISHED, "state": News.PUBLISHED,
"slug": 'test-news-slug_post', "slug": 'test-news-slug_post',
"country_id": self.country_ru.id, "country_id": self.country_ru.id,
"site_id": self.site_ru.id
} }
url = reverse("back:news:list-create") url = reverse("back:news:list-create")
@ -107,11 +116,12 @@ class NewsTestCase(BaseTestCase):
url = reverse('back:news:retrieve-update-destroy', kwargs={'pk': self.test_news.id}) url = reverse('back:news:retrieve-update-destroy', kwargs={'pk': self.test_news.id})
data = { data = {
'id': self.test_news.id, 'id': self.test_news.id,
'description': {"en-GB": "Description test news!"}, 'description': {"ru-RU": "Description test news!"},
'slug': self.test_news.slug, 'slug': self.test_news.slug,
'start': self.test_news.start, 'start': self.test_news.start,
'news_type_id': self.test_news.news_type_id, 'news_type_id': self.test_news.news_type_id,
'country_id': self.country_ru.id 'country_id': self.country_ru.id,
"site_id": self.site_ru.id
} }
response = self.client.put(url, data=data, format='json') response = self.client.put(url, data=data, format='json')
@ -128,3 +138,17 @@ class NewsTestCase(BaseTestCase):
response = self.client.delete(f'/api/web/news/slug/{self.test_news.slug}/favorites/', format='json') response = self.client.delete(f'/api/web/news/slug/{self.test_news.slug}/favorites/', format='json')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
class NewsCarouselTests(BaseTestCase):
def test_back_carousel_CR(self):
data = {
"object_id": self.test_news.id
}
response = self.client.post(f'/api/back/news/{self.test_news.id}/carousels/', data=data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
response = self.client.delete(f'/api/back/news/{self.test_news.id}/carousels/')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)

View File

@ -13,4 +13,5 @@ urlpatterns = [
name='gallery-list'), name='gallery-list'),
path('<int:pk>/gallery/<int:image_id>/', views.NewsBackOfficeGalleryCreateDestroyView.as_view(), path('<int:pk>/gallery/<int:image_id>/', views.NewsBackOfficeGalleryCreateDestroyView.as_view(),
name='gallery-create-destroy'), name='gallery-create-destroy'),
] path('<int:pk>/carousels/', views.NewsCarouselCreateDestroyView.as_view(), name='create-destroy-carousels'),
]

View File

@ -5,5 +5,6 @@ common_urlpatterns = [
path('', views.NewsListView.as_view(), name='list'), path('', views.NewsListView.as_view(), name='list'),
path('types/', views.NewsTypeListView.as_view(), name='type'), path('types/', views.NewsTypeListView.as_view(), name='type'),
path('slug/<slug:slug>/', views.NewsDetailView.as_view(), name='rud'), path('slug/<slug:slug>/', views.NewsDetailView.as_view(), name='rud'),
path('slug/<slug:slug>/favorites/', views.NewsFavoritesCreateDestroyView.as_view(), name='create-destroy-favorites') path('slug/<slug:slug>/favorites/', views.NewsFavoritesCreateDestroyView.as_view(),
name='create-destroy-favorites'),
] ]

View File

@ -6,7 +6,7 @@ from rest_framework import generics, permissions
from news import filters, models, serializers from news import filters, models, serializers
from rating.tasks import add_rating from rating.tasks import add_rating
from utils.permissions import IsCountryAdmin, IsContentPageManager from utils.permissions import IsCountryAdmin, IsContentPageManager
from utils.views import CreateDestroyGalleryViewMixin from utils.views import CreateDestroyGalleryViewMixin, FavoritesCreateDestroyMixinView, CarouselCreateDestroyMixinView
from utils.serializers import ImageBaseSerializer from utils.serializers import ImageBaseSerializer
@ -21,7 +21,7 @@ class NewsMixinView:
qs = models.News.objects.published() \ qs = models.News.objects.published() \
.with_base_related() \ .with_base_related() \
.annotate_in_favorites(self.request.user) \ .annotate_in_favorites(self.request.user) \
.order_by('-is_highlighted', '-created') .order_by('-is_highlighted', '-start')
country_code = self.request.country_code country_code = self.request.country_code
if country_code: if country_code:
@ -84,6 +84,7 @@ class NewsBackOfficeLCView(NewsBackOfficeMixinView,
serializer_class = serializers.NewsBackOfficeBaseSerializer serializer_class = serializers.NewsBackOfficeBaseSerializer
filter_class = filters.NewsListFilterSet filter_class = filters.NewsListFilterSet
create_serializers_class = serializers.NewsBackOfficeDetailSerializer create_serializers_class = serializers.NewsBackOfficeDetailSerializer
permission_classes = [IsCountryAdmin | IsContentPageManager] permission_classes = [IsCountryAdmin | IsContentPageManager]
def get_serializer_class(self): def get_serializer_class(self):
@ -150,18 +151,15 @@ class NewsBackOfficeRUDView(NewsBackOfficeMixinView,
return self.retrieve(request, *args, **kwargs) return self.retrieve(request, *args, **kwargs)
class NewsFavoritesCreateDestroyView(generics.CreateAPIView, generics.DestroyAPIView): class NewsFavoritesCreateDestroyView(FavoritesCreateDestroyMixinView):
"""View for create/destroy news from favorites.""" """View for create/destroy news from favorites."""
_model = models.News
serializer_class = serializers.NewsFavoritesCreateSerializer serializer_class = serializers.NewsFavoritesCreateSerializer
lookup_field = 'slug'
def get_object(self):
""" class NewsCarouselCreateDestroyView(CarouselCreateDestroyMixinView):
Returns the object the view is displaying. """View for create/destroy news from carousel."""
"""
news = get_object_or_404(models.News, slug=self.kwargs.get('slug')) _model = models.News
favorites = get_object_or_404(news.favorites.filter(user=self.request.user)) serializer_class = serializers.NewsCarouselCreateSerializer
# May raise a permission denied
self.check_object_permissions(self.request, favorites)
return favorites

View File

@ -0,0 +1,21 @@
"""Back account serializers"""
from rest_framework import serializers
from partner.models import Partner
class BackPartnerSerializer(serializers.ModelSerializer):
class Meta:
model = Partner
fields = (
'id',
'old_id',
'name',
'url',
'image',
'establishment',
'establishment_id',
'type',
'starting_date',
'expiry_date',
'price_per_month',
)

View File

@ -1,16 +1,90 @@
# Create your tests here. # Create your tests here.
from rest_framework.test import APITestCase from http.cookies import SimpleCookie
from rest_framework import status from rest_framework import status
from rest_framework.test import APITestCase
from account.models import User, Role, UserRole
from establishment.models import EstablishmentType, Establishment
from location.models import Country, Region, City, Address
from partner.models import Partner from partner.models import Partner
from translation.models import Language
class PartnerTestCase(APITestCase): class BaseTestCase(APITestCase):
def setUp(self): def setUp(self):
self.test_url = "www.example.com" self.username = 'test_user'
self.test_partner = Partner.objects.create(url=self.test_url) self.password = 'test_user_password'
self.email = 'test_user@mail.com'
self.user = User.objects.create_user(
username=self.username,
email=self.email,
password=self.password,
is_staff=True,
)
tokens = User.create_jwt_tokens(self.user)
self.client.cookies = SimpleCookie({
'access_token': tokens.get('access_token'),
'refresh_token': tokens.get('refresh_token'),
})
self.establishment_type = EstablishmentType.objects.create(name="Test establishment type")
self.role = Role.objects.create(role=Role.ESTABLISHMENT_MANAGER)
self.establishment = Establishment.objects.create(
name="Test establishment",
establishment_type_id=self.establishment_type.id,
is_publish=True,
slug="test",
)
self.user_role = UserRole.objects.create(
user=self.user,
role=self.role,
establishment=self.establishment,
)
self.partner = Partner.objects.create(
url='www.ya.ru',
establishment=self.establishment,
)
class PartnerWebTestCase(BaseTestCase):
def test_partner_list(self): def test_partner_list(self):
response = self.client.get("/api/web/partner/") response = self.client.get("/api/web/partner/")
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
class PartnerBackTestCase(BaseTestCase):
def test_partner_list(self):
response = self.client.get('/api/back/partner/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_partner_post(self):
test_partner = {
'url': 'http://google.com',
}
response = self.client.post('/api/back/partner/', data=test_partner, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
def test_partner_detail(self):
response = self.client.get(f'/api/back/partner/{self.partner.id}/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_partner_detail_put(self):
data = {
'url': 'http://yandex.com',
'name': 'Yandex',
}
response = self.client.put(f'/api/back/partner/{self.partner.id}/', data=data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_partner_delete(self):
response = self.client.delete(f'/api/back/partner/{self.partner.id}/')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)

View File

@ -1,11 +1,15 @@
from pprint import pprint from pprint import pprint
from establishment.models import Establishment from establishment.models import Establishment
from partner.models import Partner
from transfer.models import EstablishmentBacklinks from transfer.models import EstablishmentBacklinks
from transfer.serializers.partner import PartnerSerializer from transfer.serializers.partner import PartnerSerializer
def transfer_partner(): def transfer_partner():
"""
Transfer data to Partner model only after transfer Establishment
"""
establishments = Establishment.objects.filter(old_id__isnull=False).values_list('old_id', flat=True) establishments = Establishment.objects.filter(old_id__isnull=False).values_list('old_id', flat=True)
queryset = EstablishmentBacklinks.objects.filter( queryset = EstablishmentBacklinks.objects.filter(
establishment_id__in=list(establishments), establishment_id__in=list(establishments),
@ -24,6 +28,7 @@ def transfer_partner():
serialized_data = PartnerSerializer(data=list(queryset), many=True) serialized_data = PartnerSerializer(data=list(queryset), many=True)
if serialized_data.is_valid(): if serialized_data.is_valid():
Partner.objects.all().delete() # TODO: закоментить, если требуется сохранить старые записи
serialized_data.save() serialized_data.save()
else: else:
pprint(f"Partner serializer errors: {serialized_data.errors}") pprint(f"Partner serializer errors: {serialized_data.errors}")

11
apps/partner/urls/back.py Normal file
View File

@ -0,0 +1,11 @@
"""Back account URLs"""
from django.urls import path
from partner.views import back as views
app_name = 'partner'
urlpatterns = [
path('', views.PartnerLstView.as_view(), name='partner-list-create'),
path('<int:id>/', views.PartnerRUDView.as_view(), name='partner-rud'),
]

View File

@ -0,0 +1,27 @@
from django_filters.rest_framework import DjangoFilterBackend, filters
from rest_framework import generics, permissions
from partner.models import Partner
from partner.serializers import back as serializers
from utils.permissions import IsEstablishmentManager
class PartnerLstView(generics.ListCreateAPIView):
"""Partner list create view."""
queryset = Partner.objects.all()
serializer_class = serializers.BackPartnerSerializer
pagination_class = None
permission_classes = [permissions.IsAdminUser | IsEstablishmentManager]
filter_backends = (DjangoFilterBackend,)
filterset_fields = (
'establishment',
'type',
)
class PartnerRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Partner RUD view."""
queryset = Partner.objects.all()
serializer_class = serializers.BackPartnerSerializer
permission_classes = [permissions.IsAdminUser | IsEstablishmentManager]
lookup_field = 'id'

View File

@ -1 +0,0 @@
# Create your views here.

View File

@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from utils.models import (BaseAttributes, ProjectBaseMixin, HasTagsMixin, from utils.models import (BaseAttributes, ProjectBaseMixin, HasTagsMixin,
TranslatedFieldsMixin, TJSONField, TranslatedFieldsMixin, TJSONField, FavoritesMixin,
GalleryModelMixin, IntermediateGalleryModelMixin) GalleryModelMixin, IntermediateGalleryModelMixin)
@ -131,7 +131,8 @@ class ProductQuerySet(models.QuerySet):
) )
class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes, HasTagsMixin): class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes,
HasTagsMixin, FavoritesMixin):
"""Product models.""" """Product models."""
EARLIEST_VINTAGE_YEAR = 1700 EARLIEST_VINTAGE_YEAR = 1700

View File

@ -2,17 +2,6 @@ from pprint import pprint
from transfer import models as transfer_models from transfer import models as transfer_models
from transfer.serializers import product as product_serializers from transfer.serializers import product as product_serializers
from transfer.serializers.partner import PartnerSerializer
def transfer_partner():
queryset = transfer_models.EstablishmentBacklinks.objects.filter(type="Partner")
serialized_data = PartnerSerializer(data=list(queryset.values()), many=True)
if serialized_data.is_valid():
serialized_data.save()
else:
pprint(f"News serializer errors: {serialized_data.errors}")
def transfer_wine_color(): def transfer_wine_color():
@ -50,8 +39,8 @@ def transfer_wine_bottles_produced():
) )
queryset = [vars(query) for query in raw_queryset] queryset = [vars(query) for query in raw_queryset]
serialized_data = product_serializers.WineBottlesProducedSerializer( serialized_data = product_serializers.WineBottlesProducedSerializer(
data=queryset, data=queryset,
many=True) many=True)
if serialized_data.is_valid(): if serialized_data.is_valid():
serialized_data.save() serialized_data.save()
else: else:
@ -69,8 +58,8 @@ def transfer_wine_classification_type():
) )
queryset = [vars(query) for query in raw_queryset] queryset = [vars(query) for query in raw_queryset]
serialized_data = product_serializers.WineClassificationTypeSerializer( serialized_data = product_serializers.WineClassificationTypeSerializer(
data=queryset, data=queryset,
many=True) many=True)
if serialized_data.is_valid(): if serialized_data.is_valid():
serialized_data.save() serialized_data.save()
else: else:
@ -79,10 +68,10 @@ def transfer_wine_classification_type():
def transfer_wine_standard(): def transfer_wine_standard():
queryset = transfer_models.ProductClassification.objects.filter(parent_id__isnull=True) \ queryset = transfer_models.ProductClassification.objects.filter(parent_id__isnull=True) \
.exclude(type='Classification') .exclude(type='Classification')
serialized_data = product_serializers.ProductStandardSerializer( serialized_data = product_serializers.ProductStandardSerializer(
data=list(queryset.values()), data=list(queryset.values()),
many=True) many=True)
if serialized_data.is_valid(): if serialized_data.is_valid():
serialized_data.save() serialized_data.save()
else: else:
@ -92,8 +81,8 @@ def transfer_wine_standard():
def transfer_wine_classifications(): def transfer_wine_classifications():
queryset = transfer_models.ProductClassification.objects.filter(type='Classification') queryset = transfer_models.ProductClassification.objects.filter(type='Classification')
serialized_data = product_serializers.ProductClassificationSerializer( serialized_data = product_serializers.ProductClassificationSerializer(
data=list(queryset.values()), data=list(queryset.values()),
many=True) many=True)
if serialized_data.is_valid(): if serialized_data.is_valid():
serialized_data.save() serialized_data.save()
else: else:
@ -104,8 +93,8 @@ def transfer_product():
errors = [] errors = []
queryset = transfer_models.Products.objects.all() queryset = transfer_models.Products.objects.all()
serialized_data = product_serializers.ProductSerializer( serialized_data = product_serializers.ProductSerializer(
data=list(queryset.values()), data=list(queryset.values()),
many=True) many=True)
if serialized_data.is_valid(): if serialized_data.is_valid():
serialized_data.save() serialized_data.save()
else: else:
@ -117,8 +106,8 @@ def transfer_product_note():
errors = [] errors = []
queryset = transfer_models.ProductNotes.objects.exclude(text='') queryset = transfer_models.ProductNotes.objects.exclude(text='')
serialized_data = product_serializers.ProductNoteSerializer( serialized_data = product_serializers.ProductNoteSerializer(
data=list(queryset.values()), data=list(queryset.values()),
many=True) many=True)
if serialized_data.is_valid(): if serialized_data.is_valid():
serialized_data.save() serialized_data.save()
else: else:
@ -130,8 +119,8 @@ def transfer_plate():
errors = [] errors = []
queryset = transfer_models.Merchandise.objects.all() queryset = transfer_models.Merchandise.objects.all()
serialized_data = product_serializers.PlateSerializer( serialized_data = product_serializers.PlateSerializer(
data=list(queryset.values()), data=list(queryset.values()),
many=True) many=True)
if serialized_data.is_valid(): if serialized_data.is_valid():
serialized_data.save() serialized_data.save()
else: else:
@ -143,8 +132,8 @@ def transfer_plate_image():
errors = [] errors = []
queryset = transfer_models.Merchandise.objects.all() queryset = transfer_models.Merchandise.objects.all()
serialized_data = product_serializers.PlateImageSerializer( serialized_data = product_serializers.PlateImageSerializer(
data=list(queryset.values()), data=list(queryset.values()),
many=True) many=True)
if serialized_data.is_valid(): if serialized_data.is_valid():
serialized_data.save() serialized_data.save()
else: else:
@ -153,7 +142,6 @@ def transfer_plate_image():
data_types = { data_types = {
"partner": [transfer_partner],
"wine_characteristics": [ "wine_characteristics": [
transfer_wine_sugar_content, transfer_wine_sugar_content,
transfer_wine_color, transfer_wine_color,
@ -161,12 +149,12 @@ data_types = {
transfer_wine_classification_type, transfer_wine_classification_type,
transfer_wine_standard, transfer_wine_standard,
transfer_wine_classifications, transfer_wine_classifications,
], ],
"product": [ "product": [
transfer_product, transfer_product,
], ],
"product_note": [ "product_note": [
transfer_product_note, transfer_product_note,
], ],
"souvenir": [ "souvenir": [
transfer_plate, transfer_plate,

View File

@ -3,9 +3,9 @@ from rest_framework import generics, permissions
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from product.models import Product from product.models import Product
from comment.models import Comment from comment.models import Comment
from product import serializers from product import filters, serializers
from product import filters
from comment.serializers import CommentRUDSerializer from comment.serializers import CommentRUDSerializer
from utils.views import FavoritesCreateDestroyMixinView
class ProductBaseView(generics.GenericAPIView): class ProductBaseView(generics.GenericAPIView):
@ -37,22 +37,11 @@ class ProductDetailView(ProductBaseView, generics.RetrieveAPIView):
serializer_class = serializers.ProductDetailSerializer serializer_class = serializers.ProductDetailSerializer
class CreateFavoriteProductView(generics.CreateAPIView, class CreateFavoriteProductView(FavoritesCreateDestroyMixinView):
generics.DestroyAPIView):
"""View for create/destroy product in favorites.""" """View for create/destroy product in favorites."""
_model = Product
serializer_class = serializers.ProductFavoritesCreateSerializer 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
class ProductCommentCreateView(generics.CreateAPIView): class ProductCommentCreateView(generics.CreateAPIView):

View File

View File

@ -0,0 +1,23 @@
from django.core.management.base import BaseCommand
from tqdm import tqdm
from review.models import Review
from transfer.models import Reviews
class Command(BaseCommand):
help = '''Add review priority from old db to new db.'''
def handle(self, *args, **kwargs):
reviews = Review.objects.all().values_list('old_id', flat=True)
queryset = Reviews.objects.exclude(product_id__isnull=False).filter(
id__in=list(reviews),
).values_list('id', 'priority')
for old_id, priority in tqdm(queryset, desc='Add priority to reviews'):
review = Review.objects.filter(old_id=old_id).first()
if review:
review.priority = priority
review.save()
self.stdout.write(self.style.WARNING(f'Priority added to review objects.'))

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2019-11-28 13:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('review', '0018_auto_20191117_1117'),
]
operations = [
migrations.AddField(
model_name='review',
name='priority',
field=models.PositiveSmallIntegerField(blank=True, default=None, null=True, verbose_name='Priority'),
),
]

View File

@ -39,7 +39,6 @@ class Review(BaseAttributes, TranslatedFieldsMixin):
(TO_REVIEW, _('To review')), (TO_REVIEW, _('To review')),
(READY, _('Ready')), (READY, _('Ready')),
) )
reviewer = models.ForeignKey( reviewer = models.ForeignKey(
'account.User', 'account.User',
related_name='reviews', related_name='reviews',
@ -83,6 +82,7 @@ class Review(BaseAttributes, TranslatedFieldsMixin):
) )
vintage = models.IntegerField(_('Year of review'), validators=[MinValueValidator(1900), MaxValueValidator(2100)]) vintage = models.IntegerField(_('Year of review'), validators=[MinValueValidator(1900), MaxValueValidator(2100)])
mark = models.FloatField(verbose_name=_('mark'), blank=True, null=True, default=None) mark = models.FloatField(verbose_name=_('mark'), blank=True, null=True, default=None)
priority = models.PositiveSmallIntegerField(_('Priority'), blank=True, null=True, default=None)
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None) old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
objects = ReviewQuerySet.as_manager() objects = ReviewQuerySet.as_manager()

View File

@ -1,3 +1,54 @@
"""Review app back serializers.""" """Review app back serializers."""
from review import models from django.contrib.contenttypes.models import ContentType
from rest_framework import serializers from rest_framework import serializers
from account.models import User
from review.models import Review
class _ReviewerSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = (
'id',
'username',
'first_name',
'last_name',
'email',
)
class _ContentTypeSerializer(serializers.ModelSerializer):
class Meta:
model = ContentType
fields = (
'id',
'app_label',
'model',
)
class ReviewBackSerializer(serializers.ModelSerializer):
reviewer_data = _ReviewerSerializer(read_only=True, source='reviewer')
content_type_data = _ContentTypeSerializer(read_only=True, source='content_type')
status_display = serializers.CharField(read_only=True, source='get_status_display')
class Meta:
model = Review
fields = (
'id',
'reviewer',
'reviewer_data',
'text',
'status',
'status_display',
'mark',
'priority',
# 'child',
'published_at',
'vintage',
# 'country',
'content_type',
'content_type_data',
'object_id',
)

View File

@ -10,6 +10,7 @@ class ReviewBaseSerializer(serializers.ModelSerializer):
'id', 'id',
'reviewer', 'reviewer',
'text', 'text',
'priority',
'status', 'status',
'child', 'child',
'published_at', 'published_at',
@ -33,6 +34,7 @@ class ReviewShortSerializer(ReviewBaseSerializer):
class InquiriesBaseSerializer(serializers.ModelSerializer): class InquiriesBaseSerializer(serializers.ModelSerializer):
"""Serializer for model Inquiries.""" """Serializer for model Inquiries."""
class Meta: class Meta:
model = Inquiries model = Inquiries
fields = ( fields = (
@ -56,6 +58,7 @@ class InquiriesBaseSerializer(serializers.ModelSerializer):
class GridItemsBaseSerializer(serializers.ModelSerializer): class GridItemsBaseSerializer(serializers.ModelSerializer):
"""Serializer for model GridItems.""" """Serializer for model GridItems."""
class Meta: class Meta:
model = GridItems model = GridItems
fields = ( fields = (

View File

@ -4,19 +4,34 @@ from review import filters
from review import models from review import models
from review import serializers from review import serializers
from utils.permissions import IsReviewerManager, IsRestaurantReviewer from utils.permissions import IsReviewerManager, IsRestaurantReviewer
from review.serializers.back import ReviewBackSerializer
class ReviewLstView(generics.ListCreateAPIView): class ReviewLstView(generics.ListCreateAPIView):
"""Comment list create view.""" """Review list create view.
serializer_class = serializers.ReviewBaseSerializer
status values:
TO_INVESTIGATE = 0
TO_REVIEW = 1
READY = 2
"""
serializer_class = ReviewBackSerializer
queryset = models.Review.objects.all() queryset = models.Review.objects.all()
permission_classes = [permissions.IsAuthenticatedOrReadOnly, ] permission_classes = [permissions.IsAuthenticatedOrReadOnly, ]
filterset_class = filters.ReviewFilter filterset_class = filters.ReviewFilter
class ReviewRUDView(generics.RetrieveUpdateDestroyAPIView): class ReviewRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Comment RUD view.""" """Review RUD view.
serializer_class = serializers.ReviewBaseSerializer
status values:
TO_INVESTIGATE = 0
TO_REVIEW = 1
READY = 2
"""
serializer_class = ReviewBackSerializer
queryset = models.Review.objects.all() queryset = models.Review.objects.all()
permission_classes = [permissions.IsAdminUser | IsReviewerManager | IsRestaurantReviewer] permission_classes = [permissions.IsAdminUser | IsReviewerManager | IsRestaurantReviewer]
lookup_field = 'id' lookup_field = 'id'

View File

@ -1,11 +1,12 @@
from search_indexes.documents.establishment import EstablishmentDocument from search_indexes.documents.establishment import EstablishmentDocument
from search_indexes.documents.news import NewsDocument from search_indexes.documents.news import NewsDocument
from search_indexes.documents.product import ProductDocument from search_indexes.documents.product import ProductDocument
from search_indexes.tasks import es_update
# todo: make signal to update documents on related fields # todo: make signal to update documents on related fields
__all__ = [ __all__ = [
'EstablishmentDocument', 'EstablishmentDocument',
'NewsDocument', 'NewsDocument',
'ProductDocument', 'ProductDocument',
'es_update',
] ]

View File

@ -46,6 +46,7 @@ class EstablishmentDocument(Document):
'id': fields.IntegerField(attr='id'), 'id': fields.IntegerField(attr='id'),
'label': fields.ObjectField(attr='label_indexing', 'label': fields.ObjectField(attr='label_indexing',
properties=OBJECT_FIELD_PROPERTIES), properties=OBJECT_FIELD_PROPERTIES),
'value': fields.KeywordField(),
}, },
multi=True) multi=True)
visible_tags = fields.ObjectField( visible_tags = fields.ObjectField(
@ -53,6 +54,7 @@ class EstablishmentDocument(Document):
'id': fields.IntegerField(attr='id'), 'id': fields.IntegerField(attr='id'),
'label': fields.ObjectField(attr='label_indexing', 'label': fields.ObjectField(attr='label_indexing',
properties=OBJECT_FIELD_PROPERTIES), properties=OBJECT_FIELD_PROPERTIES),
'value': fields.KeywordField(),
}, },
multi=True) multi=True)
products = fields.ObjectField( products = fields.ObjectField(
@ -69,6 +71,14 @@ class EstablishmentDocument(Document):
# 'coordinates': fields.GeoPointField(), # 'coordinates': fields.GeoPointField(),
'description': fields.ObjectField(attr='description_indexing', properties=OBJECT_FIELD_PROPERTIES), 'description': fields.ObjectField(attr='description_indexing', properties=OBJECT_FIELD_PROPERTIES),
}), }),
'wine_colors': fields.ObjectField(
properties={
'id': fields.IntegerField(),
'label': fields.ObjectField(attr='label_indexing', properties=OBJECT_FIELD_PROPERTIES),
'value': fields.KeywordField(),
},
multi=True,
),
'wine_sub_region': fields.ObjectField(properties={ 'wine_sub_region': fields.ObjectField(properties={
'id': fields.IntegerField(), 'id': fields.IntegerField(),
'name': fields.KeywordField(), 'name': fields.KeywordField(),
@ -113,6 +123,7 @@ class EstablishmentDocument(Document):
), ),
}, },
) )
favorites_for_users = fields.ListField(field=fields.IntegerField())
class Django: class Django:

View File

@ -41,13 +41,13 @@ class NewsDocument(Document):
properties=OBJECT_FIELD_PROPERTIES), properties=OBJECT_FIELD_PROPERTIES),
}, },
multi=True) multi=True)
favorites_for_users = fields.ListField(field=fields.IntegerField())
start = fields.DateField(attr='start')
class Django: class Django:
model = models.News model = models.News
fields = ( fields = (
'id', 'id',
'start',
'end', 'end',
'slug', 'slug',
'state', 'state',
@ -57,7 +57,7 @@ class NewsDocument(Document):
related_models = [models.NewsType] related_models = [models.NewsType]
def get_queryset(self): def get_queryset(self):
return super().get_queryset().published().with_base_related().sort_by_start() return super().get_queryset().published().with_base_related()
def get_instances_from_related(self, related_instance): def get_instances_from_related(self, related_instance):
"""If related_models is set, define how to retrieve the Car instance(s) from the related model. """If related_models is set, define how to retrieve the Car instance(s) from the related model.

View File

@ -148,6 +148,8 @@ class ProductDocument(Document):
name = fields.TextField(attr='display_name', analyzer='english') name = fields.TextField(attr='display_name', analyzer='english')
name_ru = fields.TextField(attr='display_name', analyzer='russian') name_ru = fields.TextField(attr='display_name', analyzer='russian')
name_fr = fields.TextField(attr='display_name', analyzer='french') name_fr = fields.TextField(attr='display_name', analyzer='french')
favorites_for_users = fields.ListField(field=fields.IntegerField())
created = fields.DateField(attr='created') # publishing date (?)
class Django: class Django:
model = models.Product model = models.Product

View File

@ -1,7 +1,81 @@
"""Search indexes filters.""" """Search indexes filters."""
from elasticsearch_dsl.query import Q from elasticsearch_dsl.query import Q
from django_elasticsearch_dsl_drf.filter_backends import SearchFilterBackend from django_elasticsearch_dsl_drf.filter_backends import SearchFilterBackend, \
FacetedSearchFilterBackend, GeoSpatialFilteringFilterBackend
from search_indexes.utils import OBJECT_FIELD_PROPERTIES from search_indexes.utils import OBJECT_FIELD_PROPERTIES
from six import iteritems
class CustomGeoSpatialFilteringFilterBackend(GeoSpatialFilteringFilterBackend):
"""Automatically adds centering and sorting within bounding box."""
@staticmethod
def calculate_center(a, b):
return (a[0] + b[0]) / 2, (a[1] + b[1]) / 2
def filter_queryset(self, request, queryset, view):
ret = super().filter_queryset(request, queryset, view)
bb = request.query_params.get('location__geo_bounding_box')
if bb:
center = self.calculate_center(*map(lambda p: list(map(lambda x: float(x),p.split(','))), bb.split('__')))
request.GET._mutable = True
request.query_params.update({
'ordering': f'location__{center[0]}__{center[1]}__km'
})
request.GET._mutable = False
return ret
class CustomFacetedSearchFilterBackend(FacetedSearchFilterBackend):
def __init__(self):
self.facets_computed = {}
def aggregate(self, request, queryset, view):
"""Aggregate.
:param request:
:param queryset:
:param view:
:return:
"""
def makefilter(cur_facet):
def myfilter(x):
return cur_facet['facet']._params['field'] != next(iter(x._params))
return myfilter
__facets = self.construct_facets(request, view)
setattr(view.paginator, 'facets_computed', {})
for __field, __facet in iteritems(__facets):
agg = __facet['facet'].get_aggregation()
agg_filter = Q('match_all')
if __facet['global']:
queryset.aggs.bucket(
'_filter_' + __field,
'global'
).bucket(__field, agg)
else:
qs = queryset.__copy__()
qs.query = queryset.query._clone()
filterer = makefilter(__facet)
for param_type in ['must', 'must_not', 'should']:
if qs.query._proxied._params.get(param_type):
qs.query._proxied._params[param_type] = list(
filter(
filterer, qs.query._proxied._params[param_type]
)
)
sh = qs.query._proxied._params.get('should')
if (not sh or not len(sh)) \
and qs.query._proxied._params.get('minimum_should_match'):
qs.query._proxied._params.pop('minimum_should_match')
facet_name = '_filter_' + __field
qs.aggs.bucket(
facet_name,
'filter',
filter=agg_filter
).bucket(__field, agg)
view.paginator.facets_computed.update({facet_name: qs.execute().aggregations[facet_name]})
return queryset
class CustomSearchFilterBackend(SearchFilterBackend): class CustomSearchFilterBackend(SearchFilterBackend):

View File

@ -135,6 +135,9 @@ class ProductEstablishmentDocumentSerializer(serializers.Serializer):
index_name = serializers.CharField() index_name = serializers.CharField()
city = AnotherCityDocumentShortSerializer() city = AnotherCityDocumentShortSerializer()
def get_attribute(self, instance):
return instance.establishment if instance and instance.establishment else None
class AddressDocumentSerializer(serializers.Serializer): class AddressDocumentSerializer(serializers.Serializer):
"""Address serializer for ES Document.""" """Address serializer for ES Document."""
@ -167,7 +170,25 @@ class ScheduleDocumentSerializer(serializers.Serializer):
closed_at = serializers.CharField() closed_at = serializers.CharField()
class NewsDocumentSerializer(DocumentSerializer): class InFavoritesMixin(DocumentSerializer):
"""Append in_favorites field."""
in_favorites = serializers.SerializerMethodField()
def get_in_favorites(self, obj):
request = self.context['request']
user = request.user
if user.is_authenticated:
return user.id in obj.favorites_for_users
return False
class Meta:
"""Meta class."""
_abstract = True
class NewsDocumentSerializer(InFavoritesMixin, DocumentSerializer):
"""News document serializer.""" """News document serializer."""
title_translated = serializers.SerializerMethodField(allow_null=True) title_translated = serializers.SerializerMethodField(allow_null=True)
@ -188,6 +209,7 @@ class NewsDocumentSerializer(DocumentSerializer):
'preview_image_url', 'preview_image_url',
'news_type', 'news_type',
'tags', 'tags',
'start',
'slug', 'slug',
) )
@ -200,7 +222,7 @@ class NewsDocumentSerializer(DocumentSerializer):
return get_translated_value(obj.subtitle) return get_translated_value(obj.subtitle)
class EstablishmentDocumentSerializer(DocumentSerializer): class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer):
"""Establishment document serializer.""" """Establishment document serializer."""
establishment_type = EstablishmentTypeSerializer() establishment_type = EstablishmentTypeSerializer()
@ -236,7 +258,7 @@ class EstablishmentDocumentSerializer(DocumentSerializer):
) )
class ProductDocumentSerializer(DocumentSerializer): class ProductDocumentSerializer(InFavoritesMixin, DocumentSerializer):
"""Product document serializer""" """Product document serializer"""
tags = TagsDocumentSerializer(many=True, source='related_tags') tags = TagsDocumentSerializer(many=True, source='related_tags')
@ -271,4 +293,5 @@ class ProductDocumentSerializer(DocumentSerializer):
'grape_variety', 'grape_variety',
'establishment_detail', 'establishment_detail',
'average_price', 'average_price',
'created',
) )

View File

@ -0,0 +1,50 @@
"""SearchIndex tasks."""
import logging
from django.db import models
from celery.schedules import crontab
from celery.task import periodic_task
from django_elasticsearch_dsl.registries import registry
from django_redis import get_redis_connection
from establishment.models import Establishment
from news.models import News
from product.models import Product
logger = logging.getLogger(__name__)
@periodic_task(run_every=crontab(minute='*/1'))
def update_index():
"""Updates ES index."""
try:
cn = get_redis_connection('es_queue')
for model in [Establishment, News, Product]:
model_name = model.__name__.lower()
while True:
ids = cn.spop(model_name, 500)
if not ids:
break
qs = model.objects.filter(id__in=ids)
try:
doc = registry.get_documents([model]).pop()
except KeyError:
pass
else:
doc().update(qs)
except Exception as ex:
logger.error(f'Updating index failed: {ex}')
def es_update(obj):
"""Adds object to set of objects for indexing."""
try:
cn = get_redis_connection('es_queue')
allowed_models = [Establishment, News, Product]
if isinstance(obj, models.QuerySet) and obj.model in allowed_models:
key = obj.model.__name__.lower()
cn.sadd(key, *obj.values_list('id', flat=True))
elif isinstance(obj, models.Model) and obj.__class__ in allowed_models:
key = obj.__class__.__name__.lower()
cn.sadd(key, obj.id)
except Exception as ex:
logger.warning(f'Send obj to ES failed: {ex}')

View File

@ -6,9 +6,11 @@ from search_indexes import views
router = routers.SimpleRouter() router = routers.SimpleRouter()
# router.register(r'news', views.NewsDocumentViewSet, basename='news') # temporarily disabled # router.register(r'news', views.NewsDocumentViewSet, basename='news') # temporarily disabled
router.register(r'establishments', views.EstablishmentDocumentViewSet, basename='establishment') router.register(r'establishments', views.EstablishmentDocumentViewSet, basename='establishment')
router.register(r'mobile/establishments', views.EstablishmentDocumentViewSet, basename='establishment-mobile') router.register(r'mobile/establishments', views.MobileEstablishmentDocumentViewSet, basename='establishment-mobile')
router.register(r'news', views.NewsDocumentViewSet, basename='news') router.register(r'news', views.NewsDocumentViewSet, basename='news')
router.register(r'mobile/news', views.MobileNewsDocumentViewSet, basename='news-mobile')
router.register(r'products', views.ProductDocumentViewSet, basename='product') router.register(r'products', views.ProductDocumentViewSet, basename='product')
router.register(r'mobile/products', views.MobileProductDocumentViewSet, basename='product-mobile')
urlpatterns = router.urls urlpatterns = router.urls

View File

@ -2,6 +2,8 @@
from django_elasticsearch_dsl import fields from django_elasticsearch_dsl import fields
from utils.models import get_current_locale, get_default_locale from utils.models import get_current_locale, get_default_locale
FACET_MAX_RESPONSE = 9999999 # Unlimited
ALL_LOCALES_LIST = [ ALL_LOCALES_LIST = [
'hr-HR', 'hr-HR',
'ro-RO', 'ro-RO',

View File

@ -4,13 +4,15 @@ 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, GeoSpatialOrderingFilterBackend,
OrderingFilterBackend,
) )
from elasticsearch_dsl import TermsFacet
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, utils
from search_indexes.documents import EstablishmentDocument, NewsDocument from search_indexes.documents import EstablishmentDocument, NewsDocument
from search_indexes.documents.product import ProductDocument from search_indexes.documents.product import ProductDocument
from utils.pagination import ProjectMobilePagination from utils.pagination import ESDocumentPagination
class NewsDocumentViewSet(BaseDocumentViewSet): class NewsDocumentViewSet(BaseDocumentViewSet):
@ -18,15 +20,34 @@ class NewsDocumentViewSet(BaseDocumentViewSet):
document = NewsDocument document = NewsDocument
lookup_field = 'slug' lookup_field = 'slug'
pagination_class = ProjectMobilePagination pagination_class = ESDocumentPagination
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
serializer_class = serializers.NewsDocumentSerializer serializer_class = serializers.NewsDocumentSerializer
filter_backends = [ filter_backends = [
filters.CustomSearchFilterBackend, filters.CustomSearchFilterBackend,
FilteringFilterBackend, FilteringFilterBackend,
filters.CustomFacetedSearchFilterBackend,
OrderingFilterBackend
] ]
ordering_fields = {
'start': {
'field': 'start',
},
}
faceted_search_fields = {
'tag': {
'field': 'tags.id',
'enabled': True,
'facet': TermsFacet,
'options': {
'size': utils.FACET_MAX_RESPONSE,
},
},
}
search_fields = { search_fields = {
'title': {'fuzziness': 'auto:2,5', 'title': {'fuzziness': 'auto:2,5',
'boost': 3}, 'boost': 3},
@ -65,12 +86,19 @@ class NewsDocumentViewSet(BaseDocumentViewSet):
} }
class MobileNewsDocumentViewSet(NewsDocumentViewSet):
filter_backends = [
filters.CustomSearchFilterBackend,
FilteringFilterBackend,
]
class EstablishmentDocumentViewSet(BaseDocumentViewSet): class EstablishmentDocumentViewSet(BaseDocumentViewSet):
"""Establishment document ViewSet.""" """Establishment document ViewSet."""
document = EstablishmentDocument document = EstablishmentDocument
lookup_field = 'slug' pagination_class = ESDocumentPagination
pagination_class = ProjectMobilePagination
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
serializer_class = serializers.EstablishmentDocumentSerializer serializer_class = serializers.EstablishmentDocumentSerializer
@ -82,10 +110,63 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
filter_backends = [ filter_backends = [
FilteringFilterBackend, FilteringFilterBackend,
filters.CustomSearchFilterBackend, filters.CustomSearchFilterBackend,
GeoSpatialFilteringFilterBackend, filters.CustomGeoSpatialFilteringFilterBackend,
# DefaultOrderingFilterBackend, filters.CustomFacetedSearchFilterBackend,
GeoSpatialOrderingFilterBackend,
] ]
faceted_search_fields = {
'works_at_weekday': {
'field': 'works_at_weekday',
'facet': TermsFacet,
'enabled': True,
},
'toque_number': {
'field': 'toque_number',
'enabled': True,
'facet': TermsFacet,
},
'works_noon': {
'field': 'works_noon',
'facet': TermsFacet,
'enabled': True,
},
'works_evening': {
'field': 'works_evening',
'facet': TermsFacet,
'enabled': True,
},
'works_now': {
'field': 'works_now',
'facet': TermsFacet,
'enabled': True,
},
'tag': {
'field': 'tags.id',
'facet': TermsFacet,
'enabled': True,
'options': {
'size': utils.FACET_MAX_RESPONSE,
},
},
'wine_colors': {
'field': 'products.wine_colors.id',
'facet': TermsFacet,
'enabled': True,
'options': {
'size': utils.FACET_MAX_RESPONSE,
},
},
'wine_region_id': {
'field': 'products.wine_region.id',
'facet': TermsFacet,
'enabled': True,
'options': {
'size': utils.FACET_MAX_RESPONSE,
},
}
}
search_fields = { search_fields = {
'name': {'fuzziness': 'auto:2,5', 'name': {'fuzziness': 'auto:2,5',
'boost': 4}, 'boost': 4},
@ -124,6 +205,13 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
constants.LOOKUP_QUERY_IN, constants.LOOKUP_QUERY_IN,
] ]
}, },
'wine_colors_id': {
'field': 'products.wine_colors.id',
'lookups': [
constants.LOOKUP_QUERY_IN,
constants.LOOKUP_QUERY_EXCLUDE,
],
},
'wine_region_id': { 'wine_region_id': {
'field': 'products.wine_region.id', 'field': 'products.wine_region.id',
'lookups': [ 'lookups': [
@ -206,20 +294,44 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
} }
} }
geo_spatial_ordering_fields = {
'location': {
'field': 'address.coordinates',
},
}
class MobileEstablishmentDocumentViewSet(EstablishmentDocumentViewSet):
filter_backends = [
FilteringFilterBackend,
filters.CustomSearchFilterBackend,
GeoSpatialFilteringFilterBackend,
]
class ProductDocumentViewSet(BaseDocumentViewSet): class ProductDocumentViewSet(BaseDocumentViewSet):
"""Product document ViewSet.""" """Product document ViewSet."""
document = ProductDocument document = ProductDocument
pagination_class = ProjectMobilePagination pagination_class = ESDocumentPagination
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
serializer_class = serializers.ProductDocumentSerializer serializer_class = serializers.ProductDocumentSerializer
filter_backends = [ filter_backends = [
FilteringFilterBackend, FilteringFilterBackend,
filters.CustomSearchFilterBackend, filters.CustomSearchFilterBackend,
filters.CustomFacetedSearchFilterBackend,
OrderingFilterBackend,
# GeoSpatialOrderingFilterBackend,
] ]
ordering_fields = {
'created': {
'field': 'created',
},
}
search_fields = { search_fields = {
'name': {'fuzziness': 'auto:2,5', 'name': {'fuzziness': 'auto:2,5',
'boost': 8}, 'boost': 8},
@ -232,6 +344,25 @@ class ProductDocumentViewSet(BaseDocumentViewSet):
'description': {'fuzziness': 'auto:2,5'}, 'description': {'fuzziness': 'auto:2,5'},
} }
faceted_search_fields = {
'tag': {
'field': 'wine_colors.id',
'enabled': True,
'facet': TermsFacet,
'options': {
'size': utils.FACET_MAX_RESPONSE,
},
},
'wine_region_id': {
'field': 'wine_region.id',
'enabled': True,
'facet': TermsFacet,
'options': {
'size': utils.FACET_MAX_RESPONSE,
},
},
}
translated_search_fields = ( translated_search_fields = (
'description', 'description',
) )
@ -289,4 +420,12 @@ class ProductDocumentViewSet(BaseDocumentViewSet):
constants.LOOKUP_QUERY_EXCLUDE, constants.LOOKUP_QUERY_EXCLUDE,
], ],
}, },
} }
class MobileProductDocumentViewSet(ProductDocumentViewSet):
filter_backends = [
FilteringFilterBackend,
filters.CustomSearchFilterBackend,
]

View File

@ -43,8 +43,8 @@ class TagCategoryFilterSet(TagsBaseFilterSet):
'product_type', ) 'product_type', )
def by_product_type(self, queryset, name, value): def by_product_type(self, queryset, name, value):
# if value == product_models.ProductType.WINE: if value == product_models.ProductType.WINE:
# queryset = queryset.filter(index_name='wine-color').filter(tags__products__isnull=False) return queryset.wine_tags_category().filter(tags__products__isnull=False)
queryset = queryset.by_product_type(value) queryset = queryset.by_product_type(value)
return queryset return queryset
@ -86,7 +86,7 @@ class TagsFilterSet(TagsBaseFilterSet):
if self.NEWS in value: if self.NEWS in value:
queryset = queryset.for_news().filter(value__in=settings.NEWS_CHOSEN_TAGS).distinct('value') queryset = queryset.for_news().filter(value__in=settings.NEWS_CHOSEN_TAGS).distinct('value')
if self.ESTABLISHMENT in value: if self.ESTABLISHMENT in value:
queryset = queryset.for_establishments().filter(value__in=settings.ESTABLISHMENT_CHOSEN_TAGS).distinct( queryset = queryset.for_establishments().filter(category__value_type='list').filter(value__in=settings.ESTABLISHMENT_CHOSEN_TAGS).distinct(
'value') 'value')
return queryset return queryset

View File

@ -111,6 +111,9 @@ class TagCategoryQuerySet(models.QuerySet):
"""Filter by product type index name.""" """Filter by product type index name."""
return self.filter(tags__products__product_type__index_name=index_name) return self.filter(tags__products__product_type__index_name=index_name)
def wine_tags_category(self):
return self.filter(index_name='wine-color')
def with_tags(self, switcher=True): def with_tags(self, switcher=True):
"""Filter by existing tags.""" """Filter by existing tags."""
return self.exclude(tags__isnull=switcher) return self.exclude(tags__isnull=switcher)

View File

@ -20,6 +20,8 @@ class ScheduleRUDSerializer(serializers.ModelSerializer):
dinner_end = serializers.TimeField(required=False) dinner_end = serializers.TimeField(required=False)
opening_at = serializers.TimeField(required=False) opening_at = serializers.TimeField(required=False)
closed_at = serializers.TimeField(required=False) closed_at = serializers.TimeField(required=False)
# For permission!!
establishment_id = serializers.ReadOnlyField(source='establishment.id')
class Meta: class Meta:
"""Meta class.""" """Meta class."""
@ -34,6 +36,7 @@ class ScheduleRUDSerializer(serializers.ModelSerializer):
'dinner_end', 'dinner_end',
'opening_at', 'opening_at',
'closed_at', 'closed_at',
'establishment_id'
] ]
def validate(self, attrs): def validate(self, attrs):

View File

@ -369,44 +369,45 @@ class GuideFilters(MigrateMixin):
db_table = 'guide_filters' db_table = 'guide_filters'
# class GuideSections(MigrateMixin):
# class GuideSections(MigrateMixin): using = 'legacy'
# using = 'legacy'
# type = models.CharField(max_length=255)
# type = models.CharField(max_length=255) key_name = models.CharField(max_length=255, blank=True, null=True)
# key_name = models.CharField(max_length=255, blank=True, null=True) value_name = models.CharField(max_length=255, blank=True, null=True)
# value_name = models.CharField(max_length=255, blank=True, null=True) right = models.IntegerField(blank=True, null=True)
# right = models.IntegerField(blank=True, null=True) created_at = models.DateTimeField()
# created_at = models.DateTimeField() updated_at = models.DateTimeField()
# updated_at = models.DateTimeField()
class Meta:
managed = False
db_table = 'guide_elements'
# class GuideElements(MigrateMixin): class GuideElements(MigrateMixin):
# using = 'legacy' using = 'legacy'
#
# type = models.CharField(max_length=255) type = models.CharField(max_length=255)
# establishment = models.ForeignKey(Establishments, models.DO_NOTHING, blank=True, null=True) establishment = models.ForeignKey('Establishments', models.DO_NOTHING, blank=True, null=True)
# review = models.ForeignKey('Reviews', models.DO_NOTHING, blank=True, null=True) review = models.ForeignKey('Reviews', models.DO_NOTHING, blank=True, null=True)
# review_text = models.ForeignKey('ReviewTexts', models.DO_NOTHING, blank=True, null=True) review_text = models.ForeignKey('ReviewTexts', models.DO_NOTHING, blank=True, null=True)
# wine_region = models.ForeignKey('WineLocations', models.DO_NOTHING, blank=True, null=True) wine_region = models.ForeignKey('WineLocations', models.DO_NOTHING, blank=True, null=True)
# wine = models.ForeignKey('Products', models.DO_NOTHING, blank=True, null=True) wine = models.ForeignKey('Products', models.DO_NOTHING, blank=True, null=True)
# color = models.CharField(max_length=255, blank=True, null=True) color = models.CharField(max_length=255, blank=True, null=True)
# order_number = models.IntegerField(blank=True, null=True) order_number = models.IntegerField(blank=True, null=True)
# guide_ad = models.ForeignKey(GuideAds, models.DO_NOTHING, blank=True, null=True) guide_ad = models.ForeignKey(GuideAds, models.DO_NOTHING, blank=True, null=True)
# city = models.ForeignKey(Cities, models.DO_NOTHING, blank=True, null=True) city = models.ForeignKey(Cities, models.DO_NOTHING, blank=True, null=True)
# section = models.ForeignKey('GuideSections', models.DO_NOTHING, blank=True, null=True) section = models.ForeignKey('GuideSections', models.DO_NOTHING, blank=True, null=True)
# guide_id = models.IntegerField(blank=True, null=True) guide_id = models.IntegerField(blank=True, null=True)
# parent_id = models.IntegerField(blank=True, null=True) parent = models.ForeignKey('self', models.DO_NOTHING, blank=True, null=True)
# lft = models.IntegerField() lft = models.IntegerField()
# rgt = models.IntegerField() rgt = models.IntegerField()
# depth = models.IntegerField() depth = models.IntegerField()
# children_count = models.IntegerField() children_count = models.IntegerField()
# created_at = models.DateTimeField()
# updated_at = models.DateTimeField() class Meta:
# managed = False
# class Meta: db_table = 'guide_elements'
# managed = False
# db_table = 'guide_elements'
class Establishments(MigrateMixin): class Establishments(MigrateMixin):

View File

@ -1,29 +1,53 @@
from rest_framework import serializers from rest_framework import serializers
from establishment.models import Establishment
from partner.models import Partner from partner.models import Partner
class PartnerSerializer(serializers.Serializer): class PartnerSerializer(serializers.Serializer):
pass id = serializers.IntegerField()
# 'id', establishment_id = serializers.IntegerField()
# 'establishment_id', partnership_name = serializers.CharField(allow_null=True)
# 'partnership_name', partnership_icon = serializers.CharField(allow_null=True)
# 'partnership_icon', backlink_url = serializers.CharField(allow_null=True)
# 'backlink_url', created_at = serializers.DateTimeField(format='%m-%d-%Y %H:%M:%S')
# 'created_at', type = serializers.CharField(allow_null=True)
# 'type', starting_date = serializers.DateField(allow_null=True)
# 'starting_date', expiry_date = serializers.DateField(allow_null=True)
# 'expiry_date', price_per_month = serializers.DecimalField(max_digits=10, decimal_places=2, allow_null=True)
# 'price_per_month',
def validate(self, data):
data.update({
'old_id': data.pop('id'),
'name': data['partnership_name'],
'url': data.pop('backlink_url'),
'image': self.get_image(data),
'establishment': self.get_establishment(data),
'type': Partner.PARTNER if data['type'] == 'Partner' else Partner.SPONSOR,
'created': data.pop('created_at'),
})
data.pop('partnership_icon')
data.pop('partnership_name')
data.pop('establishment_id')
return data
# def validate(self, data): @staticmethod
# data["image"] = partnership_to_image_url.get(data["partnership_name"]).get(data["partnership_icon"]) def get_image(data):
# data.pop("partnership_name") return partnership_to_image_url.get(data['partnership_name']).get(data['partnership_icon'])
# data.pop("partnership_icon")
# return data @staticmethod
# def get_establishment(data):
# def create(self, validated_data): establishment = Establishment.objects.filter(old_id=data['establishment_id']).first()
# return Partner.objects.create(**validated_data) if not establishment:
raise ValueError(f"Establishment not found with old_id {data['establishment_id']}: ")
return establishment
def create(self, validated_data):
obj, _ = Partner.objects.update_or_create(
old_id=validated_data['old_id'],
defaults=validated_data,
)
return obj
partnership_to_image_url = { partnership_to_image_url = {

View File

@ -1,3 +1,5 @@
from django.contrib.auth.models import AbstractUser
def with_base_attributes(cls): def with_base_attributes(cls):
@ -8,7 +10,7 @@ def with_base_attributes(cls):
if request and hasattr(request, "user"): if request and hasattr(request, "user"):
user = request.user user = request.user
if user is not None: if user is not None and isinstance(user, AbstractUser):
data.update({'modified_by': user}) data.update({'modified_by': user})
if not self.instance: if not self.instance:

View File

@ -135,6 +135,14 @@ class FavoritesError(exceptions.APIException):
default_detail = _('Item is already in favorites.') default_detail = _('Item is already in favorites.')
class CarouselError(exceptions.APIException):
"""
The exception should be thrown when the object is already in carousels.
"""
status_code = status.HTTP_400_BAD_REQUEST
default_detail = _('Item is already in carousels.')
class PasswordResetRequestExistedError(exceptions.APIException): class PasswordResetRequestExistedError(exceptions.APIException):
""" """
The exception should be thrown when password reset request The exception should be thrown when password reset request

View File

@ -5,16 +5,17 @@ from os.path import exists
from django.conf import settings from django.conf import settings
from django.contrib.auth.tokens import PasswordResetTokenGenerator from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.contrib.gis.db import models from django.contrib.gis.db import models
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import JSONField from django.contrib.postgres.fields import JSONField
from django.contrib.postgres.fields.jsonb import KeyTextTransform from django.contrib.postgres.fields.jsonb import KeyTextTransform
from django.utils import timezone from django.utils import timezone
from django.utils.html import mark_safe from django.utils.html import mark_safe
from django.utils.translation import ugettext_lazy as _, get_language from django.utils.translation import ugettext_lazy as _, get_language
from configuration.models import TranslationSettings
from easy_thumbnails.fields import ThumbnailerImageField from easy_thumbnails.fields import ThumbnailerImageField
from sorl.thumbnail import get_thumbnail from sorl.thumbnail import get_thumbnail
from sorl.thumbnail.fields import ImageField as SORLImageField from sorl.thumbnail.fields import ImageField as SORLImageField
from configuration.models import TranslationSettings
from utils.methods import image_path, svg_image_path from utils.methods import image_path, svg_image_path
from utils.validators import svg_image_validator from utils.validators import svg_image_validator
@ -35,10 +36,6 @@ class ProjectBaseMixin(models.Model):
abstract = True abstract = True
def valid(value):
print("Run")
class TJSONField(JSONField): class TJSONField(JSONField):
"""Overrided JsonField.""" """Overrided JsonField."""
@ -226,6 +223,18 @@ class SORLImageMixin(models.Model):
else: else:
return None return None
def get_cropped_image(self, geometry: str, quality: int, crop: str) -> dict:
cropped_image = get_thumbnail(self.image,
geometry_string=geometry,
crop=crop,
quality=quality)
return {
'geometry_string': geometry,
'crop_url': cropped_image.url,
'quality': quality,
'crop': crop
}
image_tag.short_description = _('Image') image_tag.short_description = _('Image')
image_tag.allow_tags = True image_tag.allow_tags = True
@ -435,4 +444,12 @@ class HasTagsMixin(models.Model):
abstract = True abstract = True
class FavoritesMixin:
"""Append favorites_for_user property."""
@property
def favorites_for_users(self):
return self.favorites.aggregate(arr=ArrayAgg('user_id')).get('arr')
timezone.datetime.now().date().isoformat() timezone.datetime.now().date().isoformat()

View File

@ -3,8 +3,8 @@ from base64 import b64encode
from urllib import parse as urlparse from urllib import parse as urlparse
from django.conf import settings from django.conf import settings
from rest_framework.pagination import PageNumberPagination, CursorPagination from rest_framework.pagination import CursorPagination, PageNumberPagination
from django_elasticsearch_dsl_drf.pagination import PageNumberPagination as ESPagination
class ProjectPageNumberPagination(PageNumberPagination): class ProjectPageNumberPagination(PageNumberPagination):
"""Customized pagination class.""" """Customized pagination class."""
@ -48,6 +48,40 @@ class ProjectMobilePagination(ProjectPageNumberPagination):
return self.page.previous_page_number() return self.page.previous_page_number()
class ESDocumentPagination(ESPagination):
"""Pagination class for ES results. (includes facets)"""
page_size_query_param = 'page_size'
def get_next_link(self):
"""Get next link method."""
if not self.page.has_next():
return None
return self.page.next_page_number()
def get_previous_link(self):
"""Get previous link method."""
if not self.page.has_previous():
return None
return self.page.previous_page_number()
def get_facets(self, page=None):
"""Get facets.
:param page:
:return:
"""
if page is None:
page = self.page
if hasattr(self, 'facets_computed'):
ret = {}
for filter_field, bucket_data in self.facets_computed.items():
ret.update({filter_field: bucket_data.__dict__['_d_']})
return ret
elif hasattr(page, 'facets') and hasattr(page.facets, '_d_'):
return page.facets._d_
class EstablishmentPortionPagination(ProjectMobilePagination): class EstablishmentPortionPagination(ProjectMobilePagination):
""" """
Pagination for app establishments with limit page size equal to 12 Pagination for app establishments with limit page size equal to 12

View File

@ -100,7 +100,10 @@ class IsStandardUser(IsGuest):
if hasattr(obj, 'user'): if hasattr(obj, 'user'):
rules = [ rules = [
obj.user == request.user and obj.user.email_confirmed, obj.user == request.user
and obj.user.email_confirmed
and request.user.is_authenticated,
super().has_object_permission(request, view, obj) super().has_object_permission(request, view, obj)
] ]
@ -117,31 +120,50 @@ class IsContentPageManager(IsStandardUser):
rules = [ rules = [
super().has_permission(request, view) 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 = [ if hasattr(request, 'user'):
UserRole.objects.filter(user=request.user, role=role).exists(), if hasattr(request.data, 'site_id'):
# and obj.user != request.user, role = Role.objects.filter(role=Role.CONTENT_PAGE_MANAGER,
super().has_permission(request, view) site_id=request.data.site_id,) \
] .first()
rules = [
UserRole.objects.filter(user=request.user, role=role).exists(),
super().has_permission(request, view)
]
elif hasattr(request.data, 'country_id'):
role = Role.objects.filter(role=Role.CONTENT_PAGE_MANAGER,
country_id=request.data.country_id) \
.first()
rules = [
UserRole.objects.filter(user=request.user, role=role).exists(),
super().has_permission(request, view)
]
return any(rules) 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.
if hasattr(obj, 'site_id'):
role = Role.objects.filter(role=Role.CONTENT_PAGE_MANAGER,
site_id=obj.site_id) \
.first()
role = Role.objects.filter(role=Role.CONTENT_PAGE_MANAGER, rules = [
country_id=obj.country_id) \ UserRole.objects.filter(user=request.user, role=role).exists(),
.first() # 'Comments moderator' super().has_object_permission(request, view, obj)
]
elif hasattr(obj, 'country_id'):
role = Role.objects.filter(role=Role.CONTENT_PAGE_MANAGER,
country_id=obj.country_id) \
.first()
rules = [
UserRole.objects.filter(user=request.user, role=role).exists(),
super().has_object_permission(request, view, obj)
]
rules = [
UserRole.objects.filter(user=request.user, role=role).exists(),
# and obj.user != request.user,
super().has_object_permission(request, view, obj)
]
return any(rules) return any(rules)
@ -150,36 +172,55 @@ 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): def has_permission(self, request, view):
rules = [ rules = [
super().has_permission(request, view) super().has_permission(request, view)
] ]
# and request.user.email_confirmed, # and request.user.email_confirmed,
if hasattr(request.data, 'user') and hasattr(request.data, 'country_id'): if hasattr(request.data, 'user'):
# Read permissions are allowed to any request. if hasattr(request.data, 'site_id'):
# Read permissions are allowed to any request.
role = Role.objects.filter(role=Role.COUNTRY_ADMIN,
site_id=request.data.site_id) \
.first()
rules = [
UserRole.objects.filter(user=request.user, role=role).exists(),
super().has_permission(request, view)
]
elif hasattr(request.data, 'country_id'):
role = Role.objects.filter(role=Role.COUNTRY_ADMIN, role = Role.objects.filter(role=Role.COUNTRY_ADMIN,
country_id=request.data.country_id) \ country_id=request.data.country_id) \
.first() # 'Comments moderator' .first()
rules = [ rules = [
UserRole.objects.filter(user=request.user, role=role).exists(), UserRole.objects.filter(user=request.user, role=role).exists(),
super().has_permission(request, view) super().has_permission(request, view)
] ]
return any(rules) 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, if hasattr(obj, 'site_id'):
country_id=obj.country_id) \ role = Role.objects.filter(role=Role.COUNTRY_ADMIN,
.first() # 'Comments moderator' site_id=obj.site_id) \
.first()
rules = [
super().has_object_permission(request, view, obj)
]
elif hasattr(obj, 'country_id'):
role = Role.objects.filter(role=Role.COUNTRY_ADMIN,
country_id=obj.country_id) \
.first()
rules = [
super().has_object_permission(request, view, obj)
]
rules = [
super().has_object_permission(request, view, obj)
]
# and request.user.email_confirmed,
if hasattr(request, 'user') and request.user.is_authenticated: if hasattr(request, 'user') and request.user.is_authenticated:
rules = [ rules = [
UserRole.objects.filter(user=request.user, role=role).exists(), UserRole.objects.filter(user=request.user, role=role).exists(),
@ -206,13 +247,12 @@ class IsCommentModerator(IsStandardUser):
super().has_permission(request, view) super().has_permission(request, view)
] ]
# and request.user.email_confirmed, if any(rules) and hasattr(request.data, 'site_id'):
if hasattr(request.data, 'user') and hasattr(request.data, 'country_id'):
# 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=request.data.country_id) \ site_id=request.data.site_id) \
.first() # 'Comments moderator' .first()
rules = [ rules = [
UserRole.objects.filter(user=request.user, role=role).exists(), UserRole.objects.filter(user=request.user, role=role).exists(),
@ -222,16 +262,22 @@ class IsCommentModerator(IsStandardUser):
return any(rules) 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.
role = Role.objects.filter(role=Role.COMMENTS_MODERATOR,
country_id=obj.country_id) \
.first() # 'Comments moderator'
rules = [ rules = [
UserRole.objects.filter(user=request.user, role=role).exists() and
obj.user != request.user,
super().has_object_permission(request, view, obj) super().has_object_permission(request, view, obj)
] ]
if request.user.is_authenticated:
role = Role.objects.filter(role=Role.COMMENTS_MODERATOR,
site_id=obj.site_id) \
.first() # 'Comments moderator'
rules = [
UserRole.objects.filter(user=request.user, role=role).exists() and
obj.user != request.user,
super().has_object_permission(request, view, obj)
]
return any(rules) return any(rules)
@ -242,30 +288,40 @@ class IsEstablishmentManager(IsStandardUser):
super().has_permission(request, view) super().has_permission(request, view)
] ]
# and request.user.email_confirmed, if hasattr(request.data, 'user'):
if hasattr(request.data, 'user') and hasattr(request.data, 'establishment_id'): if hasattr(request.data, 'establishment_id'):
role = Role.objects.filter(role=Role.ESTABLISHMENT_MANAGER) \ role = Role.objects.filter(role=Role.ESTABLISHMENT_MANAGER) \
.first() # 'Comments moderator' .first()
rules = [ rules = [
UserRole.objects.filter(user=request.user, role=role, UserRole.objects.filter(user=request.user, role=role,
establishment_id=request.data.establishment_id establishment_id=request.data.establishment_id
).exists(), ).exists(),
super().has_permission(request, view) super().has_permission(request, view)
] ]
return any(rules) return any(rules)
def has_object_permission(self, request, view, obj): 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, # special!
establishment_id=obj.establishment_id super().has_permission(request, view)
).exists(), # super().has_object_permission(request, view, obj)
super().has_object_permission(request, view, obj)
] ]
role = Role.objects.filter(role=Role.ESTABLISHMENT_MANAGER) \
.first()
if hasattr(obj, 'establishment_id'):
rules = [
UserRole.objects.filter(user=request.user, role=role,
establishment_id=obj.establishment_id
).exists(),
# special!
super().has_permission(request, view)
# super().has_object_permission(request, view, obj)
]
return any(rules) return any(rules)
@ -277,13 +333,13 @@ class IsReviewerManager(IsStandardUser):
] ]
# and request.user.email_confirmed, # and request.user.email_confirmed,
if hasattr(request.data, 'user') and hasattr(request.data, 'country_id'): if hasattr(request.data, 'user') and hasattr(request.data, 'site_id'):
role = Role.objects.filter(role=Role.REVIEWER_MANGER) \ role = Role.objects.filter(role=Role.REVIEWER_MANGER) \
.first() # 'Comments moderator' .first()
rules = [ rules = [
UserRole.objects.filter(user=request.user, role=role, UserRole.objects.filter(user=request.user, role=role,
establishment_id=request.data.country_id establishment_id=request.data.site_id
).exists(), ).exists(),
super().has_permission(request, view) super().has_permission(request, view)
] ]

View File

@ -2,10 +2,11 @@
import pytz import pytz
from django.core import exceptions from django.core import exceptions
from rest_framework import serializers from rest_framework import serializers
from utils import models
from translation.models import Language
from favorites.models import Favorites from favorites.models import Favorites
from gallery.models import Image from main.models import Carousel
from translation.models import Language
from utils import models
class EmptySerializer(serializers.Serializer): class EmptySerializer(serializers.Serializer):
@ -80,7 +81,6 @@ class FavoritesCreateSerializer(serializers.ModelSerializer):
"""Serializer to favorite object.""" """Serializer to favorite object."""
class Meta: class Meta:
"""Serializer for model Comment."""
model = Favorites model = Favorites
fields = [ fields = [
'id', 'id',
@ -101,6 +101,24 @@ class FavoritesCreateSerializer(serializers.ModelSerializer):
return self.request.parser_context.get('kwargs').get('slug') return self.request.parser_context.get('kwargs').get('slug')
class CarouselCreateSerializer(serializers.ModelSerializer):
"""Carousel to favorite object."""
class Meta:
model = Carousel
fields = [
'id',
]
@property
def request(self):
return self.context.get('request')
@property
def pk(self):
return self.request.parser_context.get('kwargs').get('pk')
class RecursiveFieldSerializer(serializers.Serializer): class RecursiveFieldSerializer(serializers.Serializer):
def to_representation(self, value): def to_representation(self, value):
serializer = self.parent.parent.__class__(value, context=self.context) serializer = self.parent.parent.__class__(value, context=self.context)

View File

@ -5,15 +5,13 @@ from translation.models import Language
class BasePermissionTests(APITestCase): class BasePermissionTests(APITestCase):
def setUp(self): def setUp(self):
self.lang = Language.objects.get( self.lang, created = Language.objects.get_or_create(
title='Russia', title='Russia',
locale='ru-RU' locale='ru-RU'
) )
self.lang.save()
self.country_ru = Country.objects.get( self.country_ru, created = Country.objects.get_or_create(
name={"en-GB": "Russian"} name={"en-GB": "Russian"}
) )
self.country_ru.save()

View File

@ -40,8 +40,7 @@ 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, created = Country.objects.get_or_create(
self.country_ru = Country.objects.get(
name={"en-GB": "Russian"} name={"en-GB": "Russian"}
) )

View File

@ -0,0 +1,19 @@
"""Overridden thumbnail engine."""
from sorl.thumbnail.engines.pil_engine import Engine as PILEngine
class GMEngine(PILEngine):
def create(self, image, geometry, options):
"""
Processing conductor, returns the thumbnail as an image engine instance
"""
image = self.cropbox(image, geometry, options)
image = self.orientation(image, geometry, options)
image = self.colorspace(image, geometry, options)
image = self.remove_border(image, options)
image = self.crop(image, geometry, options)
image = self.rounded(image, geometry, options)
image = self.blur(image, geometry, options)
image = self.padding(image, geometry, options)
return image

View File

@ -2,12 +2,13 @@ from collections import namedtuple
from django.conf import settings from django.conf import settings
from django.db.transaction import on_commit from django.db.transaction import on_commit
from rest_framework import generics from django.shortcuts import get_object_or_404
from rest_framework import status from rest_framework import generics, status
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
from gallery.tasks import delete_image from gallery.tasks import delete_image
from search_indexes.documents import es_update
# JWT # JWT
@ -69,22 +70,12 @@ class JWTGenericViewMixin:
def _put_cookies_in_response(self, cookies: list, response: Response): def _put_cookies_in_response(self, cookies: list, response: Response):
"""Update COOKIES in response from namedtuple""" """Update COOKIES in response from namedtuple"""
for cookie in cookies: for cookie in cookies:
# todo: remove config for develop response.set_cookie(key=cookie.key,
from os import environ value=cookie.value,
configuration = environ.get('SETTINGS_CONFIGURATION', None) secure=cookie.secure,
if configuration == 'development' or configuration == 'stage': httponly=cookie.http_only,
response.set_cookie(key=cookie.key, max_age=cookie.max_age,
value=cookie.value, domain=settings.COOKIE_DOMAIN)
secure=cookie.secure,
httponly=cookie.http_only,
max_age=cookie.max_age,
domain='.id-east.ru')
else:
response.set_cookie(key=cookie.key,
value=cookie.value,
secure=cookie.secure,
httponly=cookie.http_only,
max_age=cookie.max_age,)
return response return response
def _get_tokens_from_cookies(self, request, cookies: dict = None): def _get_tokens_from_cookies(self, request, cookies: dict = None):
@ -125,6 +116,62 @@ class CreateDestroyGalleryViewMixin(generics.CreateAPIView,
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
class BaseCreateDestroyMixinView(generics.CreateAPIView, generics.DestroyAPIView):
"""Base Create Destroy mixin."""
_model = None
serializer_class = None
lookup_field = 'slug'
def get_base_object(self):
return get_object_or_404(self._model, slug=self.kwargs['slug'])
def es_update_base_object(self):
es_update(self.get_base_object())
def perform_create(self, serializer):
serializer.save()
self.es_update_base_object()
def perform_destroy(self, instance):
instance.delete()
self.es_update_base_object()
class FavoritesCreateDestroyMixinView(BaseCreateDestroyMixinView):
"""Favorites Create Destroy mixin."""
def get_object(self):
"""
Returns the object the view is displaying.
"""
obj = self.get_base_object()
favorites = get_object_or_404(obj.favorites.filter(user=self.request.user))
# May raise a permission denied
self.check_object_permissions(self.request, favorites)
return favorites
class CarouselCreateDestroyMixinView(BaseCreateDestroyMixinView):
"""Carousel Create Destroy mixin."""
lookup_field = 'id'
def get_base_object(self):
return get_object_or_404(self._model, id=self.kwargs['pk'])
def get_object(self):
"""
Returns the object the view is displaying.
"""
obj = self.get_base_object()
carousels = get_object_or_404(obj.carousels.all())
# May raise a permission denied
# TODO: возможно нужны пермишены
# self.check_object_permissions(self.request, carousels)
return carousels
# BackOffice user`s views & viewsets # BackOffice user`s views & viewsets
class BindObjectMixin: class BindObjectMixin:
"""Bind object mixin.""" """Bind object mixin."""
@ -149,4 +196,4 @@ class BindObjectMixin:
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
elif request.method == 'DELETE': elif request.method == 'DELETE':
self.perform_unbinding(serializer) self.perform_unbinding(serializer)
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)

View File

@ -30,7 +30,7 @@ services:
# Redis # Redis
redis: redis:
image: redis:2.8.23 image: redis:latest
# Celery # Celery
worker: worker:

View File

@ -1,23 +0,0 @@
#!/bin/bash
DB_CITY_URL="https://geolite.maxmind.com/download/geoip/database/GeoLite2-City.tar.gz"
DB_COUNTRY_URL="https://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.tar.gz"
DIR_PATH="geoip_db"
ARCH_PATH="archive"
mkdir -p $DIR_PATH
cd $DIR_PATH
mkdir -p $ARCH_PATH
find . -not -path "./$ARCH_PATH/*" -type f -name "*.mmdb" -exec mv -t "./$ARCH_PATH/" {} \+
filename=$(basename $DB_CITY_URL)
wget -O $filename $DB_CITY_URL
tar xzvf "$filename"
filename=$(basename $DB_COUNTRY_URL)
wget -O $filename $DB_COUNTRY_URL
tar xzvf "$filename"
find . -mindepth 1 -type f -name "*.mmdb" -not -path "./$ARCH_PATH/*" -exec mv -t . {} \+

14
make_data_migration.sh Executable file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env bash
./manage.py transfer -a
#./manage.py transfer -d
./manage.py transfer -e
./manage.py transfer --fill_city_gallery
./manage.py transfer -l
./manage.py transfer --product
./manage.py transfer --souvenir
./manage.py transfer --establishment_note
./manage.py transfer --product_note
./manage.py transfer --wine_characteristics
./manage.py transfer --inquiries
./manage.py transfer --assemblage
./manage.py transfer --purchased_plaques

View File

@ -13,9 +13,9 @@ AWS_S3_OBJECT_PARAMETERS = {'CacheControl': 'max-age=86400'}
AWS_S3_ADDRESSING_STYLE = 'path' AWS_S3_ADDRESSING_STYLE = 'path'
# Static settings # Static settings
# PUBLIC_STATIC_LOCATION = 'static' PUBLIC_STATIC_LOCATION = 'static-dev'
# STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{PUBLIC_STATIC_LOCATION}/' STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{PUBLIC_STATIC_LOCATION}/'
# STATICFILES_STORAGE = 'project.storage_backends.PublicStaticStorage' STATICFILES_STORAGE = 'project.storage_backends.PublicStaticStorage'
# Public media settings # Public media settings
MEDIA_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{MEDIA_LOCATION}/' MEDIA_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{MEDIA_LOCATION}/'

View File

@ -254,6 +254,17 @@ AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', 'django.contrib.auth.backends.ModelBackend',
) )
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
},
'es_queue': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://redis:6379/2'
}
}
# Override default OAuth2 namespace # Override default OAuth2 namespace
DRFSO2_URL_NAMESPACE = 'auth' DRFSO2_URL_NAMESPACE = 'auth'
SOCIAL_AUTH_URL_NAMESPACE = 'auth' SOCIAL_AUTH_URL_NAMESPACE = 'auth'
@ -399,6 +410,13 @@ SORL_THUMBNAIL_ALIASES = {
'establishment_xlarge': {'geometry_string': '640x360', 'crop': 'center'}, 'establishment_xlarge': {'geometry_string': '640x360', 'crop': 'center'},
'establishment_detail': {'geometry_string': '2048x1152', 'crop': 'center'}, 'establishment_detail': {'geometry_string': '2048x1152', 'crop': 'center'},
'establishment_original': {'geometry_string': '1920x1080', 'crop': 'center'}, 'establishment_original': {'geometry_string': '1920x1080', 'crop': 'center'},
'city_xsmall': {'geometry_string': '70x70', 'crop': 'center'},
'city_small': {'geometry_string': '140x140', 'crop': 'center'},
'city_medium': {'geometry_string': '280x280', 'crop': 'center'},
'city_large': {'geometry_string': '280x280', 'crop': 'center'},
'city_xlarge': {'geometry_string': '560x560', 'crop': 'center'},
'city_detail': {'geometry_string': '1120x1120', 'crop': 'center'},
'city_original': {'geometry_string': '2048x1536', 'crop': 'center'},
} }
@ -487,7 +505,6 @@ LIMITING_QUERY_OBJECTS = QUERY_OUTPUT_OBJECTS * 3
# GEO # GEO
# A Spatial Reference System Identifier # A Spatial Reference System Identifier
GEO_DEFAULT_SRID = 4326 GEO_DEFAULT_SRID = 4326
GEOIP_PATH = os.path.join(PROJECT_ROOT, 'geoip_db')
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.2/howto/static-files/ # https://docs.djangoproject.com/en/2.2/howto/static-files/
@ -509,8 +526,11 @@ FALLBACK_LOCALE = 'en-GB'
# TMP TODO remove it later # TMP TODO remove it later
# Временный хардкод для демонстрации > 15 ноября, потом удалить! # Временный хардкод для демонстрации > 15 ноября, потом удалить!
CAROUSEL_ITEMS = [230, 231, 232] CAROUSEL_ITEMS = [465]
ESTABLISHMENT_CHOSEN_TAGS = ['gastronomic', 'en_vogue', 'terrace', 'streetfood', 'business', 'bar_cocktail', 'brunch', 'pop'] ESTABLISHMENT_CHOSEN_TAGS = ['gastronomic', 'en_vogue', 'terrace', 'streetfood', 'business', 'bar_cocktail', 'brunch', 'pop']
NEWS_CHOSEN_TAGS = ['eat', 'drink', 'cook', 'style', 'international', 'event', 'partnership'] NEWS_CHOSEN_TAGS = ['eat', 'drink', 'cook', 'style', 'international', 'event', 'partnership']
INTERNATIONAL_COUNTRY_CODES = ['www', 'main', 'next'] INTERNATIONAL_COUNTRY_CODES = ['www', 'main', 'next']
THUMBNAIL_ENGINE = 'utils.thumbnail_engine.GMEngine'
COOKIE_DOMAIN = None

View File

@ -18,6 +18,17 @@ SITE_DOMAIN_URI = 'id-east.ru'
DOMAIN_URI = 'gm.id-east.ru' DOMAIN_URI = 'gm.id-east.ru'
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
},
'es_queue': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://localhost:6379/2'
}
}
# ELASTICSEARCH SETTINGS # ELASTICSEARCH SETTINGS
ELASTICSEARCH_DSL = { ELASTICSEARCH_DSL = {
'default': { 'default': {
@ -60,3 +71,5 @@ INSTALLED_APPS.append('transfer.apps.TransferConfig')
BROKER_URL = 'redis://localhost:6379/1' BROKER_URL = 'redis://localhost:6379/1'
CELERY_RESULT_BACKEND = BROKER_URL CELERY_RESULT_BACKEND = BROKER_URL
CELERY_BROKER_URL = BROKER_URL CELERY_BROKER_URL = BROKER_URL
COOKIE_DOMAIN = '.id-east.ru'

Some files were not shown because too many files have changed in this diff Show More