Merge branch 'develop' into feature/fix-country-region-city-transfer
This commit is contained in:
commit
4c273a00be
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -24,4 +24,5 @@ logs/
|
|||
./docker-compose.override.yml
|
||||
|
||||
celerybeat-schedule
|
||||
local_files
|
||||
local_files
|
||||
celerybeat.pid
|
||||
|
|
|
|||
20
apps/account/migrations/0020_role_site.py
Normal file
20
apps/account/migrations/0020_role_site.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -46,10 +46,8 @@ class Role(ProjectBaseMixin):
|
|||
null=False, blank=False)
|
||||
country = models.ForeignKey(Country, verbose_name=_('Country'),
|
||||
null=True, blank=True, on_delete=models.SET_NULL)
|
||||
# is_list = models.BooleanField(verbose_name=_('list'), default=True, null=False)
|
||||
# is_create = models.BooleanField(verbose_name=_('create'), default=False, null=False)
|
||||
# is_update = models.BooleanField(verbose_name=_('update'), default=False, null=False)
|
||||
# is_delete = models.BooleanField(verbose_name=_('delete'), default=False, null=False)
|
||||
site = models.ForeignKey(SiteSettings, verbose_name=_('Site settings'),
|
||||
null=True, blank=True, on_delete=models.SET_NULL)
|
||||
|
||||
|
||||
class UserManager(BaseUserManager):
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class CheckWhetherBookingAvailable(generics.GenericAPIView):
|
|||
periods = response['periods']
|
||||
periods_by_name = {period['period']: period for period in periods if 'period' in period}
|
||||
if not periods_by_name:
|
||||
raise ValueError('Empty guestonline response')
|
||||
return None
|
||||
|
||||
period_template = iter(periods_by_name.values()).__next__().copy()
|
||||
period_template.pop('total_left_seats')
|
||||
|
|
@ -84,8 +84,10 @@ class CheckWhetherBookingAvailable(generics.GenericAPIView):
|
|||
|
||||
service_response = self._preprocess_guestonline_response(service.response) \
|
||||
if establishment.guestonline_id is not None \
|
||||
else service.response
|
||||
response.update({'details': service_response} if service and service.response else {})
|
||||
else service.response if service else None
|
||||
response.update({'details': service_response})
|
||||
if service_response is None:
|
||||
response['available'] = False
|
||||
return Response(data=response, status=200)
|
||||
|
||||
|
||||
|
|
|
|||
32
apps/collection/management/commands/fix_collection.py
Normal file
32
apps/collection/management/commands/fix_collection.py
Normal 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)}'))
|
||||
|
|
@ -3,6 +3,7 @@ from establishment.models import Establishment
|
|||
from location.models import Country, Language
|
||||
from transfer.models import Collections
|
||||
from collection.models import Collection
|
||||
from django.conf import settings
|
||||
from news.models import News
|
||||
|
||||
|
||||
|
|
@ -93,9 +94,11 @@ class Command(BaseCommand):
|
|||
country = Country.objects.filter(code=obj['country_code']).first()
|
||||
if country:
|
||||
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,
|
||||
description=obj['description'],
|
||||
description={settings.FALLBACK_LOCALE: obj['description']},
|
||||
slug=obj['slug'], old_id=obj['collection_id'],
|
||||
start=obj['start'],
|
||||
image_url='https://s3.eu-central-1.amazonaws.com/gm-test.com/media/'+obj['attachment_suffix_url']
|
||||
|
|
|
|||
20
apps/comment/migrations/0007_comment_site.py
Normal file
20
apps/comment/migrations/0007_comment_site.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -35,7 +35,8 @@ class Comment(ProjectBaseMixin):
|
|||
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)
|
||||
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)
|
||||
object_id = models.PositiveIntegerField()
|
||||
content_object = generic.GenericForeignKey('content_type', 'object_id')
|
||||
|
|
|
|||
|
|
@ -8,15 +8,20 @@ from account.models import Role, User, UserRole
|
|||
from authorization.tests.tests_authorization import get_tokens_for_user
|
||||
from comment.models import Comment
|
||||
from utils.tests.tests_permissions import BasePermissionTests
|
||||
from main.models import SiteSettings
|
||||
|
||||
|
||||
class CommentModeratorPermissionTests(BasePermissionTests):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.site_ru, created = SiteSettings.objects.get_or_create(
|
||||
subdomain='ru'
|
||||
)
|
||||
|
||||
self.role = Role.objects.create(
|
||||
role=2,
|
||||
country=self.country_ru
|
||||
site=self.site_ru
|
||||
)
|
||||
self.role.save()
|
||||
|
||||
|
|
@ -33,11 +38,12 @@ class CommentModeratorPermissionTests(BasePermissionTests):
|
|||
self.content_type = ContentType.objects.get(app_label='location', model='country')
|
||||
|
||||
self.user_test = get_tokens_for_user()
|
||||
|
||||
self.comment = Comment.objects.create(text='Test comment', mark=1,
|
||||
user=self.user_test["user"],
|
||||
object_id=self.country_ru.pk,
|
||||
content_type_id=self.content_type.id,
|
||||
country=self.country_ru
|
||||
site=self.site_ru
|
||||
)
|
||||
self.comment.save()
|
||||
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,
|
||||
"object_id": self.country_ru.pk,
|
||||
"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)
|
||||
|
|
@ -61,7 +67,7 @@ class CommentModeratorPermissionTests(BasePermissionTests):
|
|||
"user": self.moderator.id,
|
||||
"object_id": self.country_ru.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)
|
||||
|
|
@ -83,8 +89,9 @@ class CommentModeratorPermissionTests(BasePermissionTests):
|
|||
"text": "test text moderator",
|
||||
"mark": 1,
|
||||
"user": self.moderator.id,
|
||||
"object_id": self.comment.country_id,
|
||||
"content_type": self.content_type.id
|
||||
"object_id": self.country_ru.id,
|
||||
"content_type": self.content_type.id,
|
||||
'site_id': self.site_ru.id
|
||||
}
|
||||
|
||||
response = self.client.put(self.url, data=data, format='json')
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@ class CommentLstView(generics.ListCreateAPIView):
|
|||
"""Comment list create view."""
|
||||
serializer_class = serializers.CommentBaseSerializer
|
||||
queryset = models.Comment.objects.all()
|
||||
permission_classes = [permissions.IsAuthenticatedOrReadOnly| IsCommentModerator|IsCountryAdmin]
|
||||
# permission_classes = [permissions.IsAuthenticatedOrReadOnly| IsCommentModerator|IsCountryAdmin]
|
||||
|
||||
|
||||
class CommentRUDView(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""Comment RUD view."""
|
||||
serializer_class = serializers.CommentBaseSerializer
|
||||
queryset = models.Comment.objects.all()
|
||||
|
||||
permission_classes = [IsCountryAdmin | IsCommentModerator]
|
||||
permission_classes = [IsCommentModerator]
|
||||
# permission_classes = [IsCountryAdmin | IsCommentModerator]
|
||||
lookup_field = 'id'
|
||||
|
|
|
|||
|
|
@ -55,3 +55,23 @@ class EstablishmentTypeTagFilter(filters.FilterSet):
|
|||
fields = (
|
||||
'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
|
||||
|
|
|
|||
|
|
@ -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.'))
|
||||
28
apps/establishment/migrations/0066_auto_20191122_1144.py
Normal file
28
apps/establishment/migrations/0066_auto_20191122_1144.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
39
apps/establishment/migrations/0067_auto_20191122_1244.py
Normal file
39
apps/establishment/migrations/0067_auto_20191122_1244.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
"""Establishment models."""
|
||||
from datetime import datetime
|
||||
from functools import reduce
|
||||
from typing import List
|
||||
from operator import or_
|
||||
|
||||
import elasticsearch_dsl
|
||||
|
|
@ -25,7 +26,8 @@ from main.models import Award, Currency
|
|||
from review.models import Review
|
||||
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
|
||||
TranslatedFieldsMixin, BaseAttributes, GalleryModelMixin,
|
||||
IntermediateGalleryModelMixin, HasTagsMixin)
|
||||
IntermediateGalleryModelMixin, HasTagsMixin,
|
||||
FavoritesMixin)
|
||||
|
||||
|
||||
# todo: establishment type&subtypes check
|
||||
|
|
@ -117,9 +119,10 @@ class EstablishmentQuerySet(models.QuerySet):
|
|||
'address__city__country')
|
||||
|
||||
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',
|
||||
'phones'). \
|
||||
'phones', 'gallery', 'menu_set', 'menu_set__plate_set',
|
||||
'menu_set__plate_set__currency', 'currency'). \
|
||||
prefetch_actual_employees()
|
||||
|
||||
def with_type_related(self):
|
||||
|
|
@ -319,7 +322,8 @@ class EstablishmentQuerySet(models.QuerySet):
|
|||
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."""
|
||||
|
||||
# todo: delete image URL fields after moving on gallery
|
||||
|
|
@ -393,6 +397,7 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, Translat
|
|||
verbose_name=_('Tag'))
|
||||
reviews = generic.GenericRelation(to='review.Review')
|
||||
comments = generic.GenericRelation(to='comment.Comment')
|
||||
carousels = generic.GenericRelation(to='main.Carousel')
|
||||
favorites = generic.GenericRelation(to='favorites.Favorites')
|
||||
currency = models.ForeignKey(Currency, blank=True, null=True, default=None,
|
||||
on_delete=models.PROTECT,
|
||||
|
|
@ -432,11 +437,12 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, Translat
|
|||
|
||||
@property
|
||||
def visible_tags(self):
|
||||
return super().visible_tags\
|
||||
.exclude(category__index_name__in=['guide', 'collection', 'purchased_item',
|
||||
'business_tag', 'business_tags_de'])\
|
||||
return super().visible_tags \
|
||||
.exclude(category__index_name__in=['guide', 'collection', 'purchased_item',
|
||||
'business_tag', 'business_tags_de']) \
|
||||
\
|
||||
# todo: recalculate toque_number
|
||||
|
||||
# todo: recalculate toque_number
|
||||
def recalculate_toque_number(self):
|
||||
toque_number = 0
|
||||
if self.address and self.public_mark:
|
||||
|
|
@ -609,7 +615,6 @@ class EstablishmentNote(ProjectBaseMixin):
|
|||
|
||||
|
||||
class EstablishmentGallery(IntermediateGalleryModelMixin):
|
||||
|
||||
establishment = models.ForeignKey(Establishment, null=True,
|
||||
related_name='establishment_gallery',
|
||||
on_delete=models.CASCADE,
|
||||
|
|
@ -660,6 +665,16 @@ class EstablishmentEmployeeQuerySet(models.QuerySet):
|
|||
class EstablishmentEmployee(BaseAttributes):
|
||||
"""EstablishmentEmployee model."""
|
||||
|
||||
IDLE = 'I'
|
||||
ACCEPTED = 'A'
|
||||
DECLINED = 'D'
|
||||
|
||||
STATUS_CHOICES = (
|
||||
(IDLE, 'Idle'),
|
||||
(ACCEPTED, 'Accepted'),
|
||||
(DECLINED, 'Declined'),
|
||||
)
|
||||
|
||||
establishment = models.ForeignKey(Establishment, on_delete=models.PROTECT,
|
||||
verbose_name=_('Establishment'))
|
||||
employee = models.ForeignKey('establishment.Employee', on_delete=models.PROTECT,
|
||||
|
|
@ -670,19 +685,53 @@ class EstablishmentEmployee(BaseAttributes):
|
|||
verbose_name=_('To date'))
|
||||
position = models.ForeignKey(Position, on_delete=models.PROTECT,
|
||||
verbose_name=_('Position'))
|
||||
|
||||
status = models.CharField(max_length=1, choices=STATUS_CHOICES, default=IDLE)
|
||||
|
||||
# old_id = affiliations_id
|
||||
old_id = models.IntegerField(verbose_name=_('Old id'), null=True, blank=True)
|
||||
|
||||
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):
|
||||
"""Employee model."""
|
||||
|
||||
user = models.OneToOneField('account.User', on_delete=models.PROTECT,
|
||||
null=True, blank=True, default=None,
|
||||
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',
|
||||
through=EstablishmentEmployee, )
|
||||
awards = generic.GenericRelation(to='main.Award', related_query_name='employees')
|
||||
|
|
@ -691,6 +740,8 @@ class Employee(BaseAttributes):
|
|||
# old_id = profile_id
|
||||
old_id = models.IntegerField(verbose_name=_('Old id'), null=True, blank=True)
|
||||
|
||||
objects = EmployeeQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
"""Meta class."""
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@ from rest_framework import serializers
|
|||
|
||||
from establishment import models
|
||||
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.serializers import AwardSerializer
|
||||
from utils.decorators import with_base_attributes
|
||||
from utils.serializers import TimeZoneChoiceField
|
||||
from gallery.models import Image
|
||||
|
|
@ -161,12 +162,54 @@ class ContactEmailBackSerializers(model_serializers.PlateSerializer):
|
|||
class EmployeeBackSerializers(serializers.ModelSerializer):
|
||||
"""Employee serializers."""
|
||||
|
||||
awards = AwardSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.Employee
|
||||
fields = [
|
||||
'id',
|
||||
'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',
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from review.serializers import ReviewShortSerializer
|
|||
from tag.serializers import TagBaseSerializer
|
||||
from timetable.serialziers import ScheduleRUDSerializer
|
||||
from utils import exceptions as utils_exceptions
|
||||
from utils.serializers import ImageBaseSerializer
|
||||
from utils.serializers import ImageBaseSerializer, CarouselCreateSerializer
|
||||
from utils.serializers import (ProjectModelSerializer, TranslatedField,
|
||||
FavoritesCreateSerializer)
|
||||
|
||||
|
|
@ -168,12 +168,51 @@ class EstablishmentEmployeeSerializer(serializers.ModelSerializer):
|
|||
awards = AwardSerializer(source='employee.awards', many=True)
|
||||
priority = serializers.IntegerField(source='position.priority')
|
||||
position_index_name = serializers.CharField(source='position.index_name')
|
||||
status = serializers.CharField()
|
||||
|
||||
class Meta:
|
||||
"""Meta class."""
|
||||
|
||||
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):
|
||||
|
|
@ -396,6 +435,22 @@ class EstablishmentCommentCreateSerializer(comment_serializers.CommentSerializer
|
|||
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):
|
||||
"""Serializer to favorite object w/ model Establishment."""
|
||||
|
||||
|
|
@ -426,6 +481,27 @@ class EstablishmentFavoritesCreateSerializer(FavoritesCreateSerializer):
|
|||
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):
|
||||
"""Company base serializer"""
|
||||
phone_list = serializers.SerializerMethodField(source='phones', read_only=True)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ from translation.models import Language
|
|||
from account.models import Role, UserRole
|
||||
from location.models import Country, Address, City, Region
|
||||
from pytz import timezone as py_tz
|
||||
from main.models import SiteSettings
|
||||
from timetable.models import Timetable
|
||||
|
||||
|
||||
class BaseTestCase(APITestCase):
|
||||
|
|
@ -278,13 +280,13 @@ class PlateTests(ChildTestCase):
|
|||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
menu = Menu.objects.create(
|
||||
category=json.dumps({"en-GB": "Test category"}),
|
||||
category=json.dumps({"ru-RU": "Test category"}),
|
||||
establishment=self.establishment
|
||||
)
|
||||
currency = Currency.objects.create(name="Test currency")
|
||||
|
||||
data = {
|
||||
'name': json.dumps({"en-GB": "Test plate"}),
|
||||
'name': json.dumps({"ru-RU": "Test plate"}),
|
||||
'establishment': self.establishment.id,
|
||||
'price': 10,
|
||||
'menu': menu.id,
|
||||
|
|
@ -298,7 +300,7 @@ class PlateTests(ChildTestCase):
|
|||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
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)
|
||||
|
|
@ -314,7 +316,7 @@ class MenuTests(ChildTestCase):
|
|||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
data = {
|
||||
'category': json.dumps({"en-GB": "Test category"}),
|
||||
'category': json.dumps({"ru-RU": "Test category"}),
|
||||
'establishment': self.establishment.id
|
||||
}
|
||||
|
||||
|
|
@ -325,7 +327,7 @@ class MenuTests(ChildTestCase):
|
|||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
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)
|
||||
|
|
@ -336,24 +338,56 @@ class MenuTests(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 = {
|
||||
'weekday': 1
|
||||
}
|
||||
|
||||
response = self.client.post(f'/api/back/establishments/{self.establishment.id}/schedule/', data=data)
|
||||
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)
|
||||
|
||||
update_data = {
|
||||
'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)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
|
|
@ -441,3 +475,17 @@ class EstablishmentWebFavoriteTests(ChildTestCase):
|
|||
f'/api/web/establishments/slug/{self.establishment.slug}/favorites/',
|
||||
format='json')
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ app_name = 'establishment'
|
|||
urlpatterns = [
|
||||
path('', views.EstablishmentListCreateView.as_view(), name='list'),
|
||||
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(),
|
||||
name='schedule-rud'),
|
||||
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('emails/', views.EmailListCreateView.as_view(), name='emails'),
|
||||
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/<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/<int:pk>/', views.EstablishmentTypeRUDView.as_view(), name='type-rud'),
|
||||
path('subtypes/', views.EstablishmentSubtypeListCreateView.as_view(), name='subtype-list'),
|
||||
path('subtypes/<int:pk>/', views.EstablishmentSubtypeRUDView.as_view(), name='subtype-rud'),
|
||||
path('positions/', views.EstablishmentPositionListView.as_view(), name='position-list'),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -17,5 +17,5 @@ urlpatterns = [
|
|||
path('slug/<slug:slug>/comments/<int:comment_id>/', views.EstablishmentCommentRUDView.as_view(),
|
||||
name='rud-comment'),
|
||||
path('slug/<slug:slug>/favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(),
|
||||
name='create-destroy-favorites')
|
||||
name='create-destroy-favorites'),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
"""Establishment app views."""
|
||||
from django.http import Http404, HttpResponse
|
||||
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 timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer
|
||||
from utils.permissions import IsCountryAdmin, IsEstablishmentManager
|
||||
|
|
@ -43,8 +45,8 @@ class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView):
|
|||
"""
|
||||
Returns the object the view is displaying.
|
||||
"""
|
||||
establishment_pk = self.kwargs.get('pk')
|
||||
schedule_id = self.kwargs.get('schedule_id')
|
||||
establishment_pk = self.kwargs['pk']
|
||||
schedule_id = self.kwargs['schedule_id']
|
||||
|
||||
establishment = get_object_or_404(klass=models.Establishment.objects.all(),
|
||||
pk=establishment_pk)
|
||||
|
|
@ -156,11 +158,23 @@ class EmailRUDView(generics.RetrieveUpdateDestroyAPIView):
|
|||
|
||||
class EmployeeListCreateView(generics.ListCreateAPIView):
|
||||
"""Emplyoee list create view."""
|
||||
permission_classes = (permissions.AllowAny, )
|
||||
filter_class = filters.EmployeeBackFilter
|
||||
serializer_class = serializers.EmployeeBackSerializers
|
||||
queryset = models.Employee.objects.all()
|
||||
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):
|
||||
"""Employee RUD view."""
|
||||
serializer_class = serializers.EmployeeBackSerializers
|
||||
|
|
@ -318,3 +332,36 @@ class EstablishmentNoteRUDView(EstablishmentMixinViews,
|
|||
self.check_object_permissions(self.request, 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
|
||||
|
|
|
|||
|
|
@ -5,12 +5,11 @@ from django.shortcuts import get_object_or_404
|
|||
from rest_framework import generics, permissions
|
||||
|
||||
from comment import models as comment_models
|
||||
from establishment import filters
|
||||
from establishment import models, serializers
|
||||
from comment.serializers import CommentRUDSerializer
|
||||
from establishment import filters, models, serializers
|
||||
from main import methods
|
||||
from utils.pagination import EstablishmentPortionPagination
|
||||
from utils.permissions import IsCountryAdmin
|
||||
from comment.serializers import CommentRUDSerializer
|
||||
from utils.views import FavoritesCreateDestroyMixinView, CarouselCreateDestroyMixinView
|
||||
|
||||
|
||||
class EstablishmentMixinView:
|
||||
|
|
@ -35,7 +34,7 @@ class EstablishmentListView(EstablishmentMixinView, generics.ListAPIView):
|
|||
serializer_class = serializers.EstablishmentListRetrieveSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().with_schedule()\
|
||||
return super().get_queryset().with_schedule() \
|
||||
.with_extended_address_related().with_currency_related()
|
||||
|
||||
|
||||
|
|
@ -57,12 +56,11 @@ class EstablishmentRecentReviewListView(EstablishmentListView):
|
|||
def get_queryset(self):
|
||||
"""Overridden method 'get_queryset'."""
|
||||
qs = super().get_queryset()
|
||||
user_ip = methods.get_user_ip(self.request)
|
||||
query_params = self.request.query_params
|
||||
if 'longitude' in query_params and 'latitude' in query_params:
|
||||
longitude, latitude = query_params.get('longitude'), query_params.get('latitude')
|
||||
else:
|
||||
longitude, latitude = methods.determine_coordinates(user_ip)
|
||||
longitude, latitude = methods.determine_coordinates(self.request)
|
||||
if not longitude or not latitude:
|
||||
return qs.none()
|
||||
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'])
|
||||
return comment_models.Comment.objects.by_content_type(app_label='establishment',
|
||||
model='establishment')\
|
||||
.by_object_id(object_id=establishment.pk)\
|
||||
.order_by('-created')
|
||||
model='establishment') \
|
||||
.by_object_id(object_id=establishment.pk) \
|
||||
.order_by('-created')
|
||||
|
||||
|
||||
class EstablishmentCommentRUDView(generics.RetrieveUpdateDestroyAPIView):
|
||||
|
|
@ -134,21 +132,18 @@ class EstablishmentCommentRUDView(generics.RetrieveUpdateDestroyAPIView):
|
|||
return comment_obj
|
||||
|
||||
|
||||
class EstablishmentFavoritesCreateDestroyView(generics.CreateAPIView, generics.DestroyAPIView):
|
||||
class EstablishmentFavoritesCreateDestroyView(FavoritesCreateDestroyMixinView):
|
||||
"""View for create/destroy establishment from favorites."""
|
||||
serializer_class = serializers.EstablishmentFavoritesCreateSerializer
|
||||
lookup_field = 'slug'
|
||||
|
||||
def get_object(self):
|
||||
"""
|
||||
Returns the object the view is displaying.
|
||||
"""
|
||||
establishment = get_object_or_404(models.Establishment,
|
||||
slug=self.kwargs['slug'])
|
||||
favorites = get_object_or_404(establishment.favorites.filter(user=self.request.user))
|
||||
# May raise a permission denied
|
||||
self.check_object_permissions(self.request, favorites)
|
||||
return favorites
|
||||
_model = models.Establishment
|
||||
serializer_class = serializers.EstablishmentFavoritesCreateSerializer
|
||||
|
||||
|
||||
class EstablishmentCarouselCreateDestroyView(CarouselCreateDestroyMixinView):
|
||||
"""View for create/destroy establishment from carousel."""
|
||||
|
||||
_model = models.Establishment
|
||||
serializer_class = serializers.EstablishmentCarouselCreateSerializer
|
||||
|
||||
|
||||
class EstablishmentNearestRetrieveView(EstablishmentListView, generics.ListAPIView):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
from django.conf import settings
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
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
|
||||
|
||||
|
|
@ -29,3 +34,86 @@ class ImageSerializer(serializers.ModelSerializer):
|
|||
extra_kwargs = {
|
||||
'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.')})
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from . import views
|
|||
app_name = 'gallery'
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.ImageListCreateView.as_view(), name='list-create-image'),
|
||||
path('<int:pk>/', views.ImageRetrieveDestroyView.as_view(), name='retrieve-destroy-image'),
|
||||
path('', views.ImageListCreateView.as_view(), name='list-create'),
|
||||
path('<int:pk>/', views.ImageRetrieveDestroyView.as_view(), name='retrieve-destroy'),
|
||||
path('<int:pk>/crop/', views.CropImageCreateView.as_view(), name='create-crop'),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -28,3 +28,8 @@ class ImageRetrieveDestroyView(ImageBaseView, generics.RetrieveDestroyAPIView):
|
|||
else:
|
||||
on_commit(lambda: tasks.delete_image(image_id=instance.id))
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class CropImageCreateView(ImageBaseView, generics.CreateAPIView):
|
||||
"""Create crop image."""
|
||||
serializer_class = serializers.CropImageSerializer
|
||||
|
|
|
|||
|
|
@ -25,12 +25,12 @@ class BaseTestCase(APITestCase):
|
|||
{'access_token': tokens.get('access_token'),
|
||||
'refresh_token': tokens.get('refresh_token')})
|
||||
|
||||
self.lang = Language.objects.get(
|
||||
self.lang, created = Language.objects.get_or_create(
|
||||
title='Russia',
|
||||
locale='ru-RU'
|
||||
)
|
||||
|
||||
self.country_ru = Country.objects.get(
|
||||
self.country_ru, created = Country.objects.get_or_create(
|
||||
name={"en-GB": "Russian"}
|
||||
)
|
||||
|
||||
|
|
@ -72,7 +72,7 @@ class CountryTests(BaseTestCase):
|
|||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -3,9 +3,15 @@ from django.contrib import admin
|
|||
from main import models
|
||||
|
||||
|
||||
class SiteSettingsInline(admin.TabularInline):
|
||||
model = models.SiteFeature
|
||||
extra = 1
|
||||
|
||||
|
||||
@admin.register(models.SiteSettings)
|
||||
class SiteSettingsAdmin(admin.ModelAdmin):
|
||||
"""Site settings admin conf."""
|
||||
inlines = [SiteSettingsInline,]
|
||||
|
||||
|
||||
@admin.register(models.Feature)
|
||||
|
|
|
|||
94
apps/main/management/commands/add_site_features.py
Normal file
94
apps/main/management/commands/add_site_features.py
Normal 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()
|
||||
|
||||
|
||||
56
apps/main/management/commands/add_site_settings.py
Normal file
56
apps/main/management/commands/add_site_settings.py
Normal 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()
|
||||
|
|
@ -28,31 +28,25 @@ def get_user_ip(request):
|
|||
return ip
|
||||
|
||||
|
||||
def determine_country_code(ip_addr):
|
||||
def determine_country_code(request):
|
||||
"""Determine country code."""
|
||||
country_code = None
|
||||
if ip_addr:
|
||||
try:
|
||||
geoip = GeoIP2()
|
||||
country_code = geoip.country_code(ip_addr)
|
||||
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
|
||||
META = request.META
|
||||
country_code = META.get('X-GeoIP-Country-Code',
|
||||
META.get('HTTP_X_GEOIP_COUNTRY_CODE'))
|
||||
if isinstance(country_code, str):
|
||||
return country_code.lower()
|
||||
|
||||
|
||||
def determine_coordinates(ip_addr: str) -> Tuple[Optional[float], Optional[float]]:
|
||||
if ip_addr:
|
||||
try:
|
||||
geoip = GeoIP2()
|
||||
return geoip.coords(ip_addr)
|
||||
except GeoIP2Exception as ex:
|
||||
logger.warning(f'GEOIP Exception: {ex}. ip: {ip_addr}')
|
||||
except Exception as ex:
|
||||
logger.warning(f'GEOIP Base exception: {ex}')
|
||||
return None, None
|
||||
def determine_coordinates(request):
|
||||
META = request.META
|
||||
longitude = META.get('X-GeoIP-Longitude',
|
||||
META.get('HTTP_X_GEOIP_LONGITUDE'))
|
||||
latitude = META.get('X-GeoIP-Latitude',
|
||||
META.get('HTTP_X_GEOIP_LATITUDE'))
|
||||
try:
|
||||
return float(longitude), float(latitude)
|
||||
except (TypeError, ValueError):
|
||||
return None, None
|
||||
|
||||
|
||||
def determine_user_site_url(country_code):
|
||||
|
|
@ -76,15 +70,11 @@ def determine_user_site_url(country_code):
|
|||
return site.site_url
|
||||
|
||||
|
||||
def determine_user_city(ip_addr: str) -> Optional[City]:
|
||||
try:
|
||||
geoip = GeoIP2()
|
||||
return geoip.city(ip_addr)
|
||||
except GeoIP2Exception as ex:
|
||||
logger.warning(f'GEOIP Exception: {ex}. ip: {ip_addr}')
|
||||
except Exception as ex:
|
||||
logger.warning(f'GEOIP Base exception: {ex}')
|
||||
return None
|
||||
def determine_user_city(request):
|
||||
META = request.META
|
||||
city = META.get('X-GeoIP-City',
|
||||
META.get('HTTP_X_GEOIP_CITY'))
|
||||
return city
|
||||
|
||||
|
||||
def determine_subdivision(
|
||||
|
|
|
|||
18
apps/main/migrations/0037_sitesettings_old_id.py
Normal file
18
apps/main/migrations/0037_sitesettings_old_id.py
Normal 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),
|
||||
),
|
||||
]
|
||||
18
apps/main/migrations/0038_feature_old_id.py
Normal file
18
apps/main/migrations/0038_feature_old_id.py
Normal 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),
|
||||
),
|
||||
]
|
||||
18
apps/main/migrations/0039_sitefeature_old_id.py
Normal file
18
apps/main/migrations/0039_sitefeature_old_id.py
Normal 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),
|
||||
),
|
||||
]
|
||||
|
|
@ -69,6 +69,8 @@ class SiteSettings(ProjectBaseMixin):
|
|||
verbose_name=_('AD config'))
|
||||
currency = models.ForeignKey(Currency, on_delete=models.PROTECT, null=True, default=None)
|
||||
|
||||
old_id = models.IntegerField(blank=True, null=True)
|
||||
|
||||
objects = SiteSettingsQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
|
|
@ -105,6 +107,7 @@ class Feature(ProjectBaseMixin, PlatformMixin):
|
|||
priority = models.IntegerField(unique=True, null=True, default=None)
|
||||
route = models.ForeignKey('PageType', on_delete=models.PROTECT, null=True, default=None)
|
||||
site_settings = models.ManyToManyField(SiteSettings, through='SiteFeature')
|
||||
old_id = models.IntegerField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
"""Meta class."""
|
||||
|
|
@ -136,6 +139,7 @@ class SiteFeature(ProjectBaseMixin):
|
|||
published = models.BooleanField(default=False, verbose_name=_('Published'))
|
||||
main = models.BooleanField(default=False, verbose_name=_('Main'))
|
||||
nested = models.ManyToManyField('self', symmetrical=False)
|
||||
old_id = models.IntegerField(null=True, blank=True)
|
||||
|
||||
objects = SiteFeatureQuerySet.as_manager()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
"""Main app serializers."""
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from rest_framework import serializers
|
||||
|
||||
from location.serializers import CountrySerializer
|
||||
|
|
@ -71,7 +72,7 @@ class SiteSettingsSerializer(serializers.ModelSerializer):
|
|||
"""Meta class."""
|
||||
|
||||
model = models.SiteSettings
|
||||
fields = (
|
||||
fields = [
|
||||
'country_code',
|
||||
'time_format',
|
||||
'subdomain',
|
||||
|
|
@ -85,7 +86,17 @@ class SiteSettingsSerializer(serializers.ModelSerializer):
|
|||
'published_features',
|
||||
'currency',
|
||||
'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):
|
||||
|
|
@ -94,7 +105,11 @@ class SiteSerializer(serializers.ModelSerializer):
|
|||
class Meta:
|
||||
"""Meta class."""
|
||||
model = models.SiteSettings
|
||||
fields = ('subdomain', 'site_url', 'country')
|
||||
fields = [
|
||||
'subdomain',
|
||||
'site_url',
|
||||
'country'
|
||||
]
|
||||
|
||||
|
||||
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):
|
||||
# """Site feature serializer."""
|
||||
#
|
||||
|
|
@ -216,3 +241,11 @@ class PageTypeBaseSerializer(serializers.ModelSerializer):
|
|||
'id',
|
||||
'name',
|
||||
]
|
||||
|
||||
|
||||
class ContentTypeBackSerializer(serializers.ModelSerializer):
|
||||
"""Serializer fro model ContentType."""
|
||||
|
||||
class Meta:
|
||||
model = ContentType
|
||||
fields = '__all__'
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from location.models import Country
|
|||
from main.models import Award, AwardType
|
||||
|
||||
|
||||
class AwardTestCase(APITestCase):
|
||||
class BaseTestCase(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(
|
||||
|
|
@ -25,6 +25,12 @@ class AwardTestCase(APITestCase):
|
|||
{'access_token': tokens.get('access_token'),
|
||||
'refresh_token': tokens.get('refresh_token')})
|
||||
|
||||
|
||||
class AwardTestCase(BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.country_ru = Country.objects.create(
|
||||
name={'en-GB': 'Russian'},
|
||||
code='RU',
|
||||
|
|
@ -71,3 +77,13 @@ class AwardTestCase(APITestCase):
|
|||
|
||||
response = self.client.delete(f'/api/back/main/awards/{self.award.id}/')
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
"""Back main URLs"""
|
||||
from django.urls import path
|
||||
|
||||
from main.views import back as views
|
||||
from main import views
|
||||
|
||||
app_name = 'main'
|
||||
|
||||
urlpatterns = [
|
||||
path('awards/', views.AwardLstView.as_view(), name='awards-list-create'),
|
||||
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'),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"""Main app urls."""
|
||||
from django.urls import path
|
||||
from main.views.common import *
|
||||
from main.views import *
|
||||
|
||||
app = 'main'
|
||||
|
||||
|
|
@ -8,5 +8,5 @@ common_urlpatterns = [
|
|||
path('awards/', AwardView.as_view(), name='awards_list'),
|
||||
path('awards/<int:pk>/', AwardRetrieveView.as_view(), name='awards_retrieve'),
|
||||
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'),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
from main.urls.common import common_urlpatterns
|
||||
from django.urls import path
|
||||
|
||||
from main.urls.common import common_urlpatterns
|
||||
from main.views.web import DetermineSiteView, SiteListView, SiteSettingsView
|
||||
|
||||
urlpatterns = [
|
||||
path('determine-site/', DetermineSiteView.as_view(), name='determine-site'),
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
from .common import *
|
||||
from .mobile import *
|
||||
from .web import *
|
||||
from .back import *
|
||||
|
|
@ -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 main import serializers
|
||||
from main.filters import AwardFilter
|
||||
from main.models import Award
|
||||
from main.views import SiteSettingsView, SiteListView
|
||||
|
||||
|
||||
class AwardLstView(generics.ListCreateAPIView):
|
||||
|
|
@ -19,3 +22,28 @@ class AwardRUDView(generics.RetrieveUpdateDestroyAPIView):
|
|||
serializer_class = serializers.BackAwardSerializer
|
||||
permission_classes = (permissions.IsAdminUser,)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ class CarouselListView(generics.ListAPIView):
|
|||
|
||||
def get_queryset(self):
|
||||
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)
|
||||
return qs
|
||||
qs = models.Carousel.objects.is_parsed().active()
|
||||
|
|
@ -86,9 +86,8 @@ class DetermineLocation(generics.GenericAPIView):
|
|||
serializer_class = EmptySerializer
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
user_ip = methods.get_user_ip(request)
|
||||
longitude, latitude = methods.determine_coordinates(user_ip)
|
||||
city = methods.determine_user_city(user_ip)
|
||||
longitude, latitude = methods.determine_coordinates(request)
|
||||
city = methods.determine_user_city(request)
|
||||
if longitude and latitude and city:
|
||||
return Response(data={'latitude': latitude, 'longitude': longitude, 'city': city})
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -14,8 +14,7 @@ class DetermineSiteView(generics.GenericAPIView):
|
|||
serializer_class = EmptySerializer
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
user_ip = methods.get_user_ip(request)
|
||||
country_code = methods.determine_country_code(user_ip)
|
||||
country_code = methods.determine_country_code(request)
|
||||
url = methods.determine_user_site_url(country_code)
|
||||
return Response(data={'url': url})
|
||||
|
||||
|
|
@ -26,7 +25,7 @@ class SiteSettingsView(generics.RetrieveAPIView):
|
|||
lookup_field = 'subdomain'
|
||||
permission_classes = (permissions.AllowAny,)
|
||||
queryset = models.SiteSettings.objects.all()
|
||||
serializer_class = serializers.SiteSettingsSerializer
|
||||
serializer_class = serializers.SiteSettingsBackOfficeSerializer
|
||||
|
||||
|
||||
class SiteListView(generics.ListAPIView):
|
||||
|
|
|
|||
|
|
@ -20,6 +20,16 @@ class NewsListFilterSet(filters.FilterSet):
|
|||
tag_value__in = filters.CharFilter(method='in_tags')
|
||||
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:
|
||||
"""Meta class"""
|
||||
model = models.News
|
||||
|
|
@ -29,6 +39,8 @@ class NewsListFilterSet(filters.FilterSet):
|
|||
'tag_group',
|
||||
'tag_value__exclude',
|
||||
'tag_value__in',
|
||||
'state',
|
||||
'sort_by',
|
||||
)
|
||||
|
||||
def in_tags(self, queryset, name, value):
|
||||
|
|
@ -58,3 +70,6 @@ class NewsListFilterSet(filters.FilterSet):
|
|||
return queryset.filter(news_type__name=value)
|
||||
else:
|
||||
return queryset
|
||||
|
||||
def sort_by_field(self, queryset, name, value):
|
||||
return queryset.order_by(f'-{value}')
|
||||
|
|
|
|||
20
apps/news/migrations/0036_news_site.py
Normal file
20
apps/news/migrations/0036_news_site.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
18
apps/news/migrations/0037_auto_20191129_1320.py
Normal file
18
apps/news/migrations/0037_auto_20191129_1320.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -8,7 +8,8 @@ from rest_framework.reverse import reverse
|
|||
|
||||
from rating.models import Rating, ViewCount
|
||||
from utils.models import (BaseAttributes, TJSONField, TranslatedFieldsMixin, HasTagsMixin,
|
||||
ProjectBaseMixin, GalleryModelMixin, IntermediateGalleryModelMixin)
|
||||
ProjectBaseMixin, GalleryModelMixin, IntermediateGalleryModelMixin,
|
||||
FavoritesMixin)
|
||||
from utils.querysets import TranslationQuerysetMixin
|
||||
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."""
|
||||
|
||||
STR_FIELD_NAME = 'title'
|
||||
|
|
@ -172,7 +174,8 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
|
|||
description = TJSONField(blank=True, null=True, default=None,
|
||||
verbose_name=_('description'),
|
||||
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,
|
||||
verbose_name=_('End'))
|
||||
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)
|
||||
ratings = generic.GenericRelation(Rating)
|
||||
favorites = generic.GenericRelation(to='favorites.Favorites')
|
||||
carousels = generic.GenericRelation(to='main.Carousel')
|
||||
agenda = models.ForeignKey('news.Agenda', blank=True, null=True,
|
||||
on_delete=models.SET_NULL,
|
||||
verbose_name=_('agenda'))
|
||||
|
|
@ -201,7 +205,8 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
|
|||
banner = models.ForeignKey('news.NewsBanner', blank=True, null=True,
|
||||
on_delete=models.SET_NULL,
|
||||
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()
|
||||
|
||||
class Meta:
|
||||
|
|
@ -217,6 +222,10 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
|
|||
def is_publish(self):
|
||||
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
|
||||
def web_url(self):
|
||||
return reverse('web:news:rud', kwargs={'slug': self.slug})
|
||||
|
|
|
|||
|
|
@ -5,13 +5,14 @@ from rest_framework.fields import SerializerMethodField
|
|||
|
||||
from account.serializers.common import UserBaseSerializer
|
||||
from gallery.models import Image
|
||||
from main.models import SiteSettings
|
||||
from location import models as location_models
|
||||
from location.serializers import CountrySimpleSerializer, AddressBaseSerializer
|
||||
from news import models
|
||||
from tag.serializers import TagBaseSerializer
|
||||
from utils import exceptions as utils_exceptions
|
||||
from utils.serializers import (TranslatedField, ProjectModelSerializer,
|
||||
FavoritesCreateSerializer, ImageBaseSerializer)
|
||||
FavoritesCreateSerializer, ImageBaseSerializer, CarouselCreateSerializer)
|
||||
|
||||
|
||||
class AgendaSerializer(ProjectModelSerializer):
|
||||
|
|
@ -65,7 +66,7 @@ class NewsBaseSerializer(ProjectModelSerializer):
|
|||
subtitle_translated = TranslatedField()
|
||||
news_type = NewsTypeSerializer(read_only=True)
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
|
|
@ -80,7 +81,6 @@ class NewsBaseSerializer(ProjectModelSerializer):
|
|||
'news_type',
|
||||
'tags',
|
||||
'slug',
|
||||
'in_favorites',
|
||||
'view_counter',
|
||||
)
|
||||
|
||||
|
|
@ -162,6 +162,7 @@ class NewsDetailWebSerializer(NewsDetailSerializer):
|
|||
|
||||
class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
|
||||
"""News back office base serializer."""
|
||||
is_published = serializers.BooleanField(source='is_publish', read_only=True)
|
||||
|
||||
class Meta(NewsBaseSerializer.Meta):
|
||||
"""Meta class."""
|
||||
|
|
@ -169,6 +170,7 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
|
|||
fields = NewsBaseSerializer.Meta.fields + (
|
||||
'title',
|
||||
'subtitle',
|
||||
'is_published',
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -182,6 +184,9 @@ class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer,
|
|||
country_id = serializers.PrimaryKeyRelatedField(
|
||||
source='country', write_only=True,
|
||||
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',
|
||||
read_only=True)
|
||||
|
||||
|
|
@ -193,8 +198,10 @@ class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer,
|
|||
'description',
|
||||
'news_type_id',
|
||||
'country_id',
|
||||
'site_id',
|
||||
'template',
|
||||
'template_display',
|
||||
'is_international',
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -267,3 +274,24 @@ class NewsFavoritesCreateSerializer(FavoritesCreateSerializer):
|
|||
'content_object': validated_data.pop('news')
|
||||
})
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from rest_framework.test import APITestCase
|
|||
from rest_framework import status
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from main.models import SiteSettings
|
||||
from news.models import NewsType, News
|
||||
from account.models import User, Role, UserRole
|
||||
from translation.models import Language
|
||||
|
|
@ -30,18 +31,23 @@ class BaseTestCase(APITestCase):
|
|||
'refresh_token': tokens.get('refresh_token')})
|
||||
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',
|
||||
locale='ru-RU'
|
||||
)
|
||||
|
||||
self.country_ru = Country.objects.get(
|
||||
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 = Role.objects.create(
|
||||
role=Role.CONTENT_PAGE_MANAGER,
|
||||
country=self.country_ru
|
||||
site_id=self.site_ru.id
|
||||
)
|
||||
role.save()
|
||||
|
||||
|
|
@ -51,16 +57,18 @@ class BaseTestCase(APITestCase):
|
|||
)
|
||||
user_role.save()
|
||||
|
||||
|
||||
self.test_news = News.objects.create(
|
||||
created_by=self.user, modified_by=self.user,
|
||||
title={"en-GB": "Test news"},
|
||||
title={"ru-RU": "Test news"},
|
||||
news_type=self.test_news_type,
|
||||
description={"en-GB": "Description test news"},
|
||||
description={"ru-RU": "Description test news"},
|
||||
start=datetime.now() + timedelta(hours=-2),
|
||||
end=datetime.now() + timedelta(hours=2),
|
||||
state=News.PUBLISHED,
|
||||
slug='test-news-slug',
|
||||
country=self.country_ru,
|
||||
site=self.site_ru
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -70,14 +78,15 @@ class NewsTestCase(BaseTestCase):
|
|||
|
||||
def test_news_post(self):
|
||||
test_news = {
|
||||
"title": {"en-GB": "Test news POST"},
|
||||
"title": {"ru-RU": "Test news POST"},
|
||||
"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),
|
||||
"end": datetime.now() + timedelta(hours=2),
|
||||
"state": News.PUBLISHED,
|
||||
"slug": 'test-news-slug_post',
|
||||
"country_id": self.country_ru.id,
|
||||
"site_id": self.site_ru.id
|
||||
}
|
||||
|
||||
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})
|
||||
data = {
|
||||
'id': self.test_news.id,
|
||||
'description': {"en-GB": "Description test news!"},
|
||||
'description': {"ru-RU": "Description test news!"},
|
||||
'slug': self.test_news.slug,
|
||||
'start': self.test_news.start,
|
||||
'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')
|
||||
|
|
@ -128,3 +138,17 @@ class NewsTestCase(BaseTestCase):
|
|||
|
||||
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)
|
||||
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -13,4 +13,5 @@ urlpatterns = [
|
|||
name='gallery-list'),
|
||||
path('<int:pk>/gallery/<int:image_id>/', views.NewsBackOfficeGalleryCreateDestroyView.as_view(),
|
||||
name='gallery-create-destroy'),
|
||||
]
|
||||
path('<int:pk>/carousels/', views.NewsCarouselCreateDestroyView.as_view(), name='create-destroy-carousels'),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -5,5 +5,6 @@ common_urlpatterns = [
|
|||
path('', views.NewsListView.as_view(), name='list'),
|
||||
path('types/', views.NewsTypeListView.as_view(), name='type'),
|
||||
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'),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from rest_framework import generics, permissions
|
|||
from news import filters, models, serializers
|
||||
from rating.tasks import add_rating
|
||||
from utils.permissions import IsCountryAdmin, IsContentPageManager
|
||||
from utils.views import CreateDestroyGalleryViewMixin
|
||||
from utils.views import CreateDestroyGalleryViewMixin, FavoritesCreateDestroyMixinView, CarouselCreateDestroyMixinView
|
||||
from utils.serializers import ImageBaseSerializer
|
||||
|
||||
|
||||
|
|
@ -21,7 +21,7 @@ class NewsMixinView:
|
|||
qs = models.News.objects.published() \
|
||||
.with_base_related() \
|
||||
.annotate_in_favorites(self.request.user) \
|
||||
.order_by('-is_highlighted', '-created')
|
||||
.order_by('-is_highlighted', '-start')
|
||||
|
||||
country_code = self.request.country_code
|
||||
if country_code:
|
||||
|
|
@ -84,6 +84,7 @@ class NewsBackOfficeLCView(NewsBackOfficeMixinView,
|
|||
serializer_class = serializers.NewsBackOfficeBaseSerializer
|
||||
filter_class = filters.NewsListFilterSet
|
||||
create_serializers_class = serializers.NewsBackOfficeDetailSerializer
|
||||
|
||||
permission_classes = [IsCountryAdmin | IsContentPageManager]
|
||||
|
||||
def get_serializer_class(self):
|
||||
|
|
@ -150,18 +151,15 @@ class NewsBackOfficeRUDView(NewsBackOfficeMixinView,
|
|||
return self.retrieve(request, *args, **kwargs)
|
||||
|
||||
|
||||
class NewsFavoritesCreateDestroyView(generics.CreateAPIView, generics.DestroyAPIView):
|
||||
class NewsFavoritesCreateDestroyView(FavoritesCreateDestroyMixinView):
|
||||
"""View for create/destroy news from favorites."""
|
||||
|
||||
_model = models.News
|
||||
serializer_class = serializers.NewsFavoritesCreateSerializer
|
||||
lookup_field = 'slug'
|
||||
|
||||
def get_object(self):
|
||||
"""
|
||||
Returns the object the view is displaying.
|
||||
"""
|
||||
news = get_object_or_404(models.News, slug=self.kwargs.get('slug'))
|
||||
favorites = get_object_or_404(news.favorites.filter(user=self.request.user))
|
||||
# May raise a permission denied
|
||||
self.check_object_permissions(self.request, favorites)
|
||||
return favorites
|
||||
|
||||
class NewsCarouselCreateDestroyView(CarouselCreateDestroyMixinView):
|
||||
"""View for create/destroy news from carousel."""
|
||||
|
||||
_model = models.News
|
||||
serializer_class = serializers.NewsCarouselCreateSerializer
|
||||
|
|
|
|||
21
apps/partner/serializers/back.py
Normal file
21
apps/partner/serializers/back.py
Normal 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',
|
||||
)
|
||||
|
|
@ -1,16 +1,90 @@
|
|||
# Create your tests here.
|
||||
from rest_framework.test import APITestCase
|
||||
from http.cookies import SimpleCookie
|
||||
|
||||
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 translation.models import Language
|
||||
|
||||
|
||||
class PartnerTestCase(APITestCase):
|
||||
class BaseTestCase(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.test_url = "www.example.com"
|
||||
self.test_partner = Partner.objects.create(url=self.test_url)
|
||||
self.username = 'test_user'
|
||||
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):
|
||||
response = self.client.get("/api/web/partner/")
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
from pprint import pprint
|
||||
|
||||
from establishment.models import Establishment
|
||||
from partner.models import Partner
|
||||
from transfer.models import EstablishmentBacklinks
|
||||
from transfer.serializers.partner import PartnerSerializer
|
||||
|
||||
|
||||
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)
|
||||
queryset = EstablishmentBacklinks.objects.filter(
|
||||
establishment_id__in=list(establishments),
|
||||
|
|
@ -24,6 +28,7 @@ def transfer_partner():
|
|||
|
||||
serialized_data = PartnerSerializer(data=list(queryset), many=True)
|
||||
if serialized_data.is_valid():
|
||||
Partner.objects.all().delete() # TODO: закоментить, если требуется сохранить старые записи
|
||||
serialized_data.save()
|
||||
else:
|
||||
pprint(f"Partner serializer errors: {serialized_data.errors}")
|
||||
|
|
|
|||
11
apps/partner/urls/back.py
Normal file
11
apps/partner/urls/back.py
Normal 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'),
|
||||
]
|
||||
27
apps/partner/views/back.py
Normal file
27
apps/partner/views/back.py
Normal 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'
|
||||
|
|
@ -1 +0,0 @@
|
|||
# Create your views here.
|
||||
|
|
@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _
|
|||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
|
||||
from utils.models import (BaseAttributes, ProjectBaseMixin, HasTagsMixin,
|
||||
TranslatedFieldsMixin, TJSONField,
|
||||
TranslatedFieldsMixin, TJSONField, FavoritesMixin,
|
||||
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."""
|
||||
|
||||
EARLIEST_VINTAGE_YEAR = 1700
|
||||
|
|
|
|||
|
|
@ -2,17 +2,6 @@ from pprint import pprint
|
|||
|
||||
from transfer import models as transfer_models
|
||||
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():
|
||||
|
|
@ -50,8 +39,8 @@ def transfer_wine_bottles_produced():
|
|||
)
|
||||
queryset = [vars(query) for query in raw_queryset]
|
||||
serialized_data = product_serializers.WineBottlesProducedSerializer(
|
||||
data=queryset,
|
||||
many=True)
|
||||
data=queryset,
|
||||
many=True)
|
||||
if serialized_data.is_valid():
|
||||
serialized_data.save()
|
||||
else:
|
||||
|
|
@ -69,8 +58,8 @@ def transfer_wine_classification_type():
|
|||
)
|
||||
queryset = [vars(query) for query in raw_queryset]
|
||||
serialized_data = product_serializers.WineClassificationTypeSerializer(
|
||||
data=queryset,
|
||||
many=True)
|
||||
data=queryset,
|
||||
many=True)
|
||||
if serialized_data.is_valid():
|
||||
serialized_data.save()
|
||||
else:
|
||||
|
|
@ -79,10 +68,10 @@ def transfer_wine_classification_type():
|
|||
|
||||
def transfer_wine_standard():
|
||||
queryset = transfer_models.ProductClassification.objects.filter(parent_id__isnull=True) \
|
||||
.exclude(type='Classification')
|
||||
.exclude(type='Classification')
|
||||
serialized_data = product_serializers.ProductStandardSerializer(
|
||||
data=list(queryset.values()),
|
||||
many=True)
|
||||
data=list(queryset.values()),
|
||||
many=True)
|
||||
if serialized_data.is_valid():
|
||||
serialized_data.save()
|
||||
else:
|
||||
|
|
@ -92,8 +81,8 @@ def transfer_wine_standard():
|
|||
def transfer_wine_classifications():
|
||||
queryset = transfer_models.ProductClassification.objects.filter(type='Classification')
|
||||
serialized_data = product_serializers.ProductClassificationSerializer(
|
||||
data=list(queryset.values()),
|
||||
many=True)
|
||||
data=list(queryset.values()),
|
||||
many=True)
|
||||
if serialized_data.is_valid():
|
||||
serialized_data.save()
|
||||
else:
|
||||
|
|
@ -104,8 +93,8 @@ def transfer_product():
|
|||
errors = []
|
||||
queryset = transfer_models.Products.objects.all()
|
||||
serialized_data = product_serializers.ProductSerializer(
|
||||
data=list(queryset.values()),
|
||||
many=True)
|
||||
data=list(queryset.values()),
|
||||
many=True)
|
||||
if serialized_data.is_valid():
|
||||
serialized_data.save()
|
||||
else:
|
||||
|
|
@ -117,8 +106,8 @@ def transfer_product_note():
|
|||
errors = []
|
||||
queryset = transfer_models.ProductNotes.objects.exclude(text='')
|
||||
serialized_data = product_serializers.ProductNoteSerializer(
|
||||
data=list(queryset.values()),
|
||||
many=True)
|
||||
data=list(queryset.values()),
|
||||
many=True)
|
||||
if serialized_data.is_valid():
|
||||
serialized_data.save()
|
||||
else:
|
||||
|
|
@ -130,8 +119,8 @@ def transfer_plate():
|
|||
errors = []
|
||||
queryset = transfer_models.Merchandise.objects.all()
|
||||
serialized_data = product_serializers.PlateSerializer(
|
||||
data=list(queryset.values()),
|
||||
many=True)
|
||||
data=list(queryset.values()),
|
||||
many=True)
|
||||
if serialized_data.is_valid():
|
||||
serialized_data.save()
|
||||
else:
|
||||
|
|
@ -143,8 +132,8 @@ def transfer_plate_image():
|
|||
errors = []
|
||||
queryset = transfer_models.Merchandise.objects.all()
|
||||
serialized_data = product_serializers.PlateImageSerializer(
|
||||
data=list(queryset.values()),
|
||||
many=True)
|
||||
data=list(queryset.values()),
|
||||
many=True)
|
||||
if serialized_data.is_valid():
|
||||
serialized_data.save()
|
||||
else:
|
||||
|
|
@ -153,7 +142,6 @@ def transfer_plate_image():
|
|||
|
||||
|
||||
data_types = {
|
||||
"partner": [transfer_partner],
|
||||
"wine_characteristics": [
|
||||
transfer_wine_sugar_content,
|
||||
transfer_wine_color,
|
||||
|
|
@ -161,12 +149,12 @@ data_types = {
|
|||
transfer_wine_classification_type,
|
||||
transfer_wine_standard,
|
||||
transfer_wine_classifications,
|
||||
],
|
||||
],
|
||||
"product": [
|
||||
transfer_product,
|
||||
],
|
||||
"product_note": [
|
||||
transfer_product_note,
|
||||
transfer_product_note,
|
||||
],
|
||||
"souvenir": [
|
||||
transfer_plate,
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ from rest_framework import generics, permissions
|
|||
from django.shortcuts import get_object_or_404
|
||||
from product.models import Product
|
||||
from comment.models import Comment
|
||||
from product import serializers
|
||||
from product import filters
|
||||
from product import filters, serializers
|
||||
from comment.serializers import CommentRUDSerializer
|
||||
from utils.views import FavoritesCreateDestroyMixinView
|
||||
|
||||
|
||||
class ProductBaseView(generics.GenericAPIView):
|
||||
|
|
@ -37,22 +37,11 @@ class ProductDetailView(ProductBaseView, generics.RetrieveAPIView):
|
|||
serializer_class = serializers.ProductDetailSerializer
|
||||
|
||||
|
||||
class CreateFavoriteProductView(generics.CreateAPIView,
|
||||
generics.DestroyAPIView):
|
||||
class CreateFavoriteProductView(FavoritesCreateDestroyMixinView):
|
||||
"""View for create/destroy product in favorites."""
|
||||
|
||||
_model = Product
|
||||
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):
|
||||
|
|
|
|||
0
apps/review/management/__init__.py
Normal file
0
apps/review/management/__init__.py
Normal file
0
apps/review/management/commands/__init__.py
Normal file
0
apps/review/management/commands/__init__.py
Normal file
23
apps/review/management/commands/add_review_priority.py
Normal file
23
apps/review/management/commands/add_review_priority.py
Normal 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.'))
|
||||
18
apps/review/migrations/0019_review_priority.py
Normal file
18
apps/review/migrations/0019_review_priority.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -39,7 +39,6 @@ class Review(BaseAttributes, TranslatedFieldsMixin):
|
|||
(TO_REVIEW, _('To review')),
|
||||
(READY, _('Ready')),
|
||||
)
|
||||
|
||||
reviewer = models.ForeignKey(
|
||||
'account.User',
|
||||
related_name='reviews',
|
||||
|
|
@ -83,6 +82,7 @@ class Review(BaseAttributes, TranslatedFieldsMixin):
|
|||
)
|
||||
vintage = models.IntegerField(_('Year of review'), validators=[MinValueValidator(1900), MaxValueValidator(2100)])
|
||||
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)
|
||||
|
||||
objects = ReviewQuerySet.as_manager()
|
||||
|
|
|
|||
|
|
@ -1,3 +1,54 @@
|
|||
"""Review app back serializers."""
|
||||
from review import models
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
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',
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ class ReviewBaseSerializer(serializers.ModelSerializer):
|
|||
'id',
|
||||
'reviewer',
|
||||
'text',
|
||||
'priority',
|
||||
'status',
|
||||
'child',
|
||||
'published_at',
|
||||
|
|
@ -33,6 +34,7 @@ class ReviewShortSerializer(ReviewBaseSerializer):
|
|||
|
||||
class InquiriesBaseSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for model Inquiries."""
|
||||
|
||||
class Meta:
|
||||
model = Inquiries
|
||||
fields = (
|
||||
|
|
@ -56,6 +58,7 @@ class InquiriesBaseSerializer(serializers.ModelSerializer):
|
|||
|
||||
class GridItemsBaseSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for model GridItems."""
|
||||
|
||||
class Meta:
|
||||
model = GridItems
|
||||
fields = (
|
||||
|
|
|
|||
|
|
@ -4,19 +4,34 @@ from review import filters
|
|||
from review import models
|
||||
from review import serializers
|
||||
from utils.permissions import IsReviewerManager, IsRestaurantReviewer
|
||||
from review.serializers.back import ReviewBackSerializer
|
||||
|
||||
|
||||
class ReviewLstView(generics.ListCreateAPIView):
|
||||
"""Comment list create view."""
|
||||
serializer_class = serializers.ReviewBaseSerializer
|
||||
"""Review list create view.
|
||||
|
||||
status values:
|
||||
|
||||
TO_INVESTIGATE = 0
|
||||
TO_REVIEW = 1
|
||||
READY = 2
|
||||
"""
|
||||
serializer_class = ReviewBackSerializer
|
||||
queryset = models.Review.objects.all()
|
||||
permission_classes = [permissions.IsAuthenticatedOrReadOnly, ]
|
||||
filterset_class = filters.ReviewFilter
|
||||
|
||||
|
||||
class ReviewRUDView(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""Comment RUD view."""
|
||||
serializer_class = serializers.ReviewBaseSerializer
|
||||
"""Review RUD view.
|
||||
|
||||
status values:
|
||||
|
||||
TO_INVESTIGATE = 0
|
||||
TO_REVIEW = 1
|
||||
READY = 2
|
||||
"""
|
||||
serializer_class = ReviewBackSerializer
|
||||
queryset = models.Review.objects.all()
|
||||
permission_classes = [permissions.IsAdminUser | IsReviewerManager | IsRestaurantReviewer]
|
||||
lookup_field = 'id'
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
from search_indexes.documents.establishment import EstablishmentDocument
|
||||
from search_indexes.documents.news import NewsDocument
|
||||
from search_indexes.documents.product import ProductDocument
|
||||
|
||||
from search_indexes.tasks import es_update
|
||||
|
||||
# todo: make signal to update documents on related fields
|
||||
__all__ = [
|
||||
'EstablishmentDocument',
|
||||
'NewsDocument',
|
||||
'ProductDocument',
|
||||
'es_update',
|
||||
]
|
||||
|
|
@ -46,6 +46,7 @@ class EstablishmentDocument(Document):
|
|||
'id': fields.IntegerField(attr='id'),
|
||||
'label': fields.ObjectField(attr='label_indexing',
|
||||
properties=OBJECT_FIELD_PROPERTIES),
|
||||
'value': fields.KeywordField(),
|
||||
},
|
||||
multi=True)
|
||||
visible_tags = fields.ObjectField(
|
||||
|
|
@ -53,6 +54,7 @@ class EstablishmentDocument(Document):
|
|||
'id': fields.IntegerField(attr='id'),
|
||||
'label': fields.ObjectField(attr='label_indexing',
|
||||
properties=OBJECT_FIELD_PROPERTIES),
|
||||
'value': fields.KeywordField(),
|
||||
},
|
||||
multi=True)
|
||||
products = fields.ObjectField(
|
||||
|
|
@ -69,6 +71,14 @@ class EstablishmentDocument(Document):
|
|||
# 'coordinates': fields.GeoPointField(),
|
||||
'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={
|
||||
'id': fields.IntegerField(),
|
||||
'name': fields.KeywordField(),
|
||||
|
|
@ -113,6 +123,7 @@ class EstablishmentDocument(Document):
|
|||
),
|
||||
},
|
||||
)
|
||||
favorites_for_users = fields.ListField(field=fields.IntegerField())
|
||||
|
||||
class Django:
|
||||
|
||||
|
|
|
|||
|
|
@ -41,13 +41,13 @@ class NewsDocument(Document):
|
|||
properties=OBJECT_FIELD_PROPERTIES),
|
||||
},
|
||||
multi=True)
|
||||
|
||||
favorites_for_users = fields.ListField(field=fields.IntegerField())
|
||||
start = fields.DateField(attr='start')
|
||||
class Django:
|
||||
|
||||
model = models.News
|
||||
fields = (
|
||||
'id',
|
||||
'start',
|
||||
'end',
|
||||
'slug',
|
||||
'state',
|
||||
|
|
@ -57,7 +57,7 @@ class NewsDocument(Document):
|
|||
related_models = [models.NewsType]
|
||||
|
||||
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):
|
||||
"""If related_models is set, define how to retrieve the Car instance(s) from the related model.
|
||||
|
|
|
|||
|
|
@ -148,6 +148,8 @@ class ProductDocument(Document):
|
|||
name = fields.TextField(attr='display_name', analyzer='english')
|
||||
name_ru = fields.TextField(attr='display_name', analyzer='russian')
|
||||
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:
|
||||
model = models.Product
|
||||
|
|
|
|||
|
|
@ -1,7 +1,81 @@
|
|||
"""Search indexes filters."""
|
||||
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 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):
|
||||
|
|
|
|||
|
|
@ -135,6 +135,9 @@ class ProductEstablishmentDocumentSerializer(serializers.Serializer):
|
|||
index_name = serializers.CharField()
|
||||
city = AnotherCityDocumentShortSerializer()
|
||||
|
||||
def get_attribute(self, instance):
|
||||
return instance.establishment if instance and instance.establishment else None
|
||||
|
||||
|
||||
class AddressDocumentSerializer(serializers.Serializer):
|
||||
"""Address serializer for ES Document."""
|
||||
|
|
@ -167,7 +170,25 @@ class ScheduleDocumentSerializer(serializers.Serializer):
|
|||
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."""
|
||||
|
||||
title_translated = serializers.SerializerMethodField(allow_null=True)
|
||||
|
|
@ -188,6 +209,7 @@ class NewsDocumentSerializer(DocumentSerializer):
|
|||
'preview_image_url',
|
||||
'news_type',
|
||||
'tags',
|
||||
'start',
|
||||
'slug',
|
||||
)
|
||||
|
||||
|
|
@ -200,7 +222,7 @@ class NewsDocumentSerializer(DocumentSerializer):
|
|||
return get_translated_value(obj.subtitle)
|
||||
|
||||
|
||||
class EstablishmentDocumentSerializer(DocumentSerializer):
|
||||
class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer):
|
||||
"""Establishment document serializer."""
|
||||
|
||||
establishment_type = EstablishmentTypeSerializer()
|
||||
|
|
@ -236,7 +258,7 @@ class EstablishmentDocumentSerializer(DocumentSerializer):
|
|||
)
|
||||
|
||||
|
||||
class ProductDocumentSerializer(DocumentSerializer):
|
||||
class ProductDocumentSerializer(InFavoritesMixin, DocumentSerializer):
|
||||
"""Product document serializer"""
|
||||
|
||||
tags = TagsDocumentSerializer(many=True, source='related_tags')
|
||||
|
|
@ -271,4 +293,5 @@ class ProductDocumentSerializer(DocumentSerializer):
|
|||
'grape_variety',
|
||||
'establishment_detail',
|
||||
'average_price',
|
||||
'created',
|
||||
)
|
||||
|
|
|
|||
50
apps/search_indexes/tasks.py
Normal file
50
apps/search_indexes/tasks.py
Normal 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}')
|
||||
|
|
@ -6,9 +6,11 @@ from search_indexes import views
|
|||
router = routers.SimpleRouter()
|
||||
# router.register(r'news', views.NewsDocumentViewSet, basename='news') # temporarily disabled
|
||||
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'mobile/news', views.MobileNewsDocumentViewSet, basename='news-mobile')
|
||||
router.register(r'products', views.ProductDocumentViewSet, basename='product')
|
||||
router.register(r'mobile/products', views.MobileProductDocumentViewSet, basename='product-mobile')
|
||||
|
||||
urlpatterns = router.urls
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
from django_elasticsearch_dsl import fields
|
||||
from utils.models import get_current_locale, get_default_locale
|
||||
|
||||
FACET_MAX_RESPONSE = 9999999 # Unlimited
|
||||
|
||||
ALL_LOCALES_LIST = [
|
||||
'hr-HR',
|
||||
'ro-RO',
|
||||
|
|
|
|||
|
|
@ -4,13 +4,15 @@ from django_elasticsearch_dsl_drf import constants
|
|||
from django_elasticsearch_dsl_drf.filter_backends import (
|
||||
FilteringFilterBackend,
|
||||
GeoSpatialFilteringFilterBackend,
|
||||
DefaultOrderingFilterBackend,
|
||||
GeoSpatialOrderingFilterBackend,
|
||||
OrderingFilterBackend,
|
||||
)
|
||||
from elasticsearch_dsl import TermsFacet
|
||||
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.product import ProductDocument
|
||||
from utils.pagination import ProjectMobilePagination
|
||||
from utils.pagination import ESDocumentPagination
|
||||
|
||||
|
||||
class NewsDocumentViewSet(BaseDocumentViewSet):
|
||||
|
|
@ -18,15 +20,34 @@ class NewsDocumentViewSet(BaseDocumentViewSet):
|
|||
|
||||
document = NewsDocument
|
||||
lookup_field = 'slug'
|
||||
pagination_class = ProjectMobilePagination
|
||||
pagination_class = ESDocumentPagination
|
||||
permission_classes = (permissions.AllowAny,)
|
||||
serializer_class = serializers.NewsDocumentSerializer
|
||||
|
||||
filter_backends = [
|
||||
filters.CustomSearchFilterBackend,
|
||||
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 = {
|
||||
'title': {'fuzziness': 'auto:2,5',
|
||||
'boost': 3},
|
||||
|
|
@ -65,12 +86,19 @@ class NewsDocumentViewSet(BaseDocumentViewSet):
|
|||
}
|
||||
|
||||
|
||||
class MobileNewsDocumentViewSet(NewsDocumentViewSet):
|
||||
|
||||
filter_backends = [
|
||||
filters.CustomSearchFilterBackend,
|
||||
FilteringFilterBackend,
|
||||
]
|
||||
|
||||
|
||||
class EstablishmentDocumentViewSet(BaseDocumentViewSet):
|
||||
"""Establishment document ViewSet."""
|
||||
|
||||
document = EstablishmentDocument
|
||||
lookup_field = 'slug'
|
||||
pagination_class = ProjectMobilePagination
|
||||
pagination_class = ESDocumentPagination
|
||||
permission_classes = (permissions.AllowAny,)
|
||||
serializer_class = serializers.EstablishmentDocumentSerializer
|
||||
|
||||
|
|
@ -82,10 +110,63 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
|
|||
filter_backends = [
|
||||
FilteringFilterBackend,
|
||||
filters.CustomSearchFilterBackend,
|
||||
GeoSpatialFilteringFilterBackend,
|
||||
# DefaultOrderingFilterBackend,
|
||||
filters.CustomGeoSpatialFilteringFilterBackend,
|
||||
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 = {
|
||||
'name': {'fuzziness': 'auto:2,5',
|
||||
'boost': 4},
|
||||
|
|
@ -124,6 +205,13 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
|
|||
constants.LOOKUP_QUERY_IN,
|
||||
]
|
||||
},
|
||||
'wine_colors_id': {
|
||||
'field': 'products.wine_colors.id',
|
||||
'lookups': [
|
||||
constants.LOOKUP_QUERY_IN,
|
||||
constants.LOOKUP_QUERY_EXCLUDE,
|
||||
],
|
||||
},
|
||||
'wine_region_id': {
|
||||
'field': 'products.wine_region.id',
|
||||
'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):
|
||||
"""Product document ViewSet."""
|
||||
|
||||
document = ProductDocument
|
||||
pagination_class = ProjectMobilePagination
|
||||
pagination_class = ESDocumentPagination
|
||||
permission_classes = (permissions.AllowAny,)
|
||||
serializer_class = serializers.ProductDocumentSerializer
|
||||
|
||||
filter_backends = [
|
||||
FilteringFilterBackend,
|
||||
filters.CustomSearchFilterBackend,
|
||||
filters.CustomFacetedSearchFilterBackend,
|
||||
OrderingFilterBackend,
|
||||
# GeoSpatialOrderingFilterBackend,
|
||||
]
|
||||
|
||||
ordering_fields = {
|
||||
'created': {
|
||||
'field': 'created',
|
||||
},
|
||||
}
|
||||
|
||||
search_fields = {
|
||||
'name': {'fuzziness': 'auto:2,5',
|
||||
'boost': 8},
|
||||
|
|
@ -232,6 +344,25 @@ class ProductDocumentViewSet(BaseDocumentViewSet):
|
|||
'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 = (
|
||||
'description',
|
||||
)
|
||||
|
|
@ -289,4 +420,12 @@ class ProductDocumentViewSet(BaseDocumentViewSet):
|
|||
constants.LOOKUP_QUERY_EXCLUDE,
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class MobileProductDocumentViewSet(ProductDocumentViewSet):
|
||||
|
||||
filter_backends = [
|
||||
FilteringFilterBackend,
|
||||
filters.CustomSearchFilterBackend,
|
||||
]
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@ class TagCategoryFilterSet(TagsBaseFilterSet):
|
|||
'product_type', )
|
||||
|
||||
def by_product_type(self, queryset, name, value):
|
||||
# if value == product_models.ProductType.WINE:
|
||||
# queryset = queryset.filter(index_name='wine-color').filter(tags__products__isnull=False)
|
||||
if value == product_models.ProductType.WINE:
|
||||
return queryset.wine_tags_category().filter(tags__products__isnull=False)
|
||||
queryset = queryset.by_product_type(value)
|
||||
return queryset
|
||||
|
||||
|
|
@ -86,7 +86,7 @@ class TagsFilterSet(TagsBaseFilterSet):
|
|||
if self.NEWS in value:
|
||||
queryset = queryset.for_news().filter(value__in=settings.NEWS_CHOSEN_TAGS).distinct('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')
|
||||
return queryset
|
||||
|
||||
|
|
|
|||
|
|
@ -111,6 +111,9 @@ class TagCategoryQuerySet(models.QuerySet):
|
|||
"""Filter by product type 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):
|
||||
"""Filter by existing tags."""
|
||||
return self.exclude(tags__isnull=switcher)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ class ScheduleRUDSerializer(serializers.ModelSerializer):
|
|||
dinner_end = serializers.TimeField(required=False)
|
||||
opening_at = serializers.TimeField(required=False)
|
||||
closed_at = serializers.TimeField(required=False)
|
||||
# For permission!!
|
||||
establishment_id = serializers.ReadOnlyField(source='establishment.id')
|
||||
|
||||
class Meta:
|
||||
"""Meta class."""
|
||||
|
|
@ -34,6 +36,7 @@ class ScheduleRUDSerializer(serializers.ModelSerializer):
|
|||
'dinner_end',
|
||||
'opening_at',
|
||||
'closed_at',
|
||||
'establishment_id'
|
||||
]
|
||||
|
||||
def validate(self, attrs):
|
||||
|
|
|
|||
|
|
@ -369,44 +369,45 @@ class GuideFilters(MigrateMixin):
|
|||
db_table = 'guide_filters'
|
||||
|
||||
|
||||
#
|
||||
# class GuideSections(MigrateMixin):
|
||||
# using = 'legacy'
|
||||
#
|
||||
# type = models.CharField(max_length=255)
|
||||
# key_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)
|
||||
# created_at = models.DateTimeField()
|
||||
# updated_at = models.DateTimeField()
|
||||
class GuideSections(MigrateMixin):
|
||||
using = 'legacy'
|
||||
|
||||
type = models.CharField(max_length=255)
|
||||
key_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)
|
||||
created_at = models.DateTimeField()
|
||||
updated_at = models.DateTimeField()
|
||||
|
||||
class Meta:
|
||||
managed = False
|
||||
db_table = 'guide_elements'
|
||||
|
||||
|
||||
# class GuideElements(MigrateMixin):
|
||||
# using = 'legacy'
|
||||
#
|
||||
# type = models.CharField(max_length=255)
|
||||
# establishment = models.ForeignKey(Establishments, 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)
|
||||
# wine_region = models.ForeignKey('WineLocations', 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)
|
||||
# order_number = models.IntegerField(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)
|
||||
# section = models.ForeignKey('GuideSections', models.DO_NOTHING, blank=True, null=True)
|
||||
# guide_id = models.IntegerField(blank=True, null=True)
|
||||
# parent_id = models.IntegerField(blank=True, null=True)
|
||||
# lft = models.IntegerField()
|
||||
# rgt = models.IntegerField()
|
||||
# depth = models.IntegerField()
|
||||
# children_count = models.IntegerField()
|
||||
# created_at = models.DateTimeField()
|
||||
# updated_at = models.DateTimeField()
|
||||
#
|
||||
# class Meta:
|
||||
# managed = False
|
||||
# db_table = 'guide_elements'
|
||||
class GuideElements(MigrateMixin):
|
||||
using = 'legacy'
|
||||
|
||||
type = models.CharField(max_length=255)
|
||||
establishment = models.ForeignKey('Establishments', 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)
|
||||
wine_region = models.ForeignKey('WineLocations', 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)
|
||||
order_number = models.IntegerField(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)
|
||||
section = models.ForeignKey('GuideSections', models.DO_NOTHING, blank=True, null=True)
|
||||
guide_id = models.IntegerField(blank=True, null=True)
|
||||
parent = models.ForeignKey('self', models.DO_NOTHING, blank=True, null=True)
|
||||
lft = models.IntegerField()
|
||||
rgt = models.IntegerField()
|
||||
depth = models.IntegerField()
|
||||
children_count = models.IntegerField()
|
||||
|
||||
class Meta:
|
||||
managed = False
|
||||
db_table = 'guide_elements'
|
||||
|
||||
|
||||
class Establishments(MigrateMixin):
|
||||
|
|
|
|||
|
|
@ -1,29 +1,53 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from establishment.models import Establishment
|
||||
from partner.models import Partner
|
||||
|
||||
|
||||
class PartnerSerializer(serializers.Serializer):
|
||||
pass
|
||||
# 'id',
|
||||
# 'establishment_id',
|
||||
# 'partnership_name',
|
||||
# 'partnership_icon',
|
||||
# 'backlink_url',
|
||||
# 'created_at',
|
||||
# 'type',
|
||||
# 'starting_date',
|
||||
# 'expiry_date',
|
||||
# 'price_per_month',
|
||||
id = serializers.IntegerField()
|
||||
establishment_id = serializers.IntegerField()
|
||||
partnership_name = serializers.CharField(allow_null=True)
|
||||
partnership_icon = serializers.CharField(allow_null=True)
|
||||
backlink_url = serializers.CharField(allow_null=True)
|
||||
created_at = serializers.DateTimeField(format='%m-%d-%Y %H:%M:%S')
|
||||
type = serializers.CharField(allow_null=True)
|
||||
starting_date = serializers.DateField(allow_null=True)
|
||||
expiry_date = serializers.DateField(allow_null=True)
|
||||
price_per_month = serializers.DecimalField(max_digits=10, decimal_places=2, allow_null=True)
|
||||
|
||||
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):
|
||||
# data["image"] = partnership_to_image_url.get(data["partnership_name"]).get(data["partnership_icon"])
|
||||
# data.pop("partnership_name")
|
||||
# data.pop("partnership_icon")
|
||||
# return data
|
||||
#
|
||||
# def create(self, validated_data):
|
||||
# return Partner.objects.create(**validated_data)
|
||||
@staticmethod
|
||||
def get_image(data):
|
||||
return partnership_to_image_url.get(data['partnership_name']).get(data['partnership_icon'])
|
||||
|
||||
@staticmethod
|
||||
def get_establishment(data):
|
||||
establishment = Establishment.objects.filter(old_id=data['establishment_id']).first()
|
||||
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 = {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
from django.contrib.auth.models import AbstractUser
|
||||
|
||||
|
||||
def with_base_attributes(cls):
|
||||
|
||||
|
|
@ -8,7 +10,7 @@ def with_base_attributes(cls):
|
|||
if request and hasattr(request, "user"):
|
||||
user = request.user
|
||||
|
||||
if user is not None:
|
||||
if user is not None and isinstance(user, AbstractUser):
|
||||
data.update({'modified_by': user})
|
||||
|
||||
if not self.instance:
|
||||
|
|
|
|||
|
|
@ -135,6 +135,14 @@ class FavoritesError(exceptions.APIException):
|
|||
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):
|
||||
"""
|
||||
The exception should be thrown when password reset request
|
||||
|
|
|
|||
|
|
@ -5,16 +5,17 @@ from os.path import exists
|
|||
from django.conf import settings
|
||||
from django.contrib.auth.tokens import PasswordResetTokenGenerator
|
||||
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.jsonb import KeyTextTransform
|
||||
from django.utils import timezone
|
||||
from django.utils.html import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _, get_language
|
||||
from configuration.models import TranslationSettings
|
||||
from easy_thumbnails.fields import ThumbnailerImageField
|
||||
from sorl.thumbnail import get_thumbnail
|
||||
from sorl.thumbnail.fields import ImageField as SORLImageField
|
||||
|
||||
from configuration.models import TranslationSettings
|
||||
from utils.methods import image_path, svg_image_path
|
||||
from utils.validators import svg_image_validator
|
||||
|
||||
|
|
@ -35,10 +36,6 @@ class ProjectBaseMixin(models.Model):
|
|||
abstract = True
|
||||
|
||||
|
||||
def valid(value):
|
||||
print("Run")
|
||||
|
||||
|
||||
class TJSONField(JSONField):
|
||||
"""Overrided JsonField."""
|
||||
|
||||
|
|
@ -226,6 +223,18 @@ class SORLImageMixin(models.Model):
|
|||
else:
|
||||
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.allow_tags = True
|
||||
|
||||
|
|
@ -435,4 +444,12 @@ class HasTagsMixin(models.Model):
|
|||
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()
|
||||
|
|
@ -3,8 +3,8 @@ from base64 import b64encode
|
|||
from urllib import parse as urlparse
|
||||
|
||||
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):
|
||||
"""Customized pagination class."""
|
||||
|
|
@ -48,6 +48,40 @@ class ProjectMobilePagination(ProjectPageNumberPagination):
|
|||
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):
|
||||
"""
|
||||
Pagination for app establishments with limit page size equal to 12
|
||||
|
|
|
|||
|
|
@ -100,7 +100,10 @@ class IsStandardUser(IsGuest):
|
|||
|
||||
if hasattr(obj, 'user'):
|
||||
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)
|
||||
]
|
||||
|
||||
|
|
@ -117,31 +120,50 @@ class IsContentPageManager(IsStandardUser):
|
|||
rules = [
|
||||
super().has_permission(request, view)
|
||||
]
|
||||
# and request.user.email_confirmed,
|
||||
if hasattr(request, 'user'):
|
||||
role = Role.objects.filter(role=Role.CONTENT_PAGE_MANAGER,
|
||||
country_id=request.country_id) \
|
||||
.first() # 'Comments moderator'
|
||||
|
||||
rules = [
|
||||
UserRole.objects.filter(user=request.user, role=role).exists(),
|
||||
# and obj.user != request.user,
|
||||
super().has_permission(request, view)
|
||||
]
|
||||
if hasattr(request, 'user'):
|
||||
if hasattr(request.data, 'site_id'):
|
||||
role = Role.objects.filter(role=Role.CONTENT_PAGE_MANAGER,
|
||||
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)
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
# 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,
|
||||
country_id=obj.country_id) \
|
||||
.first() # 'Comments moderator'
|
||||
rules = [
|
||||
UserRole.objects.filter(user=request.user, role=role).exists(),
|
||||
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)
|
||||
|
||||
|
||||
|
|
@ -150,36 +172,55 @@ class IsCountryAdmin(IsStandardUser):
|
|||
Object-level permission to only allow owners of an object to edit it.
|
||||
Assumes the model instance has an `owner` attribute.
|
||||
"""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
|
||||
rules = [
|
||||
super().has_permission(request, view)
|
||||
]
|
||||
# and request.user.email_confirmed,
|
||||
if hasattr(request.data, 'user') and hasattr(request.data, 'country_id'):
|
||||
# Read permissions are allowed to any request.
|
||||
if hasattr(request.data, 'user'):
|
||||
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,
|
||||
country_id=request.data.country_id) \
|
||||
.first() # 'Comments moderator'
|
||||
.first()
|
||||
|
||||
rules = [
|
||||
UserRole.objects.filter(user=request.user, role=role).exists(),
|
||||
super().has_permission(request, view)
|
||||
]
|
||||
]
|
||||
return any(rules)
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
# Read permissions are allowed to any request.
|
||||
role = Role.objects.filter(role=Role.COUNTRY_ADMIN,
|
||||
country_id=obj.country_id) \
|
||||
.first() # 'Comments moderator'
|
||||
if hasattr(obj, 'site_id'):
|
||||
role = Role.objects.filter(role=Role.COUNTRY_ADMIN,
|
||||
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:
|
||||
rules = [
|
||||
UserRole.objects.filter(user=request.user, role=role).exists(),
|
||||
|
|
@ -206,13 +247,12 @@ class IsCommentModerator(IsStandardUser):
|
|||
super().has_permission(request, view)
|
||||
]
|
||||
|
||||
# and request.user.email_confirmed,
|
||||
if hasattr(request.data, 'user') and hasattr(request.data, 'country_id'):
|
||||
if any(rules) and hasattr(request.data, 'site_id'):
|
||||
# Read permissions are allowed to any request.
|
||||
|
||||
role = Role.objects.filter(role=Role.COMMENTS_MODERATOR,
|
||||
country_id=request.data.country_id) \
|
||||
.first() # 'Comments moderator'
|
||||
site_id=request.data.site_id) \
|
||||
.first()
|
||||
|
||||
rules = [
|
||||
UserRole.objects.filter(user=request.user, role=role).exists(),
|
||||
|
|
@ -222,16 +262,22 @@ class IsCommentModerator(IsStandardUser):
|
|||
return any(rules)
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
# Read permissions are allowed to any request.
|
||||
role = Role.objects.filter(role=Role.COMMENTS_MODERATOR,
|
||||
country_id=obj.country_id) \
|
||||
.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)
|
||||
]
|
||||
|
||||
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)
|
||||
|
||||
|
||||
|
|
@ -242,30 +288,40 @@ class IsEstablishmentManager(IsStandardUser):
|
|||
super().has_permission(request, view)
|
||||
]
|
||||
|
||||
# and request.user.email_confirmed,
|
||||
if hasattr(request.data, 'user') and hasattr(request.data, 'establishment_id'):
|
||||
role = Role.objects.filter(role=Role.ESTABLISHMENT_MANAGER) \
|
||||
.first() # 'Comments moderator'
|
||||
if hasattr(request.data, 'user'):
|
||||
if hasattr(request.data, 'establishment_id'):
|
||||
role = Role.objects.filter(role=Role.ESTABLISHMENT_MANAGER) \
|
||||
.first()
|
||||
|
||||
rules = [
|
||||
UserRole.objects.filter(user=request.user, role=role,
|
||||
establishment_id=request.data.establishment_id
|
||||
).exists(),
|
||||
super().has_permission(request, view)
|
||||
]
|
||||
rules = [
|
||||
UserRole.objects.filter(user=request.user, role=role,
|
||||
establishment_id=request.data.establishment_id
|
||||
).exists(),
|
||||
super().has_permission(request, view)
|
||||
]
|
||||
return any(rules)
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
role = Role.objects.filter(role=Role.ESTABLISHMENT_MANAGER) \
|
||||
.first() # 'Comments moderator'
|
||||
|
||||
rules = [
|
||||
UserRole.objects.filter(user=request.user, role=role,
|
||||
establishment_id=obj.establishment_id
|
||||
).exists(),
|
||||
super().has_object_permission(request, view, obj)
|
||||
# special!
|
||||
super().has_permission(request, view)
|
||||
# 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)
|
||||
|
||||
|
||||
|
|
@ -277,13 +333,13 @@ class IsReviewerManager(IsStandardUser):
|
|||
]
|
||||
|
||||
# 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) \
|
||||
.first() # 'Comments moderator'
|
||||
.first()
|
||||
|
||||
rules = [
|
||||
UserRole.objects.filter(user=request.user, role=role,
|
||||
establishment_id=request.data.country_id
|
||||
establishment_id=request.data.site_id
|
||||
).exists(),
|
||||
super().has_permission(request, view)
|
||||
]
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@
|
|||
import pytz
|
||||
from django.core import exceptions
|
||||
from rest_framework import serializers
|
||||
from utils import models
|
||||
from translation.models import Language
|
||||
|
||||
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):
|
||||
|
|
@ -80,7 +81,6 @@ class FavoritesCreateSerializer(serializers.ModelSerializer):
|
|||
"""Serializer to favorite object."""
|
||||
|
||||
class Meta:
|
||||
"""Serializer for model Comment."""
|
||||
model = Favorites
|
||||
fields = [
|
||||
'id',
|
||||
|
|
@ -101,6 +101,24 @@ class FavoritesCreateSerializer(serializers.ModelSerializer):
|
|||
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):
|
||||
def to_representation(self, value):
|
||||
serializer = self.parent.parent.__class__(value, context=self.context)
|
||||
|
|
|
|||
|
|
@ -5,15 +5,13 @@ from translation.models import Language
|
|||
|
||||
class BasePermissionTests(APITestCase):
|
||||
def setUp(self):
|
||||
self.lang = Language.objects.get(
|
||||
self.lang, created = Language.objects.get_or_create(
|
||||
title='Russia',
|
||||
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"}
|
||||
)
|
||||
self.country_ru.save()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -40,8 +40,7 @@ class TranslateFieldTests(BaseTestCase):
|
|||
self.news_type = NewsType.objects.create(name="Test news type")
|
||||
self.news_type.save()
|
||||
|
||||
|
||||
self.country_ru = Country.objects.get(
|
||||
self.country_ru, created = Country.objects.get_or_create(
|
||||
name={"en-GB": "Russian"}
|
||||
)
|
||||
|
||||
|
|
|
|||
19
apps/utils/thumbnail_engine.py
Normal file
19
apps/utils/thumbnail_engine.py
Normal 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
|
||||
|
|
@ -2,12 +2,13 @@ from collections import namedtuple
|
|||
|
||||
from django.conf import settings
|
||||
from django.db.transaction import on_commit
|
||||
from rest_framework import generics
|
||||
from rest_framework import status
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework import generics, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from gallery.tasks import delete_image
|
||||
from search_indexes.documents import es_update
|
||||
|
||||
|
||||
# JWT
|
||||
|
|
@ -69,22 +70,12 @@ class JWTGenericViewMixin:
|
|||
def _put_cookies_in_response(self, cookies: list, response: Response):
|
||||
"""Update COOKIES in response from namedtuple"""
|
||||
for cookie in cookies:
|
||||
# todo: remove config for develop
|
||||
from os import environ
|
||||
configuration = environ.get('SETTINGS_CONFIGURATION', None)
|
||||
if configuration == 'development' or configuration == 'stage':
|
||||
response.set_cookie(key=cookie.key,
|
||||
value=cookie.value,
|
||||
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,)
|
||||
response.set_cookie(key=cookie.key,
|
||||
value=cookie.value,
|
||||
secure=cookie.secure,
|
||||
httponly=cookie.http_only,
|
||||
max_age=cookie.max_age,
|
||||
domain=settings.COOKIE_DOMAIN)
|
||||
return response
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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
|
||||
class BindObjectMixin:
|
||||
"""Bind object mixin."""
|
||||
|
|
@ -149,4 +196,4 @@ class BindObjectMixin:
|
|||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
elif request.method == 'DELETE':
|
||||
self.perform_unbinding(serializer)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ services:
|
|||
|
||||
# Redis
|
||||
redis:
|
||||
image: redis:2.8.23
|
||||
image: redis:latest
|
||||
|
||||
# Celery
|
||||
worker:
|
||||
|
|
|
|||
|
|
@ -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
14
make_data_migration.sh
Executable 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
|
||||
|
|
@ -13,9 +13,9 @@ AWS_S3_OBJECT_PARAMETERS = {'CacheControl': 'max-age=86400'}
|
|||
AWS_S3_ADDRESSING_STYLE = 'path'
|
||||
|
||||
# Static settings
|
||||
# PUBLIC_STATIC_LOCATION = 'static'
|
||||
# STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{PUBLIC_STATIC_LOCATION}/'
|
||||
# STATICFILES_STORAGE = 'project.storage_backends.PublicStaticStorage'
|
||||
PUBLIC_STATIC_LOCATION = 'static-dev'
|
||||
STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{PUBLIC_STATIC_LOCATION}/'
|
||||
STATICFILES_STORAGE = 'project.storage_backends.PublicStaticStorage'
|
||||
|
||||
# Public media settings
|
||||
MEDIA_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{MEDIA_LOCATION}/'
|
||||
|
|
|
|||
|
|
@ -254,6 +254,17 @@ AUTHENTICATION_BACKENDS = (
|
|||
'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
|
||||
DRFSO2_URL_NAMESPACE = 'auth'
|
||||
SOCIAL_AUTH_URL_NAMESPACE = 'auth'
|
||||
|
|
@ -399,6 +410,13 @@ SORL_THUMBNAIL_ALIASES = {
|
|||
'establishment_xlarge': {'geometry_string': '640x360', 'crop': 'center'},
|
||||
'establishment_detail': {'geometry_string': '2048x1152', '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
|
||||
# A Spatial Reference System Identifier
|
||||
GEO_DEFAULT_SRID = 4326
|
||||
GEOIP_PATH = os.path.join(PROJECT_ROOT, 'geoip_db')
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/2.2/howto/static-files/
|
||||
|
|
@ -509,8 +526,11 @@ FALLBACK_LOCALE = 'en-GB'
|
|||
|
||||
# TMP TODO remove it later
|
||||
# Временный хардкод для демонстрации > 15 ноября, потом удалить!
|
||||
CAROUSEL_ITEMS = [230, 231, 232]
|
||||
CAROUSEL_ITEMS = [465]
|
||||
ESTABLISHMENT_CHOSEN_TAGS = ['gastronomic', 'en_vogue', 'terrace', 'streetfood', 'business', 'bar_cocktail', 'brunch', 'pop']
|
||||
NEWS_CHOSEN_TAGS = ['eat', 'drink', 'cook', 'style', 'international', 'event', 'partnership']
|
||||
INTERNATIONAL_COUNTRY_CODES = ['www', 'main', 'next']
|
||||
|
||||
THUMBNAIL_ENGINE = 'utils.thumbnail_engine.GMEngine'
|
||||
|
||||
COOKIE_DOMAIN = None
|
||||
|
|
|
|||
|
|
@ -18,6 +18,17 @@ SITE_DOMAIN_URI = '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_DSL = {
|
||||
'default': {
|
||||
|
|
@ -60,3 +71,5 @@ INSTALLED_APPS.append('transfer.apps.TransferConfig')
|
|||
BROKER_URL = 'redis://localhost:6379/1'
|
||||
CELERY_RESULT_BACKEND = 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
Loading…
Reference in New Issue
Block a user