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

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

3
.gitignore vendored
View File

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

View File

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

View File

@ -46,10 +46,8 @@ class Role(ProjectBaseMixin):
null=False, blank=False)
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):

View File

@ -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)

View File

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

View File

@ -3,6 +3,7 @@ from establishment.models import Establishment
from location.models import Country, Language
from 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']

View File

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

View File

@ -35,7 +35,8 @@ class Comment(ProjectBaseMixin):
user = models.ForeignKey('account.User', related_name='comments', on_delete=models.CASCADE, verbose_name=_('User'))
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')

View File

@ -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')

View File

@ -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'

View File

@ -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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
"""Establishment models."""
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."""

View File

@ -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',
]

View File

@ -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)

View File

@ -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)

View File

@ -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'),
]

View File

@ -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'),
]

View File

@ -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

View File

@ -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):

View File

@ -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.')})

View File

@ -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'),
]

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

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

View File

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

View File

@ -28,31 +28,25 @@ def get_user_ip(request):
return ip
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(

View File

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

View File

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

View File

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

View File

@ -69,6 +69,8 @@ class SiteSettings(ProjectBaseMixin):
verbose_name=_('AD config'))
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()

View File

@ -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__'

View File

@ -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)

View File

@ -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'),
]

View File

@ -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'),
]

View File

@ -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)

View File

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

View File

@ -1,8 +1,11 @@
from django.contrib.contenttypes.models import ContentType
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import generics, permissions
from 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

View File

@ -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:

View File

@ -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):

View File

@ -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}')

View File

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

View File

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

View File

@ -8,7 +8,8 @@ from rest_framework.reverse import reverse
from rating.models import Rating, ViewCount
from 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})

View File

@ -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)

View File

@ -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)

View File

@ -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'),
]

View File

@ -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'),
]

View File

@ -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

View File

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

View File

@ -1,16 +1,90 @@
# Create your tests here.
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)

View File

@ -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
View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _
from django.core.validators import MaxValueValidator, MinValueValidator
from 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

View File

@ -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,

View File

@ -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):

View File

View File

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

View File

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

View File

@ -39,7 +39,6 @@ class Review(BaseAttributes, TranslatedFieldsMixin):
(TO_REVIEW, _('To review')),
(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()

View File

@ -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',
)

View File

@ -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 = (

View File

@ -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'

View File

@ -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',
]

View File

@ -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:

View File

@ -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.

View File

@ -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

View File

@ -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):

View File

@ -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',
)

View File

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

View File

@ -6,9 +6,11 @@ from search_indexes import views
router = routers.SimpleRouter()
# router.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

View File

@ -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',

View File

@ -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,
]

View File

@ -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

View File

@ -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)

View File

@ -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):

View File

@ -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):

View File

@ -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 = {

View File

@ -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:

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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)
]

View File

@ -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)

View File

@ -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()

View File

@ -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"}
)

View File

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

View File

@ -2,12 +2,13 @@ from collections import namedtuple
from django.conf import settings
from django.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)

View File

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

View File

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

14
make_data_migration.sh Executable file
View File

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

View File

@ -13,9 +13,9 @@ AWS_S3_OBJECT_PARAMETERS = {'CacheControl': 'max-age=86400'}
AWS_S3_ADDRESSING_STYLE = 'path'
# 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}/'

View File

@ -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

View File

@ -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