Fix
This commit is contained in:
commit
420c673afd
|
|
@ -55,3 +55,23 @@ class EstablishmentTypeTagFilter(filters.FilterSet):
|
||||||
fields = (
|
fields = (
|
||||||
'type_id',
|
'type_id',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EmployeeBackFilter(filters.FilterSet):
|
||||||
|
"""Employee filter set."""
|
||||||
|
|
||||||
|
search = filters.CharFilter(method='search_by_name_or_last_name')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta class."""
|
||||||
|
|
||||||
|
model = models.Employee
|
||||||
|
fields = (
|
||||||
|
'search',
|
||||||
|
)
|
||||||
|
|
||||||
|
def search_by_name_or_last_name(self, queryset, name, value):
|
||||||
|
"""Search by name or last name."""
|
||||||
|
if value not in EMPTY_VALUES:
|
||||||
|
return queryset.search_by_name_or_last_name(value)
|
||||||
|
return queryset
|
||||||
|
|
|
||||||
28
apps/establishment/migrations/0066_auto_20191122_1144.py
Normal file
28
apps/establishment/migrations/0066_auto_20191122_1144.py
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-11-22 11:44
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('establishment', '0065_establishment_purchased_products'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='employee',
|
||||||
|
name='last_name',
|
||||||
|
field=models.CharField(default=None, max_length=255, null=True, verbose_name='Last Name'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='establishmentemployee',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(choices=[('I', 'Idle'), ('A', 'Accepted'), ('D', 'Declined')], default='I', max_length=1),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='employee',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=255, verbose_name='Name'),
|
||||||
|
),
|
||||||
|
]
|
||||||
39
apps/establishment/migrations/0067_auto_20191122_1244.py
Normal file
39
apps/establishment/migrations/0067_auto_20191122_1244.py
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-11-22 12:44
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import phonenumber_field.modelfields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('establishment', '0066_auto_20191122_1144'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='employee',
|
||||||
|
name='birth_date',
|
||||||
|
field=models.DateTimeField(default=None, null=True, verbose_name='Birth date'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='employee',
|
||||||
|
name='email',
|
||||||
|
field=models.EmailField(blank=True, default=None, max_length=254, null=True, verbose_name='Email'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='employee',
|
||||||
|
name='phone',
|
||||||
|
field=phonenumber_field.modelfields.PhoneNumberField(default=None, max_length=128, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='employee',
|
||||||
|
name='sex',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[(0, 'Male'), (1, 'Female')], default=None, null=True, verbose_name='Sex'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='employee',
|
||||||
|
name='toque_number',
|
||||||
|
field=models.PositiveSmallIntegerField(default=None, null=True, verbose_name='Toque number'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
"""Establishment models."""
|
"""Establishment models."""
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
from typing import List
|
||||||
from operator import or_
|
from operator import or_
|
||||||
|
|
||||||
import elasticsearch_dsl
|
import elasticsearch_dsl
|
||||||
|
|
@ -118,11 +119,13 @@ class EstablishmentQuerySet(models.QuerySet):
|
||||||
'address__city__country')
|
'address__city__country')
|
||||||
|
|
||||||
def with_extended_related(self):
|
def with_extended_related(self):
|
||||||
return self.select_related('establishment_type'). \
|
return self.with_extended_address_related().select_related('establishment_type'). \
|
||||||
prefetch_related('establishment_subtypes', 'awards', 'schedule',
|
prefetch_related('establishment_subtypes', 'awards', 'schedule',
|
||||||
'phones'). \
|
'phones', 'gallery', 'menu_set', 'menu_set__plate_set',
|
||||||
|
'menu_set__plate_set__currency', 'currency'). \
|
||||||
prefetch_actual_employees()
|
prefetch_actual_employees()
|
||||||
|
|
||||||
|
|
||||||
def with_type_related(self):
|
def with_type_related(self):
|
||||||
return self.prefetch_related('establishment_subtypes')
|
return self.prefetch_related('establishment_subtypes')
|
||||||
|
|
||||||
|
|
@ -395,6 +398,7 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin,
|
||||||
verbose_name=_('Tag'))
|
verbose_name=_('Tag'))
|
||||||
reviews = generic.GenericRelation(to='review.Review')
|
reviews = generic.GenericRelation(to='review.Review')
|
||||||
comments = generic.GenericRelation(to='comment.Comment')
|
comments = generic.GenericRelation(to='comment.Comment')
|
||||||
|
carousels = generic.GenericRelation(to='main.Carousel')
|
||||||
favorites = generic.GenericRelation(to='favorites.Favorites')
|
favorites = generic.GenericRelation(to='favorites.Favorites')
|
||||||
currency = models.ForeignKey(Currency, blank=True, null=True, default=None,
|
currency = models.ForeignKey(Currency, blank=True, null=True, default=None,
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
|
|
@ -434,11 +438,12 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin,
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def visible_tags(self):
|
def visible_tags(self):
|
||||||
return super().visible_tags\
|
return super().visible_tags \
|
||||||
.exclude(category__index_name__in=['guide', 'collection', 'purchased_item',
|
.exclude(category__index_name__in=['guide', 'collection', 'purchased_item',
|
||||||
'business_tag', 'business_tags_de'])\
|
'business_tag', 'business_tags_de']) \
|
||||||
|
\
|
||||||
|
# todo: recalculate toque_number
|
||||||
|
|
||||||
# todo: recalculate toque_number
|
|
||||||
def recalculate_toque_number(self):
|
def recalculate_toque_number(self):
|
||||||
toque_number = 0
|
toque_number = 0
|
||||||
if self.address and self.public_mark:
|
if self.address and self.public_mark:
|
||||||
|
|
@ -611,7 +616,6 @@ class EstablishmentNote(ProjectBaseMixin):
|
||||||
|
|
||||||
|
|
||||||
class EstablishmentGallery(IntermediateGalleryModelMixin):
|
class EstablishmentGallery(IntermediateGalleryModelMixin):
|
||||||
|
|
||||||
establishment = models.ForeignKey(Establishment, null=True,
|
establishment = models.ForeignKey(Establishment, null=True,
|
||||||
related_name='establishment_gallery',
|
related_name='establishment_gallery',
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
|
|
@ -662,6 +666,16 @@ class EstablishmentEmployeeQuerySet(models.QuerySet):
|
||||||
class EstablishmentEmployee(BaseAttributes):
|
class EstablishmentEmployee(BaseAttributes):
|
||||||
"""EstablishmentEmployee model."""
|
"""EstablishmentEmployee model."""
|
||||||
|
|
||||||
|
IDLE = 'I'
|
||||||
|
ACCEPTED = 'A'
|
||||||
|
DECLINED = 'D'
|
||||||
|
|
||||||
|
STATUS_CHOICES = (
|
||||||
|
(IDLE, 'Idle'),
|
||||||
|
(ACCEPTED, 'Accepted'),
|
||||||
|
(DECLINED, 'Declined'),
|
||||||
|
)
|
||||||
|
|
||||||
establishment = models.ForeignKey(Establishment, on_delete=models.PROTECT,
|
establishment = models.ForeignKey(Establishment, on_delete=models.PROTECT,
|
||||||
verbose_name=_('Establishment'))
|
verbose_name=_('Establishment'))
|
||||||
employee = models.ForeignKey('establishment.Employee', on_delete=models.PROTECT,
|
employee = models.ForeignKey('establishment.Employee', on_delete=models.PROTECT,
|
||||||
|
|
@ -672,19 +686,53 @@ class EstablishmentEmployee(BaseAttributes):
|
||||||
verbose_name=_('To date'))
|
verbose_name=_('To date'))
|
||||||
position = models.ForeignKey(Position, on_delete=models.PROTECT,
|
position = models.ForeignKey(Position, on_delete=models.PROTECT,
|
||||||
verbose_name=_('Position'))
|
verbose_name=_('Position'))
|
||||||
|
|
||||||
|
status = models.CharField(max_length=1, choices=STATUS_CHOICES, default=IDLE)
|
||||||
|
|
||||||
# old_id = affiliations_id
|
# old_id = affiliations_id
|
||||||
old_id = models.IntegerField(verbose_name=_('Old id'), null=True, blank=True)
|
old_id = models.IntegerField(verbose_name=_('Old id'), null=True, blank=True)
|
||||||
|
|
||||||
objects = EstablishmentEmployeeQuerySet.as_manager()
|
objects = EstablishmentEmployeeQuerySet.as_manager()
|
||||||
|
|
||||||
|
|
||||||
|
class EmployeeQuerySet(models.QuerySet):
|
||||||
|
|
||||||
|
def _generic_search(self, value, filter_fields_names: List[str]):
|
||||||
|
"""Generic method for searching value in specified fields"""
|
||||||
|
filters = [
|
||||||
|
{f'{field}__icontains': value}
|
||||||
|
for field in filter_fields_names
|
||||||
|
]
|
||||||
|
return self.filter(reduce(lambda x, y: x | y, [models.Q(**i) for i in filters]))
|
||||||
|
|
||||||
|
def search_by_name_or_last_name(self, value):
|
||||||
|
"""Search by name or last_name."""
|
||||||
|
return self._generic_search(value, ['name', 'last_name'])
|
||||||
|
|
||||||
|
|
||||||
class Employee(BaseAttributes):
|
class Employee(BaseAttributes):
|
||||||
"""Employee model."""
|
"""Employee model."""
|
||||||
|
|
||||||
user = models.OneToOneField('account.User', on_delete=models.PROTECT,
|
user = models.OneToOneField('account.User', on_delete=models.PROTECT,
|
||||||
null=True, blank=True, default=None,
|
null=True, blank=True, default=None,
|
||||||
verbose_name=_('User'))
|
verbose_name=_('User'))
|
||||||
name = models.CharField(max_length=255, verbose_name=_('Last name'))
|
name = models.CharField(max_length=255, verbose_name=_('Name'))
|
||||||
|
last_name = models.CharField(max_length=255, verbose_name=_('Last Name'), null=True, default=None)
|
||||||
|
|
||||||
|
# SEX CHOICES
|
||||||
|
MALE = 0
|
||||||
|
FEMALE = 1
|
||||||
|
|
||||||
|
SEX_CHOICES = (
|
||||||
|
(MALE, _('Male')),
|
||||||
|
(FEMALE, _('Female'))
|
||||||
|
)
|
||||||
|
sex = models.PositiveSmallIntegerField(choices=SEX_CHOICES, verbose_name=_('Sex'), null=True, default=None)
|
||||||
|
birth_date = models.DateTimeField(editable=True, verbose_name=_('Birth date'), null=True, default=None)
|
||||||
|
email = models.EmailField(blank=True, null=True, default=None, verbose_name=_('Email'))
|
||||||
|
phone = PhoneNumberField(null=True, default=None)
|
||||||
|
toque_number = models.PositiveSmallIntegerField(verbose_name=_('Toque number'), null=True, default=None)
|
||||||
|
|
||||||
establishments = models.ManyToManyField(Establishment, related_name='employees',
|
establishments = models.ManyToManyField(Establishment, related_name='employees',
|
||||||
through=EstablishmentEmployee, )
|
through=EstablishmentEmployee, )
|
||||||
awards = generic.GenericRelation(to='main.Award', related_query_name='employees')
|
awards = generic.GenericRelation(to='main.Award', related_query_name='employees')
|
||||||
|
|
@ -693,6 +741,8 @@ class Employee(BaseAttributes):
|
||||||
# old_id = profile_id
|
# old_id = profile_id
|
||||||
old_id = models.IntegerField(verbose_name=_('Old id'), null=True, blank=True)
|
old_id = models.IntegerField(verbose_name=_('Old id'), null=True, blank=True)
|
||||||
|
|
||||||
|
objects = EmployeeQuerySet.as_manager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,9 @@ from rest_framework import serializers
|
||||||
|
|
||||||
from establishment import models
|
from establishment import models
|
||||||
from establishment import serializers as model_serializers
|
from establishment import serializers as model_serializers
|
||||||
from location.serializers import AddressDetailSerializer
|
from location.serializers import AddressDetailSerializer, TranslatedField
|
||||||
from main.models import Currency
|
from main.models import Currency
|
||||||
|
from main.serializers import AwardSerializer
|
||||||
from utils.decorators import with_base_attributes
|
from utils.decorators import with_base_attributes
|
||||||
from utils.serializers import TimeZoneChoiceField
|
from utils.serializers import TimeZoneChoiceField
|
||||||
from gallery.models import Image
|
from gallery.models import Image
|
||||||
|
|
@ -161,12 +162,54 @@ class ContactEmailBackSerializers(model_serializers.PlateSerializer):
|
||||||
class EmployeeBackSerializers(serializers.ModelSerializer):
|
class EmployeeBackSerializers(serializers.ModelSerializer):
|
||||||
"""Employee serializers."""
|
"""Employee serializers."""
|
||||||
|
|
||||||
|
awards = AwardSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Employee
|
model = models.Employee
|
||||||
fields = [
|
fields = [
|
||||||
'id',
|
'id',
|
||||||
'user',
|
'user',
|
||||||
'name'
|
'name',
|
||||||
|
'last_name',
|
||||||
|
'sex',
|
||||||
|
'birth_date',
|
||||||
|
'email',
|
||||||
|
'phone',
|
||||||
|
'toque_number',
|
||||||
|
'awards',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class PositionBackSerializer(serializers.ModelSerializer):
|
||||||
|
"""Position Back serializer."""
|
||||||
|
|
||||||
|
name_translated = TranslatedField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Position
|
||||||
|
fields = [
|
||||||
|
'id',
|
||||||
|
'name_translated',
|
||||||
|
'priority',
|
||||||
|
'index_name',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentEmployeeBackSerializer(serializers.ModelSerializer):
|
||||||
|
"""Establishment Employee serializer."""
|
||||||
|
|
||||||
|
employee = EmployeeBackSerializers()
|
||||||
|
position = PositionBackSerializer()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.EstablishmentEmployee
|
||||||
|
fields = [
|
||||||
|
'id',
|
||||||
|
'employee',
|
||||||
|
'from_date',
|
||||||
|
'to_date',
|
||||||
|
'position',
|
||||||
|
'status',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ from review.serializers import ReviewShortSerializer
|
||||||
from tag.serializers import TagBaseSerializer
|
from tag.serializers import TagBaseSerializer
|
||||||
from timetable.serialziers import ScheduleRUDSerializer
|
from timetable.serialziers import ScheduleRUDSerializer
|
||||||
from utils import exceptions as utils_exceptions
|
from utils import exceptions as utils_exceptions
|
||||||
from utils.serializers import ImageBaseSerializer
|
from utils.serializers import ImageBaseSerializer, CarouselCreateSerializer
|
||||||
from utils.serializers import (ProjectModelSerializer, TranslatedField,
|
from utils.serializers import (ProjectModelSerializer, TranslatedField,
|
||||||
FavoritesCreateSerializer)
|
FavoritesCreateSerializer)
|
||||||
|
|
||||||
|
|
@ -168,12 +168,51 @@ class EstablishmentEmployeeSerializer(serializers.ModelSerializer):
|
||||||
awards = AwardSerializer(source='employee.awards', many=True)
|
awards = AwardSerializer(source='employee.awards', many=True)
|
||||||
priority = serializers.IntegerField(source='position.priority')
|
priority = serializers.IntegerField(source='position.priority')
|
||||||
position_index_name = serializers.CharField(source='position.index_name')
|
position_index_name = serializers.CharField(source='position.index_name')
|
||||||
|
status = serializers.CharField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
||||||
model = models.Employee
|
model = models.Employee
|
||||||
fields = ('id', 'name', 'position_translated', 'awards', 'priority', 'position_index_name')
|
fields = ('id', 'name', 'position_translated', 'awards', 'priority', 'position_index_name', 'status')
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentEmployeeCreateSerializer(serializers.ModelSerializer):
|
||||||
|
"""Serializer for establishment employee relation."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta class."""
|
||||||
|
|
||||||
|
model = models.EstablishmentEmployee
|
||||||
|
fields = ('id',)
|
||||||
|
|
||||||
|
def _validate_entity(self, entity_id_param: str, entity_class):
|
||||||
|
entity_id = self.context.get('request').parser_context.get('kwargs').get(entity_id_param)
|
||||||
|
entity_qs = entity_class.objects.filter(id=entity_id)
|
||||||
|
if not entity_qs.exists():
|
||||||
|
raise serializers.ValidationError({'detail': _(f'{entity_class.__name__} not found.')})
|
||||||
|
return entity_qs.first()
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
"""Override validate method"""
|
||||||
|
establishment = self._validate_entity("establishment_id", models.Establishment)
|
||||||
|
employee = self._validate_entity("employee_id", models.Employee)
|
||||||
|
position = self._validate_entity("position_id", models.Position)
|
||||||
|
|
||||||
|
attrs['establishment'] = establishment
|
||||||
|
attrs['employee'] = employee
|
||||||
|
attrs['position'] = position
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
def create(self, validated_data, *args, **kwargs):
|
||||||
|
"""Override create method"""
|
||||||
|
validated_data.update({
|
||||||
|
'employee': validated_data.pop('employee'),
|
||||||
|
'establishment': validated_data.pop('establishment'),
|
||||||
|
'position': validated_data.pop("position")
|
||||||
|
})
|
||||||
|
return super().create(validated_data)
|
||||||
|
|
||||||
|
|
||||||
class EstablishmentShortSerializer(serializers.ModelSerializer):
|
class EstablishmentShortSerializer(serializers.ModelSerializer):
|
||||||
|
|
@ -396,6 +435,22 @@ class EstablishmentCommentCreateSerializer(comment_serializers.CommentSerializer
|
||||||
return super().create(validated_data)
|
return super().create(validated_data)
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentCommentRUDSerializer(comment_serializers.CommentSerializer):
|
||||||
|
"""Retrieve/Update/Destroy comment serializer."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta class."""
|
||||||
|
model = comment_models.Comment
|
||||||
|
fields = [
|
||||||
|
'id',
|
||||||
|
'created',
|
||||||
|
'text',
|
||||||
|
'mark',
|
||||||
|
'nickname',
|
||||||
|
'profile_pic',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class EstablishmentFavoritesCreateSerializer(FavoritesCreateSerializer):
|
class EstablishmentFavoritesCreateSerializer(FavoritesCreateSerializer):
|
||||||
"""Serializer to favorite object w/ model Establishment."""
|
"""Serializer to favorite object w/ model Establishment."""
|
||||||
|
|
||||||
|
|
@ -426,6 +481,27 @@ class EstablishmentFavoritesCreateSerializer(FavoritesCreateSerializer):
|
||||||
return super().create(validated_data)
|
return super().create(validated_data)
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentCarouselCreateSerializer(CarouselCreateSerializer):
|
||||||
|
"""Serializer to carousel object w/ model News."""
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
establishment = models.Establishment.objects.filter(pk=self.pk).first()
|
||||||
|
if not establishment:
|
||||||
|
raise serializers.ValidationError({'detail': _('Object not found.')})
|
||||||
|
|
||||||
|
if establishment.carousels.exists():
|
||||||
|
raise utils_exceptions.CarouselError()
|
||||||
|
|
||||||
|
attrs['establishment'] = establishment
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
def create(self, validated_data, *args, **kwargs):
|
||||||
|
validated_data.update({
|
||||||
|
'content_object': validated_data.pop('establishment')
|
||||||
|
})
|
||||||
|
return super().create(validated_data)
|
||||||
|
|
||||||
|
|
||||||
class CompanyBaseSerializer(serializers.ModelSerializer):
|
class CompanyBaseSerializer(serializers.ModelSerializer):
|
||||||
"""Company base serializer"""
|
"""Company base serializer"""
|
||||||
phone_list = serializers.SerializerMethodField(source='phones', read_only=True)
|
phone_list = serializers.SerializerMethodField(source='phones', read_only=True)
|
||||||
|
|
|
||||||
|
|
@ -475,3 +475,17 @@ class EstablishmentWebFavoriteTests(ChildTestCase):
|
||||||
f'/api/web/establishments/slug/{self.establishment.slug}/favorites/',
|
f'/api/web/establishments/slug/{self.establishment.slug}/favorites/',
|
||||||
format='json')
|
format='json')
|
||||||
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentCarouselTests(ChildTestCase):
|
||||||
|
|
||||||
|
def test_back_carousel_CR(self):
|
||||||
|
data = {
|
||||||
|
"object_id": self.establishment.id
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.client.post(f'/api/back/establishments/{self.establishment.id}/carousels/', data=data)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
response = self.client.delete(f'/api/back/establishments/{self.establishment.id}/carousels/')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ app_name = 'establishment'
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', views.EstablishmentListCreateView.as_view(), name='list'),
|
path('', views.EstablishmentListCreateView.as_view(), name='list'),
|
||||||
path('<int:pk>/', views.EstablishmentRUDView.as_view(), name='detail'),
|
path('<int:pk>/', views.EstablishmentRUDView.as_view(), name='detail'),
|
||||||
|
path('<int:pk>/carousels/', views.EstablishmentCarouselCreateDestroyView.as_view(),
|
||||||
|
name='create-destroy-carousels'),
|
||||||
path('<int:pk>/schedule/<int:schedule_id>/', views.EstablishmentScheduleRUDView.as_view(),
|
path('<int:pk>/schedule/<int:schedule_id>/', views.EstablishmentScheduleRUDView.as_view(),
|
||||||
name='schedule-rud'),
|
name='schedule-rud'),
|
||||||
path('<int:pk>/schedule/', views.EstablishmentScheduleCreateView.as_view(),
|
path('<int:pk>/schedule/', views.EstablishmentScheduleCreateView.as_view(),
|
||||||
|
|
@ -38,10 +40,19 @@ urlpatterns = [
|
||||||
path('phones/<int:pk>/', views.PhonesRUDView.as_view(), name='phones-rud'),
|
path('phones/<int:pk>/', views.PhonesRUDView.as_view(), name='phones-rud'),
|
||||||
path('emails/', views.EmailListCreateView.as_view(), name='emails'),
|
path('emails/', views.EmailListCreateView.as_view(), name='emails'),
|
||||||
path('emails/<int:pk>/', views.EmailRUDView.as_view(), name='emails-rud'),
|
path('emails/<int:pk>/', views.EmailRUDView.as_view(), name='emails-rud'),
|
||||||
|
path('<int:establishment_id>/employees/', views.EstablishmentEmployeeListView.as_view(),
|
||||||
|
name='establishment-employees'),
|
||||||
path('employees/', views.EmployeeListCreateView.as_view(), name='employees'),
|
path('employees/', views.EmployeeListCreateView.as_view(), name='employees'),
|
||||||
path('employees/<int:pk>/', views.EmployeeRUDView.as_view(), name='employees-rud'),
|
path('employees/<int:pk>/', views.EmployeeRUDView.as_view(), name='employees-rud'),
|
||||||
|
path('<int:establishment_id>/employee/<int:employee_id>/position/<int:position_id>',
|
||||||
|
views.EstablishmentEmployeeCreateView.as_view(),
|
||||||
|
name='employees-establishment-create'),
|
||||||
|
path('<int:establishment_id>/employee/<int:employee_id>',
|
||||||
|
views.EstablishmentEmployeeDeleteView.as_view(),
|
||||||
|
name='employees-establishment-delete'),
|
||||||
path('types/', views.EstablishmentTypeListCreateView.as_view(), name='type-list'),
|
path('types/', views.EstablishmentTypeListCreateView.as_view(), name='type-list'),
|
||||||
path('types/<int:pk>/', views.EstablishmentTypeRUDView.as_view(), name='type-rud'),
|
path('types/<int:pk>/', views.EstablishmentTypeRUDView.as_view(), name='type-rud'),
|
||||||
path('subtypes/', views.EstablishmentSubtypeListCreateView.as_view(), name='subtype-list'),
|
path('subtypes/', views.EstablishmentSubtypeListCreateView.as_view(), name='subtype-list'),
|
||||||
path('subtypes/<int:pk>/', views.EstablishmentSubtypeRUDView.as_view(), name='subtype-rud'),
|
path('subtypes/<int:pk>/', views.EstablishmentSubtypeRUDView.as_view(), name='subtype-rud'),
|
||||||
|
path('positions/', views.EstablishmentPositionListView.as_view(), name='position-list'),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,5 @@ urlpatterns = [
|
||||||
path('slug/<slug:slug>/comments/<int:comment_id>/', views.EstablishmentCommentRUDView.as_view(),
|
path('slug/<slug:slug>/comments/<int:comment_id>/', views.EstablishmentCommentRUDView.as_view(),
|
||||||
name='rud-comment'),
|
name='rud-comment'),
|
||||||
path('slug/<slug:slug>/favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(),
|
path('slug/<slug:slug>/favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(),
|
||||||
name='create-destroy-favorites')
|
name='create-destroy-favorites'),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
"""Establishment app views."""
|
"""Establishment app views."""
|
||||||
|
from django.http import Http404, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from rest_framework import generics, permissions
|
from rest_framework import generics, permissions, status
|
||||||
|
|
||||||
|
from utils.permissions import IsCountryAdmin, IsEstablishmentManager
|
||||||
from establishment import filters, models, serializers
|
from establishment import filters, models, serializers
|
||||||
from timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer
|
from timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer
|
||||||
from utils.permissions import IsCountryAdmin, IsEstablishmentManager
|
from utils.permissions import IsCountryAdmin, IsEstablishmentManager
|
||||||
|
|
@ -43,8 +45,8 @@ class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView):
|
||||||
"""
|
"""
|
||||||
Returns the object the view is displaying.
|
Returns the object the view is displaying.
|
||||||
"""
|
"""
|
||||||
establishment_pk = self.kwargs.get('pk')
|
establishment_pk = self.kwargs['pk']
|
||||||
schedule_id = self.kwargs.get('schedule_id')
|
schedule_id = self.kwargs['schedule_id']
|
||||||
|
|
||||||
establishment = get_object_or_404(klass=models.Establishment.objects.all(),
|
establishment = get_object_or_404(klass=models.Establishment.objects.all(),
|
||||||
pk=establishment_pk)
|
pk=establishment_pk)
|
||||||
|
|
@ -156,11 +158,23 @@ class EmailRUDView(generics.RetrieveUpdateDestroyAPIView):
|
||||||
|
|
||||||
class EmployeeListCreateView(generics.ListCreateAPIView):
|
class EmployeeListCreateView(generics.ListCreateAPIView):
|
||||||
"""Emplyoee list create view."""
|
"""Emplyoee list create view."""
|
||||||
|
permission_classes = (permissions.AllowAny, )
|
||||||
|
filter_class = filters.EmployeeBackFilter
|
||||||
serializer_class = serializers.EmployeeBackSerializers
|
serializer_class = serializers.EmployeeBackSerializers
|
||||||
queryset = models.Employee.objects.all()
|
queryset = models.Employee.objects.all()
|
||||||
pagination_class = None
|
pagination_class = None
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentEmployeeListView(generics.ListAPIView):
|
||||||
|
"""Establishment emplyoees list view."""
|
||||||
|
permission_classes = (permissions.AllowAny, )
|
||||||
|
serializer_class = serializers.EstablishmentEmployeeBackSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
establishment_id = self.kwargs['establishment_id']
|
||||||
|
return models.EstablishmentEmployee.objects.filter(establishment__id=establishment_id)
|
||||||
|
|
||||||
|
|
||||||
class EmployeeRUDView(generics.RetrieveUpdateDestroyAPIView):
|
class EmployeeRUDView(generics.RetrieveUpdateDestroyAPIView):
|
||||||
"""Employee RUD view."""
|
"""Employee RUD view."""
|
||||||
serializer_class = serializers.EmployeeBackSerializers
|
serializer_class = serializers.EmployeeBackSerializers
|
||||||
|
|
@ -318,3 +332,36 @@ class EstablishmentNoteRUDView(EstablishmentMixinViews,
|
||||||
self.check_object_permissions(self.request, note)
|
self.check_object_permissions(self.request, note)
|
||||||
|
|
||||||
return note
|
return note
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentEmployeeCreateView(generics.CreateAPIView):
|
||||||
|
serializer_class = serializers.EstablishmentEmployeeCreateSerializer
|
||||||
|
queryset = models.EstablishmentEmployee.objects.all()
|
||||||
|
# TODO send email to all admins and add endpoint for changing status
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentEmployeeDeleteView(generics.DestroyAPIView):
|
||||||
|
|
||||||
|
def _get_object_to_delete(self, establishment_id, employee_id):
|
||||||
|
result_qs = models.EstablishmentEmployee\
|
||||||
|
.objects\
|
||||||
|
.filter(establishment_id=establishment_id, employee_id=employee_id)
|
||||||
|
if not result_qs.exists():
|
||||||
|
raise Http404
|
||||||
|
return result_qs.first()
|
||||||
|
|
||||||
|
def delete(self, request, *args, **kwargs):
|
||||||
|
establishment_id = self.kwargs["establishment_id"]
|
||||||
|
employee_id = self.kwargs["employee_id"]
|
||||||
|
object_to_delete = self._get_object_to_delete(establishment_id, employee_id)
|
||||||
|
object_to_delete.delete()
|
||||||
|
return HttpResponse(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentPositionListView(generics.ListAPIView):
|
||||||
|
"""Establishment positions list view."""
|
||||||
|
|
||||||
|
pagination_class = None
|
||||||
|
permission_classes = (permissions.AllowAny, )
|
||||||
|
queryset = models.Position.objects.all()
|
||||||
|
serializer_class = serializers.PositionBackSerializer
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ from comment.serializers import CommentRUDSerializer
|
||||||
from establishment import filters, models, serializers
|
from establishment import filters, models, serializers
|
||||||
from main import methods
|
from main import methods
|
||||||
from utils.pagination import EstablishmentPortionPagination
|
from utils.pagination import EstablishmentPortionPagination
|
||||||
from utils.views import FavoritesCreateDestroyMixinView
|
from utils.views import FavoritesCreateDestroyMixinView, CarouselCreateDestroyMixinView
|
||||||
|
|
||||||
|
|
||||||
class EstablishmentMixinView:
|
class EstablishmentMixinView:
|
||||||
|
|
@ -34,7 +34,7 @@ class EstablishmentListView(EstablishmentMixinView, generics.ListAPIView):
|
||||||
serializer_class = serializers.EstablishmentListRetrieveSerializer
|
serializer_class = serializers.EstablishmentListRetrieveSerializer
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return super().get_queryset().with_schedule()\
|
return super().get_queryset().with_schedule() \
|
||||||
.with_extended_address_related().with_currency_related()
|
.with_extended_address_related().with_currency_related()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -56,12 +56,11 @@ class EstablishmentRecentReviewListView(EstablishmentListView):
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Overridden method 'get_queryset'."""
|
"""Overridden method 'get_queryset'."""
|
||||||
qs = super().get_queryset()
|
qs = super().get_queryset()
|
||||||
user_ip = methods.get_user_ip(self.request)
|
|
||||||
query_params = self.request.query_params
|
query_params = self.request.query_params
|
||||||
if 'longitude' in query_params and 'latitude' in query_params:
|
if 'longitude' in query_params and 'latitude' in query_params:
|
||||||
longitude, latitude = query_params.get('longitude'), query_params.get('latitude')
|
longitude, latitude = query_params.get('longitude'), query_params.get('latitude')
|
||||||
else:
|
else:
|
||||||
longitude, latitude = methods.determine_coordinates(user_ip)
|
longitude, latitude = methods.determine_coordinates(self.request)
|
||||||
if not longitude or not latitude:
|
if not longitude or not latitude:
|
||||||
return qs.none()
|
return qs.none()
|
||||||
point = Point(x=float(longitude), y=float(latitude), srid=settings.GEO_DEFAULT_SRID)
|
point = Point(x=float(longitude), y=float(latitude), srid=settings.GEO_DEFAULT_SRID)
|
||||||
|
|
@ -106,9 +105,9 @@ class EstablishmentCommentListView(generics.ListAPIView):
|
||||||
|
|
||||||
establishment = get_object_or_404(models.Establishment, slug=self.kwargs['slug'])
|
establishment = get_object_or_404(models.Establishment, slug=self.kwargs['slug'])
|
||||||
return comment_models.Comment.objects.by_content_type(app_label='establishment',
|
return comment_models.Comment.objects.by_content_type(app_label='establishment',
|
||||||
model='establishment')\
|
model='establishment') \
|
||||||
.by_object_id(object_id=establishment.pk)\
|
.by_object_id(object_id=establishment.pk) \
|
||||||
.order_by('-created')
|
.order_by('-created')
|
||||||
|
|
||||||
|
|
||||||
class EstablishmentCommentRUDView(generics.RetrieveUpdateDestroyAPIView):
|
class EstablishmentCommentRUDView(generics.RetrieveUpdateDestroyAPIView):
|
||||||
|
|
@ -140,6 +139,13 @@ class EstablishmentFavoritesCreateDestroyView(FavoritesCreateDestroyMixinView):
|
||||||
serializer_class = serializers.EstablishmentFavoritesCreateSerializer
|
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):
|
class EstablishmentNearestRetrieveView(EstablishmentListView, generics.ListAPIView):
|
||||||
"""Resource for getting list of nearest establishments."""
|
"""Resource for getting list of nearest establishments."""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,9 @@
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from sorl.thumbnail.parsers import parse_crop
|
||||||
|
from sorl.thumbnail.parsers import ThumbnailParseError
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
@ -29,3 +34,86 @@ class ImageSerializer(serializers.ModelSerializer):
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'orientation': {'write_only': True}
|
'orientation': {'write_only': True}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CropImageSerializer(ImageSerializer):
|
||||||
|
"""Serializers for image crops."""
|
||||||
|
|
||||||
|
width = serializers.IntegerField(write_only=True, required=False)
|
||||||
|
height = serializers.IntegerField(write_only=True, required=False)
|
||||||
|
crop = serializers.CharField(write_only=True,
|
||||||
|
required=False,
|
||||||
|
default='center')
|
||||||
|
quality = serializers.IntegerField(write_only=True, required=False,
|
||||||
|
default=settings.THUMBNAIL_QUALITY,
|
||||||
|
validators=[
|
||||||
|
MinValueValidator(1),
|
||||||
|
MaxValueValidator(100)])
|
||||||
|
cropped_image = serializers.DictField(read_only=True, allow_null=True)
|
||||||
|
|
||||||
|
class Meta(ImageSerializer.Meta):
|
||||||
|
"""Meta class."""
|
||||||
|
fields = [
|
||||||
|
'id',
|
||||||
|
'url',
|
||||||
|
'orientation_display',
|
||||||
|
'width',
|
||||||
|
'height',
|
||||||
|
'crop',
|
||||||
|
'quality',
|
||||||
|
'cropped_image',
|
||||||
|
]
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
"""Overridden validate method."""
|
||||||
|
file = self._image.image
|
||||||
|
crop_width = attrs.get('width')
|
||||||
|
crop_height = attrs.get('height')
|
||||||
|
crop = attrs.get('crop')
|
||||||
|
|
||||||
|
if (crop_height and crop_width) and (crop and crop != 'smart'):
|
||||||
|
xy_image = (file.width, file.width)
|
||||||
|
xy_window = (crop_width, crop_height)
|
||||||
|
try:
|
||||||
|
parse_crop(crop, xy_image, xy_window)
|
||||||
|
attrs['image'] = file
|
||||||
|
except ThumbnailParseError:
|
||||||
|
raise serializers.ValidationError({'margin': _('Unrecognized crop option: %s') % crop})
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
"""Overridden create method."""
|
||||||
|
width = validated_data.pop('width', None)
|
||||||
|
height = validated_data.pop('height', None)
|
||||||
|
quality = validated_data.pop('quality')
|
||||||
|
crop = validated_data.pop('crop')
|
||||||
|
|
||||||
|
image = self._image
|
||||||
|
|
||||||
|
if image and width and height:
|
||||||
|
setattr(image,
|
||||||
|
'cropped_image',
|
||||||
|
image.get_cropped_image(
|
||||||
|
geometry=f'{width}x{height}',
|
||||||
|
quality=quality,
|
||||||
|
crop=crop))
|
||||||
|
return image
|
||||||
|
|
||||||
|
@property
|
||||||
|
def view(self):
|
||||||
|
return self.context.get('view')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lookup_field(self):
|
||||||
|
lookup_field = 'pk'
|
||||||
|
|
||||||
|
if lookup_field in self.view.kwargs:
|
||||||
|
return self.view.kwargs.get(lookup_field)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _image(self):
|
||||||
|
"""Return image from url_kwargs."""
|
||||||
|
qs = models.Image.objects.filter(id=self.lookup_field)
|
||||||
|
if qs.exists():
|
||||||
|
return qs.first()
|
||||||
|
raise serializers.ValidationError({'detail': _('Image not found.')})
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ from . import views
|
||||||
app_name = 'gallery'
|
app_name = 'gallery'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', views.ImageListCreateView.as_view(), name='list-create-image'),
|
path('', views.ImageListCreateView.as_view(), name='list-create'),
|
||||||
path('<int:pk>/', views.ImageRetrieveDestroyView.as_view(), name='retrieve-destroy-image'),
|
path('<int:pk>/', views.ImageRetrieveDestroyView.as_view(), name='retrieve-destroy'),
|
||||||
|
path('<int:pk>/crop/', views.CropImageCreateView.as_view(), name='create-crop'),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -28,3 +28,8 @@ class ImageRetrieveDestroyView(ImageBaseView, generics.RetrieveDestroyAPIView):
|
||||||
else:
|
else:
|
||||||
on_commit(lambda: tasks.delete_image(image_id=instance.id))
|
on_commit(lambda: tasks.delete_image(image_id=instance.id))
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
class CropImageCreateView(ImageBaseView, generics.CreateAPIView):
|
||||||
|
"""Create crop image."""
|
||||||
|
serializer_class = serializers.CropImageSerializer
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,15 @@ from django.contrib import admin
|
||||||
from main import models
|
from main import models
|
||||||
|
|
||||||
|
|
||||||
|
class SiteSettingsInline(admin.TabularInline):
|
||||||
|
model = models.SiteFeature
|
||||||
|
extra = 1
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.SiteSettings)
|
@admin.register(models.SiteSettings)
|
||||||
class SiteSettingsAdmin(admin.ModelAdmin):
|
class SiteSettingsAdmin(admin.ModelAdmin):
|
||||||
"""Site settings admin conf."""
|
"""Site settings admin conf."""
|
||||||
|
inlines = [SiteSettingsInline,]
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.Feature)
|
@admin.register(models.Feature)
|
||||||
|
|
|
||||||
|
|
@ -28,31 +28,25 @@ def get_user_ip(request):
|
||||||
return ip
|
return ip
|
||||||
|
|
||||||
|
|
||||||
def determine_country_code(ip_addr):
|
def determine_country_code(request):
|
||||||
"""Determine country code."""
|
"""Determine country code."""
|
||||||
country_code = None
|
META = request.META
|
||||||
if ip_addr:
|
country_code = META.get('X-GeoIP-Country-Code',
|
||||||
try:
|
META.get('HTTP_X_GEOIP_COUNTRY_CODE'))
|
||||||
geoip = GeoIP2()
|
if isinstance(country_code, str):
|
||||||
country_code = geoip.country_code(ip_addr)
|
return country_code.lower()
|
||||||
country_code = country_code.lower()
|
|
||||||
except GeoIP2Exception as ex:
|
|
||||||
logger.info(f'GEOIP Exception: {ex}. ip: {ip_addr}')
|
|
||||||
except Exception as ex:
|
|
||||||
logger.error(f'GEOIP Base exception: {ex}')
|
|
||||||
return country_code
|
|
||||||
|
|
||||||
|
|
||||||
def determine_coordinates(ip_addr: str) -> Tuple[Optional[float], Optional[float]]:
|
def determine_coordinates(request):
|
||||||
if ip_addr:
|
META = request.META
|
||||||
try:
|
longitude = META.get('X-GeoIP-Longitude',
|
||||||
geoip = GeoIP2()
|
META.get('HTTP_X_GEOIP_LONGITUDE'))
|
||||||
return geoip.coords(ip_addr)
|
latitude = META.get('X-GeoIP-Latitude',
|
||||||
except GeoIP2Exception as ex:
|
META.get('HTTP_X_GEOIP_LATITUDE'))
|
||||||
logger.warning(f'GEOIP Exception: {ex}. ip: {ip_addr}')
|
try:
|
||||||
except Exception as ex:
|
return float(longitude), float(latitude)
|
||||||
logger.warning(f'GEOIP Base exception: {ex}')
|
except (TypeError, ValueError):
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
def determine_user_site_url(country_code):
|
def determine_user_site_url(country_code):
|
||||||
|
|
@ -76,15 +70,11 @@ def determine_user_site_url(country_code):
|
||||||
return site.site_url
|
return site.site_url
|
||||||
|
|
||||||
|
|
||||||
def determine_user_city(ip_addr: str) -> Optional[City]:
|
def determine_user_city(request):
|
||||||
try:
|
META = request.META
|
||||||
geoip = GeoIP2()
|
city = META.get('X-GeoIP-City',
|
||||||
return geoip.city(ip_addr)
|
META.get('HTTP_X_GEOIP_CITY'))
|
||||||
except GeoIP2Exception as ex:
|
return city
|
||||||
logger.warning(f'GEOIP Exception: {ex}. ip: {ip_addr}')
|
|
||||||
except Exception as ex:
|
|
||||||
logger.warning(f'GEOIP Base exception: {ex}')
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def determine_subdivision(
|
def determine_subdivision(
|
||||||
|
|
|
||||||
|
|
@ -86,9 +86,8 @@ class DetermineLocation(generics.GenericAPIView):
|
||||||
serializer_class = EmptySerializer
|
serializer_class = EmptySerializer
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
user_ip = methods.get_user_ip(request)
|
longitude, latitude = methods.determine_coordinates(request)
|
||||||
longitude, latitude = methods.determine_coordinates(user_ip)
|
city = methods.determine_user_city(request)
|
||||||
city = methods.determine_user_city(user_ip)
|
|
||||||
if longitude and latitude and city:
|
if longitude and latitude and city:
|
||||||
return Response(data={'latitude': latitude, 'longitude': longitude, 'city': city})
|
return Response(data={'latitude': latitude, 'longitude': longitude, 'city': city})
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,7 @@ class DetermineSiteView(generics.GenericAPIView):
|
||||||
serializer_class = EmptySerializer
|
serializer_class = EmptySerializer
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
user_ip = methods.get_user_ip(request)
|
country_code = methods.determine_country_code(request)
|
||||||
country_code = methods.determine_country_code(user_ip)
|
|
||||||
url = methods.determine_user_site_url(country_code)
|
url = methods.determine_user_site_url(country_code)
|
||||||
return Response(data={'url': url})
|
return Response(data={'url': url})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -196,6 +196,7 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
|
||||||
views_count = models.OneToOneField('rating.ViewCount', blank=True, null=True, on_delete=models.SET_NULL)
|
views_count = models.OneToOneField('rating.ViewCount', blank=True, null=True, on_delete=models.SET_NULL)
|
||||||
ratings = generic.GenericRelation(Rating)
|
ratings = generic.GenericRelation(Rating)
|
||||||
favorites = generic.GenericRelation(to='favorites.Favorites')
|
favorites = generic.GenericRelation(to='favorites.Favorites')
|
||||||
|
carousels = generic.GenericRelation(to='main.Carousel')
|
||||||
agenda = models.ForeignKey('news.Agenda', blank=True, null=True,
|
agenda = models.ForeignKey('news.Agenda', blank=True, null=True,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
verbose_name=_('agenda'))
|
verbose_name=_('agenda'))
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ from news import models
|
||||||
from tag.serializers import TagBaseSerializer
|
from tag.serializers import TagBaseSerializer
|
||||||
from utils import exceptions as utils_exceptions
|
from utils import exceptions as utils_exceptions
|
||||||
from utils.serializers import (TranslatedField, ProjectModelSerializer,
|
from utils.serializers import (TranslatedField, ProjectModelSerializer,
|
||||||
FavoritesCreateSerializer, ImageBaseSerializer)
|
FavoritesCreateSerializer, ImageBaseSerializer, CarouselCreateSerializer)
|
||||||
|
|
||||||
|
|
||||||
class AgendaSerializer(ProjectModelSerializer):
|
class AgendaSerializer(ProjectModelSerializer):
|
||||||
|
|
@ -66,6 +66,7 @@ class NewsBaseSerializer(ProjectModelSerializer):
|
||||||
subtitle_translated = TranslatedField()
|
subtitle_translated = TranslatedField()
|
||||||
news_type = NewsTypeSerializer(read_only=True)
|
news_type = NewsTypeSerializer(read_only=True)
|
||||||
tags = TagBaseSerializer(read_only=True, many=True, source='visible_tags')
|
tags = TagBaseSerializer(read_only=True, many=True, source='visible_tags')
|
||||||
|
in_favorites = serializers.BooleanField(allow_null=True, read_only=True)
|
||||||
view_counter = serializers.IntegerField(read_only=True)
|
view_counter = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
@ -272,3 +273,24 @@ class NewsFavoritesCreateSerializer(FavoritesCreateSerializer):
|
||||||
'content_object': validated_data.pop('news')
|
'content_object': validated_data.pop('news')
|
||||||
})
|
})
|
||||||
return super().create(validated_data)
|
return super().create(validated_data)
|
||||||
|
|
||||||
|
|
||||||
|
class NewsCarouselCreateSerializer(CarouselCreateSerializer):
|
||||||
|
"""Serializer to carousel object w/ model News."""
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
news = models.News.objects.filter(pk=self.pk).first()
|
||||||
|
if not news:
|
||||||
|
raise serializers.ValidationError({'detail': _('Object not found.')})
|
||||||
|
|
||||||
|
if news.carousels.exists():
|
||||||
|
raise utils_exceptions.CarouselError()
|
||||||
|
|
||||||
|
attrs['news'] = news
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
def create(self, validated_data, *args, **kwargs):
|
||||||
|
validated_data.update({
|
||||||
|
'content_object': validated_data.pop('news')
|
||||||
|
})
|
||||||
|
return super().create(validated_data)
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ class BaseTestCase(APITestCase):
|
||||||
'refresh_token': tokens.get('refresh_token')})
|
'refresh_token': tokens.get('refresh_token')})
|
||||||
self.test_news_type = NewsType.objects.create(name="Test news type")
|
self.test_news_type = NewsType.objects.create(name="Test news type")
|
||||||
|
|
||||||
|
|
||||||
self.lang, created = Language.objects.get_or_create(
|
self.lang, created = Language.objects.get_or_create(
|
||||||
title='Russia',
|
title='Russia',
|
||||||
locale='ru-RU'
|
locale='ru-RU'
|
||||||
|
|
@ -137,3 +138,17 @@ class NewsTestCase(BaseTestCase):
|
||||||
|
|
||||||
response = self.client.delete(f'/api/web/news/slug/{self.test_news.slug}/favorites/', format='json')
|
response = self.client.delete(f'/api/web/news/slug/{self.test_news.slug}/favorites/', format='json')
|
||||||
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
class NewsCarouselTests(BaseTestCase):
|
||||||
|
|
||||||
|
def test_back_carousel_CR(self):
|
||||||
|
data = {
|
||||||
|
"object_id": self.test_news.id
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.client.post(f'/api/back/news/{self.test_news.id}/carousels/', data=data)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
response = self.client.delete(f'/api/back/news/{self.test_news.id}/carousels/')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
||||||
|
|
|
||||||
|
|
@ -13,4 +13,5 @@ urlpatterns = [
|
||||||
name='gallery-list'),
|
name='gallery-list'),
|
||||||
path('<int:pk>/gallery/<int:image_id>/', views.NewsBackOfficeGalleryCreateDestroyView.as_view(),
|
path('<int:pk>/gallery/<int:image_id>/', views.NewsBackOfficeGalleryCreateDestroyView.as_view(),
|
||||||
name='gallery-create-destroy'),
|
name='gallery-create-destroy'),
|
||||||
]
|
path('<int:pk>/carousels/', views.NewsCarouselCreateDestroyView.as_view(), name='create-destroy-carousels'),
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -5,5 +5,6 @@ common_urlpatterns = [
|
||||||
path('', views.NewsListView.as_view(), name='list'),
|
path('', views.NewsListView.as_view(), name='list'),
|
||||||
path('types/', views.NewsTypeListView.as_view(), name='type'),
|
path('types/', views.NewsTypeListView.as_view(), name='type'),
|
||||||
path('slug/<slug:slug>/', views.NewsDetailView.as_view(), name='rud'),
|
path('slug/<slug:slug>/', views.NewsDetailView.as_view(), name='rud'),
|
||||||
path('slug/<slug:slug>/favorites/', views.NewsFavoritesCreateDestroyView.as_view(), name='create-destroy-favorites')
|
path('slug/<slug:slug>/favorites/', views.NewsFavoritesCreateDestroyView.as_view(),
|
||||||
|
name='create-destroy-favorites'),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ from rest_framework import generics, permissions
|
||||||
from news import filters, models, serializers
|
from news import filters, models, serializers
|
||||||
from rating.tasks import add_rating
|
from rating.tasks import add_rating
|
||||||
from utils.permissions import IsCountryAdmin, IsContentPageManager
|
from utils.permissions import IsCountryAdmin, IsContentPageManager
|
||||||
from utils.views import CreateDestroyGalleryViewMixin, FavoritesCreateDestroyMixinView
|
from utils.views import CreateDestroyGalleryViewMixin, FavoritesCreateDestroyMixinView, CarouselCreateDestroyMixinView
|
||||||
from utils.serializers import ImageBaseSerializer
|
from utils.serializers import ImageBaseSerializer
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -156,3 +156,10 @@ class NewsFavoritesCreateDestroyView(FavoritesCreateDestroyMixinView):
|
||||||
|
|
||||||
_model = models.News
|
_model = models.News
|
||||||
serializer_class = serializers.NewsFavoritesCreateSerializer
|
serializer_class = serializers.NewsFavoritesCreateSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class NewsCarouselCreateDestroyView(CarouselCreateDestroyMixinView):
|
||||||
|
"""View for create/destroy news from carousel."""
|
||||||
|
|
||||||
|
_model = models.News
|
||||||
|
serializer_class = serializers.NewsCarouselCreateSerializer
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ from product.models import Product
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@periodic_task(run_every=crontab(minute=1))
|
@periodic_task(run_every=crontab(minute='*/1'))
|
||||||
def update_index():
|
def update_index():
|
||||||
"""Updates ES index."""
|
"""Updates ES index."""
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ class NewsDocumentViewSet(BaseDocumentViewSet):
|
||||||
|
|
||||||
faceted_search_fields = {
|
faceted_search_fields = {
|
||||||
'tag': {
|
'tag': {
|
||||||
'field': 'tags.value',
|
'field': 'tags.id',
|
||||||
'enabled': True,
|
'enabled': True,
|
||||||
'facet': TermsFacet,
|
'facet': TermsFacet,
|
||||||
},
|
},
|
||||||
|
|
@ -122,7 +122,7 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
|
||||||
'enabled': True,
|
'enabled': True,
|
||||||
},
|
},
|
||||||
'tag': {
|
'tag': {
|
||||||
'field': 'tags.value',
|
'field': 'visible_tags.id',
|
||||||
'facet': TermsFacet,
|
'facet': TermsFacet,
|
||||||
'enabled': True,
|
'enabled': True,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
|
||||||
|
|
||||||
def with_base_attributes(cls):
|
def with_base_attributes(cls):
|
||||||
|
|
||||||
|
|
@ -8,7 +10,7 @@ def with_base_attributes(cls):
|
||||||
if request and hasattr(request, "user"):
|
if request and hasattr(request, "user"):
|
||||||
user = request.user
|
user = request.user
|
||||||
|
|
||||||
if user is not None:
|
if user is not None and isinstance(user, AbstractUser):
|
||||||
data.update({'modified_by': user})
|
data.update({'modified_by': user})
|
||||||
|
|
||||||
if not self.instance:
|
if not self.instance:
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,14 @@ class FavoritesError(exceptions.APIException):
|
||||||
default_detail = _('Item is already in favorites.')
|
default_detail = _('Item is already in favorites.')
|
||||||
|
|
||||||
|
|
||||||
|
class CarouselError(exceptions.APIException):
|
||||||
|
"""
|
||||||
|
The exception should be thrown when the object is already in carousels.
|
||||||
|
"""
|
||||||
|
status_code = status.HTTP_400_BAD_REQUEST
|
||||||
|
default_detail = _('Item is already in carousels.')
|
||||||
|
|
||||||
|
|
||||||
class PasswordResetRequestExistedError(exceptions.APIException):
|
class PasswordResetRequestExistedError(exceptions.APIException):
|
||||||
"""
|
"""
|
||||||
The exception should be thrown when password reset request
|
The exception should be thrown when password reset request
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,11 @@ from django.contrib.postgres.fields.jsonb import KeyTextTransform
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.html import mark_safe
|
from django.utils.html import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _, get_language
|
from django.utils.translation import ugettext_lazy as _, get_language
|
||||||
from configuration.models import TranslationSettings
|
|
||||||
from easy_thumbnails.fields import ThumbnailerImageField
|
from easy_thumbnails.fields import ThumbnailerImageField
|
||||||
from sorl.thumbnail import get_thumbnail
|
from sorl.thumbnail import get_thumbnail
|
||||||
from sorl.thumbnail.fields import ImageField as SORLImageField
|
from sorl.thumbnail.fields import ImageField as SORLImageField
|
||||||
|
|
||||||
|
from configuration.models import TranslationSettings
|
||||||
from utils.methods import image_path, svg_image_path
|
from utils.methods import image_path, svg_image_path
|
||||||
from utils.validators import svg_image_validator
|
from utils.validators import svg_image_validator
|
||||||
|
|
||||||
|
|
@ -223,6 +223,18 @@ class SORLImageMixin(models.Model):
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_cropped_image(self, geometry: str, quality: int, crop: str) -> dict:
|
||||||
|
cropped_image = get_thumbnail(self.image,
|
||||||
|
geometry_string=geometry,
|
||||||
|
crop=crop,
|
||||||
|
quality=quality)
|
||||||
|
return {
|
||||||
|
'geometry_string': geometry,
|
||||||
|
'crop_url': cropped_image.url,
|
||||||
|
'quality': quality,
|
||||||
|
'crop': crop
|
||||||
|
}
|
||||||
|
|
||||||
image_tag.short_description = _('Image')
|
image_tag.short_description = _('Image')
|
||||||
image_tag.allow_tags = True
|
image_tag.allow_tags = True
|
||||||
|
|
||||||
|
|
@ -439,4 +451,5 @@ class FavoritesMixin:
|
||||||
def favorites_for_users(self):
|
def favorites_for_users(self):
|
||||||
return self.favorites.aggregate(arr=ArrayAgg('user_id')).get('arr')
|
return self.favorites.aggregate(arr=ArrayAgg('user_id')).get('arr')
|
||||||
|
|
||||||
|
|
||||||
timezone.datetime.now().date().isoformat()
|
timezone.datetime.now().date().isoformat()
|
||||||
|
|
@ -2,10 +2,11 @@
|
||||||
import pytz
|
import pytz
|
||||||
from django.core import exceptions
|
from django.core import exceptions
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from utils import models
|
|
||||||
from translation.models import Language
|
|
||||||
from favorites.models import Favorites
|
from favorites.models import Favorites
|
||||||
from gallery.models import Image
|
from main.models import Carousel
|
||||||
|
from translation.models import Language
|
||||||
|
from utils import models
|
||||||
|
|
||||||
|
|
||||||
class EmptySerializer(serializers.Serializer):
|
class EmptySerializer(serializers.Serializer):
|
||||||
|
|
@ -80,7 +81,6 @@ class FavoritesCreateSerializer(serializers.ModelSerializer):
|
||||||
"""Serializer to favorite object."""
|
"""Serializer to favorite object."""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Serializer for model Comment."""
|
|
||||||
model = Favorites
|
model = Favorites
|
||||||
fields = [
|
fields = [
|
||||||
'id',
|
'id',
|
||||||
|
|
@ -101,6 +101,24 @@ class FavoritesCreateSerializer(serializers.ModelSerializer):
|
||||||
return self.request.parser_context.get('kwargs').get('slug')
|
return self.request.parser_context.get('kwargs').get('slug')
|
||||||
|
|
||||||
|
|
||||||
|
class CarouselCreateSerializer(serializers.ModelSerializer):
|
||||||
|
"""Carousel to favorite object."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Carousel
|
||||||
|
fields = [
|
||||||
|
'id',
|
||||||
|
]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def request(self):
|
||||||
|
return self.context.get('request')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pk(self):
|
||||||
|
return self.request.parser_context.get('kwargs').get('pk')
|
||||||
|
|
||||||
|
|
||||||
class RecursiveFieldSerializer(serializers.Serializer):
|
class RecursiveFieldSerializer(serializers.Serializer):
|
||||||
def to_representation(self, value):
|
def to_representation(self, value):
|
||||||
serializer = self.parent.parent.__class__(value, context=self.context)
|
serializer = self.parent.parent.__class__(value, context=self.context)
|
||||||
|
|
|
||||||
19
apps/utils/thumbnail_engine.py
Normal file
19
apps/utils/thumbnail_engine.py
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
"""Overridden thumbnail engine."""
|
||||||
|
from sorl.thumbnail.engines.pil_engine import Engine as PILEngine
|
||||||
|
|
||||||
|
|
||||||
|
class GMEngine(PILEngine):
|
||||||
|
|
||||||
|
def create(self, image, geometry, options):
|
||||||
|
"""
|
||||||
|
Processing conductor, returns the thumbnail as an image engine instance
|
||||||
|
"""
|
||||||
|
image = self.cropbox(image, geometry, options)
|
||||||
|
image = self.orientation(image, geometry, options)
|
||||||
|
image = self.colorspace(image, geometry, options)
|
||||||
|
image = self.remove_border(image, options)
|
||||||
|
image = self.crop(image, geometry, options)
|
||||||
|
image = self.rounded(image, geometry, options)
|
||||||
|
image = self.blur(image, geometry, options)
|
||||||
|
image = self.padding(image, geometry, options)
|
||||||
|
return image
|
||||||
|
|
@ -70,22 +70,12 @@ class JWTGenericViewMixin:
|
||||||
def _put_cookies_in_response(self, cookies: list, response: Response):
|
def _put_cookies_in_response(self, cookies: list, response: Response):
|
||||||
"""Update COOKIES in response from namedtuple"""
|
"""Update COOKIES in response from namedtuple"""
|
||||||
for cookie in cookies:
|
for cookie in cookies:
|
||||||
# todo: remove config for develop
|
response.set_cookie(key=cookie.key,
|
||||||
from os import environ
|
value=cookie.value,
|
||||||
configuration = environ.get('SETTINGS_CONFIGURATION', None)
|
secure=cookie.secure,
|
||||||
if configuration == 'development' or configuration == 'stage':
|
httponly=cookie.http_only,
|
||||||
response.set_cookie(key=cookie.key,
|
max_age=cookie.max_age,
|
||||||
value=cookie.value,
|
domain=settings.COOKIE_DOMAIN)
|
||||||
secure=cookie.secure,
|
|
||||||
httponly=cookie.http_only,
|
|
||||||
max_age=cookie.max_age,
|
|
||||||
domain='.id-east.ru')
|
|
||||||
else:
|
|
||||||
response.set_cookie(key=cookie.key,
|
|
||||||
value=cookie.value,
|
|
||||||
secure=cookie.secure,
|
|
||||||
httponly=cookie.http_only,
|
|
||||||
max_age=cookie.max_age,)
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def _get_tokens_from_cookies(self, request, cookies: dict = None):
|
def _get_tokens_from_cookies(self, request, cookies: dict = None):
|
||||||
|
|
@ -126,9 +116,8 @@ class CreateDestroyGalleryViewMixin(generics.CreateAPIView,
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
class FavoritesCreateDestroyMixinView(generics.CreateAPIView,
|
class BaseCreateDestroyMixinView(generics.CreateAPIView, generics.DestroyAPIView):
|
||||||
generics.DestroyAPIView):
|
"""Base Create Destroy mixin."""
|
||||||
"""Favorites Create Destroy mixin."""
|
|
||||||
|
|
||||||
_model = None
|
_model = None
|
||||||
serializer_class = None
|
serializer_class = None
|
||||||
|
|
@ -137,16 +126,6 @@ class FavoritesCreateDestroyMixinView(generics.CreateAPIView,
|
||||||
def get_base_object(self):
|
def get_base_object(self):
|
||||||
return get_object_or_404(self._model, slug=self.kwargs['slug'])
|
return get_object_or_404(self._model, slug=self.kwargs['slug'])
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
def es_update_base_object(self):
|
def es_update_base_object(self):
|
||||||
es_update(self.get_base_object())
|
es_update(self.get_base_object())
|
||||||
|
|
||||||
|
|
@ -159,6 +138,40 @@ class FavoritesCreateDestroyMixinView(generics.CreateAPIView,
|
||||||
self.es_update_base_object()
|
self.es_update_base_object()
|
||||||
|
|
||||||
|
|
||||||
|
class FavoritesCreateDestroyMixinView(BaseCreateDestroyMixinView):
|
||||||
|
"""Favorites Create Destroy mixin."""
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
"""
|
||||||
|
Returns the object the view is displaying.
|
||||||
|
"""
|
||||||
|
obj = self.get_base_object()
|
||||||
|
favorites = get_object_or_404(obj.favorites.filter(user=self.request.user))
|
||||||
|
# May raise a permission denied
|
||||||
|
self.check_object_permissions(self.request, favorites)
|
||||||
|
return favorites
|
||||||
|
|
||||||
|
|
||||||
|
class CarouselCreateDestroyMixinView(BaseCreateDestroyMixinView):
|
||||||
|
"""Carousel Create Destroy mixin."""
|
||||||
|
|
||||||
|
lookup_field = 'id'
|
||||||
|
|
||||||
|
def get_base_object(self):
|
||||||
|
return get_object_or_404(self._model, id=self.kwargs['pk'])
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
"""
|
||||||
|
Returns the object the view is displaying.
|
||||||
|
"""
|
||||||
|
obj = self.get_base_object()
|
||||||
|
carousels = get_object_or_404(obj.carousels.all())
|
||||||
|
# May raise a permission denied
|
||||||
|
# TODO: возможно нужны пермишены
|
||||||
|
# self.check_object_permissions(self.request, carousels)
|
||||||
|
return carousels
|
||||||
|
|
||||||
|
|
||||||
# BackOffice user`s views & viewsets
|
# BackOffice user`s views & viewsets
|
||||||
class BindObjectMixin:
|
class BindObjectMixin:
|
||||||
"""Bind object mixin."""
|
"""Bind object mixin."""
|
||||||
|
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
DB_CITY_URL="https://geolite.maxmind.com/download/geoip/database/GeoLite2-City.tar.gz"
|
|
||||||
DB_COUNTRY_URL="https://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.tar.gz"
|
|
||||||
DIR_PATH="geoip_db"
|
|
||||||
ARCH_PATH="archive"
|
|
||||||
|
|
||||||
mkdir -p $DIR_PATH
|
|
||||||
cd $DIR_PATH
|
|
||||||
|
|
||||||
mkdir -p $ARCH_PATH
|
|
||||||
|
|
||||||
find . -not -path "./$ARCH_PATH/*" -type f -name "*.mmdb" -exec mv -t "./$ARCH_PATH/" {} \+
|
|
||||||
|
|
||||||
filename=$(basename $DB_CITY_URL)
|
|
||||||
wget -O $filename $DB_CITY_URL
|
|
||||||
tar xzvf "$filename"
|
|
||||||
|
|
||||||
filename=$(basename $DB_COUNTRY_URL)
|
|
||||||
wget -O $filename $DB_COUNTRY_URL
|
|
||||||
tar xzvf "$filename"
|
|
||||||
|
|
||||||
find . -mindepth 1 -type f -name "*.mmdb" -not -path "./$ARCH_PATH/*" -exec mv -t . {} \+
|
|
||||||
14
make_data_migration.sh
Executable file
14
make_data_migration.sh
Executable file
|
|
@ -0,0 +1,14 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
./manage.py transfer -a
|
||||||
|
#./manage.py transfer -d
|
||||||
|
./manage.py transfer -e
|
||||||
|
./manage.py transfer --fill_city_gallery
|
||||||
|
./manage.py transfer -l
|
||||||
|
./manage.py transfer --product
|
||||||
|
./manage.py transfer --souvenir
|
||||||
|
./manage.py transfer --establishment_note
|
||||||
|
./manage.py transfer --product_note
|
||||||
|
./manage.py transfer --wine_characteristics
|
||||||
|
./manage.py transfer --inquiries
|
||||||
|
./manage.py transfer --assemblage
|
||||||
|
./manage.py transfer --purchased_plaques
|
||||||
|
|
@ -13,9 +13,9 @@ AWS_S3_OBJECT_PARAMETERS = {'CacheControl': 'max-age=86400'}
|
||||||
AWS_S3_ADDRESSING_STYLE = 'path'
|
AWS_S3_ADDRESSING_STYLE = 'path'
|
||||||
|
|
||||||
# Static settings
|
# Static settings
|
||||||
# PUBLIC_STATIC_LOCATION = 'static'
|
PUBLIC_STATIC_LOCATION = 'static-dev'
|
||||||
# STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{PUBLIC_STATIC_LOCATION}/'
|
STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{PUBLIC_STATIC_LOCATION}/'
|
||||||
# STATICFILES_STORAGE = 'project.storage_backends.PublicStaticStorage'
|
STATICFILES_STORAGE = 'project.storage_backends.PublicStaticStorage'
|
||||||
|
|
||||||
# Public media settings
|
# Public media settings
|
||||||
MEDIA_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{MEDIA_LOCATION}/'
|
MEDIA_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{MEDIA_LOCATION}/'
|
||||||
|
|
|
||||||
|
|
@ -399,6 +399,13 @@ SORL_THUMBNAIL_ALIASES = {
|
||||||
'establishment_xlarge': {'geometry_string': '640x360', 'crop': 'center'},
|
'establishment_xlarge': {'geometry_string': '640x360', 'crop': 'center'},
|
||||||
'establishment_detail': {'geometry_string': '2048x1152', 'crop': 'center'},
|
'establishment_detail': {'geometry_string': '2048x1152', 'crop': 'center'},
|
||||||
'establishment_original': {'geometry_string': '1920x1080', 'crop': 'center'},
|
'establishment_original': {'geometry_string': '1920x1080', 'crop': 'center'},
|
||||||
|
'city_xsmall': {'geometry_string': '70x70', 'crop': 'center'},
|
||||||
|
'city_small': {'geometry_string': '140x140', 'crop': 'center'},
|
||||||
|
'city_medium': {'geometry_string': '280x280', 'crop': 'center'},
|
||||||
|
'city_large': {'geometry_string': '280x280', 'crop': 'center'},
|
||||||
|
'city_xlarge': {'geometry_string': '560x560', 'crop': 'center'},
|
||||||
|
'city_detail': {'geometry_string': '1120x1120', 'crop': 'center'},
|
||||||
|
'city_original': {'geometry_string': '2048x1536', 'crop': 'center'},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -487,7 +494,6 @@ LIMITING_QUERY_OBJECTS = QUERY_OUTPUT_OBJECTS * 3
|
||||||
# GEO
|
# GEO
|
||||||
# A Spatial Reference System Identifier
|
# A Spatial Reference System Identifier
|
||||||
GEO_DEFAULT_SRID = 4326
|
GEO_DEFAULT_SRID = 4326
|
||||||
GEOIP_PATH = os.path.join(PROJECT_ROOT, 'geoip_db')
|
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/2.2/howto/static-files/
|
# https://docs.djangoproject.com/en/2.2/howto/static-files/
|
||||||
|
|
@ -514,3 +520,6 @@ ESTABLISHMENT_CHOSEN_TAGS = ['gastronomic', 'en_vogue', 'terrace', 'streetfood',
|
||||||
NEWS_CHOSEN_TAGS = ['eat', 'drink', 'cook', 'style', 'international', 'event', 'partnership']
|
NEWS_CHOSEN_TAGS = ['eat', 'drink', 'cook', 'style', 'international', 'event', 'partnership']
|
||||||
INTERNATIONAL_COUNTRY_CODES = ['www', 'main', 'next']
|
INTERNATIONAL_COUNTRY_CODES = ['www', 'main', 'next']
|
||||||
|
|
||||||
|
THUMBNAIL_ENGINE = 'utils.thumbnail_engine.GMEngine'
|
||||||
|
|
||||||
|
COOKIE_DOMAIN = None
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,17 @@ SITE_DOMAIN_URI = 'id-east.ru'
|
||||||
DOMAIN_URI = 'gm.id-east.ru'
|
DOMAIN_URI = 'gm.id-east.ru'
|
||||||
|
|
||||||
|
|
||||||
|
CACHES = {
|
||||||
|
'default': {
|
||||||
|
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||||
|
},
|
||||||
|
'es_queue': {
|
||||||
|
'BACKEND': 'django_redis.cache.RedisCache',
|
||||||
|
'LOCATION': 'redis://localhost:6379/2'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# ELASTICSEARCH SETTINGS
|
# ELASTICSEARCH SETTINGS
|
||||||
ELASTICSEARCH_DSL = {
|
ELASTICSEARCH_DSL = {
|
||||||
'default': {
|
'default': {
|
||||||
|
|
@ -60,3 +71,5 @@ INSTALLED_APPS.append('transfer.apps.TransferConfig')
|
||||||
BROKER_URL = 'redis://localhost:6379/1'
|
BROKER_URL = 'redis://localhost:6379/1'
|
||||||
CELERY_RESULT_BACKEND = BROKER_URL
|
CELERY_RESULT_BACKEND = BROKER_URL
|
||||||
CELERY_BROKER_URL = BROKER_URL
|
CELERY_BROKER_URL = BROKER_URL
|
||||||
|
|
||||||
|
COOKIE_DOMAIN = '.id-east.ru'
|
||||||
|
|
|
||||||
|
|
@ -5,18 +5,15 @@ import sys
|
||||||
|
|
||||||
ALLOWED_HOSTS = ['*', ]
|
ALLOWED_HOSTS = ['*', ]
|
||||||
|
|
||||||
|
|
||||||
SEND_SMS = False
|
SEND_SMS = False
|
||||||
SMS_CODE_SHOW = True
|
SMS_CODE_SHOW = True
|
||||||
USE_CELERY = True
|
USE_CELERY = True
|
||||||
|
|
||||||
|
|
||||||
SCHEMA_URI = 'http'
|
SCHEMA_URI = 'http'
|
||||||
DEFAULT_SUBDOMAIN = 'www'
|
DEFAULT_SUBDOMAIN = 'www'
|
||||||
SITE_DOMAIN_URI = 'testserver.com:8000'
|
SITE_DOMAIN_URI = 'testserver.com:8000'
|
||||||
DOMAIN_URI = '0.0.0.0:8000'
|
DOMAIN_URI = '0.0.0.0:8000'
|
||||||
|
|
||||||
|
|
||||||
# CELERY
|
# CELERY
|
||||||
# RabbitMQ
|
# RabbitMQ
|
||||||
# BROKER_URL = 'amqp://rabbitmq:5672'
|
# BROKER_URL = 'amqp://rabbitmq:5672'
|
||||||
|
|
@ -25,20 +22,16 @@ BROKER_URL = 'redis://redis:6379/1'
|
||||||
CELERY_RESULT_BACKEND = BROKER_URL
|
CELERY_RESULT_BACKEND = BROKER_URL
|
||||||
CELERY_BROKER_URL = BROKER_URL
|
CELERY_BROKER_URL = BROKER_URL
|
||||||
|
|
||||||
|
|
||||||
# MEDIA
|
# MEDIA
|
||||||
MEDIA_URL = f'{SCHEMA_URI}://{DOMAIN_URI}/{MEDIA_LOCATION}/'
|
MEDIA_URL = f'{SCHEMA_URI}://{DOMAIN_URI}/{MEDIA_LOCATION}/'
|
||||||
MEDIA_ROOT = os.path.join(PUBLIC_ROOT, MEDIA_LOCATION)
|
MEDIA_ROOT = os.path.join(PUBLIC_ROOT, MEDIA_LOCATION)
|
||||||
|
|
||||||
|
|
||||||
# SORL thumbnails
|
# SORL thumbnails
|
||||||
THUMBNAIL_DEBUG = True
|
THUMBNAIL_DEBUG = True
|
||||||
|
|
||||||
|
|
||||||
# ADDED TRANSFER APP
|
# ADDED TRANSFER APP
|
||||||
# INSTALLED_APPS.append('transfer.apps.TransferConfig')
|
# INSTALLED_APPS.append('transfer.apps.TransferConfig')
|
||||||
|
|
||||||
|
|
||||||
# DATABASES
|
# DATABASES
|
||||||
DATABASES.update({
|
DATABASES.update({
|
||||||
'legacy': {
|
'legacy': {
|
||||||
|
|
@ -80,15 +73,14 @@ LOGGING = {
|
||||||
'py.warnings': {
|
'py.warnings': {
|
||||||
'handlers': ['console'],
|
'handlers': ['console'],
|
||||||
},
|
},
|
||||||
'django.db.backends': {
|
# 'django.db.backends': {
|
||||||
'handlers': ['console', ],
|
# 'handlers': ['console', ],
|
||||||
'level': 'DEBUG',
|
# 'level': 'DEBUG',
|
||||||
'propagate': False,
|
# 'propagate': False,
|
||||||
},
|
# },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# ELASTICSEARCH SETTINGS
|
# ELASTICSEARCH SETTINGS
|
||||||
ELASTICSEARCH_DSL = {
|
ELASTICSEARCH_DSL = {
|
||||||
'default': {
|
'default': {
|
||||||
|
|
@ -103,7 +95,7 @@ ELASTICSEARCH_INDEX_NAMES = {
|
||||||
}
|
}
|
||||||
ELASTICSEARCH_DSL_AUTOSYNC = False
|
ELASTICSEARCH_DSL_AUTOSYNC = False
|
||||||
|
|
||||||
|
|
||||||
TESTING = sys.argv[1:2] == ['test']
|
TESTING = sys.argv[1:2] == ['test']
|
||||||
if TESTING:
|
if TESTING:
|
||||||
ELASTICSEARCH_INDEX_NAMES = {}
|
ELASTICSEARCH_INDEX_NAMES = {}
|
||||||
|
ELASTICSEARCH_DSL_AUTOSYNC = False
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,11 @@ from .amazon_s3 import *
|
||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
from sentry_sdk.integrations.django import DjangoIntegration
|
from sentry_sdk.integrations.django import DjangoIntegration
|
||||||
|
|
||||||
|
|
||||||
|
PUBLIC_STATIC_LOCATION = 'static'
|
||||||
|
STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{PUBLIC_STATIC_LOCATION}/'
|
||||||
|
STATICFILES_STORAGE = 'project.storage_backends.PublicStaticStorage'
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
|
|
||||||
|
|
@ -20,7 +25,6 @@ DEFAULT_SUBDOMAIN = 'www'
|
||||||
SITE_DOMAIN_URI = 'gaultmillau.com'
|
SITE_DOMAIN_URI = 'gaultmillau.com'
|
||||||
DOMAIN_URI = 'next.gaultmillau.com'
|
DOMAIN_URI = 'next.gaultmillau.com'
|
||||||
|
|
||||||
|
|
||||||
# ELASTICSEARCH SETTINGS
|
# ELASTICSEARCH SETTINGS
|
||||||
ELASTICSEARCH_DSL = {
|
ELASTICSEARCH_DSL = {
|
||||||
'default': {
|
'default': {
|
||||||
|
|
@ -28,20 +32,17 @@ ELASTICSEARCH_DSL = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ELASTICSEARCH_INDEX_NAMES = {
|
ELASTICSEARCH_INDEX_NAMES = {
|
||||||
'search_indexes.documents.news': 'development_news', # temporarily disabled
|
'search_indexes.documents.news': 'development_news', # temporarily disabled
|
||||||
'search_indexes.documents.establishment': 'development_establishment',
|
'search_indexes.documents.establishment': 'development_establishment',
|
||||||
'search_indexes.documents.product': 'development_product',
|
'search_indexes.documents.product': 'development_product',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
sentry_sdk.init(
|
sentry_sdk.init(
|
||||||
dsn="https://35d9bb789677410ab84a822831c6314f@sentry.io/1729093",
|
dsn="https://35d9bb789677410ab84a822831c6314f@sentry.io/1729093",
|
||||||
integrations=[DjangoIntegration()]
|
integrations=[DjangoIntegration()]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
BROKER_URL = 'redis://redis:6379/1'
|
BROKER_URL = 'redis://redis:6379/1'
|
||||||
CELERY_RESULT_BACKEND = BROKER_URL
|
CELERY_RESULT_BACKEND = BROKER_URL
|
||||||
CELERY_BROKER_URL = BROKER_URL
|
CELERY_BROKER_URL = BROKER_URL
|
||||||
|
|
@ -51,4 +52,6 @@ GUESTONLINE_SERVICE = 'https://api.guestonline.fr/'
|
||||||
GUESTONLINE_TOKEN = ''
|
GUESTONLINE_TOKEN = ''
|
||||||
LASTABLE_SERVICE = ''
|
LASTABLE_SERVICE = ''
|
||||||
LASTABLE_TOKEN = ''
|
LASTABLE_TOKEN = ''
|
||||||
LASTABLE_PROXY = ''
|
LASTABLE_PROXY = ''
|
||||||
|
|
||||||
|
COOKIE_DOMAIN = '.gaultmillau.com'
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ DEFAULT_SUBDOMAIN = 'www'
|
||||||
SITE_DOMAIN_URI = 'id-east.ru'
|
SITE_DOMAIN_URI = 'id-east.ru'
|
||||||
DOMAIN_URI = 'gm-stage.id-east.ru'
|
DOMAIN_URI = 'gm-stage.id-east.ru'
|
||||||
|
|
||||||
|
|
||||||
# ELASTICSEARCH SETTINGS
|
# ELASTICSEARCH SETTINGS
|
||||||
ELASTICSEARCH_DSL = {
|
ELASTICSEARCH_DSL = {
|
||||||
'default': {
|
'default': {
|
||||||
|
|
@ -21,8 +20,9 @@ ELASTICSEARCH_DSL = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ELASTICSEARCH_INDEX_NAMES = {
|
ELASTICSEARCH_INDEX_NAMES = {
|
||||||
# 'search_indexes.documents.news': 'stage_news', #temporarily disabled
|
# 'search_indexes.documents.news': 'stage_news', #temporarily disabled
|
||||||
'search_indexes.documents.establishment': 'stage_establishment',
|
'search_indexes.documents.establishment': 'stage_establishment',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
COOKIE_DOMAIN = '.id-east.ru'
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user