Merge branch 'develop' into feature/collection-slugs
# Conflicts: # requirements/base.txt
This commit is contained in:
commit
2580b9f218
18
apps/account/migrations/0026_auto_20191210_1553.py
Normal file
18
apps/account/migrations/0026_auto_20191210_1553.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-10 15:53
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('account', '0025_auto_20191210_0623'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='role',
|
||||||
|
name='role',
|
||||||
|
field=models.PositiveIntegerField(choices=[(1, 'Standard user'), (2, 'Comments moderator'), (3, 'Country admin'), (4, 'Content page manager'), (5, 'Establishment manager'), (6, 'Reviewer manager'), (7, 'Restaurant reviewer'), (8, 'Sales man'), (9, 'Winery reviewer'), (10, 'Seller'), (11, 'Liquor reviewer'), (12, 'Product reviewer')], verbose_name='Role'),
|
||||||
|
),
|
||||||
|
]
|
||||||
14
apps/account/migrations/0028_merge_20191217_1127.py
Normal file
14
apps/account/migrations/0028_merge_20191217_1127.py
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-17 11:27
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('account', '0027_auto_20191211_1444'),
|
||||||
|
('account', '0026_auto_20191210_1553'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
]
|
||||||
|
|
@ -36,6 +36,8 @@ class Role(ProjectBaseMixin):
|
||||||
SALES_MAN = 8
|
SALES_MAN = 8
|
||||||
WINERY_REVIEWER = 9 # Establishments subtype "winery"
|
WINERY_REVIEWER = 9 # Establishments subtype "winery"
|
||||||
SELLER = 10
|
SELLER = 10
|
||||||
|
LIQUOR_REVIEWER = 11
|
||||||
|
PRODUCT_REVIEWER = 12
|
||||||
|
|
||||||
ROLE_CHOICES = (
|
ROLE_CHOICES = (
|
||||||
(STANDARD_USER, _('Standard user')),
|
(STANDARD_USER, _('Standard user')),
|
||||||
|
|
@ -47,7 +49,9 @@ class Role(ProjectBaseMixin):
|
||||||
(RESTAURANT_REVIEWER, 'Restaurant reviewer'),
|
(RESTAURANT_REVIEWER, 'Restaurant reviewer'),
|
||||||
(SALES_MAN, 'Sales man'),
|
(SALES_MAN, 'Sales man'),
|
||||||
(WINERY_REVIEWER, 'Winery reviewer'),
|
(WINERY_REVIEWER, 'Winery reviewer'),
|
||||||
(SELLER, 'Seller')
|
(SELLER, 'Seller'),
|
||||||
|
(LIQUOR_REVIEWER, 'Liquor reviewer'),
|
||||||
|
(PRODUCT_REVIEWER, 'Product reviewer'),
|
||||||
)
|
)
|
||||||
role = models.PositiveIntegerField(verbose_name=_('Role'), choices=ROLE_CHOICES,
|
role = models.PositiveIntegerField(verbose_name=_('Role'), choices=ROLE_CHOICES,
|
||||||
null=False, blank=False)
|
null=False, blank=False)
|
||||||
|
|
|
||||||
|
|
@ -10,5 +10,5 @@ urlpatterns = [
|
||||||
path('user-role/', views.UserRoleLstView.as_view(), name='user-role-list-create'),
|
path('user-role/', views.UserRoleLstView.as_view(), name='user-role-list-create'),
|
||||||
path('user/', views.UserLstView.as_view(), name='user-create-list'),
|
path('user/', views.UserLstView.as_view(), name='user-create-list'),
|
||||||
path('user/<int:id>/', views.UserRUDView.as_view(), name='user-rud'),
|
path('user/<int:id>/', views.UserRUDView.as_view(), name='user-rud'),
|
||||||
path('user/<int:id>/csv', views.get_user_csv, name='user-csv'),
|
path('user/<int:id>/csv/', views.get_user_csv, name='user-csv'),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ class UserRUDView(generics.RetrieveUpdateDestroyAPIView):
|
||||||
|
|
||||||
|
|
||||||
def get_user_csv(request, id):
|
def get_user_csv(request, id):
|
||||||
|
"""User CSV file download"""
|
||||||
# fields = ["id", "uuid", "nickname", "locale", "country_code", "city", "role", "consent_purpose", "consent_at",
|
# fields = ["id", "uuid", "nickname", "locale", "country_code", "city", "role", "consent_purpose", "consent_at",
|
||||||
# "last_seen_at", "created_at", "updated_at", "email", "is_admin", "ezuser_id", "ez_user_id",
|
# "last_seen_at", "created_at", "updated_at", "email", "is_admin", "ezuser_id", "ez_user_id",
|
||||||
# "encrypted_password", "reset_password_token", "reset_password_sent_at", "remember_created_at",
|
# "encrypted_password", "reset_password_token", "reset_password_sent_at", "remember_created_at",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
from rest_framework import generics, permissions, status, serializers
|
from rest_framework import generics, permissions, status, serializers
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
|
@ -96,6 +97,13 @@ class CreatePendingBooking(generics.CreateAPIView):
|
||||||
permission_classes = (permissions.AllowAny,)
|
permission_classes = (permissions.AllowAny,)
|
||||||
serializer_class = PendingBookingSerializer
|
serializer_class = PendingBookingSerializer
|
||||||
|
|
||||||
|
@swagger_auto_schema(operation_description="Request body params\n\n"
|
||||||
|
"IN GUESTONLINE (type:G): {"
|
||||||
|
"'restaurant_id', 'booking_time', "
|
||||||
|
"'booking_date', 'booked_persons_number'}\n"
|
||||||
|
"IN LASTABLE (type:L): {'booking_time', "
|
||||||
|
"'booked_persons_number', 'offer_id' (Req), "
|
||||||
|
"'email', 'phone', 'first_name', 'last_name'}")
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
data = request.data.copy()
|
data = request.data.copy()
|
||||||
if data.get('type') == Booking.LASTABLE and data.get("offer_id") is None:
|
if data.get('type') == Booking.LASTABLE and data.get("offer_id") is None:
|
||||||
|
|
@ -135,6 +143,10 @@ class UpdatePendingBooking(generics.UpdateAPIView):
|
||||||
permission_classes = (permissions.AllowAny,)
|
permission_classes = (permissions.AllowAny,)
|
||||||
serializer_class = UpdateBookingSerializer
|
serializer_class = UpdateBookingSerializer
|
||||||
|
|
||||||
|
@swagger_auto_schema(operation_description="Request body params\n\n"
|
||||||
|
"Required: 'email', 'phone', 'last_name', "
|
||||||
|
"'first_name', 'country_code', 'pending_booking_id',"
|
||||||
|
"Not req: 'note'")
|
||||||
def patch(self, request, *args, **kwargs):
|
def patch(self, request, *args, **kwargs):
|
||||||
instance = self.get_object()
|
instance = self.get_object()
|
||||||
data = request.data.copy()
|
data = request.data.copy()
|
||||||
|
|
|
||||||
27
apps/collection/migrations/0027_auto_20191218_0753.py
Normal file
27
apps/collection/migrations/0027_auto_20191218_0753.py
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-18 07:53
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('gallery', '0007_auto_20191211_1528'),
|
||||||
|
('collection', '0026_merge_20191217_1151'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='advertorial',
|
||||||
|
name='gallery',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='guideelement',
|
||||||
|
name='label_photo',
|
||||||
|
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='gallery.Image', verbose_name='label photo'),
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='AdvertorialGallery',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -11,7 +11,7 @@ from utils.models import (
|
||||||
URLImageMixin,
|
URLImageMixin,
|
||||||
)
|
)
|
||||||
from utils.querysets import RelatedObjectsCountMixin
|
from utils.querysets import RelatedObjectsCountMixin
|
||||||
from utils.models import IntermediateGalleryModelMixin, GalleryModelMixin
|
from utils.models import IntermediateGalleryModelMixin, GalleryMixin
|
||||||
|
|
||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
|
|
||||||
|
|
@ -120,22 +120,23 @@ class Collection(ProjectBaseMixin, CollectionDateMixin,
|
||||||
instances = getattr(self, f'{related_object}')
|
instances = getattr(self, f'{related_object}')
|
||||||
if instances.exists():
|
if instances.exists():
|
||||||
for instance in instances.all():
|
for instance in instances.all():
|
||||||
raw_object = (instance.id, instance.slug) if hasattr(instance, 'slug') else (
|
raw_object = (instance.id, instance.establishment_type.index_name,
|
||||||
instance.id, None
|
instance.slug) if \
|
||||||
)
|
hasattr(instance, 'slug') else (instance.id, None, None)
|
||||||
raw_objects.append(raw_object)
|
raw_objects.append(raw_object)
|
||||||
|
|
||||||
# parse slugs
|
# parse slugs
|
||||||
related_objects = []
|
related_objects = []
|
||||||
object_names = set()
|
object_names = set()
|
||||||
re_pattern = r'[\w]+'
|
re_pattern = r'[\w]+'
|
||||||
for object_id, raw_name, in raw_objects:
|
for object_id, object_type, raw_name, in raw_objects:
|
||||||
result = re.findall(re_pattern, raw_name)
|
result = re.findall(re_pattern, raw_name)
|
||||||
if result:
|
if result:
|
||||||
name = ' '.join(result).capitalize()
|
name = ' '.join(result).capitalize()
|
||||||
if name not in object_names:
|
if name not in object_names:
|
||||||
related_objects.append({
|
related_objects.append({
|
||||||
'id': object_id,
|
'id': object_id,
|
||||||
|
'establishment_type': object_type,
|
||||||
'name': name
|
'name': name
|
||||||
})
|
})
|
||||||
object_names.add(name)
|
object_names.add(name)
|
||||||
|
|
@ -238,7 +239,7 @@ class AdvertorialQuerySet(models.QuerySet):
|
||||||
"""QuerySet for model Advertorial."""
|
"""QuerySet for model Advertorial."""
|
||||||
|
|
||||||
|
|
||||||
class Advertorial(GalleryModelMixin, ProjectBaseMixin):
|
class Advertorial(ProjectBaseMixin):
|
||||||
"""Guide advertorial model."""
|
"""Guide advertorial model."""
|
||||||
number_of_pages = models.PositiveIntegerField(
|
number_of_pages = models.PositiveIntegerField(
|
||||||
verbose_name=_('number of pages'),
|
verbose_name=_('number of pages'),
|
||||||
|
|
@ -250,7 +251,6 @@ class Advertorial(GalleryModelMixin, ProjectBaseMixin):
|
||||||
related_name='advertorial',
|
related_name='advertorial',
|
||||||
verbose_name=_('guide element'))
|
verbose_name=_('guide element'))
|
||||||
old_id = models.IntegerField(blank=True, null=True)
|
old_id = models.IntegerField(blank=True, null=True)
|
||||||
gallery = models.ManyToManyField('gallery.Image', through='AdvertorialGallery')
|
|
||||||
|
|
||||||
objects = AdvertorialQuerySet.as_manager()
|
objects = AdvertorialQuerySet.as_manager()
|
||||||
|
|
||||||
|
|
@ -260,24 +260,6 @@ class Advertorial(GalleryModelMixin, ProjectBaseMixin):
|
||||||
verbose_name_plural = _('advertorials')
|
verbose_name_plural = _('advertorials')
|
||||||
|
|
||||||
|
|
||||||
class AdvertorialGallery(IntermediateGalleryModelMixin):
|
|
||||||
"""Advertorial gallery model."""
|
|
||||||
advertorial = models.ForeignKey(Advertorial, null=True,
|
|
||||||
related_name='advertorial_gallery',
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
verbose_name=_('advertorial'))
|
|
||||||
image = models.ForeignKey('gallery.Image', null=True,
|
|
||||||
related_name='advertorial_gallery',
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
verbose_name=_('image'))
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
"""Meta class."""
|
|
||||||
verbose_name = _('advertorial gallery')
|
|
||||||
verbose_name_plural = _('advertorial galleries')
|
|
||||||
unique_together = (('advertorial', 'image'), )
|
|
||||||
|
|
||||||
|
|
||||||
class GuideFilterQuerySet(models.QuerySet):
|
class GuideFilterQuerySet(models.QuerySet):
|
||||||
"""QuerySet for model GuideFilter."""
|
"""QuerySet for model GuideFilter."""
|
||||||
|
|
||||||
|
|
@ -422,6 +404,9 @@ class GuideElement(ProjectBaseMixin, MPTTModel):
|
||||||
parent = TreeForeignKey('self', on_delete=models.CASCADE,
|
parent = TreeForeignKey('self', on_delete=models.CASCADE,
|
||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
related_name='children')
|
related_name='children')
|
||||||
|
label_photo = models.ForeignKey('gallery.Image', on_delete=models.SET_NULL,
|
||||||
|
null=True, blank=True, default=None,
|
||||||
|
verbose_name=_('label photo'))
|
||||||
old_id = models.PositiveIntegerField(blank=True, null=True, default=None,
|
old_id = models.PositiveIntegerField(blank=True, null=True, default=None,
|
||||||
verbose_name=_('old id'))
|
verbose_name=_('old id'))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ from tqdm import tqdm
|
||||||
|
|
||||||
from collection.models import GuideElementSection, GuideElementSectionCategory, \
|
from collection.models import GuideElementSection, GuideElementSectionCategory, \
|
||||||
GuideWineColorSection, GuideElementType, GuideElement, \
|
GuideWineColorSection, GuideElementType, GuideElement, \
|
||||||
Guide, Advertorial, AdvertorialGallery
|
Guide, Advertorial
|
||||||
from establishment.models import Establishment
|
from establishment.models import Establishment
|
||||||
from gallery.models import Image
|
from gallery.models import Image
|
||||||
from location.models import WineRegion, City
|
from location.models import WineRegion, City
|
||||||
|
|
@ -13,6 +13,7 @@ from review.models import Review
|
||||||
from transfer.models import Guides, GuideFilters, GuideSections, GuideElements, \
|
from transfer.models import Guides, GuideFilters, GuideSections, GuideElements, \
|
||||||
GuideAds, LabelPhotos
|
GuideAds, LabelPhotos
|
||||||
from transfer.serializers.guide import GuideSerializer, GuideFilterSerializer
|
from transfer.serializers.guide import GuideSerializer, GuideFilterSerializer
|
||||||
|
from django.db.models import Subquery
|
||||||
|
|
||||||
|
|
||||||
def transfer_guide():
|
def transfer_guide():
|
||||||
|
|
@ -255,7 +256,7 @@ def transfer_guide_element_advertorials():
|
||||||
qs = GuideElement.objects.filter(old_id=old_id)
|
qs = GuideElement.objects.filter(old_id=old_id)
|
||||||
legacy_qs = GuideElements.objects.exclude(guide__isnull=True) \
|
legacy_qs = GuideElements.objects.exclude(guide__isnull=True) \
|
||||||
.exclude(guide__title__icontains='test') \
|
.exclude(guide__title__icontains='test') \
|
||||||
.filter(id=guide_ad_node_id)
|
.filter(id=old_id)
|
||||||
if qs.exists() and legacy_qs.exists():
|
if qs.exists() and legacy_qs.exists():
|
||||||
return qs.first()
|
return qs.first()
|
||||||
elif legacy_qs.exists() and not qs.exists():
|
elif legacy_qs.exists() and not qs.exists():
|
||||||
|
|
@ -288,40 +289,53 @@ def transfer_guide_element_advertorials():
|
||||||
print(f'COUNT OF CREATED OBJECTS: {len(objects_to_update)}')
|
print(f'COUNT OF CREATED OBJECTS: {len(objects_to_update)}')
|
||||||
|
|
||||||
|
|
||||||
def transfer_guide_element_advertorial_galleries():
|
def transfer_guide_element_label_photo():
|
||||||
"""Transfer galleries for Guide Advertorial model."""
|
"""Transfer galleries for Guide Advertorial model."""
|
||||||
def get_guide_element_advertorial(old_id: int):
|
def get_guide_element(guide_ad):
|
||||||
if old_id:
|
legacy_guide_element_id = guide_ad.guide_ad_node.id
|
||||||
qs = Advertorial.objects.filter(old_id=old_id)
|
|
||||||
legacy_qs = GuideAds.objects.filter(id=old_id)
|
|
||||||
if qs.exists() and legacy_qs.exists():
|
|
||||||
return qs.first()
|
|
||||||
elif legacy_qs.exists() and not qs.exists():
|
|
||||||
raise ValueError(f'Guide element advertorials was not transfer correctly - {old_id}.')
|
|
||||||
|
|
||||||
created_counter = 0
|
legacy_guide_element_qs = GuideElements.objects.filter(id=legacy_guide_element_id)
|
||||||
gallery_obj_exists_counter = 0
|
guide_element_qs = GuideElement.objects.filter(old_id=legacy_guide_element_id)
|
||||||
advertorial_galleries = LabelPhotos.objects.exclude(guide_ad__isnull=False) \
|
|
||||||
|
if guide_element_qs.exists() and legacy_guide_element_qs.exists():
|
||||||
|
return guide_element_qs.first()
|
||||||
|
else:
|
||||||
|
raise ValueError(f'Guide element was not transfer correctly - '
|
||||||
|
f'{legacy_guide_element_id}.')
|
||||||
|
|
||||||
|
to_update = []
|
||||||
|
not_updated = 0
|
||||||
|
guide_element_label_photos = LabelPhotos.objects.exclude(guide_ad__isnull=True) \
|
||||||
|
.filter(guide_ad__type='GuideAdLabel') \
|
||||||
|
.distinct() \
|
||||||
.values_list('guide_ad', 'attachment_suffix_url')
|
.values_list('guide_ad', 'attachment_suffix_url')
|
||||||
for guide_ad, attachment_suffix_url in tqdm(advertorial_galleries):
|
for guide_ad_id, attachment_suffix_url in tqdm(guide_element_label_photos):
|
||||||
advertorial = get_guide_element_advertorial(guide_ad.id)
|
legacy_guide_element_ids = Subquery(
|
||||||
|
GuideElements.objects.exclude(guide__isnull=True)
|
||||||
|
.exclude(guide__title__icontains='test')
|
||||||
|
.values_list('id', flat=True)
|
||||||
|
)
|
||||||
|
legacy_guide_ad_qs = GuideAds.objects.filter(id=guide_ad_id,
|
||||||
|
guide_ad_node_id__in=legacy_guide_element_ids)
|
||||||
|
if legacy_guide_ad_qs.exists():
|
||||||
|
guide_element = get_guide_element(legacy_guide_ad_qs.first())
|
||||||
|
if guide_element:
|
||||||
image, _ = Image.objects.get_or_create(image=attachment_suffix_url,
|
image, _ = Image.objects.get_or_create(image=attachment_suffix_url,
|
||||||
defaults={
|
defaults={
|
||||||
'image': attachment_suffix_url,
|
'image': attachment_suffix_url,
|
||||||
'orientation': Image.HORIZONTAL,
|
'orientation': Image.HORIZONTAL,
|
||||||
'title': f'{advertorial.name} - '
|
'title': f'{guide_element.__str__()} '
|
||||||
f'{attachment_suffix_url}',
|
f'{guide_element.id} - '
|
||||||
})
|
f'{attachment_suffix_url}'})
|
||||||
city_gallery, created = AdvertorialGallery.objects.get_or_create(image=image,
|
if not guide_element.label_photo:
|
||||||
advertorial=advertorial,
|
guide_element.label_photo = image
|
||||||
is_main=True)
|
to_update.append(guide_element)
|
||||||
if created:
|
|
||||||
created_counter += 1
|
|
||||||
else:
|
else:
|
||||||
gallery_obj_exists_counter += 1
|
not_updated += 1
|
||||||
|
|
||||||
print(f'Created: {created_counter}\n'
|
GuideElement.objects.bulk_update(to_update, ['label_photo', ])
|
||||||
f'Already added: {gallery_obj_exists_counter}')
|
print(f'Added label photo to {len(to_update)} objects\n'
|
||||||
|
f'Objects {not_updated} not updated')
|
||||||
|
|
||||||
|
|
||||||
data_types = {
|
data_types = {
|
||||||
|
|
@ -344,10 +358,10 @@ data_types = {
|
||||||
transfer_guide_elements_bulk,
|
transfer_guide_elements_bulk,
|
||||||
],
|
],
|
||||||
'guide_element_advertorials': [
|
'guide_element_advertorials': [
|
||||||
transfer_guide_element_advertorials
|
transfer_guide_element_advertorials,
|
||||||
],
|
],
|
||||||
'guide_element_advertorial_galleries': [
|
'guide_element_label_photo': [
|
||||||
|
transfer_guide_element_label_photo,
|
||||||
],
|
],
|
||||||
'guide_complete': [
|
'guide_complete': [
|
||||||
transfer_guide, # transfer guides from Guides
|
transfer_guide, # transfer guides from Guides
|
||||||
|
|
@ -357,6 +371,6 @@ data_types = {
|
||||||
transfer_guide_element_type, # partial transfer section types from GuideElements
|
transfer_guide_element_type, # partial transfer section types from GuideElements
|
||||||
transfer_guide_elements_bulk, # transfer result of GuideFilters from GuideElements
|
transfer_guide_elements_bulk, # transfer result of GuideFilters from GuideElements
|
||||||
transfer_guide_element_advertorials, # transfer advertorials that linked to GuideElements
|
transfer_guide_element_advertorials, # transfer advertorials that linked to GuideElements
|
||||||
transfer_guide_element_advertorial_galleries, # transfer advertorial galleries
|
transfer_guide_element_label_photo, # transfer guide element label photos
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
|
from tqdm import tqdm
|
||||||
from establishment.models import Establishment
|
from establishment.models import Establishment
|
||||||
from transfer.models import Reviews, ReviewTexts
|
from transfer.models import Reviews, ReviewTexts
|
||||||
|
|
||||||
|
|
@ -22,7 +22,7 @@ class Command(BaseCommand):
|
||||||
'updated_at',
|
'updated_at',
|
||||||
)
|
)
|
||||||
|
|
||||||
for r_id, establishment_id, new_date in queryset:
|
for r_id, establishment_id, new_date in tqdm(queryset):
|
||||||
try:
|
try:
|
||||||
review_id, date = valid_reviews[establishment_id]
|
review_id, date = valid_reviews[establishment_id]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
|
@ -41,7 +41,7 @@ class Command(BaseCommand):
|
||||||
'text',
|
'text',
|
||||||
)
|
)
|
||||||
|
|
||||||
for es_id, locale, text in text_qs:
|
for es_id, locale, text in tqdm(text_qs):
|
||||||
establishment = Establishment.objects.filter(old_id=es_id).first()
|
establishment = Establishment.objects.filter(old_id=es_id).first()
|
||||||
if establishment:
|
if establishment:
|
||||||
description = establishment.description
|
description = establishment.description
|
||||||
|
|
@ -53,7 +53,7 @@ class Command(BaseCommand):
|
||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
# Если нет en-GB в поле
|
# Если нет en-GB в поле
|
||||||
for establishment in Establishment.objects.filter(old_id__isnull=False):
|
for establishment in tqdm(Establishment.objects.filter(old_id__isnull=False)):
|
||||||
description = establishment.description
|
description = establishment.description
|
||||||
if len(description) and 'en-GB' not in description:
|
if len(description) and 'en-GB' not in description:
|
||||||
description.update({
|
description.update({
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
from establishment.models import Establishment
|
||||||
|
from transfer.models import Descriptions
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = """Add description to establishment from old db."""
|
||||||
|
|
||||||
|
def handle(self, *args, **kwarg):
|
||||||
|
establishments = Establishment.objects.exclude(old_id__isnull=True)
|
||||||
|
|
||||||
|
self.stdout.write(self.style.WARNING(f'Clear old descriptions'))
|
||||||
|
for item in tqdm(establishments):
|
||||||
|
item.description = None
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
queryset = Descriptions.objects.filter(
|
||||||
|
establishment_id__in=list(establishments.values_list('old_id', flat=True)),
|
||||||
|
).values_list('establishment_id', 'locale', 'text')
|
||||||
|
|
||||||
|
self.stdout.write(self.style.WARNING(f'Update new description'))
|
||||||
|
for establishment_id, locale, text in tqdm(queryset):
|
||||||
|
establishment = Establishment.objects.filter(old_id=establishment_id).first()
|
||||||
|
if establishment:
|
||||||
|
if establishment.description:
|
||||||
|
establishment.description.update({
|
||||||
|
locale: text
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
establishment.description = {locale: text}
|
||||||
|
establishment.save()
|
||||||
|
|
||||||
|
self.stdout.write(self.style.WARNING(f'Update en-GB description'))
|
||||||
|
for establishment in tqdm(establishments.filter(description__isnull=False)):
|
||||||
|
description = establishment.description
|
||||||
|
if len(description) and 'en-GB' not in description:
|
||||||
|
description.update({
|
||||||
|
'en-GB': next(iter(description.values()))
|
||||||
|
})
|
||||||
|
establishment.description = description
|
||||||
|
establishment.save()
|
||||||
|
|
||||||
|
self.stdout.write(self.style.WARNING(f'Done'))
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
import boto3
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from establishment.models import EstablishmentSubType
|
||||||
|
from gallery.models import Image
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = """
|
||||||
|
Fill establishment type by index names.
|
||||||
|
Steps:
|
||||||
|
1 Upload default images into s3 bucket
|
||||||
|
2 Run command ./manage.py fill_artisan_default_image
|
||||||
|
"""
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
'--template_image_folder_name',
|
||||||
|
help='Template image folder in Amazon S3 bucket'
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
not_updated = 0
|
||||||
|
template_image_folder_name = kwargs.get('template_image_folder_name')
|
||||||
|
if (template_image_folder_name and
|
||||||
|
hasattr(settings, 'AWS_ACCESS_KEY_ID') and
|
||||||
|
hasattr(settings, 'AWS_SECRET_ACCESS_KEY') and
|
||||||
|
hasattr(settings, 'AWS_STORAGE_BUCKET_NAME')):
|
||||||
|
to_update = []
|
||||||
|
s3 = boto3.resource('s3')
|
||||||
|
s3_bucket = s3.Bucket(settings.AWS_STORAGE_BUCKET_NAME)
|
||||||
|
|
||||||
|
for object_summary in s3_bucket.objects.filter(Prefix=f'media/{template_image_folder_name}/'):
|
||||||
|
uri_path = object_summary.key
|
||||||
|
filename = uri_path.split('/')[-1:][0]
|
||||||
|
if filename:
|
||||||
|
artisan_index_slice = filename.split('.')[:-1][0] \
|
||||||
|
.split('_')[2:]
|
||||||
|
if len(artisan_index_slice) > 1:
|
||||||
|
artisan_index_name = '_'.join(artisan_index_slice)
|
||||||
|
else:
|
||||||
|
artisan_index_name = artisan_index_slice[0]
|
||||||
|
|
||||||
|
attachment_suffix_url = f'{template_image_folder_name}/{filename}'
|
||||||
|
|
||||||
|
# check artisan in db
|
||||||
|
artisan_qs = EstablishmentSubType.objects.filter(index_name__iexact=artisan_index_name,
|
||||||
|
establishment_type__index_name__iexact='artisan')
|
||||||
|
if artisan_qs.exists():
|
||||||
|
artisan = artisan_qs.first()
|
||||||
|
image, created = Image.objects.get_or_create(image=attachment_suffix_url,
|
||||||
|
defaults={
|
||||||
|
'image': attachment_suffix_url,
|
||||||
|
'orientation': Image.HORIZONTAL,
|
||||||
|
'title': f'{artisan.__str__()} '
|
||||||
|
f'{artisan.id} - '
|
||||||
|
f'{attachment_suffix_url}'})
|
||||||
|
if created:
|
||||||
|
# update artisan instance
|
||||||
|
artisan.default_image = image
|
||||||
|
to_update.append(artisan)
|
||||||
|
else:
|
||||||
|
not_updated += 1
|
||||||
|
|
||||||
|
EstablishmentSubType.objects.bulk_update(to_update, ['default_image', ])
|
||||||
|
self.stdout.write(self.style.WARNING(f'Updated {len(to_update)} objects.'))
|
||||||
|
self.stdout.write(self.style.WARNING(f'Not updated {not_updated} objects.'))
|
||||||
18
apps/establishment/migrations/0068_auto_20191220_0914.py
Normal file
18
apps/establishment/migrations/0068_auto_20191220_0914.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-20 09:14
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('establishment', '0067_auto_20191122_1244'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='establishment',
|
||||||
|
name='schedule',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='schedule', to='timetable.Timetable', verbose_name='Establishment schedule'),
|
||||||
|
),
|
||||||
|
]
|
||||||
25
apps/establishment/migrations/0069_auto_20191220_1007.py
Normal file
25
apps/establishment/migrations/0069_auto_20191220_1007.py
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-20 10:07
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('gallery', '0007_auto_20191211_1528'),
|
||||||
|
('establishment', '0068_auto_20191220_0914'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='establishmentsubtype',
|
||||||
|
name='default_image',
|
||||||
|
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='establishment_sub_types', to='gallery.Image', verbose_name='default image'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='establishmenttype',
|
||||||
|
name='default_image',
|
||||||
|
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='establishment_types', to='gallery.Image', verbose_name='default image'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -27,13 +27,13 @@ from main.models import Award, Currency
|
||||||
from review.models import Review
|
from review.models import Review
|
||||||
from tag.models import Tag
|
from tag.models import Tag
|
||||||
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
|
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
|
||||||
TranslatedFieldsMixin, BaseAttributes, GalleryModelMixin,
|
TranslatedFieldsMixin, BaseAttributes, GalleryMixin,
|
||||||
IntermediateGalleryModelMixin, HasTagsMixin,
|
IntermediateGalleryModelMixin, HasTagsMixin,
|
||||||
FavoritesMixin)
|
FavoritesMixin, TypeDefaultImageMixin)
|
||||||
|
|
||||||
|
|
||||||
# todo: establishment type&subtypes check
|
# todo: establishment type&subtypes check
|
||||||
class EstablishmentType(TranslatedFieldsMixin, ProjectBaseMixin):
|
class EstablishmentType(TypeDefaultImageMixin, TranslatedFieldsMixin, ProjectBaseMixin):
|
||||||
"""Establishment type model."""
|
"""Establishment type model."""
|
||||||
|
|
||||||
STR_FIELD_NAME = 'name'
|
STR_FIELD_NAME = 'name'
|
||||||
|
|
@ -51,6 +51,10 @@ class EstablishmentType(TranslatedFieldsMixin, ProjectBaseMixin):
|
||||||
tag_categories = models.ManyToManyField('tag.TagCategory',
|
tag_categories = models.ManyToManyField('tag.TagCategory',
|
||||||
related_name='establishment_types',
|
related_name='establishment_types',
|
||||||
verbose_name=_('Tag'))
|
verbose_name=_('Tag'))
|
||||||
|
default_image = models.ForeignKey('gallery.Image', on_delete=models.SET_NULL,
|
||||||
|
related_name='establishment_types',
|
||||||
|
blank=True, null=True, default=None,
|
||||||
|
verbose_name='default image')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
@ -69,7 +73,7 @@ class EstablishmentSubTypeManager(models.Manager):
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
class EstablishmentSubType(ProjectBaseMixin, TranslatedFieldsMixin):
|
class EstablishmentSubType(TypeDefaultImageMixin, TranslatedFieldsMixin, ProjectBaseMixin):
|
||||||
"""Establishment type model."""
|
"""Establishment type model."""
|
||||||
|
|
||||||
# EXAMPLE OF INDEX NAME CHOICES
|
# EXAMPLE OF INDEX NAME CHOICES
|
||||||
|
|
@ -85,6 +89,10 @@ class EstablishmentSubType(ProjectBaseMixin, TranslatedFieldsMixin):
|
||||||
tag_categories = models.ManyToManyField('tag.TagCategory',
|
tag_categories = models.ManyToManyField('tag.TagCategory',
|
||||||
related_name='establishment_subtypes',
|
related_name='establishment_subtypes',
|
||||||
verbose_name=_('Tag'))
|
verbose_name=_('Tag'))
|
||||||
|
default_image = models.ForeignKey('gallery.Image', on_delete=models.SET_NULL,
|
||||||
|
related_name='establishment_sub_types',
|
||||||
|
blank=True, null=True, default=None,
|
||||||
|
verbose_name='default image')
|
||||||
|
|
||||||
objects = EstablishmentSubTypeManager()
|
objects = EstablishmentSubTypeManager()
|
||||||
|
|
||||||
|
|
@ -105,7 +113,7 @@ class EstablishmentQuerySet(models.QuerySet):
|
||||||
def with_base_related(self):
|
def with_base_related(self):
|
||||||
"""Return qs with related objects."""
|
"""Return qs with related objects."""
|
||||||
return self.select_related('address', 'establishment_type'). \
|
return self.select_related('address', 'establishment_type'). \
|
||||||
prefetch_related('tags')
|
prefetch_related('tags', 'tags__translation')
|
||||||
|
|
||||||
def with_schedule(self):
|
def with_schedule(self):
|
||||||
"""Return qs with related schedule."""
|
"""Return qs with related schedule."""
|
||||||
|
|
@ -221,15 +229,16 @@ class EstablishmentQuerySet(models.QuerySet):
|
||||||
Return filtered QuerySet by base filters.
|
Return filtered QuerySet by base filters.
|
||||||
Filters including:
|
Filters including:
|
||||||
1 Filter by type (and subtype) establishment.
|
1 Filter by type (and subtype) establishment.
|
||||||
2 Filter by published Review.
|
2 With annotated distance.
|
||||||
3 With annotated distance.
|
3 By country
|
||||||
"""
|
"""
|
||||||
filters = {
|
filters = {
|
||||||
'reviews__status': Review.READY,
|
|
||||||
'establishment_type': establishment.establishment_type,
|
'establishment_type': establishment.establishment_type,
|
||||||
|
'address__city__country': establishment.address.city.country
|
||||||
}
|
}
|
||||||
if establishment.establishment_subtypes.exists():
|
if establishment.establishment_subtypes.exists():
|
||||||
filters.update({'establishment_subtypes__in': establishment.establishment_subtypes.all()})
|
filters.update({'establishment_subtypes__in': establishment.establishment_subtypes.all()})
|
||||||
|
|
||||||
return self.exclude(id=establishment.id) \
|
return self.exclude(id=establishment.id) \
|
||||||
.filter(**filters) \
|
.filter(**filters) \
|
||||||
.annotate_distance(point=establishment.location)
|
.annotate_distance(point=establishment.location)
|
||||||
|
|
@ -244,32 +253,31 @@ class EstablishmentQuerySet(models.QuerySet):
|
||||||
return Subquery(
|
return Subquery(
|
||||||
self.similar_base(establishment)
|
self.similar_base(establishment)
|
||||||
.filter(**filters)
|
.filter(**filters)
|
||||||
.order_by('distance')[:settings.LIMITING_QUERY_OBJECTS]
|
.order_by('distance')
|
||||||
.values('id')
|
.distinct()
|
||||||
|
.values_list('id', flat=True)[:settings.LIMITING_QUERY_OBJECTS]
|
||||||
)
|
)
|
||||||
|
|
||||||
def similar_restaurants(self, slug):
|
def similar_restaurants(self, restaurant):
|
||||||
"""
|
"""
|
||||||
Return QuerySet with objects that similar to Restaurant.
|
Return QuerySet with objects that similar to Restaurant.
|
||||||
:param slug: str restaurant slug
|
:param restaurant: Establishment instance.
|
||||||
"""
|
"""
|
||||||
restaurant_qs = self.filter(slug=slug)
|
|
||||||
if restaurant_qs.exists():
|
|
||||||
restaurant = restaurant_qs.first()
|
|
||||||
ids_by_subquery = self.similar_base_subquery(
|
ids_by_subquery = self.similar_base_subquery(
|
||||||
establishment=restaurant,
|
establishment=restaurant,
|
||||||
filters={
|
filters={
|
||||||
|
'reviews__status': Review.READY,
|
||||||
'public_mark__gte': 10,
|
'public_mark__gte': 10,
|
||||||
'establishment_gallery__is_main': True,
|
'establishment_gallery__is_main': True,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return self.filter(id__in=ids_by_subquery) \
|
# todo: fix this - replace ids_by_subquery.queryset on ids_by_subquery
|
||||||
|
return self.filter(id__in=ids_by_subquery.queryset) \
|
||||||
.annotate_intermediate_public_mark() \
|
.annotate_intermediate_public_mark() \
|
||||||
.annotate_mark_similarity(mark=restaurant.public_mark) \
|
.annotate_mark_similarity(mark=restaurant.public_mark) \
|
||||||
.order_by('mark_similarity') \
|
.order_by('mark_similarity') \
|
||||||
.distinct('mark_similarity', 'id')
|
.distinct('mark_similarity', 'id')
|
||||||
else:
|
|
||||||
return self.none()
|
|
||||||
|
|
||||||
def same_subtype(self, establishment):
|
def same_subtype(self, establishment):
|
||||||
"""Annotate flag same subtype."""
|
"""Annotate flag same subtype."""
|
||||||
|
|
@ -282,21 +290,17 @@ class EstablishmentQuerySet(models.QuerySet):
|
||||||
output_field=models.BooleanField(default=False)
|
output_field=models.BooleanField(default=False)
|
||||||
))
|
))
|
||||||
|
|
||||||
def similar_artisans_producers(self, slug):
|
def similar_artisans_producers(self, establishment):
|
||||||
"""
|
"""
|
||||||
Return QuerySet with objects that similar to Artisan/Producer(s).
|
Return QuerySet with objects that similar to Artisan/Producer(s).
|
||||||
:param slug: str artisan/producer slug
|
:param establishment: Establishment instance
|
||||||
"""
|
"""
|
||||||
establishment_qs = self.filter(slug=slug)
|
|
||||||
if establishment_qs.exists():
|
|
||||||
establishment = establishment_qs.first()
|
|
||||||
return self.similar_base(establishment) \
|
return self.similar_base(establishment) \
|
||||||
.same_subtype(establishment) \
|
.same_subtype(establishment) \
|
||||||
|
.has_published_reviews() \
|
||||||
.order_by(F('same_subtype').desc(),
|
.order_by(F('same_subtype').desc(),
|
||||||
F('distance').asc()) \
|
F('distance').asc()) \
|
||||||
.distinct('same_subtype', 'distance', 'id')
|
.distinct('same_subtype', 'distance', 'id')
|
||||||
else:
|
|
||||||
return self.none()
|
|
||||||
|
|
||||||
def by_wine_region(self, wine_region):
|
def by_wine_region(self, wine_region):
|
||||||
"""
|
"""
|
||||||
|
|
@ -312,23 +316,19 @@ class EstablishmentQuerySet(models.QuerySet):
|
||||||
"""
|
"""
|
||||||
return self.filter(wine_origin__wine_sub_region=wine_sub_region).distinct()
|
return self.filter(wine_origin__wine_sub_region=wine_sub_region).distinct()
|
||||||
|
|
||||||
def similar_wineries(self, slug: str):
|
def similar_wineries(self, winery):
|
||||||
"""
|
"""
|
||||||
Return QuerySet with objects that similar to Winery.
|
Return QuerySet with objects that similar to Winery.
|
||||||
:param establishment_slug: str Establishment slug
|
:param winery: Establishment instance
|
||||||
"""
|
"""
|
||||||
winery_qs = self.filter(slug=slug)
|
|
||||||
if winery_qs.exists():
|
|
||||||
winery = winery_qs.first()
|
|
||||||
return self.similar_base(winery) \
|
return self.similar_base(winery) \
|
||||||
.order_by(F('wine_origins__wine_region').asc(),
|
.order_by(F('wine_origins__wine_region').asc(),
|
||||||
F('wine_origins__wine_sub_region').asc()) \
|
F('wine_origins__wine_sub_region').asc(),
|
||||||
.annotate_distance(point=winery.location) \
|
F('distance').asc()) \
|
||||||
.order_by('distance') \
|
.distinct('wine_origins__wine_region',
|
||||||
.distinct('distance', 'wine_origins__wine_region',
|
'wine_origins__wine_sub_region',
|
||||||
'wine_origins__wine_sub_region', 'id')
|
'distance',
|
||||||
else:
|
'id')
|
||||||
return self.none()
|
|
||||||
|
|
||||||
def last_reviewed(self, point: Point):
|
def last_reviewed(self, point: Point):
|
||||||
"""
|
"""
|
||||||
|
|
@ -433,7 +433,7 @@ class EstablishmentQuerySet(models.QuerySet):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin,
|
class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin,
|
||||||
TranslatedFieldsMixin, HasTagsMixin, FavoritesMixin):
|
TranslatedFieldsMixin, HasTagsMixin, FavoritesMixin):
|
||||||
"""Establishment model."""
|
"""Establishment model."""
|
||||||
|
|
||||||
|
|
@ -483,7 +483,7 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin,
|
||||||
booking = models.URLField(blank=True, null=True, default=None, max_length=255,
|
booking = models.URLField(blank=True, null=True, default=None, max_length=255,
|
||||||
verbose_name=_('Booking URL'))
|
verbose_name=_('Booking URL'))
|
||||||
is_publish = models.BooleanField(default=False, verbose_name=_('Publish status'))
|
is_publish = models.BooleanField(default=False, verbose_name=_('Publish status'))
|
||||||
schedule = models.ManyToManyField(to='timetable.Timetable',
|
schedule = models.ManyToManyField(to='timetable.Timetable', blank=True,
|
||||||
verbose_name=_('Establishment schedule'),
|
verbose_name=_('Establishment schedule'),
|
||||||
related_name='schedule')
|
related_name='schedule')
|
||||||
# holidays_from = models.DateTimeField(verbose_name=_('Holidays from'),
|
# holidays_from = models.DateTimeField(verbose_name=_('Holidays from'),
|
||||||
|
|
@ -532,12 +532,6 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin,
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'id:{self.id}-{self.name}'
|
return f'id:{self.id}-{self.name}'
|
||||||
|
|
||||||
def clean_fields(self, exclude=None):
|
|
||||||
super().clean_fields(exclude)
|
|
||||||
if self.purchased_products.filter(product_type__index_name='souvenir').exists():
|
|
||||||
raise ValidationError(
|
|
||||||
_('Only souvenirs.'))
|
|
||||||
|
|
||||||
def delete(self, using=None, keep_parents=False):
|
def delete(self, using=None, keep_parents=False):
|
||||||
"""Overridden delete method"""
|
"""Overridden delete method"""
|
||||||
# Delete all related companies
|
# Delete all related companies
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,8 @@ class MenuRUDSerializers(ProjectModelSerializer):
|
||||||
class EstablishmentTypeBaseSerializer(serializers.ModelSerializer):
|
class EstablishmentTypeBaseSerializer(serializers.ModelSerializer):
|
||||||
"""Serializer for EstablishmentType model."""
|
"""Serializer for EstablishmentType model."""
|
||||||
name_translated = TranslatedField()
|
name_translated = TranslatedField()
|
||||||
|
default_image_url = serializers.ImageField(source='default_image.image',
|
||||||
|
allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
@ -107,6 +109,7 @@ class EstablishmentTypeBaseSerializer(serializers.ModelSerializer):
|
||||||
'name_translated',
|
'name_translated',
|
||||||
'use_subtypes',
|
'use_subtypes',
|
||||||
'index_name',
|
'index_name',
|
||||||
|
'default_image_url',
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'name': {'write_only': True},
|
'name': {'write_only': True},
|
||||||
|
|
@ -129,8 +132,9 @@ class EstablishmentTypeGeoSerializer(EstablishmentTypeBaseSerializer):
|
||||||
|
|
||||||
class EstablishmentSubTypeBaseSerializer(serializers.ModelSerializer):
|
class EstablishmentSubTypeBaseSerializer(serializers.ModelSerializer):
|
||||||
"""Serializer for EstablishmentSubType models."""
|
"""Serializer for EstablishmentSubType models."""
|
||||||
|
|
||||||
name_translated = TranslatedField()
|
name_translated = TranslatedField()
|
||||||
|
default_image_url = serializers.ImageField(source='default_image.image',
|
||||||
|
allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
@ -141,6 +145,7 @@ class EstablishmentSubTypeBaseSerializer(serializers.ModelSerializer):
|
||||||
'name_translated',
|
'name_translated',
|
||||||
'establishment_type',
|
'establishment_type',
|
||||||
'index_name',
|
'index_name',
|
||||||
|
'default_image_url',
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'name': {'write_only': True},
|
'name': {'write_only': True},
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,20 @@ class EstablishmentListView(EstablishmentMixinView, generics.ListAPIView):
|
||||||
class EstablishmentSimilarView(EstablishmentListView):
|
class EstablishmentSimilarView(EstablishmentListView):
|
||||||
"""Resource for getting a list of similar establishments."""
|
"""Resource for getting a list of similar establishments."""
|
||||||
serializer_class = serializers.EstablishmentSimilarSerializer
|
serializer_class = serializers.EstablishmentSimilarSerializer
|
||||||
pagination_class = PortionPagination
|
pagination_class = None
|
||||||
|
|
||||||
|
def get_base_object(self):
|
||||||
|
"""
|
||||||
|
Return base establishment instance for a getting list of similar establishments.
|
||||||
|
"""
|
||||||
|
establishment = get_object_or_404(models.Establishment.objects.all(),
|
||||||
|
slug=self.kwargs.get('slug'))
|
||||||
|
return establishment
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""Overridden get_queryset method."""
|
||||||
|
return EstablishmentMixinView.get_queryset(self) \
|
||||||
|
.has_location()
|
||||||
|
|
||||||
|
|
||||||
class EstablishmentRetrieveView(EstablishmentMixinView, generics.RetrieveAPIView):
|
class EstablishmentRetrieveView(EstablishmentMixinView, generics.RetrieveAPIView):
|
||||||
|
|
@ -88,9 +101,14 @@ class RestaurantSimilarListView(EstablishmentSimilarView):
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Overridden get_queryset method"""
|
"""Overridden get_queryset method"""
|
||||||
|
qs = super(RestaurantSimilarListView, self).get_queryset()
|
||||||
|
base_establishment = self.get_base_object()
|
||||||
|
|
||||||
|
if base_establishment:
|
||||||
|
return qs.similar_restaurants(base_establishment)[:settings.QUERY_OUTPUT_OBJECTS]
|
||||||
|
else:
|
||||||
return EstablishmentMixinView.get_queryset(self) \
|
return EstablishmentMixinView.get_queryset(self) \
|
||||||
.has_location() \
|
.none()
|
||||||
.similar_restaurants(slug=self.kwargs.get('slug'))
|
|
||||||
|
|
||||||
|
|
||||||
class WinerySimilarListView(EstablishmentSimilarView):
|
class WinerySimilarListView(EstablishmentSimilarView):
|
||||||
|
|
@ -98,9 +116,13 @@ class WinerySimilarListView(EstablishmentSimilarView):
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Overridden get_queryset method"""
|
"""Overridden get_queryset method"""
|
||||||
return EstablishmentMixinView.get_queryset(self) \
|
qs = EstablishmentSimilarView.get_queryset(self)
|
||||||
.has_location() \
|
base_establishment = self.get_base_object()
|
||||||
.similar_wineries(slug=self.kwargs.get('slug'))
|
|
||||||
|
if base_establishment:
|
||||||
|
return qs.similar_wineries(base_establishment)[:settings.QUERY_OUTPUT_OBJECTS]
|
||||||
|
else:
|
||||||
|
return qs.none()
|
||||||
|
|
||||||
|
|
||||||
class ArtisanProducerSimilarListView(EstablishmentSimilarView):
|
class ArtisanProducerSimilarListView(EstablishmentSimilarView):
|
||||||
|
|
@ -108,9 +130,13 @@ class ArtisanProducerSimilarListView(EstablishmentSimilarView):
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Overridden get_queryset method"""
|
"""Overridden get_queryset method"""
|
||||||
return EstablishmentMixinView.get_queryset(self) \
|
qs = super(ArtisanProducerSimilarListView, self).get_queryset()
|
||||||
.has_location() \
|
base_establishment = self.get_base_object()
|
||||||
.similar_artisans_producers(slug=self.kwargs.get('slug'))
|
|
||||||
|
if base_establishment:
|
||||||
|
return qs.similar_artisans_producers(base_establishment)[:settings.QUERY_OUTPUT_OBJECTS]
|
||||||
|
else:
|
||||||
|
return qs.none()
|
||||||
|
|
||||||
|
|
||||||
class EstablishmentTypeListView(generics.ListAPIView):
|
class EstablishmentTypeListView(generics.ListAPIView):
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,6 @@ class BaseTestCase(APITestCase):
|
||||||
title={"en-GB": "Test news"},
|
title={"en-GB": "Test news"},
|
||||||
news_type=self.test_news_type,
|
news_type=self.test_news_type,
|
||||||
description={"en-GB": "Description test news"},
|
description={"en-GB": "Description test news"},
|
||||||
start=datetime.fromisoformat("2020-12-03 12:00:00"),
|
|
||||||
end=datetime.fromisoformat("2020-12-03 12:00:00"),
|
end=datetime.fromisoformat("2020-12-03 12:00:00"),
|
||||||
state=News.PUBLISHED,
|
state=News.PUBLISHED,
|
||||||
slugs={'en-GB': 'test-news'}
|
slugs={'en-GB': 'test-news'}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from sorl.thumbnail import delete
|
from sorl import thumbnail
|
||||||
from sorl.thumbnail.fields import ImageField as SORLImageField
|
from sorl.thumbnail.fields import ImageField as SORLImageField
|
||||||
|
|
||||||
from utils.methods import image_path
|
from utils.methods import image_path
|
||||||
|
|
@ -47,7 +47,7 @@ class Image(ProjectBaseMixin, SORLImageMixin, PlatformMixin):
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Delete from remote storage
|
# Delete from remote storage
|
||||||
delete(file_=self.image.file, delete_file=completely)
|
thumbnail.delete(file_=self.image.file, delete_file=completely)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
from django.conf import settings
|
|
||||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from sorl.thumbnail import get_thumbnail
|
from sorl.thumbnail import get_thumbnail
|
||||||
from sorl.thumbnail.parsers import parse_crop, ThumbnailParseError
|
from sorl.thumbnail.parsers import parse_crop, ThumbnailParseError
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.conf import settings
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -36,6 +35,15 @@ class ImageSerializer(serializers.ModelSerializer):
|
||||||
'orientation': {'write_only': True}
|
'orientation': {'write_only': True}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
"""Overridden validate method."""
|
||||||
|
image = attrs.get('image')
|
||||||
|
|
||||||
|
if image and image.size >= settings.FILE_UPLOAD_MAX_MEMORY_SIZE:
|
||||||
|
raise serializers.ValidationError({'detail': _('File size too large: %s bytes') % image.size})
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
class CropImageSerializer(ImageSerializer):
|
class CropImageSerializer(ImageSerializer):
|
||||||
"""Serializers for image crops."""
|
"""Serializers for image crops."""
|
||||||
|
|
|
||||||
|
|
@ -22,3 +22,34 @@ class CityBackFilter(filters.FilterSet):
|
||||||
if value not in EMPTY_VALUES:
|
if value not in EMPTY_VALUES:
|
||||||
return queryset.search_by_name(value)
|
return queryset.search_by_name(value)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
class RegionFilter(filters.FilterSet):
|
||||||
|
"""Region filter set."""
|
||||||
|
|
||||||
|
country_id = filters.CharFilter()
|
||||||
|
sub_regions_by_region_id = filters.CharFilter(method='by_region')
|
||||||
|
without_parent_region = filters.BooleanFilter(method='by_parent_region')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta class."""
|
||||||
|
model = models.Region
|
||||||
|
fields = (
|
||||||
|
'country_id',
|
||||||
|
'sub_regions_by_region_id',
|
||||||
|
'without_parent_region',
|
||||||
|
)
|
||||||
|
|
||||||
|
def by_region(self, queryset, name, value):
|
||||||
|
"""Search regions by sub region id."""
|
||||||
|
if value not in EMPTY_VALUES:
|
||||||
|
return queryset.sub_regions_by_region_id(value)
|
||||||
|
|
||||||
|
def by_parent_region(self, queryset, name, value):
|
||||||
|
"""
|
||||||
|
Search if region instance has a parent region..
|
||||||
|
If True then show only Regions
|
||||||
|
Otherwise show only Sub regions.
|
||||||
|
"""
|
||||||
|
if value not in EMPTY_VALUES:
|
||||||
|
return queryset.without_parent_region(value)
|
||||||
|
|
|
||||||
18
apps/location/migrations/0032_auto_20191220_1019.py
Normal file
18
apps/location/migrations/0032_auto_20191220_1019.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-20 10:19
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('location', '0031_establishmentwineoriginaddress_wineoriginaddress'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='address',
|
||||||
|
name='number',
|
||||||
|
field=models.IntegerField(blank=True, default=0, verbose_name='number'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -12,7 +12,7 @@ from typing import List
|
||||||
from translation.models import Language
|
from translation.models import Language
|
||||||
from utils.models import (ProjectBaseMixin, SVGImageMixin, TJSONField,
|
from utils.models import (ProjectBaseMixin, SVGImageMixin, TJSONField,
|
||||||
TranslatedFieldsMixin, get_current_locale,
|
TranslatedFieldsMixin, get_current_locale,
|
||||||
IntermediateGalleryModelMixin, GalleryModelMixin)
|
IntermediateGalleryModelMixin, GalleryMixin)
|
||||||
|
|
||||||
|
|
||||||
class CountryQuerySet(models.QuerySet):
|
class CountryQuerySet(models.QuerySet):
|
||||||
|
|
@ -70,6 +70,26 @@ class Country(TranslatedFieldsMixin, SVGImageMixin, ProjectBaseMixin):
|
||||||
return str_name
|
return str_name
|
||||||
|
|
||||||
|
|
||||||
|
class RegionQuerySet(models.QuerySet):
|
||||||
|
"""QuerySet for model Region."""
|
||||||
|
|
||||||
|
def without_parent_region(self, switcher: bool = True):
|
||||||
|
"""Filter regions by parent region."""
|
||||||
|
return self.filter(parent_region__isnull=switcher)
|
||||||
|
|
||||||
|
def by_region_id(self, region_id):
|
||||||
|
"""Filter regions by region id."""
|
||||||
|
return self.filter(id=region_id)
|
||||||
|
|
||||||
|
def by_sub_region_id(self, sub_region_id):
|
||||||
|
"""Filter sub regions by sub region id."""
|
||||||
|
return self.filter(parent_region_id=sub_region_id)
|
||||||
|
|
||||||
|
def sub_regions_by_region_id(self, region_id):
|
||||||
|
"""Filter regions by sub region id."""
|
||||||
|
return self.filter(parent_region_id=region_id)
|
||||||
|
|
||||||
|
|
||||||
class Region(models.Model):
|
class Region(models.Model):
|
||||||
"""Region model."""
|
"""Region model."""
|
||||||
|
|
||||||
|
|
@ -82,6 +102,8 @@ class Region(models.Model):
|
||||||
Country, verbose_name=_('country'), on_delete=models.CASCADE)
|
Country, verbose_name=_('country'), on_delete=models.CASCADE)
|
||||||
old_id = models.IntegerField(null=True, blank=True, default=None)
|
old_id = models.IntegerField(null=True, blank=True, default=None)
|
||||||
|
|
||||||
|
objects = RegionQuerySet.as_manager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
||||||
|
|
@ -112,7 +134,7 @@ class CityQuerySet(models.QuerySet):
|
||||||
return self.filter(country__code=code)
|
return self.filter(country__code=code)
|
||||||
|
|
||||||
|
|
||||||
class City(GalleryModelMixin):
|
class City(GalleryMixin, models.Model):
|
||||||
"""Region model."""
|
"""Region model."""
|
||||||
name = models.CharField(_('name'), max_length=250)
|
name = models.CharField(_('name'), max_length=250)
|
||||||
code = models.CharField(_('code'), max_length=250)
|
code = models.CharField(_('code'), max_length=250)
|
||||||
|
|
@ -163,7 +185,7 @@ class Address(models.Model):
|
||||||
_('street name 1'), max_length=500, blank=True, default='')
|
_('street name 1'), max_length=500, blank=True, default='')
|
||||||
street_name_2 = models.CharField(
|
street_name_2 = models.CharField(
|
||||||
_('street name 2'), max_length=500, blank=True, default='')
|
_('street name 2'), max_length=500, blank=True, default='')
|
||||||
number = models.IntegerField(_('number'))
|
number = models.IntegerField(_('number'), blank=True, default=0)
|
||||||
postal_code = models.CharField(
|
postal_code = models.CharField(
|
||||||
_('postal code'), max_length=10, blank=True,
|
_('postal code'), max_length=10, blank=True,
|
||||||
default='', help_text=_('Ex.: 350018'))
|
default='', help_text=_('Ex.: 350018'))
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,6 @@ class CitySerializer(serializers.ModelSerializer):
|
||||||
fields = [
|
fields = [
|
||||||
'id',
|
'id',
|
||||||
'name',
|
'name',
|
||||||
'code',
|
|
||||||
'region',
|
'region',
|
||||||
'region_id',
|
'region_id',
|
||||||
'country_id',
|
'country_id',
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,11 @@ from utils.views import CreateDestroyGalleryViewMixin
|
||||||
from rest_framework.permissions import IsAuthenticatedOrReadOnly
|
from rest_framework.permissions import IsAuthenticatedOrReadOnly
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from utils.serializers import ImageBaseSerializer
|
from utils.serializers import ImageBaseSerializer
|
||||||
|
from location.filters import RegionFilter
|
||||||
|
|
||||||
from location import filters
|
from location import filters
|
||||||
|
|
||||||
|
|
||||||
# Address
|
# Address
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -18,29 +20,36 @@ class AddressListCreateView(common.AddressViewMixin, generics.ListCreateAPIView)
|
||||||
"""Create view for model Address."""
|
"""Create view for model Address."""
|
||||||
serializer_class = serializers.AddressDetailSerializer
|
serializer_class = serializers.AddressDetailSerializer
|
||||||
queryset = models.Address.objects.all()
|
queryset = models.Address.objects.all()
|
||||||
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
|
permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
|
||||||
|
|
||||||
|
|
||||||
class AddressRUDView(common.AddressViewMixin, generics.RetrieveUpdateDestroyAPIView):
|
class AddressRUDView(common.AddressViewMixin, generics.RetrieveUpdateDestroyAPIView):
|
||||||
"""RUD view for model Address."""
|
"""RUD view for model Address."""
|
||||||
serializer_class = serializers.AddressDetailSerializer
|
serializer_class = serializers.AddressDetailSerializer
|
||||||
queryset = models.Address.objects.all()
|
queryset = models.Address.objects.all()
|
||||||
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
|
permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
|
||||||
|
|
||||||
|
|
||||||
# City
|
# City
|
||||||
class CityListCreateView(common.CityViewMixin, generics.ListCreateAPIView):
|
class CityListCreateView(common.CityViewMixin, generics.ListCreateAPIView):
|
||||||
"""Create view for model City."""
|
"""Create view for model City."""
|
||||||
serializer_class = serializers.CitySerializer
|
serializer_class = serializers.CitySerializer
|
||||||
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
|
permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
|
||||||
queryset = models.City.objects.all()
|
# queryset = models.City.objects.all()
|
||||||
filter_class = filters.CityBackFilter
|
filter_class = filters.CityBackFilter
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""Overridden method 'get_queryset'."""
|
||||||
|
qs = models.City.objects.all()
|
||||||
|
if self.request.country_code:
|
||||||
|
qs = qs.by_country_code(self.request.country_code)
|
||||||
|
return qs
|
||||||
|
|
||||||
|
|
||||||
class CityListSearchView(common.CityViewMixin, generics.ListCreateAPIView):
|
class CityListSearchView(common.CityViewMixin, generics.ListCreateAPIView):
|
||||||
"""Create view for model City."""
|
"""Create view for model City."""
|
||||||
serializer_class = serializers.CitySerializer
|
serializer_class = serializers.CitySerializer
|
||||||
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
|
permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
|
||||||
queryset = models.City.objects.all()
|
queryset = models.City.objects.all()
|
||||||
filter_class = filters.CityBackFilter
|
filter_class = filters.CityBackFilter
|
||||||
pagination_class = None
|
pagination_class = None
|
||||||
|
|
@ -49,14 +58,14 @@ class CityListSearchView(common.CityViewMixin, generics.ListCreateAPIView):
|
||||||
class CityRUDView(common.CityViewMixin, generics.RetrieveUpdateDestroyAPIView):
|
class CityRUDView(common.CityViewMixin, generics.RetrieveUpdateDestroyAPIView):
|
||||||
"""RUD view for model City."""
|
"""RUD view for model City."""
|
||||||
serializer_class = serializers.CitySerializer
|
serializer_class = serializers.CitySerializer
|
||||||
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
|
permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
|
||||||
|
|
||||||
|
|
||||||
class CityGalleryCreateDestroyView(common.CityViewMixin,
|
class CityGalleryCreateDestroyView(common.CityViewMixin,
|
||||||
CreateDestroyGalleryViewMixin):
|
CreateDestroyGalleryViewMixin):
|
||||||
"""Resource for a create gallery for product for back-office users."""
|
"""Resource for a create gallery for product for back-office users."""
|
||||||
serializer_class = serializers.CityGallerySerializer
|
serializer_class = serializers.CityGallerySerializer
|
||||||
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
|
permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
|
||||||
|
|
||||||
def get_object(self):
|
def get_object(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -77,7 +86,7 @@ class CityGalleryListView(common.CityViewMixin,
|
||||||
generics.ListAPIView):
|
generics.ListAPIView):
|
||||||
"""Resource for returning gallery for product for back-office users."""
|
"""Resource for returning gallery for product for back-office users."""
|
||||||
serializer_class = ImageBaseSerializer
|
serializer_class = ImageBaseSerializer
|
||||||
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
|
permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
|
||||||
|
|
||||||
def get_object(self):
|
def get_object(self):
|
||||||
"""Override get_object method."""
|
"""Override get_object method."""
|
||||||
|
|
@ -99,13 +108,15 @@ class RegionListCreateView(common.RegionViewMixin, generics.ListCreateAPIView):
|
||||||
"""Create view for model Region"""
|
"""Create view for model Region"""
|
||||||
pagination_class = None
|
pagination_class = None
|
||||||
serializer_class = serializers.RegionSerializer
|
serializer_class = serializers.RegionSerializer
|
||||||
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
|
permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
|
||||||
|
ordering_fields = '__all__'
|
||||||
|
filter_class = RegionFilter
|
||||||
|
|
||||||
|
|
||||||
class RegionRUDView(common.RegionViewMixin, generics.RetrieveUpdateDestroyAPIView):
|
class RegionRUDView(common.RegionViewMixin, generics.RetrieveUpdateDestroyAPIView):
|
||||||
"""Retrieve view for model Region"""
|
"""Retrieve view for model Region"""
|
||||||
serializer_class = serializers.RegionSerializer
|
serializer_class = serializers.RegionSerializer
|
||||||
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
|
permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
|
||||||
|
|
||||||
|
|
||||||
# Country
|
# Country
|
||||||
|
|
@ -114,11 +125,11 @@ class CountryListCreateView(generics.ListCreateAPIView):
|
||||||
queryset = models.Country.objects.all()
|
queryset = models.Country.objects.all()
|
||||||
serializer_class = serializers.CountryBackSerializer
|
serializer_class = serializers.CountryBackSerializer
|
||||||
pagination_class = None
|
pagination_class = None
|
||||||
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
|
permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
|
||||||
|
|
||||||
|
|
||||||
class CountryRUDView(generics.RetrieveUpdateDestroyAPIView):
|
class CountryRUDView(generics.RetrieveUpdateDestroyAPIView):
|
||||||
"""RUD view for model Country."""
|
"""RUD view for model Country."""
|
||||||
serializer_class = serializers.CountryBackSerializer
|
serializer_class = serializers.CountryBackSerializer
|
||||||
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
|
permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
|
||||||
queryset = models.Country.objects.all()
|
queryset = models.Country.objects.all()
|
||||||
|
|
@ -59,8 +59,18 @@ class PageAdmin(admin.ModelAdmin):
|
||||||
@admin.register(models.Footer)
|
@admin.register(models.Footer)
|
||||||
class FooterAdmin(admin.ModelAdmin):
|
class FooterAdmin(admin.ModelAdmin):
|
||||||
"""Footer admin."""
|
"""Footer admin."""
|
||||||
|
list_display = ('id', 'site', )
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.FooterLink)
|
@admin.register(models.FooterLink)
|
||||||
class FooterLinkAdmin(admin.ModelAdmin):
|
class FooterLinkAdmin(admin.ModelAdmin):
|
||||||
"""FooterLink admin."""
|
"""FooterLink admin."""
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(models.Panel)
|
||||||
|
class PanelAdmin(admin.ModelAdmin):
|
||||||
|
"""Panel admin."""
|
||||||
|
list_display = ('id', 'name', 'user', 'created', )
|
||||||
|
raw_id_fields = ('user', )
|
||||||
|
list_display_links = ('id', 'name', )
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,18 @@ from django.contrib.contenttypes import fields as generic
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.contrib.postgres.fields import JSONField
|
from django.contrib.postgres.fields import JSONField
|
||||||
from django.core.validators import EMPTY_VALUES
|
from django.core.validators import EMPTY_VALUES
|
||||||
|
from django.db import connections, connection
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from rest_framework import exceptions
|
||||||
|
|
||||||
from configuration.models import TranslationSettings
|
from configuration.models import TranslationSettings
|
||||||
from location.models import Country
|
from location.models import Country
|
||||||
from main import methods
|
from main import methods
|
||||||
from review.models import Review
|
from review.models import Review
|
||||||
|
from utils.exceptions import UnprocessableEntityError
|
||||||
|
from utils.methods import dictfetchall
|
||||||
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
|
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
|
||||||
TranslatedFieldsMixin, PlatformMixin)
|
TranslatedFieldsMixin, PlatformMixin)
|
||||||
|
|
||||||
|
|
@ -413,5 +417,98 @@ class Panel(ProjectBaseMixin):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def execute_query(self):
|
def execute_query(self, request):
|
||||||
pass
|
"""Execute query"""
|
||||||
|
raw = self.query
|
||||||
|
page = int(request.query_params.get('page', 0))
|
||||||
|
page_size = int(request.query_params.get('page_size', 10))
|
||||||
|
|
||||||
|
if raw:
|
||||||
|
data = {
|
||||||
|
"count": 0,
|
||||||
|
"next": 2,
|
||||||
|
"previous": None,
|
||||||
|
"columns": None,
|
||||||
|
"results": []
|
||||||
|
|
||||||
|
}
|
||||||
|
with connections['default'].cursor() as cursor:
|
||||||
|
count = self._raw_count(raw)
|
||||||
|
start = page*page_size
|
||||||
|
cursor.execute(*self.set_limits(start, page_size))
|
||||||
|
data["count"] = count
|
||||||
|
data["next"] = self.get_next_page(count, page, page_size)
|
||||||
|
data["previous"] = self.get_previous_page(count, page)
|
||||||
|
data["results"] = dictfetchall(cursor)
|
||||||
|
data["columns"] = self._raw_columns(cursor)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get_next_page(self, count, page, page_size):
|
||||||
|
max_page = count/page_size-1
|
||||||
|
if not 0 <= page <= max_page:
|
||||||
|
raise exceptions.NotFound('Invalid page.')
|
||||||
|
if max_page > page:
|
||||||
|
return page + 1
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_previous_page(self, count, page):
|
||||||
|
if page > 0:
|
||||||
|
return page - 1
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _raw_execute(row):
|
||||||
|
with connections['default'].cursor() as cursor:
|
||||||
|
try:
|
||||||
|
cursor.execute(row)
|
||||||
|
return cursor.execute(row)
|
||||||
|
except Exception as er:
|
||||||
|
# TODO: log
|
||||||
|
raise UnprocessableEntityError()
|
||||||
|
|
||||||
|
def _raw_count(self, subquery):
|
||||||
|
if ';' in subquery:
|
||||||
|
subquery = subquery.replace(';', '')
|
||||||
|
_count_query = f"""SELECT count(*) from ({subquery}) as t;"""
|
||||||
|
# cursor = self._raw_execute(_count_query)
|
||||||
|
with connections['default'].cursor() as cursor:
|
||||||
|
cursor.execute(_count_query)
|
||||||
|
row = cursor.fetchone()
|
||||||
|
return row[0]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _raw_columns(cursor):
|
||||||
|
columns = [col[0] for col in cursor.description]
|
||||||
|
return columns
|
||||||
|
|
||||||
|
def get_headers(self):
|
||||||
|
with connections['default'].cursor() as cursor:
|
||||||
|
try:
|
||||||
|
cursor.execute(self.query)
|
||||||
|
except Exception as er:
|
||||||
|
raise UnprocessableEntityError()
|
||||||
|
return self._raw_columns(cursor)
|
||||||
|
|
||||||
|
def get_data(self):
|
||||||
|
with connections['default'].cursor() as cursor:
|
||||||
|
cursor.execute(self.query)
|
||||||
|
return cursor.fetchall()
|
||||||
|
|
||||||
|
def _raw_page(self, raw, request):
|
||||||
|
page = request.query_params.get('page', 0)
|
||||||
|
page_size = request.query_params.get('page_size', 0)
|
||||||
|
raw = f"""{raw} LIMIT {page_size} OFFSET {page}"""
|
||||||
|
return raw
|
||||||
|
|
||||||
|
def set_limits(self, start, limit, params=tuple()):
|
||||||
|
limit_offset = ''
|
||||||
|
new_params = tuple()
|
||||||
|
if start > 0:
|
||||||
|
new_params += (start,)
|
||||||
|
limit_offset = ' OFFSET %s'
|
||||||
|
if limit is not None:
|
||||||
|
new_params = (limit,) + new_params
|
||||||
|
limit_offset = ' LIMIT %s' + limit_offset
|
||||||
|
params = params + new_params
|
||||||
|
query = self.query + limit_offset
|
||||||
|
return query, params
|
||||||
|
|
|
||||||
|
|
@ -296,3 +296,20 @@ class PanelSerializer(serializers.ModelSerializer):
|
||||||
'user',
|
'user',
|
||||||
'user_id'
|
'user_id'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class PanelExecuteSerializer(serializers.ModelSerializer):
|
||||||
|
"""Panel execute serializer."""
|
||||||
|
class Meta:
|
||||||
|
model = models.Panel
|
||||||
|
fields = [
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'display',
|
||||||
|
'description',
|
||||||
|
'query',
|
||||||
|
'created',
|
||||||
|
'modified',
|
||||||
|
'user',
|
||||||
|
'user_id'
|
||||||
|
]
|
||||||
|
|
|
||||||
14
apps/main/tasks.py
Normal file
14
apps/main/tasks.py
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
"""Task methods for main app."""
|
||||||
|
|
||||||
|
from celery import shared_task
|
||||||
|
|
||||||
|
from account.models import User
|
||||||
|
from main.models import Panel
|
||||||
|
from utils.export import SendExport
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def send_export_to_email(panel_id, user_id, file_type='csv'):
|
||||||
|
panel = Panel.objects.get(id=panel_id)
|
||||||
|
user = User.objects.get(id=user_id)
|
||||||
|
SendExport(user, panel, file_type).send()
|
||||||
|
|
@ -23,9 +23,10 @@ urlpatterns = [
|
||||||
path('page-types/', views.PageTypeListCreateView.as_view(),
|
path('page-types/', views.PageTypeListCreateView.as_view(),
|
||||||
name='page-types-list-create'),
|
name='page-types-list-create'),
|
||||||
path('panels/', views.PanelsListCreateView.as_view(), name='panels'),
|
path('panels/', views.PanelsListCreateView.as_view(), name='panels'),
|
||||||
path('panels/<int:pk>/', views.PanelsListCreateView.as_view(), name='panels-rud'),
|
path('panels/<int:pk>/', views.PanelsRUDView.as_view(), name='panels-rud'),
|
||||||
# path('panels/<int:pk>/execute/', views.PanelsView.as_view(), name='panels-execute')
|
path('panels/<int:pk>/execute/', views.PanelsExecuteView.as_view(), name='panels-execute'),
|
||||||
|
path('panels/<int:pk>/csv/', views.PanelsExportCSVView.as_view(), name='panels-csv'),
|
||||||
|
path('panels/<int:pk>/xls/', views.PanelsExecuteXLSView.as_view(), name='panels-xls')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from rest_framework import generics, permissions
|
from rest_framework import generics, permissions, status
|
||||||
|
from rest_framework.generics import get_object_or_404
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from main import serializers
|
from main import serializers
|
||||||
|
from main import tasks
|
||||||
from main.filters import AwardFilter
|
from main.filters import AwardFilter
|
||||||
from main.models import Award, Footer, PageType, Panel
|
from main.models import Award, Footer, PageType, Panel, SiteFeature, Feature
|
||||||
from main.views import SiteSettingsView, SiteListView
|
from main.views import SiteSettingsView, SiteListView
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -42,21 +46,29 @@ class ContentTypeView(generics.ListAPIView):
|
||||||
class FeatureBackView(generics.ListCreateAPIView):
|
class FeatureBackView(generics.ListCreateAPIView):
|
||||||
"""Feature list or create View."""
|
"""Feature list or create View."""
|
||||||
serializer_class = serializers.FeatureSerializer
|
serializer_class = serializers.FeatureSerializer
|
||||||
|
queryset = Feature.objects.all()
|
||||||
|
|
||||||
|
|
||||||
class SiteFeatureBackView(generics.ListCreateAPIView):
|
class SiteFeatureBackView(generics.ListCreateAPIView):
|
||||||
"""Feature list or create View."""
|
"""Feature list or create View."""
|
||||||
serializer_class = serializers.SiteFeatureSerializer
|
serializer_class = serializers.SiteFeatureSerializer
|
||||||
|
queryset = SiteFeature.objects.all()
|
||||||
|
pagination_class = None
|
||||||
|
permission_classes = [permissions.IsAdminUser]
|
||||||
|
|
||||||
|
|
||||||
class FeatureRUDBackView(generics.RetrieveUpdateDestroyAPIView):
|
class FeatureRUDBackView(generics.RetrieveUpdateDestroyAPIView):
|
||||||
"""Feature RUD View."""
|
"""Feature RUD View."""
|
||||||
serializer_class = serializers.FeatureSerializer
|
serializer_class = serializers.FeatureSerializer
|
||||||
|
queryset = SiteFeature.objects.all()
|
||||||
|
permission_classes = [permissions.IsAdminUser]
|
||||||
|
|
||||||
|
|
||||||
class SiteFeatureRUDBackView(generics.RetrieveUpdateDestroyAPIView):
|
class SiteFeatureRUDBackView(generics.RetrieveUpdateDestroyAPIView):
|
||||||
"""Feature RUD View."""
|
"""Feature RUD View."""
|
||||||
serializer_class = serializers.SiteFeatureSerializer
|
serializer_class = serializers.SiteFeatureSerializer
|
||||||
|
queryset = SiteFeature.objects.all()
|
||||||
|
permission_classes = [permissions.IsAdminUser]
|
||||||
|
|
||||||
|
|
||||||
class SiteSettingsBackOfficeView(SiteSettingsView):
|
class SiteSettingsBackOfficeView(SiteSettingsView):
|
||||||
|
|
@ -107,3 +119,47 @@ class PanelsRUDView(generics.RetrieveUpdateDestroyAPIView):
|
||||||
)
|
)
|
||||||
serializer_class = serializers.PanelSerializer
|
serializer_class = serializers.PanelSerializer
|
||||||
queryset = Panel.objects.all()
|
queryset = Panel.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class PanelsExecuteView(generics.ListAPIView):
|
||||||
|
"""Custom panels view."""
|
||||||
|
permission_classes = (
|
||||||
|
permissions.IsAdminUser,
|
||||||
|
)
|
||||||
|
queryset = Panel.objects.all()
|
||||||
|
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
panel = get_object_or_404(Panel, id=self.kwargs['pk'])
|
||||||
|
return Response(panel.execute_query(request))
|
||||||
|
|
||||||
|
|
||||||
|
class PanelsExportCSVView(PanelsExecuteView):
|
||||||
|
"""Export panels via csv view."""
|
||||||
|
permission_classes = (permissions.IsAdminUser,)
|
||||||
|
queryset = Panel.objects.all()
|
||||||
|
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
panel = get_object_or_404(Panel, id=self.kwargs['pk'])
|
||||||
|
# make task for celery
|
||||||
|
tasks.send_export_to_email.delay(
|
||||||
|
panel_id=panel.id, user_id=request.user.id)
|
||||||
|
return Response(
|
||||||
|
{"success": _('The file will be sent to your email.')},
|
||||||
|
status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PanelsExecuteXLSView(PanelsExecuteView):
|
||||||
|
"""Export panels via xlsx view."""
|
||||||
|
permission_classes = (permissions.IsAdminUser,)
|
||||||
|
queryset = Panel.objects.all()
|
||||||
|
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
panel = get_object_or_404(Panel, id=self.kwargs['pk'])
|
||||||
|
# make task for celery
|
||||||
|
tasks.send_export_to_email.delay(
|
||||||
|
panel_id=panel.id, user_id=request.user.id, file_type='xls')
|
||||||
|
return Response(
|
||||||
|
{"success": _('The file will be sent to your email.')},
|
||||||
|
status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ from rest_framework.response import Response
|
||||||
|
|
||||||
from main import methods, models, serializers
|
from main import methods, models, serializers
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# class FeatureViewMixin:
|
# class FeatureViewMixin:
|
||||||
# """Feature view mixin."""
|
# """Feature view mixin."""
|
||||||
|
|
@ -86,8 +85,13 @@ class DetermineLocation(generics.GenericAPIView):
|
||||||
longitude, latitude = methods.determine_coordinates(request)
|
longitude, latitude = methods.determine_coordinates(request)
|
||||||
city = methods.determine_user_city(request)
|
city = methods.determine_user_city(request)
|
||||||
country_name = methods.determine_country_name(request)
|
country_name = methods.determine_country_name(request)
|
||||||
|
country_code = methods.determine_country_code(request)
|
||||||
if longitude and latitude and city and country_name:
|
if longitude and latitude and city and country_name:
|
||||||
return Response(data={'latitude': latitude, 'longitude': longitude,
|
return Response(data={
|
||||||
'city': city, 'country_name': country_name})
|
'latitude': latitude,
|
||||||
|
'longitude': longitude,
|
||||||
|
'city': city,
|
||||||
|
'country_name': country_name,
|
||||||
|
'country_code': country_code,
|
||||||
|
})
|
||||||
raise Http404
|
raise Http404
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,4 +72,6 @@ class NewsListFilterSet(filters.FilterSet):
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def sort_by_field(self, queryset, name, value):
|
def sort_by_field(self, queryset, name, value):
|
||||||
|
if value == self.SORT_BY_START_CHOICE:
|
||||||
|
return queryset.order_by('-publication_date', '-publication_time')
|
||||||
return queryset.order_by(f'-{value}')
|
return queryset.order_by(f'-{value}')
|
||||||
|
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
from django.core.management.base import BaseCommand
|
|
||||||
from django.db.models import F
|
|
||||||
from tqdm import tqdm
|
|
||||||
|
|
||||||
from account.models import User
|
|
||||||
from news.models import News
|
|
||||||
from transfer.models import PageTexts
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
help = 'Add author of News'
|
|
||||||
|
|
||||||
def handle(self, *args, **kwargs):
|
|
||||||
count = 0
|
|
||||||
news_list = News.objects.filter(created_by__isnull=True)
|
|
||||||
|
|
||||||
for news in tqdm(news_list, desc="Find author for exist news"):
|
|
||||||
old_news = PageTexts.objects.filter(id=news.old_id).annotate(
|
|
||||||
account_id=F('page__account_id'),
|
|
||||||
).first()
|
|
||||||
if old_news:
|
|
||||||
user = User.objects.filter(old_id=old_news.account_id).first()
|
|
||||||
if user:
|
|
||||||
news.created_by = user
|
|
||||||
news.modified_by = user
|
|
||||||
news.save()
|
|
||||||
count += 1
|
|
||||||
|
|
||||||
self.stdout.write(self.style.WARNING(f'Update {count} objects.'))
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
from django.core.management.base import BaseCommand
|
|
||||||
|
|
||||||
from news.models import News, NewsType
|
|
||||||
from tag.models import Tag, TagCategory
|
|
||||||
from transfer.models import PageMetadata, Pages, PageTexts
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
help = 'Remove old news from new bd'
|
|
||||||
|
|
||||||
def handle(self, *args, **kwargs):
|
|
||||||
count = 0
|
|
||||||
news_type, _ = NewsType.objects.get_or_create(name='News')
|
|
||||||
tag_cat, _ = TagCategory.objects.get_or_create(index_name='category')
|
|
||||||
news_type.tag_categories.add(tag_cat)
|
|
||||||
news_type.save()
|
|
||||||
|
|
||||||
old_news_tag = PageMetadata.objects.filter(key='category', page__pagetexts__isnull=False)
|
|
||||||
for old_tag in old_news_tag:
|
|
||||||
old_id_list = old_tag.page.pagetexts_set.all().values_list('id', flat=True)
|
|
||||||
|
|
||||||
# Make Tag
|
|
||||||
new_tag, created = Tag.objects.get_or_create(category=tag_cat, value=old_tag.value)
|
|
||||||
if created:
|
|
||||||
text_value = ' '.join(new_tag.value.split('_'))
|
|
||||||
new_tag.label = {'en-GB': text_value}
|
|
||||||
new_tag.save()
|
|
||||||
for id in old_id_list:
|
|
||||||
if isinstance(id, int):
|
|
||||||
news = News.objects.filter(old_id=id).first()
|
|
||||||
if news:
|
|
||||||
news.tags.add(new_tag)
|
|
||||||
news.save()
|
|
||||||
count += 1
|
|
||||||
|
|
||||||
self.stdout.write(self.style.WARNING(f'Create or update {count} objects.'))
|
|
||||||
18
apps/news/migrations/0045_news_must_of_the_week.py
Normal file
18
apps/news/migrations/0045_news_must_of_the_week.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-17 17:59
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('news', '0044_auto_20191216_2044'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='news',
|
||||||
|
name='must_of_the_week',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
38
apps/news/migrations/0046_auto_20191218_1437.py
Normal file
38
apps/news/migrations/0046_auto_20191218_1437.py
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-18 14:37
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def fill_publication_date_and_time(apps, schema_editor):
|
||||||
|
News = apps.get_model('news', 'News')
|
||||||
|
for news in News.objects.all():
|
||||||
|
if news.start is not None:
|
||||||
|
news.publication_date = news.start.date()
|
||||||
|
news.publication_time = news.start.time()
|
||||||
|
news.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('news', '0045_news_must_of_the_week'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='news',
|
||||||
|
name='publication_date',
|
||||||
|
field=models.DateField(blank=True, help_text='date since when news item is published', null=True, verbose_name='News publication date'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='news',
|
||||||
|
name='publication_time',
|
||||||
|
field=models.TimeField(blank=True, help_text='time since when news item is published', null=True, verbose_name='News publication time'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='news',
|
||||||
|
name='must_of_the_week',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='Show in the carousel'),
|
||||||
|
),
|
||||||
|
migrations.RunPython(fill_publication_date_and_time, migrations.RunPython.noop),
|
||||||
|
]
|
||||||
17
apps/news/migrations/0047_remove_news_start.py
Normal file
17
apps/news/migrations/0047_remove_news_start.py
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-18 16:20
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('news', '0046_auto_20191218_1437'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='news',
|
||||||
|
name='start',
|
||||||
|
),
|
||||||
|
]
|
||||||
17
apps/news/migrations/0048_remove_news_must_of_the_week.py
Normal file
17
apps/news/migrations/0048_remove_news_must_of_the_week.py
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-19 13:47
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('news', '0047_remove_news_start'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='news',
|
||||||
|
name='must_of_the_week',
|
||||||
|
),
|
||||||
|
]
|
||||||
19
apps/news/migrations/0049_auto_20191223_0619.py
Normal file
19
apps/news/migrations/0049_auto_20191223_0619.py
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-23 06:19
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('news', '0048_remove_news_must_of_the_week'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='news',
|
||||||
|
name='views_count',
|
||||||
|
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='news', to='rating.ViewCount'),
|
||||||
|
),
|
||||||
|
]
|
||||||
34
apps/news/migrations/0050_auto_20191223_1148.py
Normal file
34
apps/news/migrations/0050_auto_20191223_1148.py
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-23 11:48
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.utils.timezone
|
||||||
|
import utils.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('news', '0049_auto_20191223_0619'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='agenda',
|
||||||
|
name='event_datetime',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='agenda',
|
||||||
|
name='end_datetime',
|
||||||
|
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='End datetime'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='agenda',
|
||||||
|
name='event_name',
|
||||||
|
field=utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='event name'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='agenda',
|
||||||
|
name='start_datetime',
|
||||||
|
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Start datetime'),
|
||||||
|
),
|
||||||
|
]
|
||||||
19
apps/news/migrations/0050_auto_20191223_1238.py
Normal file
19
apps/news/migrations/0050_auto_20191223_1238.py
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-23 12:38
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('news', '0049_auto_20191223_0619'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='news',
|
||||||
|
name='news_type',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='news', to='news.NewsType', verbose_name='news type'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -3,6 +3,7 @@ import uuid
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes import fields as generic
|
from django.contrib.contenttypes import fields as generic
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.contrib.postgres.fields import HStoreField
|
from django.contrib.postgres.fields import HStoreField
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Case, When
|
from django.db.models import Case, When
|
||||||
|
|
@ -10,21 +11,27 @@ from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework.reverse import reverse
|
from rest_framework.reverse import reverse
|
||||||
|
|
||||||
|
from main.models import Carousel
|
||||||
from rating.models import Rating, ViewCount
|
from rating.models import Rating, ViewCount
|
||||||
from utils.models import (BaseAttributes, TJSONField, TranslatedFieldsMixin, HasTagsMixin,
|
from utils.models import (BaseAttributes, TJSONField, TranslatedFieldsMixin, HasTagsMixin,
|
||||||
ProjectBaseMixin, GalleryModelMixin, IntermediateGalleryModelMixin,
|
ProjectBaseMixin, GalleryMixin, IntermediateGalleryModelMixin,
|
||||||
FavoritesMixin)
|
FavoritesMixin)
|
||||||
from utils.querysets import TranslationQuerysetMixin
|
from utils.querysets import TranslationQuerysetMixin
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
class Agenda(ProjectBaseMixin, TranslatedFieldsMixin):
|
class Agenda(ProjectBaseMixin, TranslatedFieldsMixin):
|
||||||
"""News agenda model"""
|
"""News agenda model"""
|
||||||
|
start_datetime = models.DateTimeField(default=timezone.now, editable=True,
|
||||||
event_datetime = models.DateTimeField(default=timezone.now, editable=False,
|
verbose_name=_('Start datetime'))
|
||||||
verbose_name=_('Event datetime'))
|
end_datetime = models.DateTimeField(default=timezone.now, editable=True,
|
||||||
|
verbose_name=_('End datetime'))
|
||||||
address = models.ForeignKey('location.Address', blank=True, null=True,
|
address = models.ForeignKey('location.Address', blank=True, null=True,
|
||||||
default=None, verbose_name=_('address'),
|
default=None, verbose_name=_('address'),
|
||||||
on_delete=models.SET_NULL)
|
on_delete=models.SET_NULL)
|
||||||
|
event_name = TJSONField(blank=True, null=True, default=None,
|
||||||
|
verbose_name=_('event name'),
|
||||||
|
help_text='{"en-GB":"some text"}')
|
||||||
content = TJSONField(blank=True, null=True, default=None,
|
content = TJSONField(blank=True, null=True, default=None,
|
||||||
verbose_name=_('content'),
|
verbose_name=_('content'),
|
||||||
help_text='{"en-GB":"some text"}')
|
help_text='{"en-GB":"some text"}')
|
||||||
|
|
@ -64,14 +71,14 @@ class NewsQuerySet(TranslationQuerysetMixin):
|
||||||
|
|
||||||
def sort_by_start(self):
|
def sort_by_start(self):
|
||||||
"""Return qs sorted by start DESC"""
|
"""Return qs sorted by start DESC"""
|
||||||
return self.order_by('-start')
|
return self.order_by('-publication_date', '-publication_time')
|
||||||
|
|
||||||
def rating_value(self):
|
def rating_value(self):
|
||||||
return self.annotate(rating=models.Count('ratings__ip', distinct=True))
|
return self.annotate(rating=models.Count('ratings__ip', distinct=True))
|
||||||
|
|
||||||
def with_base_related(self):
|
def with_base_related(self):
|
||||||
"""Return qs with related objects."""
|
"""Return qs with related objects."""
|
||||||
return self.select_related('news_type', 'country').prefetch_related('tags')
|
return self.select_related('news_type', 'country').prefetch_related('tags', 'tags__translation')
|
||||||
|
|
||||||
def with_extended_related(self):
|
def with_extended_related(self):
|
||||||
"""Return qs with related objects."""
|
"""Return qs with related objects."""
|
||||||
|
|
@ -99,9 +106,13 @@ class NewsQuerySet(TranslationQuerysetMixin):
|
||||||
def published(self):
|
def published(self):
|
||||||
"""Return only published news"""
|
"""Return only published news"""
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
return self.filter(models.Q(models.Q(end__gte=now) |
|
date_now = now.date()
|
||||||
|
time_now = now.time()
|
||||||
|
return self.exclude(models.Q(publication_date__isnull=True) | models.Q(publication_time__isnull=True)). \
|
||||||
|
filter(models.Q(models.Q(end__gte=now) |
|
||||||
models.Q(end__isnull=True)),
|
models.Q(end__isnull=True)),
|
||||||
state__in=self.model.PUBLISHED_STATES, start__lte=now)
|
state__in=self.model.PUBLISHED_STATES, publication_date__lte=date_now,
|
||||||
|
publication_time__lte=time_now)
|
||||||
|
|
||||||
# todo: filter by best score
|
# todo: filter by best score
|
||||||
# todo: filter by country?
|
# todo: filter by country?
|
||||||
|
|
@ -114,7 +125,7 @@ class NewsQuerySet(TranslationQuerysetMixin):
|
||||||
return self.model.objects.exclude(pk=news.pk).published(). \
|
return self.model.objects.exclude(pk=news.pk).published(). \
|
||||||
annotate_in_favorites(user). \
|
annotate_in_favorites(user). \
|
||||||
with_base_related().by_type(news.news_type). \
|
with_base_related().by_type(news.news_type). \
|
||||||
by_tags(news.tags.all()).distinct().order_by('-start')
|
by_tags(news.tags.all()).distinct().sort_by_start()
|
||||||
|
|
||||||
def annotate_in_favorites(self, user):
|
def annotate_in_favorites(self, user):
|
||||||
"""Annotate flag in_favorites"""
|
"""Annotate flag in_favorites"""
|
||||||
|
|
@ -133,7 +144,7 @@ class NewsQuerySet(TranslationQuerysetMixin):
|
||||||
return self.filter(title__icontains=locale)
|
return self.filter(title__icontains=locale)
|
||||||
|
|
||||||
|
|
||||||
class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin,
|
class News(GalleryMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin,
|
||||||
FavoritesMixin):
|
FavoritesMixin):
|
||||||
"""News model."""
|
"""News model."""
|
||||||
|
|
||||||
|
|
@ -170,7 +181,7 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
|
||||||
|
|
||||||
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
|
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
|
||||||
news_type = models.ForeignKey(NewsType, on_delete=models.PROTECT,
|
news_type = models.ForeignKey(NewsType, on_delete=models.PROTECT,
|
||||||
verbose_name=_('news type'))
|
verbose_name=_('news type'), related_name='news')
|
||||||
title = TJSONField(blank=True, null=True, default=None,
|
title = TJSONField(blank=True, null=True, default=None,
|
||||||
verbose_name=_('title'),
|
verbose_name=_('title'),
|
||||||
help_text='{"en-GB":"some text"}')
|
help_text='{"en-GB":"some text"}')
|
||||||
|
|
@ -185,8 +196,10 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
|
||||||
locale_to_description_is_active = HStoreField(null=True, default=dict, blank=True,
|
locale_to_description_is_active = HStoreField(null=True, default=dict, blank=True,
|
||||||
verbose_name=_('Is description for certain locale active'),
|
verbose_name=_('Is description for certain locale active'),
|
||||||
help_text='{"en-GB": true, "fr-FR": false}')
|
help_text='{"en-GB": true, "fr-FR": false}')
|
||||||
start = models.DateTimeField(blank=True, null=True, default=None,
|
publication_date = models.DateField(blank=True, null=True, verbose_name=_('News publication date'),
|
||||||
verbose_name=_('Start'))
|
help_text=_('date since when news item is published'))
|
||||||
|
publication_time = models.TimeField(blank=True, null=True, verbose_name=_('News publication time'),
|
||||||
|
help_text=_('time since when news item is published'))
|
||||||
end = models.DateTimeField(blank=True, null=True, default=None,
|
end = models.DateTimeField(blank=True, null=True, default=None,
|
||||||
verbose_name=_('End'))
|
verbose_name=_('End'))
|
||||||
slugs = HStoreField(null=True, blank=True, default=dict,
|
slugs = HStoreField(null=True, blank=True, default=dict,
|
||||||
|
|
@ -206,7 +219,8 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
|
||||||
tags = models.ManyToManyField('tag.Tag', related_name='news',
|
tags = models.ManyToManyField('tag.Tag', related_name='news',
|
||||||
verbose_name=_('Tags'))
|
verbose_name=_('Tags'))
|
||||||
gallery = models.ManyToManyField('gallery.Image', through='news.NewsGallery')
|
gallery = models.ManyToManyField('gallery.Image', through='news.NewsGallery')
|
||||||
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,
|
||||||
|
related_name='news')
|
||||||
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')
|
carousels = generic.GenericRelation(to='main.Carousel')
|
||||||
|
|
@ -243,6 +257,24 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
|
||||||
self.duplication_date = timezone.now()
|
self.duplication_date = timezone.now()
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def must_of_the_week(self) -> bool:
|
||||||
|
"""Detects whether current item in carousel"""
|
||||||
|
kwargs = {
|
||||||
|
'content_type': ContentType.objects.get_for_model(self),
|
||||||
|
'object_id': self.pk,
|
||||||
|
'country': self.country,
|
||||||
|
}
|
||||||
|
return Carousel.objects.filter(**kwargs).exists()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def publication_datetime(self):
|
||||||
|
"""Represents datetime object combined from `publication_date` & `publication_time` fields"""
|
||||||
|
try:
|
||||||
|
return datetime.combine(date=self.publication_date, time=self.publication_time)
|
||||||
|
except TypeError:
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def duplicates(self):
|
def duplicates(self):
|
||||||
"""Duplicates for this news item excluding same country code labeled"""
|
"""Duplicates for this news item excluding same country code labeled"""
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,29 @@
|
||||||
"""News app common serializers."""
|
"""News app common serializers."""
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.fields import SerializerMethodField
|
from rest_framework.fields import SerializerMethodField
|
||||||
|
|
||||||
from account.serializers.common import UserBaseSerializer
|
from account.serializers.common import UserBaseSerializer
|
||||||
from gallery.models import Image
|
from gallery.models import Image
|
||||||
from main.models import SiteSettings
|
|
||||||
from location import models as location_models
|
from location import models as location_models
|
||||||
from location.serializers import CountrySimpleSerializer, AddressBaseSerializer
|
from location.serializers import AddressBaseSerializer, CountrySimpleSerializer
|
||||||
|
from main.models import SiteSettings
|
||||||
from news import models
|
from news import models
|
||||||
|
from rating import models as rating_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,
|
|
||||||
FavoritesCreateSerializer, ImageBaseSerializer, CarouselCreateSerializer)
|
|
||||||
from rating import models as rating_models
|
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
from utils.models import get_current_locale, get_default_locale
|
from utils.models import get_current_locale, get_default_locale
|
||||||
|
from utils.serializers import (
|
||||||
|
CarouselCreateSerializer, FavoritesCreateSerializer, ImageBaseSerializer, ProjectModelSerializer, TranslatedField,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AgendaSerializer(ProjectModelSerializer):
|
class AgendaSerializer(ProjectModelSerializer):
|
||||||
event_datetime = serializers.DateTimeField()
|
start_datetime = serializers.DateTimeField()
|
||||||
|
end_datetime = serializers.DateTimeField()
|
||||||
address = AddressBaseSerializer()
|
address = AddressBaseSerializer()
|
||||||
|
event_name_translated = TranslatedField()
|
||||||
content_translated = TranslatedField()
|
content_translated = TranslatedField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
@ -29,9 +32,11 @@ class AgendaSerializer(ProjectModelSerializer):
|
||||||
model = models.Agenda
|
model = models.Agenda
|
||||||
fields = (
|
fields = (
|
||||||
'id',
|
'id',
|
||||||
'event_datetime',
|
'start_datetime',
|
||||||
|
'end_datetime',
|
||||||
'address',
|
'address',
|
||||||
'content_translated'
|
'content_translated',
|
||||||
|
'event_name_translated'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -125,9 +130,9 @@ class NewsDetailSerializer(NewsBaseSerializer):
|
||||||
description_translated = TranslatedField()
|
description_translated = TranslatedField()
|
||||||
country = CountrySimpleSerializer(read_only=True)
|
country = CountrySimpleSerializer(read_only=True)
|
||||||
author = UserBaseSerializer(source='created_by', read_only=True)
|
author = UserBaseSerializer(source='created_by', read_only=True)
|
||||||
state_display = serializers.CharField(source='get_state_display',
|
state_display = serializers.CharField(source='get_state_display', read_only=True)
|
||||||
read_only=True)
|
|
||||||
gallery = ImageBaseSerializer(read_only=True, source='crop_gallery', many=True)
|
gallery = ImageBaseSerializer(read_only=True, source='crop_gallery', many=True)
|
||||||
|
start = serializers.DateTimeField(source='publication_datetime', read_only=True)
|
||||||
|
|
||||||
class Meta(NewsBaseSerializer.Meta):
|
class Meta(NewsBaseSerializer.Meta):
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
@ -185,20 +190,34 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
|
||||||
'locale_to_description_is_active',
|
'locale_to_description_is_active',
|
||||||
'is_published',
|
'is_published',
|
||||||
'duplication_date',
|
'duplication_date',
|
||||||
|
'must_of_the_week',
|
||||||
|
'publication_date',
|
||||||
|
'publication_time',
|
||||||
|
'created',
|
||||||
|
'modified',
|
||||||
)
|
)
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'backoffice_title': {'allow_null': False},
|
'created': {'read_only': True},
|
||||||
|
'modified': {'read_only': True},
|
||||||
'duplication_date': {'read_only': True},
|
'duplication_date': {'read_only': True},
|
||||||
'locale_to_description_is_active': {'allow_null': False}
|
'locale_to_description_is_active': {'allow_null': False},
|
||||||
|
'must_of_the_week': {'read_only': True},
|
||||||
}
|
}
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
slugs = validated_data.get('slugs')
|
slugs = validated_data.get('slugs')
|
||||||
|
|
||||||
if slugs:
|
if slugs:
|
||||||
if models.News.objects.filter(
|
if models.News.objects.filter(
|
||||||
slugs__values__contains=list(slugs.values())
|
slugs__values__contains=list(slugs.values())
|
||||||
).exists():
|
).exists():
|
||||||
raise serializers.ValidationError({'slugs': _('News with this slug already exists.')})
|
raise serializers.ValidationError({'slugs': _('News with this slug already exists.')})
|
||||||
|
|
||||||
|
request = self.context.get("request")
|
||||||
|
if request and hasattr(request, "user"):
|
||||||
|
user = request.user
|
||||||
|
validated_data['created_by'] = user
|
||||||
|
|
||||||
return super().create(validated_data)
|
return super().create(validated_data)
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
|
|
@ -276,8 +295,8 @@ class NewsBackOfficeGallerySerializer(serializers.ModelSerializer):
|
||||||
return self.context.get('request').parser_context.get('kwargs')
|
return self.context.get('request').parser_context.get('kwargs')
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
news_pk = self.get_request_kwargs().get('pk')
|
news_pk = self.request_kwargs.get('pk')
|
||||||
image_id = self.get_request_kwargs().get('image_id')
|
image_id = self.request_kwargs.get('image_id')
|
||||||
qs = models.NewsGallery.objects.filter(image_id=image_id, news_id=news_pk)
|
qs = models.NewsGallery.objects.filter(image_id=image_id, news_id=news_pk)
|
||||||
instance = qs.first()
|
instance = qs.first()
|
||||||
if instance:
|
if instance:
|
||||||
|
|
@ -356,7 +375,12 @@ class NewsCarouselCreateSerializer(CarouselCreateSerializer):
|
||||||
|
|
||||||
def create(self, validated_data, *args, **kwargs):
|
def create(self, validated_data, *args, **kwargs):
|
||||||
validated_data.update({
|
validated_data.update({
|
||||||
'content_object': validated_data.pop('news')
|
'country': validated_data['news'].country
|
||||||
|
})
|
||||||
|
validated_data.update({
|
||||||
|
'content_object': validated_data.pop('news'),
|
||||||
|
'is_parse': True,
|
||||||
|
'active': True,
|
||||||
})
|
})
|
||||||
return super().create(validated_data)
|
return super().create(validated_data)
|
||||||
|
|
||||||
|
|
@ -367,6 +391,7 @@ class NewsCloneCreateSerializer(NewsBackOfficeBaseSerializer,
|
||||||
template_display = serializers.CharField(source='get_template_display',
|
template_display = serializers.CharField(source='get_template_display',
|
||||||
read_only=True)
|
read_only=True)
|
||||||
duplicates = NewsBackOfficeDuplicationInfoSerializer(many=True, allow_null=True, read_only=True)
|
duplicates = NewsBackOfficeDuplicationInfoSerializer(many=True, allow_null=True, read_only=True)
|
||||||
|
|
||||||
class Meta(NewsBackOfficeBaseSerializer.Meta, NewsDetailSerializer.Meta):
|
class Meta(NewsBackOfficeBaseSerializer.Meta, NewsDetailSerializer.Meta):
|
||||||
fields = NewsBackOfficeBaseSerializer.Meta.fields + NewsDetailSerializer.Meta.fields + (
|
fields = NewsBackOfficeBaseSerializer.Meta.fields + NewsDetailSerializer.Meta.fields + (
|
||||||
'template_display',
|
'template_display',
|
||||||
|
|
@ -381,4 +406,3 @@ class NewsCloneCreateSerializer(NewsBackOfficeBaseSerializer,
|
||||||
view_count_model = rating_models.ViewCount.objects.create(count=0)
|
view_count_model = rating_models.ViewCount.objects.create(count=0)
|
||||||
instance.create_duplicate(new_country, view_count_model)
|
instance.create_duplicate(new_country, view_count_model)
|
||||||
return get_object_or_404(models.News, pk=kwargs['pk'])
|
return get_object_or_404(models.News, pk=kwargs['pk'])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ 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'
|
||||||
|
|
@ -57,13 +56,11 @@ class BaseTestCase(APITestCase):
|
||||||
)
|
)
|
||||||
user_role.save()
|
user_role.save()
|
||||||
|
|
||||||
|
|
||||||
self.test_news = News.objects.create(
|
self.test_news = News.objects.create(
|
||||||
created_by=self.user, modified_by=self.user,
|
created_by=self.user, modified_by=self.user,
|
||||||
title={"ru-RU": "Test news"},
|
title={"ru-RU": "Test news"},
|
||||||
news_type=self.test_news_type,
|
news_type=self.test_news_type,
|
||||||
description={"ru-RU": "Description test news"},
|
description={"ru-RU": "Description test news"},
|
||||||
start=datetime.now() + timedelta(hours=-2),
|
|
||||||
end=datetime.now() + timedelta(hours=2),
|
end=datetime.now() + timedelta(hours=2),
|
||||||
state=News.PUBLISHED,
|
state=News.PUBLISHED,
|
||||||
slugs={'en-GB': 'test-news-slug'},
|
slugs={'en-GB': 'test-news-slug'},
|
||||||
|
|
@ -82,7 +79,6 @@ class NewsTestCase(BaseTestCase):
|
||||||
"title": {"ru-RU": "Test news POST"},
|
"title": {"ru-RU": "Test news POST"},
|
||||||
"news_type_id": self.test_news_type.id,
|
"news_type_id": self.test_news_type.id,
|
||||||
"description": {"ru-RU": "Description test news"},
|
"description": {"ru-RU": "Description test news"},
|
||||||
"start": datetime.now() + timedelta(hours=-2),
|
|
||||||
"end": datetime.now() + timedelta(hours=2),
|
"end": datetime.now() + timedelta(hours=2),
|
||||||
"state": News.PUBLISHED,
|
"state": News.PUBLISHED,
|
||||||
"slugs": {'en-GB': 'test-news-slug_post'},
|
"slugs": {'en-GB': 'test-news-slug_post'},
|
||||||
|
|
@ -119,7 +115,6 @@ class NewsTestCase(BaseTestCase):
|
||||||
'id': self.test_news.id,
|
'id': self.test_news.id,
|
||||||
'description': {"ru-RU": "Description test news!"},
|
'description': {"ru-RU": "Description test news!"},
|
||||||
'slugs': self.test_news.slugs,
|
'slugs': self.test_news.slugs,
|
||||||
'start': self.test_news.start,
|
|
||||||
'news_type_id': self.test_news.news_type_id,
|
'news_type_id': self.test_news.news_type_id,
|
||||||
'country_id': self.country_ru.id,
|
'country_id': self.country_ru.id,
|
||||||
"site_id": self.site_ru.id
|
"site_id": self.site_ru.id
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,62 @@
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
|
|
||||||
from django.db.models import Aggregate, CharField, Value
|
|
||||||
from django.db.models import IntegerField, F
|
from django.db.models import IntegerField, F
|
||||||
|
from django.db.models import Value
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
from news.models import NewsType
|
from gallery.models import Image
|
||||||
from tag.models import TagCategory
|
from news.models import NewsType, News
|
||||||
from transfer.models import PageTexts
|
from rating.models import ViewCount
|
||||||
|
from tag.models import TagCategory, Tag
|
||||||
|
from transfer.models import PageTexts, PageCounters, PageMetadata
|
||||||
from transfer.serializers.news import NewsSerializer
|
from transfer.serializers.news import NewsSerializer
|
||||||
|
|
||||||
|
|
||||||
class GroupConcat(Aggregate):
|
def add_locale(locale, data):
|
||||||
function = 'GROUP_CONCAT'
|
if isinstance(data, dict) and locale not in data:
|
||||||
template = '%(function)s(%(expressions)s)'
|
data.update({
|
||||||
|
locale: next(iter(data.values()))
|
||||||
|
})
|
||||||
|
return data
|
||||||
|
|
||||||
def __init__(self, expression, **extra):
|
|
||||||
output_field = extra.pop('output_field', CharField())
|
|
||||||
super().__init__(expression, output_field=output_field, **extra)
|
|
||||||
|
|
||||||
def as_postgresql(self, compiler, connection):
|
def clear_old_news():
|
||||||
self.function = 'STRING_AGG'
|
"""
|
||||||
return super().as_sql(compiler, connection)
|
Clear lod news and news images
|
||||||
|
"""
|
||||||
|
images = Image.objects.filter(
|
||||||
|
news_gallery__isnull=False,
|
||||||
|
news__gallery__news__old_id__isnull=False
|
||||||
|
)
|
||||||
|
img_num = images.count()
|
||||||
|
|
||||||
|
news = News.objects.filter(old_id__isnull=False)
|
||||||
|
news_num = news.count()
|
||||||
|
|
||||||
|
images.delete()
|
||||||
|
news.delete()
|
||||||
|
# NewsType.objects.all().delete()
|
||||||
|
|
||||||
|
print(f'Deleted {img_num} images')
|
||||||
|
print(f'Deleted {news_num} news')
|
||||||
|
|
||||||
|
|
||||||
def transfer_news():
|
def transfer_news():
|
||||||
news_type, _ = NewsType.objects.get_or_create(name='News')
|
news_type, _ = NewsType.objects.get_or_create(name='news')
|
||||||
tag_cat, _ = TagCategory.objects.get_or_create(index_name='tag')
|
|
||||||
news_type.tag_categories.add(tag_cat)
|
|
||||||
news_type.save()
|
|
||||||
|
|
||||||
queryset = PageTexts.objects.filter(
|
queryset = PageTexts.objects.filter(
|
||||||
page__type='News',
|
page__type='News',
|
||||||
).annotate(
|
).annotate(
|
||||||
tag_cat_id=Value(tag_cat.id, output_field=IntegerField()),
|
page__id=F('page__id'),
|
||||||
news_type_id=Value(news_type.id, output_field=IntegerField()),
|
news_type_id=Value(news_type.id, output_field=IntegerField()),
|
||||||
country_code=F('page__site__country_code_2'),
|
page__created_at=F('page__created_at'),
|
||||||
news_title=F('page__root_title'),
|
page__account_id=F('page__account_id'),
|
||||||
image=F('page__attachment_suffix_url'),
|
page__state=F('page__state'),
|
||||||
template=F('page__template'),
|
page__template=F('page__template'),
|
||||||
tags=GroupConcat('page__tags__id'),
|
page__site__country_code_2=F('page__site__country_code_2'),
|
||||||
account_id=F('page__account_id'),
|
page__root_title=F('page__root_title'),
|
||||||
|
page__attachment_suffix_url=F('page__attachment_suffix_url'),
|
||||||
|
page__published_at=F('page__published_at'),
|
||||||
)
|
)
|
||||||
|
|
||||||
serialized_data = NewsSerializer(data=list(queryset.values()), many=True)
|
serialized_data = NewsSerializer(data=list(queryset.values()), many=True)
|
||||||
|
|
@ -48,6 +66,102 @@ def transfer_news():
|
||||||
pprint(f'News serializer errors: {serialized_data.errors}')
|
pprint(f'News serializer errors: {serialized_data.errors}')
|
||||||
|
|
||||||
|
|
||||||
|
def update_en_gb_locales():
|
||||||
|
"""
|
||||||
|
Update default locales (en-GB)
|
||||||
|
"""
|
||||||
|
news = News.objects.filter(old_id__isnull=False)
|
||||||
|
|
||||||
|
update_news = []
|
||||||
|
for news_item in tqdm(news):
|
||||||
|
news_item.slugs = add_locale('en-GB', news_item.slugs)
|
||||||
|
news_item.title = add_locale('en-GB', news_item.title)
|
||||||
|
news_item.locale_to_description_is_active = add_locale('en-GB', news_item.locale_to_description_is_active)
|
||||||
|
news_item.description = add_locale('en-GB', news_item.description)
|
||||||
|
news_item.subtitle = add_locale('en-GB', news_item.subtitle)
|
||||||
|
update_news.append(news_item)
|
||||||
|
News.objects.bulk_update(update_news, [
|
||||||
|
'slugs',
|
||||||
|
'title',
|
||||||
|
'locale_to_description_is_active',
|
||||||
|
'description',
|
||||||
|
'subtitle',
|
||||||
|
])
|
||||||
|
print(f'Updated {len(update_news)} news locales')
|
||||||
|
|
||||||
|
|
||||||
|
def add_views_count():
|
||||||
|
"""
|
||||||
|
Add views count to news from page_counters
|
||||||
|
"""
|
||||||
|
|
||||||
|
news = News.objects.filter(old_id__isnull=False).values_list('old_id', flat=True)
|
||||||
|
counters = PageCounters.objects.filter(page_id__in=list(news))
|
||||||
|
|
||||||
|
update_counters = []
|
||||||
|
for counter in tqdm(counters):
|
||||||
|
news_item = News.objects.filter(old_id=counter.page_id).first()
|
||||||
|
if news_item:
|
||||||
|
obj, _ = ViewCount.objects.update_or_create(
|
||||||
|
news=news_item,
|
||||||
|
defaults={'count': counter.count},
|
||||||
|
)
|
||||||
|
news_item.views_count = obj
|
||||||
|
update_counters.append(news_item)
|
||||||
|
News.objects.bulk_update(update_counters, ['views_count', ])
|
||||||
|
print(f'Updated {len(update_counters)} news counters')
|
||||||
|
|
||||||
|
|
||||||
|
def add_tags():
|
||||||
|
"""
|
||||||
|
Add news tags
|
||||||
|
"""
|
||||||
|
|
||||||
|
news_type, _ = NewsType.objects.get_or_create(name='news')
|
||||||
|
tag_category, _ = TagCategory.objects.get_or_create(index_name='category')
|
||||||
|
tag_tag, _ = TagCategory.objects.get_or_create(index_name='tag')
|
||||||
|
news_type.tag_categories.add(tag_category)
|
||||||
|
news_type.tag_categories.add(tag_tag)
|
||||||
|
news_type.save()
|
||||||
|
|
||||||
|
tag_cat = {
|
||||||
|
'category': tag_category,
|
||||||
|
'tag': tag_tag,
|
||||||
|
}
|
||||||
|
|
||||||
|
news = News.objects.filter(old_id__isnull=False).values_list('old_id', flat=True)
|
||||||
|
old_news_tag = PageMetadata.objects.filter(
|
||||||
|
key__in=('category', 'tag'),
|
||||||
|
page_id__in=list(news),
|
||||||
|
)
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for old_tag in tqdm(old_news_tag):
|
||||||
|
old_id = old_tag.page.id
|
||||||
|
new_tag, created = Tag.objects.get_or_create(
|
||||||
|
category=tag_cat.get(old_tag.key),
|
||||||
|
value=old_tag.value,
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
text_value = ' '.join(new_tag.value.split('_'))
|
||||||
|
new_tag.label = {'en-GB': text_value}
|
||||||
|
new_tag.save()
|
||||||
|
|
||||||
|
news = News.objects.filter(old_id=old_id).first()
|
||||||
|
if news:
|
||||||
|
news.tags.add(new_tag)
|
||||||
|
news.save()
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
print(f'Updated {count} tags')
|
||||||
|
|
||||||
|
|
||||||
data_types = {
|
data_types = {
|
||||||
'news': [transfer_news]
|
'news': [
|
||||||
|
clear_old_news,
|
||||||
|
transfer_news,
|
||||||
|
update_en_gb_locales,
|
||||||
|
add_views_count,
|
||||||
|
add_tags,
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,5 +14,5 @@ urlpatterns = [
|
||||||
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'),
|
path('<int:pk>/carousels/', views.NewsCarouselCreateDestroyView.as_view(), name='create-destroy-carousels'),
|
||||||
path('<int:pk>/clone/<str:country_code>', views.NewsCloneView.as_view(), name='create-destroy-carousels'),
|
path('<int:pk>/clone/<str:country_code>', views.NewsCloneView.as_view(), name='clone-news-item'),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ class NewsMixinView:
|
||||||
qs = models.News.objects.published() \
|
qs = models.News.objects.published() \
|
||||||
.with_base_related() \
|
.with_base_related() \
|
||||||
.annotate_in_favorites(self.request.user) \
|
.annotate_in_favorites(self.request.user) \
|
||||||
.order_by('-is_highlighted', '-start')
|
.order_by('-is_highlighted', '-publication_date', '-publication_time')
|
||||||
|
|
||||||
country_code = self.request.country_code
|
country_code = self.request.country_code
|
||||||
if country_code:
|
if country_code:
|
||||||
|
|
@ -31,9 +31,9 @@ class NewsMixinView:
|
||||||
else:
|
else:
|
||||||
qs = qs.by_country_code(country_code)
|
qs = qs.by_country_code(country_code)
|
||||||
|
|
||||||
locale = kwargs.get('locale')
|
# locale = kwargs.get('locale')
|
||||||
if locale:
|
# if locale:
|
||||||
qs = qs.by_locale(locale)
|
# qs = qs.by_locale(locale)
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|
@ -125,7 +125,7 @@ class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView,
|
||||||
_ = super().create(request, *args, **kwargs)
|
_ = super().create(request, *args, **kwargs)
|
||||||
news_qs = self.filter_queryset(self.get_queryset())
|
news_qs = self.filter_queryset(self.get_queryset())
|
||||||
return response.Response(
|
return response.Response(
|
||||||
data=serializers.NewsDetailSerializer(get_object_or_404(news_qs, pk=kwargs.get('pk'))).data
|
data=serializers.NewsBackOfficeDetailSerializer(get_object_or_404(news_qs, pk=kwargs.get('pk'))).data
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_object(self):
|
def get_object(self):
|
||||||
|
|
|
||||||
35
apps/notification/migrations/0004_auto_20191118_1307.py
Normal file
35
apps/notification/migrations/0004_auto_20191118_1307.py
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-11-18 13:07
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
import utils.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('notification', '0003_auto_20191116_1248'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SubscriptionType',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')),
|
||||||
|
('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||||
|
('index_name', models.CharField(max_length=255, unique=True, verbose_name='Index name')),
|
||||||
|
('name', utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='name')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
bases=(models.Model, utils.models.TranslatedFieldsMixin),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='subscriber',
|
||||||
|
name='subscription_type',
|
||||||
|
field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='notification.SubscriptionType'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -4,7 +4,14 @@ from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from account.models import User
|
from account.models import User
|
||||||
from utils.methods import generate_string_code
|
from utils.methods import generate_string_code
|
||||||
from utils.models import ProjectBaseMixin
|
from utils.models import ProjectBaseMixin, TranslatedFieldsMixin, TJSONField
|
||||||
|
|
||||||
|
|
||||||
|
class SubscriptionType(ProjectBaseMixin, TranslatedFieldsMixin):
|
||||||
|
index_name = models.CharField(max_length=255, verbose_name=_('Index name'), unique=True)
|
||||||
|
name = TJSONField(blank=True, null=True, default=None,
|
||||||
|
verbose_name=_('name'),
|
||||||
|
help_text='{"en-GB":"some text"}')
|
||||||
|
|
||||||
|
|
||||||
# todo: associate user & subscriber after users registration
|
# todo: associate user & subscriber after users registration
|
||||||
|
|
@ -12,7 +19,7 @@ class SubscriberManager(models.Manager):
|
||||||
"""Extended manager for Subscriber model."""
|
"""Extended manager for Subscriber model."""
|
||||||
|
|
||||||
def make_subscriber(self, email=None, user=None, ip_address=None, country_code=None,
|
def make_subscriber(self, email=None, user=None, ip_address=None, country_code=None,
|
||||||
locale=None, *args, **kwargs):
|
locale=None, subscription_type=None, *args, **kwargs):
|
||||||
"""Make subscriber and update info."""
|
"""Make subscriber and update info."""
|
||||||
# search existing object
|
# search existing object
|
||||||
if not user:
|
if not user:
|
||||||
|
|
@ -35,10 +42,12 @@ class SubscriberManager(models.Manager):
|
||||||
obj.locale = locale
|
obj.locale = locale
|
||||||
obj.state = self.model.USABLE
|
obj.state = self.model.USABLE
|
||||||
obj.update_code = generate_string_code()
|
obj.update_code = generate_string_code()
|
||||||
|
obj.subscription_type = subscription_type
|
||||||
obj.save()
|
obj.save()
|
||||||
else:
|
else:
|
||||||
obj = self.model.objects.create(user=user, email=email, ip_address=ip_address,
|
obj = self.model.objects.create(user=user, email=email, ip_address=ip_address,
|
||||||
country_code=country_code, locale=locale)
|
country_code=country_code, locale=locale,
|
||||||
|
subscription_type=subscription_type)
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def associate_user(self, user):
|
def associate_user(self, user):
|
||||||
|
|
@ -98,6 +107,8 @@ class Subscriber(ProjectBaseMixin):
|
||||||
)
|
)
|
||||||
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
|
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
|
||||||
|
|
||||||
|
subscription_type = models.ForeignKey(SubscriptionType, on_delete=models.CASCADE, null=True, default=None)
|
||||||
|
|
||||||
objects = SubscriberManager.from_queryset(SubscriberQuerySet)()
|
objects = SubscriberManager.from_queryset(SubscriberQuerySet)()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
||||||
|
|
@ -5,16 +5,35 @@ from notification import models
|
||||||
from utils.methods import get_user_ip
|
from utils.methods import get_user_ip
|
||||||
|
|
||||||
|
|
||||||
|
class SubscriptionTypeSerializer(serializers.ModelSerializer):
|
||||||
|
"""Subscription type serializer."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta class."""
|
||||||
|
|
||||||
|
model = models.SubscriptionType
|
||||||
|
fields = (
|
||||||
|
'id',
|
||||||
|
'index_name',
|
||||||
|
'name_translated',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SubscribeSerializer(serializers.ModelSerializer):
|
class SubscribeSerializer(serializers.ModelSerializer):
|
||||||
"""Subscribe serializer."""
|
"""Subscribe serializer."""
|
||||||
|
|
||||||
email = serializers.EmailField(required=False, source='send_to')
|
email = serializers.EmailField(required=False, source='send_to')
|
||||||
|
subscription_type = SubscriptionTypeSerializer(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
||||||
model = models.Subscriber
|
model = models.Subscriber
|
||||||
fields = ('email', 'state',)
|
fields = (
|
||||||
|
'email',
|
||||||
|
'subscription_type',
|
||||||
|
'state',
|
||||||
|
)
|
||||||
read_only_fields = ('state',)
|
read_only_fields = ('state',)
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
|
|
@ -38,9 +57,16 @@ class SubscribeSerializer(serializers.ModelSerializer):
|
||||||
attrs['ip_address'] = get_user_ip(request)
|
attrs['ip_address'] = get_user_ip(request)
|
||||||
if user.is_authenticated:
|
if user.is_authenticated:
|
||||||
attrs['user'] = user
|
attrs['user'] = user
|
||||||
|
|
||||||
|
subscription_type_id = self.context.get('request').parser_context.get('kwargs').get("subscription_type_pk")
|
||||||
|
subscription_type_qs = models.SubscriptionType.objects.filter(id=subscription_type_id)
|
||||||
|
if not subscription_type_qs.exists():
|
||||||
|
raise serializers.ValidationError({'detail': _(f'SubscriptionType not found.')})
|
||||||
|
attrs["subscription_type"] = subscription_type_qs.first()
|
||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
"""Create obj."""
|
"""Create obj."""
|
||||||
obj = models.Subscriber.objects.make_subscriber(**validated_data)
|
subscriber = models.Subscriber.objects.make_subscriber(**validated_data)
|
||||||
return obj
|
return subscriber
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,10 @@ from notification.views import common
|
||||||
app_name = "notification"
|
app_name = "notification"
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('subscribe/', common.SubscribeView.as_view(), name='subscribe'),
|
path('subscribe/<int:subscription_type_pk>', common.SubscribeView.as_view(), name='subscribe'),
|
||||||
path('subscribe-info/', common.SubscribeInfoAuthUserView.as_view(), name='check-code-auth'),
|
path('subscribe-info/', common.SubscribeInfoAuthUserView.as_view(), name='check-code-auth'),
|
||||||
path('subscribe-info/<code>/', common.SubscribeInfoView.as_view(), name='check-code'),
|
path('subscribe-info/<code>/', common.SubscribeInfoView.as_view(), name='check-code'),
|
||||||
path('unsubscribe/', common.UnsubscribeAuthUserView.as_view(), name='unsubscribe-auth'),
|
path('unsubscribe/', common.UnsubscribeAuthUserView.as_view(), name='unsubscribe-auth'),
|
||||||
path('unsubscribe/<code>/', common.UnsubscribeView.as_view(), name='unsubscribe'),
|
path('unsubscribe/<code>/', common.UnsubscribeView.as_view(), name='unsubscribe'),
|
||||||
|
path('subscription-types/', common.SubscriptionTypesView.as_view(), name='subscription-types'),
|
||||||
]
|
]
|
||||||
|
|
@ -30,20 +30,16 @@ class SubscribeInfoView(generics.RetrieveAPIView):
|
||||||
serializer_class = serializers.SubscribeSerializer
|
serializer_class = serializers.SubscribeSerializer
|
||||||
|
|
||||||
|
|
||||||
class SubscribeInfoAuthUserView(generics.RetrieveAPIView):
|
class SubscribeInfoAuthUserView(generics.ListAPIView):
|
||||||
"""Subscribe info auth user view."""
|
"""Subscribe info auth user view."""
|
||||||
|
|
||||||
permission_classes = (permissions.IsAuthenticated, )
|
permission_classes = (permissions.IsAuthenticated, )
|
||||||
queryset = models.Subscriber.objects.all()
|
|
||||||
serializer_class = serializers.SubscribeSerializer
|
serializer_class = serializers.SubscribeSerializer
|
||||||
|
|
||||||
def get_object(self):
|
def get_queryset(self):
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
queryset = self.filter_queryset(self.get_queryset())
|
queryset = self.filter_queryset(models.Subscriber.objects.all())
|
||||||
filter_kwargs = {'user': user}
|
return queryset.filter(user=user)
|
||||||
obj = get_object_or_404(queryset, **filter_kwargs)
|
|
||||||
self.check_object_permissions(self.request, obj)
|
|
||||||
return obj
|
|
||||||
|
|
||||||
|
|
||||||
class UnsubscribeView(generics.GenericAPIView):
|
class UnsubscribeView(generics.GenericAPIView):
|
||||||
|
|
@ -76,3 +72,10 @@ class UnsubscribeAuthUserView(generics.GenericAPIView):
|
||||||
serializer = self.get_serializer(instance=obj)
|
serializer = self.get_serializer(instance=obj)
|
||||||
return Response(data=serializer.data)
|
return Response(data=serializer.data)
|
||||||
|
|
||||||
|
|
||||||
|
class SubscriptionTypesView(generics.ListAPIView):
|
||||||
|
pagination_class = None
|
||||||
|
permission_classes = (permissions.AllowAny,)
|
||||||
|
queryset = models.SubscriptionType.objects.all()
|
||||||
|
serializer_class = serializers.SubscriptionTypeSerializer
|
||||||
|
|
||||||
|
|
|
||||||
20
apps/product/migrations/0021_product_site.py
Normal file
20
apps/product/migrations/0021_product_site.py
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-10 14:13
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('main', '0040_footer'),
|
||||||
|
('product', '0020_merge_20191209_0911'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='site',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='main.SiteSettings'),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
apps/product/migrations/0022_auto_20191210_1517.py
Normal file
18
apps/product/migrations/0022_auto_20191210_1517.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-10 15:17
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('product', '0021_product_site'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='producttype',
|
||||||
|
name='index_name',
|
||||||
|
field=models.CharField(choices=[('food', 'food'), ('wine', 'wine'), ('liquor', 'liquor'), ('souvenir', 'souvenir'), ('book', 'book')], db_index=True, max_length=50, unique=True, verbose_name='Index name'),
|
||||||
|
),
|
||||||
|
]
|
||||||
25
apps/product/migrations/0022_auto_20191220_1007.py
Normal file
25
apps/product/migrations/0022_auto_20191220_1007.py
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-20 10:07
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('gallery', '0007_auto_20191211_1528'),
|
||||||
|
('product', '0021_auto_20191212_0926'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='productsubtype',
|
||||||
|
name='default_image',
|
||||||
|
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='product_sub_types', to='gallery.Image', verbose_name='default image'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='producttype',
|
||||||
|
name='default_image',
|
||||||
|
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='product_types', to='gallery.Image', verbose_name='default image'),
|
||||||
|
),
|
||||||
|
]
|
||||||
14
apps/product/migrations/0023_merge_20191217_1127.py
Normal file
14
apps/product/migrations/0023_merge_20191217_1127.py
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-17 11:27
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('product', '0022_auto_20191210_1517'),
|
||||||
|
('product', '0021_auto_20191212_0926'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
]
|
||||||
|
|
@ -14,10 +14,11 @@ from location.models import WineOriginAddressMixin
|
||||||
from review.models import Review
|
from review.models import Review
|
||||||
from utils.models import (BaseAttributes, ProjectBaseMixin, HasTagsMixin,
|
from utils.models import (BaseAttributes, ProjectBaseMixin, HasTagsMixin,
|
||||||
TranslatedFieldsMixin, TJSONField, FavoritesMixin,
|
TranslatedFieldsMixin, TJSONField, FavoritesMixin,
|
||||||
GalleryModelMixin, IntermediateGalleryModelMixin)
|
GalleryMixin, IntermediateGalleryModelMixin,
|
||||||
|
TypeDefaultImageMixin)
|
||||||
|
|
||||||
|
|
||||||
class ProductType(TranslatedFieldsMixin, ProjectBaseMixin):
|
class ProductType(TypeDefaultImageMixin, TranslatedFieldsMixin, ProjectBaseMixin):
|
||||||
"""ProductType model."""
|
"""ProductType model."""
|
||||||
|
|
||||||
STR_FIELD_NAME = 'name'
|
STR_FIELD_NAME = 'name'
|
||||||
|
|
@ -29,14 +30,25 @@ class ProductType(TranslatedFieldsMixin, ProjectBaseMixin):
|
||||||
SOUVENIR = 'souvenir'
|
SOUVENIR = 'souvenir'
|
||||||
BOOK = 'book'
|
BOOK = 'book'
|
||||||
|
|
||||||
|
INDEX_CHOICES = (
|
||||||
|
(FOOD, 'food'),
|
||||||
|
(WINE, 'wine'),
|
||||||
|
(LIQUOR, 'liquor'),
|
||||||
|
(SOUVENIR, 'souvenir'),
|
||||||
|
(BOOK, 'book')
|
||||||
|
)
|
||||||
name = TJSONField(blank=True, null=True, default=None,
|
name = TJSONField(blank=True, null=True, default=None,
|
||||||
verbose_name=_('Name'), help_text='{"en-GB":"some text"}')
|
verbose_name=_('Name'), help_text='{"en-GB":"some text"}')
|
||||||
index_name = models.CharField(max_length=50, unique=True, db_index=True,
|
index_name = models.CharField(max_length=50, unique=True, db_index=True,
|
||||||
verbose_name=_('Index name'))
|
verbose_name=_('Index name'), choices=INDEX_CHOICES)
|
||||||
use_subtypes = models.BooleanField(_('Use subtypes'), default=True)
|
use_subtypes = models.BooleanField(_('Use subtypes'), default=True)
|
||||||
tag_categories = models.ManyToManyField('tag.TagCategory',
|
tag_categories = models.ManyToManyField('tag.TagCategory',
|
||||||
related_name='product_types',
|
related_name='product_types',
|
||||||
verbose_name=_('Tag categories'))
|
verbose_name=_('Tag categories'))
|
||||||
|
default_image = models.ForeignKey('gallery.Image', on_delete=models.SET_NULL,
|
||||||
|
related_name='product_types',
|
||||||
|
blank=True, null=True, default=None,
|
||||||
|
verbose_name='default image')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
@ -45,7 +57,7 @@ class ProductType(TranslatedFieldsMixin, ProjectBaseMixin):
|
||||||
verbose_name_plural = _('Product types')
|
verbose_name_plural = _('Product types')
|
||||||
|
|
||||||
|
|
||||||
class ProductSubType(TranslatedFieldsMixin, ProjectBaseMixin):
|
class ProductSubType(TypeDefaultImageMixin, TranslatedFieldsMixin, ProjectBaseMixin):
|
||||||
"""ProductSubtype model."""
|
"""ProductSubtype model."""
|
||||||
|
|
||||||
STR_FIELD_NAME = 'name'
|
STR_FIELD_NAME = 'name'
|
||||||
|
|
@ -62,6 +74,10 @@ class ProductSubType(TranslatedFieldsMixin, ProjectBaseMixin):
|
||||||
verbose_name=_('Name'), help_text='{"en-GB":"some text"}')
|
verbose_name=_('Name'), help_text='{"en-GB":"some text"}')
|
||||||
index_name = models.CharField(max_length=50, unique=True, db_index=True,
|
index_name = models.CharField(max_length=50, unique=True, db_index=True,
|
||||||
verbose_name=_('Index name'))
|
verbose_name=_('Index name'))
|
||||||
|
default_image = models.ForeignKey('gallery.Image', on_delete=models.SET_NULL,
|
||||||
|
related_name='product_sub_types',
|
||||||
|
blank=True, null=True, default=None,
|
||||||
|
verbose_name='default image')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
@ -83,7 +99,7 @@ class ProductQuerySet(models.QuerySet):
|
||||||
|
|
||||||
def with_base_related(self):
|
def with_base_related(self):
|
||||||
return self.select_related('product_type', 'establishment') \
|
return self.select_related('product_type', 'establishment') \
|
||||||
.prefetch_related('product_type__subtypes')
|
.prefetch_related('product_type__subtypes', 'tags', 'tags__translation')
|
||||||
|
|
||||||
def with_extended_related(self):
|
def with_extended_related(self):
|
||||||
"""Returns qs with almost all related objects."""
|
"""Returns qs with almost all related objects."""
|
||||||
|
|
@ -195,7 +211,7 @@ class ProductQuerySet(models.QuerySet):
|
||||||
return self.none()
|
return self.none()
|
||||||
|
|
||||||
|
|
||||||
class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes,
|
class Product(GalleryMixin, TranslatedFieldsMixin, BaseAttributes,
|
||||||
HasTagsMixin, FavoritesMixin):
|
HasTagsMixin, FavoritesMixin):
|
||||||
"""Product models."""
|
"""Product models."""
|
||||||
|
|
||||||
|
|
@ -280,6 +296,8 @@ class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes,
|
||||||
default=None, null=True,
|
default=None, null=True,
|
||||||
verbose_name=_('Serial number'))
|
verbose_name=_('Serial number'))
|
||||||
|
|
||||||
|
site = models.ForeignKey(to='main.SiteSettings', null=True, blank=True, on_delete=models.CASCADE)
|
||||||
|
|
||||||
objects = ProductManager.from_queryset(ProductQuerySet)()
|
objects = ProductManager.from_queryset(ProductQuerySet)()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ from product.serializers import ProductDetailSerializer, ProductTypeBaseSerializ
|
||||||
ProductSubTypeBaseSerializer
|
ProductSubTypeBaseSerializer
|
||||||
from tag.models import TagCategory
|
from tag.models import TagCategory
|
||||||
from account.serializers.common import UserShortSerializer
|
from account.serializers.common import UserShortSerializer
|
||||||
|
from main.serializers import SiteSettingsSerializer
|
||||||
|
|
||||||
class ProductBackOfficeGallerySerializer(serializers.ModelSerializer):
|
class ProductBackOfficeGallerySerializer(serializers.ModelSerializer):
|
||||||
"""Serializer class for model ProductGallery."""
|
"""Serializer class for model ProductGallery."""
|
||||||
|
|
@ -55,6 +55,7 @@ class ProductBackOfficeGallerySerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class ProductBackOfficeDetailSerializer(ProductDetailSerializer):
|
class ProductBackOfficeDetailSerializer(ProductDetailSerializer):
|
||||||
"""Product back-office detail serializer."""
|
"""Product back-office detail serializer."""
|
||||||
|
in_favorites = serializers.BooleanField(allow_null=True, read_only=True)
|
||||||
|
|
||||||
class Meta(ProductDetailSerializer.Meta):
|
class Meta(ProductDetailSerializer.Meta):
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
@ -68,9 +69,10 @@ class ProductBackOfficeDetailSerializer(ProductDetailSerializer):
|
||||||
# 'wine_sub_region',
|
# 'wine_sub_region',
|
||||||
'wine_village',
|
'wine_village',
|
||||||
'state',
|
'state',
|
||||||
|
'site',
|
||||||
|
'product_type'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ProductTypeBackOfficeDetailSerializer(ProductTypeBaseSerializer):
|
class ProductTypeBackOfficeDetailSerializer(ProductTypeBaseSerializer):
|
||||||
"""Product type back-office detail serializer."""
|
"""Product type back-office detail serializer."""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,8 @@ class ProductSubTypeBaseSerializer(serializers.ModelSerializer):
|
||||||
name_translated = TranslatedField()
|
name_translated = TranslatedField()
|
||||||
index_name_display = serializers.CharField(source='get_index_name_display',
|
index_name_display = serializers.CharField(source='get_index_name_display',
|
||||||
read_only=True)
|
read_only=True)
|
||||||
|
default_image_url = serializers.ImageField(source='default_image.image',
|
||||||
|
allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.ProductSubType
|
model = models.ProductSubType
|
||||||
|
|
@ -41,12 +43,15 @@ class ProductSubTypeBaseSerializer(serializers.ModelSerializer):
|
||||||
'id',
|
'id',
|
||||||
'name_translated',
|
'name_translated',
|
||||||
'index_name_display',
|
'index_name_display',
|
||||||
|
'default_image_url',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ProductTypeBaseSerializer(serializers.ModelSerializer):
|
class ProductTypeBaseSerializer(serializers.ModelSerializer):
|
||||||
"""ProductType base serializer"""
|
"""ProductType base serializer"""
|
||||||
name_translated = TranslatedField()
|
name_translated = TranslatedField()
|
||||||
|
default_image_url = serializers.ImageField(source='default_image.image',
|
||||||
|
allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.ProductType
|
model = models.ProductType
|
||||||
|
|
@ -54,6 +59,7 @@ class ProductTypeBaseSerializer(serializers.ModelSerializer):
|
||||||
'id',
|
'id',
|
||||||
'name_translated',
|
'name_translated',
|
||||||
'index_name',
|
'index_name',
|
||||||
|
'default_image_url',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -95,7 +101,7 @@ class ProductBaseSerializer(serializers.ModelSerializer):
|
||||||
wine_colors = TagBaseSerializer(many=True, read_only=True)
|
wine_colors = TagBaseSerializer(many=True, read_only=True)
|
||||||
preview_image_url = serializers.URLField(allow_null=True,
|
preview_image_url = serializers.URLField(allow_null=True,
|
||||||
read_only=True)
|
read_only=True)
|
||||||
in_favorites = serializers.BooleanField(allow_null=True)
|
in_favorites = serializers.BooleanField(allow_null=True, read_only=True)
|
||||||
wine_origins = EstablishmentWineOriginBaseSerializer(many=True, read_only=True)
|
wine_origins = EstablishmentWineOriginBaseSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
||||||
121
apps/product/tests.py
Normal file
121
apps/product/tests.py
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
from rest_framework import status
|
||||||
|
from account.models import User
|
||||||
|
from http.cookies import SimpleCookie
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
|
from translation.models import Language
|
||||||
|
from account.models import Role, UserRole
|
||||||
|
from location.models import Country, Address, City, Region
|
||||||
|
from main.models import SiteSettings
|
||||||
|
from product.models import Product, ProductType
|
||||||
|
|
||||||
|
class BaseTestCase(APITestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.username = 'sedragurda'
|
||||||
|
self.password = 'sedragurdaredips19'
|
||||||
|
self.email = 'sedragurda@desoz.com'
|
||||||
|
self.newsletter = True
|
||||||
|
self.user = User.objects.create_user(
|
||||||
|
username=self.username,
|
||||||
|
email=self.email,
|
||||||
|
password=self.password,
|
||||||
|
is_staff=True,
|
||||||
|
)
|
||||||
|
# get tokens
|
||||||
|
tokens = User.create_jwt_tokens(self.user)
|
||||||
|
self.client.cookies = SimpleCookie(
|
||||||
|
{'access_token': tokens.get('access_token'),
|
||||||
|
'refresh_token': tokens.get('refresh_token')})
|
||||||
|
|
||||||
|
|
||||||
|
self.lang = Language.objects.create(
|
||||||
|
title='Russia',
|
||||||
|
locale='ru-RU'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.country_ru = Country.objects.create(
|
||||||
|
name={'en-GB': 'Russian'},
|
||||||
|
code='RU',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.region = Region.objects.create(name='Moscow area', code='01',
|
||||||
|
country=self.country_ru)
|
||||||
|
self.region.save()
|
||||||
|
|
||||||
|
self.city = City.objects.create(
|
||||||
|
name='Mosocow', code='01',
|
||||||
|
region=self.region,
|
||||||
|
country=self.country_ru)
|
||||||
|
self.city.save()
|
||||||
|
|
||||||
|
self.address = Address.objects.create(
|
||||||
|
city=self.city, street_name_1='Krasnaya',
|
||||||
|
number=2, postal_code='010100')
|
||||||
|
self.address.save()
|
||||||
|
|
||||||
|
self.site = SiteSettings.objects.create(
|
||||||
|
subdomain='ru',
|
||||||
|
country=self.country_ru
|
||||||
|
)
|
||||||
|
|
||||||
|
self.site.save()
|
||||||
|
|
||||||
|
self.role = Role.objects.create(role=Role.LIQUOR_REVIEWER,
|
||||||
|
site=self.site)
|
||||||
|
self.role.save()
|
||||||
|
|
||||||
|
self.user_role = UserRole.objects.create(
|
||||||
|
user=self.user, role=self.role)
|
||||||
|
|
||||||
|
self.user_role.save()
|
||||||
|
|
||||||
|
self.product_type = ProductType.objects.create(index_name=ProductType.LIQUOR)
|
||||||
|
self.product_type.save()
|
||||||
|
|
||||||
|
self.product = Product.objects.create(name='Product')
|
||||||
|
self.product.save()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class LiquorReviewerTests(BaseTestCase):
|
||||||
|
def test_get(self):
|
||||||
|
self.product.product_type = self.product_type
|
||||||
|
self.product.save()
|
||||||
|
|
||||||
|
url = reverse("back:product:list-create")
|
||||||
|
response = self.client.get(url, format='json')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
url = reverse("back:product:rud", kwargs={'pk': self.product.id})
|
||||||
|
response = self.client.get(url, format='json')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def test_post_patch_put_delete(self):
|
||||||
|
data_post = {
|
||||||
|
"slug": None,
|
||||||
|
"public_mark": None,
|
||||||
|
"vintage": None,
|
||||||
|
"average_price": None,
|
||||||
|
"description": None,
|
||||||
|
"available": False,
|
||||||
|
"establishment": None,
|
||||||
|
"wine_village": None,
|
||||||
|
"state": Product.PUBLISHED,
|
||||||
|
"site_id": self.site.id,
|
||||||
|
"product_type_id": self.product_type.id
|
||||||
|
}
|
||||||
|
url = reverse("back:product:list-create")
|
||||||
|
response = self.client.post(url, data=data_post, format='json')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
data_patch = {
|
||||||
|
'name': 'Test product'
|
||||||
|
}
|
||||||
|
url = reverse("back:product:rud", kwargs={'pk': self.product.id})
|
||||||
|
response = self.client.patch(url, data=data_patch, format='json')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -7,6 +7,7 @@ from product import serializers, models
|
||||||
from product.views import ProductBaseView
|
from product.views import ProductBaseView
|
||||||
from utils.serializers import ImageBaseSerializer
|
from utils.serializers import ImageBaseSerializer
|
||||||
from utils.views import CreateDestroyGalleryViewMixin
|
from utils.views import CreateDestroyGalleryViewMixin
|
||||||
|
from utils.permissions import IsLiquorReviewer, IsProductReviewer
|
||||||
|
|
||||||
|
|
||||||
class ProductBackOfficeMixinView(ProductBaseView):
|
class ProductBackOfficeMixinView(ProductBaseView):
|
||||||
|
|
@ -91,12 +92,14 @@ class ProductDetailBackOfficeView(ProductBackOfficeMixinView,
|
||||||
generics.RetrieveUpdateDestroyAPIView):
|
generics.RetrieveUpdateDestroyAPIView):
|
||||||
"""Product back-office R/U/D view."""
|
"""Product back-office R/U/D view."""
|
||||||
serializer_class = serializers.ProductBackOfficeDetailSerializer
|
serializer_class = serializers.ProductBackOfficeDetailSerializer
|
||||||
|
permission_classes = [IsLiquorReviewer | IsProductReviewer]
|
||||||
|
|
||||||
|
|
||||||
class ProductListCreateBackOfficeView(BackOfficeListCreateMixin, ProductBackOfficeMixinView,
|
class ProductListCreateBackOfficeView(BackOfficeListCreateMixin, ProductBackOfficeMixinView,
|
||||||
generics.ListCreateAPIView):
|
generics.ListCreateAPIView):
|
||||||
"""Product back-office list-create view."""
|
"""Product back-office list-create view."""
|
||||||
serializer_class = serializers.ProductBackOfficeDetailSerializer
|
serializer_class = serializers.ProductBackOfficeDetailSerializer
|
||||||
|
permission_classes = [IsLiquorReviewer | IsProductReviewer]
|
||||||
|
|
||||||
|
|
||||||
class ProductTypeListCreateBackOfficeView(BackOfficeListCreateMixin,
|
class ProductTypeListCreateBackOfficeView(BackOfficeListCreateMixin,
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
"""Product app views."""
|
"""Product app views."""
|
||||||
from rest_framework import generics, permissions
|
from django.conf import settings
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from product.models import Product
|
from rest_framework import generics, permissions
|
||||||
|
|
||||||
from comment.models import Comment
|
from comment.models import Comment
|
||||||
from product import filters, serializers
|
|
||||||
from comment.serializers import CommentRUDSerializer
|
from comment.serializers import CommentRUDSerializer
|
||||||
|
from product import filters, serializers
|
||||||
|
from product.models import Product
|
||||||
from utils.views import FavoritesCreateDestroyMixinView
|
from utils.views import FavoritesCreateDestroyMixinView
|
||||||
from utils.pagination import PortionPagination
|
|
||||||
|
|
||||||
|
|
||||||
class ProductBaseView(generics.GenericAPIView):
|
class ProductBaseView(generics.GenericAPIView):
|
||||||
|
|
@ -35,7 +36,15 @@ class ProductListView(ProductBaseView, generics.ListAPIView):
|
||||||
class ProductSimilarView(ProductListView):
|
class ProductSimilarView(ProductListView):
|
||||||
"""Resource for getting a list of similar product."""
|
"""Resource for getting a list of similar product."""
|
||||||
serializer_class = serializers.ProductBaseSerializer
|
serializer_class = serializers.ProductBaseSerializer
|
||||||
pagination_class = PortionPagination
|
pagination_class = None
|
||||||
|
|
||||||
|
def get_base_object(self):
|
||||||
|
"""
|
||||||
|
Return base product instance for a getting list of similar products.
|
||||||
|
"""
|
||||||
|
product = get_object_or_404(Product.objects.all(),
|
||||||
|
slug=self.kwargs.get('slug'))
|
||||||
|
return product
|
||||||
|
|
||||||
|
|
||||||
class ProductDetailView(ProductBaseView, generics.RetrieveAPIView):
|
class ProductDetailView(ProductBaseView, generics.RetrieveAPIView):
|
||||||
|
|
@ -95,7 +104,10 @@ class SimilarListView(ProductSimilarView):
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Overridden get_queryset method."""
|
"""Overridden get_queryset method."""
|
||||||
return super().get_queryset() \
|
qs = super(SimilarListView, self).get_queryset()
|
||||||
.has_location() \
|
base_product = self.get_base_object()
|
||||||
.similar(slug=self.kwargs.get('slug'))
|
|
||||||
|
|
||||||
|
if base_product:
|
||||||
|
return qs.has_location().similar(slug=self.kwargs.get('slug'))[:settings.QUERY_OUTPUT_OBJECTS]
|
||||||
|
else:
|
||||||
|
return qs.none()
|
||||||
|
|
|
||||||
18
apps/rating/migrations/0005_auto_20191223_0850.py
Normal file
18
apps/rating/migrations/0005_auto_20191223_0850.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-23 08:50
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('rating', '0004_auto_20191114_2041'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='viewcount',
|
||||||
|
name='count',
|
||||||
|
field=models.PositiveIntegerField(),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -23,4 +23,4 @@ class Rating(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class ViewCount(models.Model):
|
class ViewCount(models.Model):
|
||||||
count = models.IntegerField()
|
count = models.PositiveIntegerField()
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ class RecipeQuerySet(models.QuerySet):
|
||||||
|
|
||||||
# todo: what records are considered published?
|
# todo: what records are considered published?
|
||||||
def published(self):
|
def published(self):
|
||||||
|
# TODO: проверка по полю published_at
|
||||||
return self.filter(state__in=[self.model.PUBLISHED,
|
return self.filter(state__in=[self.model.PUBLISHED,
|
||||||
self.model.PUBLISHED_EXCLUSIVE])
|
self.model.PUBLISHED_EXCLUSIVE])
|
||||||
|
|
||||||
|
|
@ -67,3 +68,6 @@ class Recipe(TranslatedFieldsMixin, ImageMixin, BaseAttributes):
|
||||||
|
|
||||||
verbose_name = _('Recipe')
|
verbose_name = _('Recipe')
|
||||||
verbose_name_plural = _('Recipes')
|
verbose_name_plural = _('Recipes')
|
||||||
|
|
||||||
|
# TODO: в save добавить обновление published_at если state в PUBLISHED или PUBLISHED_EXCLUSIVE
|
||||||
|
# TODO: в save добавить обновление published_at в None если state в WAITING или HIDDEN
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from utils.models import (BaseAttributes, TranslatedFieldsMixin,
|
from utils.models import (BaseAttributes, TranslatedFieldsMixin,
|
||||||
ProjectBaseMixin, GalleryModelMixin,
|
ProjectBaseMixin, GalleryMixin,
|
||||||
TJSONField, IntermediateGalleryModelMixin)
|
TJSONField, IntermediateGalleryModelMixin)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -93,7 +93,7 @@ class Review(BaseAttributes, TranslatedFieldsMixin):
|
||||||
verbose_name_plural = _('Reviews')
|
verbose_name_plural = _('Reviews')
|
||||||
|
|
||||||
|
|
||||||
class Inquiries(GalleryModelMixin, ProjectBaseMixin):
|
class Inquiries(GalleryMixin, ProjectBaseMixin):
|
||||||
NONE = 0
|
NONE = 0
|
||||||
DINER = 1
|
DINER = 1
|
||||||
LUNCH = 2
|
LUNCH = 2
|
||||||
|
|
|
||||||
|
|
@ -23,12 +23,14 @@ class EstablishmentDocument(Document):
|
||||||
'name': fields.ObjectField(attr='name_indexing',
|
'name': fields.ObjectField(attr='name_indexing',
|
||||||
properties=OBJECT_FIELD_PROPERTIES),
|
properties=OBJECT_FIELD_PROPERTIES),
|
||||||
'index_name': fields.KeywordField(attr='index_name'),
|
'index_name': fields.KeywordField(attr='index_name'),
|
||||||
|
'default_image': fields.KeywordField(attr='default_image_url'),
|
||||||
})
|
})
|
||||||
establishment_subtypes = fields.ObjectField(
|
establishment_subtypes = fields.ObjectField(
|
||||||
properties={
|
properties={
|
||||||
'id': fields.IntegerField(),
|
'id': fields.IntegerField(),
|
||||||
'name': fields.ObjectField(attr='name_indexing'),
|
'name': fields.ObjectField(attr='name_indexing'),
|
||||||
'index_name': fields.KeywordField(attr='index_name'),
|
'index_name': fields.KeywordField(attr='index_name'),
|
||||||
|
'default_image': fields.KeywordField(attr='default_image_url'),
|
||||||
},
|
},
|
||||||
multi=True)
|
multi=True)
|
||||||
works_evening = fields.ListField(fields.IntegerField(
|
works_evening = fields.ListField(fields.IntegerField(
|
||||||
|
|
@ -143,6 +145,8 @@ class EstablishmentDocument(Document):
|
||||||
'weekday_display': fields.KeywordField(attr='get_weekday_display'),
|
'weekday_display': fields.KeywordField(attr='get_weekday_display'),
|
||||||
'closed_at': fields.KeywordField(attr='closed_at_str'),
|
'closed_at': fields.KeywordField(attr='closed_at_str'),
|
||||||
'opening_at': fields.KeywordField(attr='opening_at_str'),
|
'opening_at': fields.KeywordField(attr='opening_at_str'),
|
||||||
|
'closed_at_indexing': fields.DateField(),
|
||||||
|
'opening_at_indexing': fields.DateField(),
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
address = fields.ObjectField(
|
address = fields.ObjectField(
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ class NewsDocument(Document):
|
||||||
},
|
},
|
||||||
multi=True)
|
multi=True)
|
||||||
favorites_for_users = fields.ListField(field=fields.IntegerField())
|
favorites_for_users = fields.ListField(field=fields.IntegerField())
|
||||||
start = fields.DateField(attr='start')
|
start = fields.DateField(attr='publication_datetime')
|
||||||
has_any_desc_active = fields.BooleanField()
|
has_any_desc_active = fields.BooleanField()
|
||||||
|
|
||||||
def prepare_slugs(self, instance):
|
def prepare_slugs(self, instance):
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ class ProductDocument(Document):
|
||||||
'id': fields.IntegerField(),
|
'id': fields.IntegerField(),
|
||||||
'name': fields.ObjectField(attr='name_indexing', properties=OBJECT_FIELD_PROPERTIES),
|
'name': fields.ObjectField(attr='name_indexing', properties=OBJECT_FIELD_PROPERTIES),
|
||||||
'index_name': fields.KeywordField(),
|
'index_name': fields.KeywordField(),
|
||||||
|
'default_image': fields.KeywordField(attr='default_image_url'),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
subtypes = fields.ObjectField(
|
subtypes = fields.ObjectField(
|
||||||
|
|
@ -26,6 +27,7 @@ class ProductDocument(Document):
|
||||||
'id': fields.IntegerField(),
|
'id': fields.IntegerField(),
|
||||||
'name': fields.ObjectField(attr='name_indexing', properties=OBJECT_FIELD_PROPERTIES),
|
'name': fields.ObjectField(attr='name_indexing', properties=OBJECT_FIELD_PROPERTIES),
|
||||||
'index_name': fields.KeywordField(),
|
'index_name': fields.KeywordField(),
|
||||||
|
'default_image': fields.KeywordField(attr='default_image_url'),
|
||||||
},
|
},
|
||||||
multi=True
|
multi=True
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
"""Search indexes app views."""
|
"""Search indexes app views."""
|
||||||
from rest_framework import permissions
|
|
||||||
from django_elasticsearch_dsl_drf import constants
|
from django_elasticsearch_dsl_drf import constants
|
||||||
from django_elasticsearch_dsl_drf.filter_backends import (
|
from django_elasticsearch_dsl_drf.filter_backends import (
|
||||||
FilteringFilterBackend,
|
FilteringFilterBackend,
|
||||||
GeoSpatialOrderingFilterBackend,
|
GeoSpatialOrderingFilterBackend,
|
||||||
OrderingFilterBackend,
|
OrderingFilterBackend,
|
||||||
)
|
)
|
||||||
from elasticsearch_dsl import TermsFacet
|
|
||||||
from django_elasticsearch_dsl_drf.viewsets import BaseDocumentViewSet
|
from django_elasticsearch_dsl_drf.viewsets import BaseDocumentViewSet
|
||||||
|
from elasticsearch_dsl import TermsFacet
|
||||||
|
from rest_framework import permissions
|
||||||
|
|
||||||
|
from product.models import Product
|
||||||
from search_indexes import serializers, filters, utils
|
from search_indexes import serializers, filters, utils
|
||||||
from search_indexes.documents import EstablishmentDocument, NewsDocument
|
from search_indexes.documents import EstablishmentDocument, NewsDocument
|
||||||
from search_indexes.documents.product import ProductDocument
|
from search_indexes.documents.product import ProductDocument
|
||||||
|
|
@ -346,6 +348,12 @@ class ProductDocumentViewSet(BaseDocumentViewSet):
|
||||||
# GeoSpatialOrderingFilterBackend,
|
# GeoSpatialOrderingFilterBackend,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
qs = super(ProductDocumentViewSet, self).get_queryset()
|
||||||
|
qs = qs.filter('match', state=Product.PUBLISHED)
|
||||||
|
return qs
|
||||||
|
|
||||||
ordering_fields = {
|
ordering_fields = {
|
||||||
'created': {
|
'created': {
|
||||||
'field': 'created',
|
'field': 'created',
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ class TagCategoryFilterSet(TagsBaseFilterSet):
|
||||||
# todo: filter by establishment type
|
# todo: filter by establishment type
|
||||||
def by_establishment_type(self, queryset, name, value):
|
def by_establishment_type(self, queryset, name, value):
|
||||||
if value == EstablishmentType.ARTISAN:
|
if value == EstablishmentType.ARTISAN:
|
||||||
qs = models.TagCategory.objects.filter(index_name='shop_category')
|
qs = models.TagCategory.objects.with_base_related().filter(index_name='shop_category')
|
||||||
else:
|
else:
|
||||||
qs = queryset.by_establishment_type(value)
|
qs = queryset.by_establishment_type(value)
|
||||||
return qs
|
return qs
|
||||||
|
|
@ -73,10 +73,10 @@ class TagsFilterSet(TagsBaseFilterSet):
|
||||||
|
|
||||||
def by_establishment_type(self, queryset, name, value):
|
def by_establishment_type(self, queryset, name, value):
|
||||||
if value == EstablishmentType.ARTISAN:
|
if value == EstablishmentType.ARTISAN:
|
||||||
qs = models.Tag.objects.by_category_index_name('shop_category')
|
qs = models.Tag.objects.filter(index_name__in=settings.ARTISANS_CHOSEN_TAGS)
|
||||||
if self.request.country_code and self.request.country_code not in settings.INTERNATIONAL_COUNTRY_CODES:
|
if self.request.country_code and self.request.country_code not in settings.INTERNATIONAL_COUNTRY_CODES:
|
||||||
qs = qs.filter(establishments__address__city__country__code=self.request.country_code).distinct('id')
|
qs = qs.filter(establishments__address__city__country__code=self.request.country_code).distinct('id')
|
||||||
return qs.exclude(establishments__isnull=True)[0:8]
|
return qs.exclude(establishments__isnull=True)
|
||||||
return queryset.by_establishment_type(value)
|
return queryset.by_establishment_type(value)
|
||||||
|
|
||||||
# TMP TODO remove it later
|
# TMP TODO remove it later
|
||||||
|
|
|
||||||
46
apps/tag/migrations/0016_auto_20191220_1224.py
Normal file
46
apps/tag/migrations/0016_auto_20191220_1224.py
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-20 12:24
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
def fill_translations(apps, schemaeditor):
|
||||||
|
Tag = apps.get_model('tag', 'Tag')
|
||||||
|
TagCategory = apps.get_model('tag', 'TagCategory')
|
||||||
|
SiteInterfaceDictionary = apps.get_model('translation', 'SiteInterfaceDictionary')
|
||||||
|
|
||||||
|
for tag_category in TagCategory.objects.all():
|
||||||
|
if tag_category.label:
|
||||||
|
t = SiteInterfaceDictionary(text=tag_category.label)
|
||||||
|
t.save()
|
||||||
|
tag_category.translation = t
|
||||||
|
tag_category.save()
|
||||||
|
|
||||||
|
for tag in Tag.objects.all():
|
||||||
|
if tag.label:
|
||||||
|
t = SiteInterfaceDictionary(text=tag.label)
|
||||||
|
t.save()
|
||||||
|
tag.translation = t
|
||||||
|
tag.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('translation', '0007_language_is_active'),
|
||||||
|
('tag', '0015_auto_20191118_1210'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='tag',
|
||||||
|
name='translation',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tag', to='translation.SiteInterfaceDictionary', verbose_name='Translation'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='tagcategory',
|
||||||
|
name='translation',
|
||||||
|
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tag_category', to='translation.SiteInterfaceDictionary', verbose_name='Translation'),
|
||||||
|
),
|
||||||
|
migrations.RunPython(fill_translations, migrations.RunPython.noop)
|
||||||
|
]
|
||||||
21
apps/tag/migrations/0017_auto_20191220_1623.py
Normal file
21
apps/tag/migrations/0017_auto_20191220_1623.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-20 16:23
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('tag', '0016_auto_20191220_1224'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='tag',
|
||||||
|
name='label',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='tagcategory',
|
||||||
|
name='label',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -5,7 +5,7 @@ from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from configuration.models import TranslationSettings
|
from configuration.models import TranslationSettings
|
||||||
from location.models import Country
|
from location.models import Country
|
||||||
from utils.models import TJSONField, TranslatedFieldsMixin
|
from utils.models import IndexJSON
|
||||||
|
|
||||||
|
|
||||||
class TagQuerySet(models.QuerySet):
|
class TagQuerySet(models.QuerySet):
|
||||||
|
|
@ -29,12 +29,9 @@ class TagQuerySet(models.QuerySet):
|
||||||
return self.filter(category__establishment_types__index_name=index_name)
|
return self.filter(category__establishment_types__index_name=index_name)
|
||||||
|
|
||||||
|
|
||||||
class Tag(TranslatedFieldsMixin, models.Model):
|
class Tag(models.Model):
|
||||||
"""Tag model."""
|
"""Tag model."""
|
||||||
|
|
||||||
label = TJSONField(blank=True, null=True, default=None,
|
|
||||||
verbose_name=_('label'),
|
|
||||||
help_text='{"en-GB":"some text"}')
|
|
||||||
value = models.CharField(_('indexing name'), max_length=255, blank=True, db_index=True,
|
value = models.CharField(_('indexing name'), max_length=255, blank=True, db_index=True,
|
||||||
null=True, default=None)
|
null=True, default=None)
|
||||||
category = models.ForeignKey('TagCategory', on_delete=models.CASCADE,
|
category = models.ForeignKey('TagCategory', on_delete=models.CASCADE,
|
||||||
|
|
@ -48,6 +45,16 @@ class Tag(TranslatedFieldsMixin, models.Model):
|
||||||
|
|
||||||
old_id_meta_product = models.PositiveIntegerField(_('old id metadata product'),
|
old_id_meta_product = models.PositiveIntegerField(_('old id metadata product'),
|
||||||
blank=True, null=True, default=None)
|
blank=True, null=True, default=None)
|
||||||
|
translation = models.ForeignKey('translation.SiteInterfaceDictionary', on_delete=models.SET_NULL,
|
||||||
|
null=True, related_name='tag', verbose_name=_('Translation'))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def label_indexing(self):
|
||||||
|
base_dict = self.translation.text if self.translation and isinstance(self.translation.text, dict) else {}
|
||||||
|
index = IndexJSON()
|
||||||
|
for k, v in base_dict.items():
|
||||||
|
setattr(index, k, v)
|
||||||
|
return index
|
||||||
|
|
||||||
objects = TagQuerySet.as_manager()
|
objects = TagQuerySet.as_manager()
|
||||||
|
|
||||||
|
|
@ -88,7 +95,7 @@ class TagCategoryQuerySet(models.QuerySet):
|
||||||
|
|
||||||
def with_base_related(self):
|
def with_base_related(self):
|
||||||
"""Select related objects."""
|
"""Select related objects."""
|
||||||
return self.prefetch_related('tags')
|
return self.prefetch_related('tags', 'tags__translation').select_related('translation')
|
||||||
|
|
||||||
def with_extended_related(self):
|
def with_extended_related(self):
|
||||||
"""Select related objects."""
|
"""Select related objects."""
|
||||||
|
|
@ -119,7 +126,7 @@ class TagCategoryQuerySet(models.QuerySet):
|
||||||
return self.exclude(tags__isnull=switcher)
|
return self.exclude(tags__isnull=switcher)
|
||||||
|
|
||||||
|
|
||||||
class TagCategory(TranslatedFieldsMixin, models.Model):
|
class TagCategory(models.Model):
|
||||||
"""Tag base category model."""
|
"""Tag base category model."""
|
||||||
|
|
||||||
STRING = 'string'
|
STRING = 'string'
|
||||||
|
|
@ -137,10 +144,6 @@ class TagCategory(TranslatedFieldsMixin, models.Model):
|
||||||
(PERCENTAGE, _('percentage')),
|
(PERCENTAGE, _('percentage')),
|
||||||
(BOOLEAN, _('boolean')),
|
(BOOLEAN, _('boolean')),
|
||||||
)
|
)
|
||||||
|
|
||||||
label = TJSONField(blank=True, null=True, default=None,
|
|
||||||
verbose_name=_('label'),
|
|
||||||
help_text='{"en-GB":"some text"}')
|
|
||||||
country = models.ForeignKey('location.Country',
|
country = models.ForeignKey('location.Country',
|
||||||
on_delete=models.SET_NULL, null=True,
|
on_delete=models.SET_NULL, null=True,
|
||||||
default=None)
|
default=None)
|
||||||
|
|
@ -151,6 +154,16 @@ class TagCategory(TranslatedFieldsMixin, models.Model):
|
||||||
value_type = models.CharField(_('value type'), max_length=255,
|
value_type = models.CharField(_('value type'), max_length=255,
|
||||||
choices=VALUE_TYPE_CHOICES, default=LIST, )
|
choices=VALUE_TYPE_CHOICES, default=LIST, )
|
||||||
old_id = models.IntegerField(blank=True, null=True)
|
old_id = models.IntegerField(blank=True, null=True)
|
||||||
|
translation = models.OneToOneField('translation.SiteInterfaceDictionary', on_delete=models.SET_NULL,
|
||||||
|
null=True, related_name='tag_category', verbose_name=_('Translation'))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def label_indexing(self):
|
||||||
|
base_dict = self.translation.text if self.translation and isinstance(self.translation.text, dict) else {}
|
||||||
|
index = IndexJSON()
|
||||||
|
for k, v in base_dict.items():
|
||||||
|
setattr(index, k, v)
|
||||||
|
return index
|
||||||
|
|
||||||
objects = TagCategoryQuerySet.as_manager()
|
objects = TagCategoryQuerySet.as_manager()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,25 @@
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.fields import SerializerMethodField
|
from rest_framework.fields import SerializerMethodField
|
||||||
|
|
||||||
from establishment.models import Establishment
|
from establishment.models import Establishment, EstablishmentType
|
||||||
from establishment.models import EstablishmentType
|
|
||||||
from news.models import News
|
from news.models import News
|
||||||
from news.models import NewsType
|
from news.models import NewsType
|
||||||
from tag import models
|
from tag import models
|
||||||
from utils.exceptions import BindingObjectNotFound
|
from utils.exceptions import BindingObjectNotFound, ObjectAlreadyAdded, RemovedBindingObjectNotFound
|
||||||
from utils.exceptions import ObjectAlreadyAdded
|
|
||||||
from utils.exceptions import RemovedBindingObjectNotFound
|
|
||||||
from utils.serializers import TranslatedField
|
from utils.serializers import TranslatedField
|
||||||
|
from utils.models import get_default_locale, get_language, to_locale
|
||||||
|
|
||||||
|
|
||||||
|
def translate_obj(obj):
|
||||||
|
if not obj.translation or not isinstance(obj.translation.text, dict):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
field = obj.translation.text
|
||||||
|
return field.get(to_locale(get_language()),
|
||||||
|
field.get(get_default_locale(),
|
||||||
|
next(iter(field.values()))))
|
||||||
|
except StopIteration:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class TagBaseSerializer(serializers.ModelSerializer):
|
class TagBaseSerializer(serializers.ModelSerializer):
|
||||||
|
|
@ -19,8 +29,11 @@ class TagBaseSerializer(serializers.ModelSerializer):
|
||||||
def get_extra_kwargs(self):
|
def get_extra_kwargs(self):
|
||||||
return super().get_extra_kwargs()
|
return super().get_extra_kwargs()
|
||||||
|
|
||||||
label_translated = TranslatedField()
|
|
||||||
index_name = serializers.CharField(source='value', read_only=True, allow_null=True)
|
index_name = serializers.CharField(source='value', read_only=True, allow_null=True)
|
||||||
|
label_translated = serializers.SerializerMethodField(read_only=True, allow_null=True)
|
||||||
|
|
||||||
|
def get_label_translated(self, obj):
|
||||||
|
return translate_obj(obj)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
@ -36,6 +49,8 @@ class TagBaseSerializer(serializers.ModelSerializer):
|
||||||
class TagBackOfficeSerializer(TagBaseSerializer):
|
class TagBackOfficeSerializer(TagBaseSerializer):
|
||||||
"""Serializer for Tag model for Back office users."""
|
"""Serializer for Tag model for Back office users."""
|
||||||
|
|
||||||
|
label = serializers.DictField(source='translation.text')
|
||||||
|
|
||||||
class Meta(TagBaseSerializer.Meta):
|
class Meta(TagBaseSerializer.Meta):
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
||||||
|
|
@ -48,7 +63,8 @@ class TagBackOfficeSerializer(TagBaseSerializer):
|
||||||
class TagCategoryProductSerializer(serializers.ModelSerializer):
|
class TagCategoryProductSerializer(serializers.ModelSerializer):
|
||||||
"""SHORT Serializer for TagCategory"""
|
"""SHORT Serializer for TagCategory"""
|
||||||
|
|
||||||
label_translated = TranslatedField()
|
def get_label_translated(self, obj):
|
||||||
|
return translate_obj(obj)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
@ -56,7 +72,6 @@ class TagCategoryProductSerializer(serializers.ModelSerializer):
|
||||||
model = models.TagCategory
|
model = models.TagCategory
|
||||||
fields = (
|
fields = (
|
||||||
'id',
|
'id',
|
||||||
'label_translated',
|
|
||||||
'index_name',
|
'index_name',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -64,8 +79,8 @@ class TagCategoryProductSerializer(serializers.ModelSerializer):
|
||||||
class TagCategoryBaseSerializer(serializers.ModelSerializer):
|
class TagCategoryBaseSerializer(serializers.ModelSerializer):
|
||||||
"""Serializer for model TagCategory."""
|
"""Serializer for model TagCategory."""
|
||||||
|
|
||||||
label_translated = TranslatedField()
|
tags = TagBaseSerializer(many=True, allow_null=True)
|
||||||
tags = SerializerMethodField()
|
label_translated = serializers.SerializerMethodField(read_only=True, allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
@ -78,33 +93,17 @@ class TagCategoryBaseSerializer(serializers.ModelSerializer):
|
||||||
'tags',
|
'tags',
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_tags(self, obj):
|
def get_label_translated(self, obj):
|
||||||
query_params = dict(self.context['request'].query_params)
|
return translate_obj(obj)
|
||||||
|
|
||||||
if len(query_params) > 1:
|
|
||||||
return []
|
|
||||||
|
|
||||||
params = {}
|
|
||||||
if 'establishment_type' in query_params:
|
|
||||||
params = {
|
|
||||||
'establishments__isnull': False,
|
|
||||||
}
|
|
||||||
elif 'product_type' in query_params:
|
|
||||||
params = {
|
|
||||||
'products__isnull': False,
|
|
||||||
}
|
|
||||||
|
|
||||||
tags = obj.tags.filter(**params).distinct()
|
|
||||||
return TagBaseSerializer(instance=tags, many=True, read_only=True).data
|
|
||||||
|
|
||||||
|
|
||||||
class FiltersTagCategoryBaseSerializer(serializers.ModelSerializer):
|
class FiltersTagCategoryBaseSerializer(serializers.ModelSerializer):
|
||||||
"""Serializer for model TagCategory."""
|
"""Serializer for model TagCategory."""
|
||||||
|
|
||||||
label_translated = TranslatedField()
|
|
||||||
filters = SerializerMethodField()
|
filters = SerializerMethodField()
|
||||||
param_name = SerializerMethodField()
|
param_name = SerializerMethodField()
|
||||||
type = SerializerMethodField()
|
type = SerializerMethodField()
|
||||||
|
label_translated = serializers.SerializerMethodField(allow_null=True, read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
@ -127,6 +126,9 @@ class FiltersTagCategoryBaseSerializer(serializers.ModelSerializer):
|
||||||
return 'wine_colors_id__in'
|
return 'wine_colors_id__in'
|
||||||
return 'tags_id__in'
|
return 'tags_id__in'
|
||||||
|
|
||||||
|
def get_label_translated(self, obj):
|
||||||
|
return translate_obj(obj)
|
||||||
|
|
||||||
def get_fields(self, *args, **kwargs):
|
def get_fields(self, *args, **kwargs):
|
||||||
fields = super(FiltersTagCategoryBaseSerializer, self).get_fields()
|
fields = super(FiltersTagCategoryBaseSerializer, self).get_fields()
|
||||||
|
|
||||||
|
|
@ -157,10 +159,13 @@ class FiltersTagCategoryBaseSerializer(serializers.ModelSerializer):
|
||||||
class TagCategoryShortSerializer(serializers.ModelSerializer):
|
class TagCategoryShortSerializer(serializers.ModelSerializer):
|
||||||
"""Serializer for model TagCategory."""
|
"""Serializer for model TagCategory."""
|
||||||
|
|
||||||
label_translated = TranslatedField()
|
label_translated = serializers.SerializerMethodField(allow_null=True, read_only=True)
|
||||||
value_type_display = serializers.CharField(source='get_value_type_display',
|
value_type_display = serializers.CharField(source='get_value_type_display',
|
||||||
read_only=True)
|
read_only=True)
|
||||||
|
|
||||||
|
def get_label_translated(self, obj):
|
||||||
|
return translate_obj(obj)
|
||||||
|
|
||||||
class Meta(TagCategoryBaseSerializer.Meta):
|
class Meta(TagCategoryBaseSerializer.Meta):
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
fields = [
|
fields = [
|
||||||
|
|
@ -173,6 +178,7 @@ class TagCategoryBackOfficeDetailSerializer(TagCategoryBaseSerializer):
|
||||||
"""Tag Category detail serializer for back-office users."""
|
"""Tag Category detail serializer for back-office users."""
|
||||||
|
|
||||||
country_translated = TranslatedField(source='country.name_translated')
|
country_translated = TranslatedField(source='country.name_translated')
|
||||||
|
label = serializers.DictField(source='translation.text')
|
||||||
|
|
||||||
class Meta(TagCategoryBaseSerializer.Meta):
|
class Meta(TagCategoryBaseSerializer.Meta):
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from datetime import time
|
from datetime import time, datetime
|
||||||
|
|
||||||
from utils.models import ProjectBaseMixin
|
from utils.models import ProjectBaseMixin
|
||||||
|
|
||||||
|
|
@ -35,6 +35,22 @@ class Timetable(ProjectBaseMixin):
|
||||||
opening_at = models.TimeField(verbose_name=_('Opening time'), null=True)
|
opening_at = models.TimeField(verbose_name=_('Opening time'), null=True)
|
||||||
closed_at = models.TimeField(verbose_name=_('Closed time'), null=True)
|
closed_at = models.TimeField(verbose_name=_('Closed time'), null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta class."""
|
||||||
|
verbose_name = _('Timetable')
|
||||||
|
verbose_name_plural = _('Timetables')
|
||||||
|
ordering = ['weekday']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Overridden str dunder."""
|
||||||
|
return f'{self.get_weekday_display()} ' \
|
||||||
|
f'(closed_at - {self.closed_at_str}, ' \
|
||||||
|
f'opening_at - {self.opening_at_str}, ' \
|
||||||
|
f'opening_time - {self.opening_time}, ' \
|
||||||
|
f'ending_time - {self.ending_time}, ' \
|
||||||
|
f'works_at_noon - {self.works_at_noon}, ' \
|
||||||
|
f'works_at_afternoon: {self.works_at_afternoon})'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def closed_at_str(self):
|
def closed_at_str(self):
|
||||||
return str(self.closed_at) if self.closed_at else None
|
return str(self.closed_at) if self.closed_at else None
|
||||||
|
|
@ -43,6 +59,14 @@ class Timetable(ProjectBaseMixin):
|
||||||
def opening_at_str(self):
|
def opening_at_str(self):
|
||||||
return str(self.opening_at) if self.opening_at else None
|
return str(self.opening_at) if self.opening_at else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def closed_at_indexing(self):
|
||||||
|
return datetime.combine(time=self.closed_at, date=datetime(1970, 1, 1 + self.weekday).date()) if self.closed_at else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def opening_at_indexing(self):
|
||||||
|
return datetime.combine(time=self.opening_at, date=datetime(1970, 1, 1 + self.weekday).date()) if self.opening_at else None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def opening_time(self):
|
def opening_time(self):
|
||||||
return self.opening_at or self.lunch_start or self.dinner_start
|
return self.opening_at or self.lunch_start or self.dinner_start
|
||||||
|
|
@ -58,9 +82,3 @@ class Timetable(ProjectBaseMixin):
|
||||||
@property
|
@property
|
||||||
def works_at_afternoon(self):
|
def works_at_afternoon(self):
|
||||||
return bool(self.ending_time and self.ending_time > self.NOON)
|
return bool(self.ending_time and self.ending_time > self.NOON)
|
||||||
|
|
||||||
class Meta:
|
|
||||||
"""Meta class."""
|
|
||||||
verbose_name = _('Timetable')
|
|
||||||
verbose_name_plural = _('Timetables')
|
|
||||||
ordering = ['weekday']
|
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ class Command(BaseCommand):
|
||||||
'guide_element_types',
|
'guide_element_types',
|
||||||
'guide_elements_bulk',
|
'guide_elements_bulk',
|
||||||
'guide_element_advertorials',
|
'guide_element_advertorials',
|
||||||
|
'guide_element_label_photo',
|
||||||
'guide_complete',
|
'guide_complete',
|
||||||
'languages', # №4 - перенос языков
|
'languages', # №4 - перенос языков
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -343,7 +343,7 @@ class GuideAds(MigrateMixin):
|
||||||
nb_right_pages = models.IntegerField(blank=True, null=True)
|
nb_right_pages = models.IntegerField(blank=True, null=True)
|
||||||
created_at = models.DateTimeField()
|
created_at = models.DateTimeField()
|
||||||
updated_at = models.DateTimeField()
|
updated_at = models.DateTimeField()
|
||||||
guide_ad_node_id = models.IntegerField(blank=True, null=True)
|
guide_ad_node = models.ForeignKey('GuideElements', on_delete=models.DO_NOTHING, blank=True, null=True)
|
||||||
type = models.CharField(max_length=255, blank=True, null=True)
|
type = models.CharField(max_length=255, blank=True, null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
@ -1232,7 +1232,7 @@ class LabelPhotos(MigrateMixin):
|
||||||
attachment_content_type = models.CharField(max_length=255)
|
attachment_content_type = models.CharField(max_length=255)
|
||||||
attachment_file_size = models.IntegerField()
|
attachment_file_size = models.IntegerField()
|
||||||
attachment_updated_at = models.DateTimeField()
|
attachment_updated_at = models.DateTimeField()
|
||||||
attachment_suffix_url = models.DateTimeField()
|
attachment_suffix_url = models.CharField(max_length=255)
|
||||||
geometries = models.CharField(max_length=1024)
|
geometries = models.CharField(max_length=1024)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,11 @@ class EstablishmentSerializer(serializers.ModelSerializer):
|
||||||
schedules = validated_data.pop('schedules')
|
schedules = validated_data.pop('schedules')
|
||||||
subtypes = [validated_data.pop('subtype', None)]
|
subtypes = [validated_data.pop('subtype', None)]
|
||||||
|
|
||||||
establishment = Establishment.objects.create(**validated_data)
|
# establishment = Establishment.objects.create(**validated_data)
|
||||||
|
establishment, _ = Establishment.objects.update_or_create(
|
||||||
|
old_id=validated_data['old_id'],
|
||||||
|
defaults=validated_data,
|
||||||
|
)
|
||||||
if email:
|
if email:
|
||||||
ContactEmail.objects.get_or_create(
|
ContactEmail.objects.get_or_create(
|
||||||
email=email,
|
email=email,
|
||||||
|
|
|
||||||
|
|
@ -1,101 +1,92 @@
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from account.models import User
|
||||||
from gallery.models import Image
|
from gallery.models import Image
|
||||||
from location.models import Country
|
from location.models import Country
|
||||||
from news.models import News, NewsGallery
|
from news.models import News, NewsGallery
|
||||||
from tag.models import Tag
|
|
||||||
from transfer.models import PageMetadata
|
|
||||||
from utils.legacy_parser import parse_legacy_news_content
|
from utils.legacy_parser import parse_legacy_news_content
|
||||||
from utils.slug_generator import generate_unique_slug
|
|
||||||
from account.models import User
|
|
||||||
|
|
||||||
|
|
||||||
class NewsSerializer(serializers.Serializer):
|
class NewsSerializer(serializers.Serializer):
|
||||||
id = serializers.IntegerField()
|
|
||||||
account_id = serializers.IntegerField(allow_null=True)
|
|
||||||
tag_cat_id = serializers.IntegerField()
|
|
||||||
news_type_id = serializers.IntegerField()
|
|
||||||
news_title = serializers.CharField()
|
|
||||||
title = serializers.CharField()
|
|
||||||
summary = serializers.CharField(allow_null=True, allow_blank=True)
|
|
||||||
body = serializers.CharField(allow_null=True)
|
|
||||||
created_at = serializers.DateTimeField(format='%m-%d-%Y %H:%M:%S')
|
|
||||||
slug = serializers.CharField()
|
|
||||||
state = serializers.CharField()
|
|
||||||
template = serializers.CharField()
|
|
||||||
country_code = serializers.CharField(allow_null=True)
|
|
||||||
locale = serializers.CharField()
|
locale = serializers.CharField()
|
||||||
image = serializers.CharField()
|
page__id = serializers.IntegerField()
|
||||||
tags = serializers.CharField(allow_null=True)
|
news_type_id = serializers.IntegerField()
|
||||||
|
page__created_at = serializers.DateTimeField(format='%m-%d-%Y %H:%M:%S')
|
||||||
def create(self, validated_data):
|
page__account_id = serializers.IntegerField(allow_null=True)
|
||||||
|
page__state = serializers.CharField()
|
||||||
|
page__template = serializers.CharField()
|
||||||
|
page__site__country_code_2 = serializers.CharField(allow_null=True)
|
||||||
|
slug = serializers.CharField()
|
||||||
|
body = serializers.CharField(allow_null=True)
|
||||||
|
title = serializers.CharField()
|
||||||
|
page__root_title = serializers.CharField()
|
||||||
|
summary = serializers.CharField(allow_null=True, allow_blank=True)
|
||||||
|
page__attachment_suffix_url = serializers.CharField()
|
||||||
|
page__published_at = serializers.DateTimeField(format='%m-%d-%Y %H:%M:%S', allow_null=True)
|
||||||
|
|
||||||
|
def create(self, data):
|
||||||
|
account = self.get_account(data)
|
||||||
payload = {
|
payload = {
|
||||||
'old_id': validated_data['id'],
|
'old_id': data['page__id'],
|
||||||
'news_type_id': validated_data['news_type_id'],
|
'news_type_id': data['news_type_id'],
|
||||||
'title': {validated_data['locale']: validated_data['news_title']},
|
'created': data['page__created_at'],
|
||||||
'subtitle': self.get_subtitle(validated_data),
|
'created_by': account,
|
||||||
'description': self.get_description(validated_data),
|
'modified_by': account,
|
||||||
'start': validated_data['created_at'],
|
'state': self.get_state(data),
|
||||||
'slug': generate_unique_slug(News, validated_data['slug']),
|
'template': self.get_template(data),
|
||||||
'state': self.get_state(validated_data),
|
'country': self.get_country(data),
|
||||||
'template': self.get_template(validated_data),
|
'slugs': {data['locale']: data['slug']},
|
||||||
'country': self.get_country(validated_data),
|
'description': self.get_description(data),
|
||||||
'created_by': self.get_account(validated_data),
|
'title': {data['locale']: data['title']},
|
||||||
'modified_by': self.get_account(validated_data),
|
'backoffice_title': data['page__root_title'],
|
||||||
|
'subtitle': self.get_subtitle(data),
|
||||||
|
'locale_to_description_is_active': {data['locale']: True},
|
||||||
|
'publication_date': self.get_publication_date(data),
|
||||||
|
'publication_time': self.get_publication_time(data),
|
||||||
}
|
}
|
||||||
obj = News.objects.create(**payload)
|
|
||||||
|
|
||||||
tags = self.get_tags(validated_data)
|
obj, created = News.objects.get_or_create(
|
||||||
for tag in tags:
|
old_id=payload['old_id'],
|
||||||
obj.tags.add(tag)
|
defaults=payload,
|
||||||
|
)
|
||||||
|
if not created:
|
||||||
|
obj.slugs.update(payload['slugs'])
|
||||||
|
obj.title.update(payload['title'])
|
||||||
|
obj.locale_to_description_is_active.update(payload['locale_to_description_is_active'])
|
||||||
|
|
||||||
|
if obj.description and payload['description']:
|
||||||
|
obj.description.update(payload['description'])
|
||||||
|
else:
|
||||||
|
obj.description = payload['description']
|
||||||
|
|
||||||
|
if obj.subtitle and payload['subtitle']:
|
||||||
|
obj.subtitle.update(payload['subtitle'])
|
||||||
|
else:
|
||||||
|
obj.subtitle = payload['subtitle']
|
||||||
|
|
||||||
obj.save()
|
obj.save()
|
||||||
|
|
||||||
self.make_gallery(validated_data, obj)
|
self.make_gallery(data, obj)
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def make_gallery(data, obj):
|
def get_publication_date(data):
|
||||||
if not data['image'] or data['image'] == 'default/missing.png':
|
published_at = data.get('page__published_at')
|
||||||
return
|
if published_at:
|
||||||
|
return published_at.date()
|
||||||
img = Image.objects.create(
|
|
||||||
image=data['image'],
|
|
||||||
title=data['news_title'],
|
|
||||||
)
|
|
||||||
NewsGallery.objects.create(
|
|
||||||
news=obj,
|
|
||||||
image=img,
|
|
||||||
is_main=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_tags(data):
|
|
||||||
results = []
|
|
||||||
if not data['tags']:
|
|
||||||
return results
|
|
||||||
|
|
||||||
meta_ids = (int(_id) for _id in data['tags'].split(','))
|
|
||||||
tags = PageMetadata.objects.filter(
|
|
||||||
id__in=meta_ids,
|
|
||||||
key='tag',
|
|
||||||
value__isnull=False,
|
|
||||||
)
|
|
||||||
for old_tag in tags:
|
|
||||||
tag, _ = Tag.objects.get_or_create(
|
|
||||||
category_id=data['tag_cat_id'],
|
|
||||||
label={data['locale']: old_tag.value},
|
|
||||||
)
|
|
||||||
results.append(tag)
|
|
||||||
return results
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_description(data):
|
|
||||||
if data['body']:
|
|
||||||
content = parse_legacy_news_content(data['body'])
|
|
||||||
return {data['locale']: content}
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_publication_time(data):
|
||||||
|
published_at = data.get('page__published_at')
|
||||||
|
if published_at:
|
||||||
|
return published_at.time()
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_account(data):
|
||||||
|
return User.objects.filter(old_id=data['page__account_id']).first()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_state(data):
|
def get_state(data):
|
||||||
states = {
|
states = {
|
||||||
|
|
@ -105,33 +96,47 @@ class NewsSerializer(serializers.Serializer):
|
||||||
'published_exclusive': News.PUBLISHED_EXCLUSIVE,
|
'published_exclusive': News.PUBLISHED_EXCLUSIVE,
|
||||||
'scheduled_exclusively': News.WAITING,
|
'scheduled_exclusively': News.WAITING,
|
||||||
}
|
}
|
||||||
return states.get(data['state'], News.WAITING)
|
return states.get(data['page__state'], News.WAITING)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_template(data):
|
def get_template(data):
|
||||||
templates = {
|
templates = {
|
||||||
'main': News.MAIN,
|
'main': News.MAIN,
|
||||||
'main.pdf.erb': News.MAIN_PDF_ERB,
|
'main.pdf.erb': News.MAIN_PDF_ERB,
|
||||||
|
'newspaper': News.NEWSPAPER,
|
||||||
}
|
}
|
||||||
return templates.get(data['template'], News.MAIN)
|
return templates.get(data['page__template'], News.MAIN)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_country(data):
|
def get_country(data):
|
||||||
return Country.objects.filter(code__iexact=data['country_code']).first()
|
return Country.objects.filter(code__iexact=data['page__site__country_code_2']).first()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_title(data):
|
def get_description(data):
|
||||||
return {data['locale']: data['title']}
|
if data['body']:
|
||||||
|
content = parse_legacy_news_content(data['body'])
|
||||||
|
return {data['locale']: content}
|
||||||
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_subtitle(data):
|
def get_subtitle(data):
|
||||||
if data.get('summary'):
|
if data.get('summary'):
|
||||||
content = {data['locale']: data['summary']}
|
return {data['locale']: data['summary']}
|
||||||
else:
|
return None
|
||||||
content = {data['locale']: data['title']}
|
|
||||||
return content
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_account(data):
|
def make_gallery(data, obj):
|
||||||
"""Get account"""
|
if not data['page__attachment_suffix_url'] or data['page__attachment_suffix_url'] == 'default/missing.png':
|
||||||
return User.objects.filter(old_id=data['account_id']).first()
|
return
|
||||||
|
|
||||||
|
img, _ = Image.objects.get_or_create(
|
||||||
|
image=data['page__attachment_suffix_url'],
|
||||||
|
title=data['page__root_title'],
|
||||||
|
created=data['page__created_at']
|
||||||
|
)
|
||||||
|
|
||||||
|
gal, _ = NewsGallery.objects.get_or_create(
|
||||||
|
news=obj,
|
||||||
|
image=img,
|
||||||
|
is_main=True,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ from django.utils.text import slugify
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from tag.models import Tag
|
from tag.models import Tag
|
||||||
|
from translation.models import SiteInterfaceDictionary
|
||||||
from transfer.mixins import TransferSerializerMixin
|
from transfer.mixins import TransferSerializerMixin
|
||||||
from transfer.models import Cepages
|
from transfer.models import Cepages
|
||||||
|
|
||||||
|
|
@ -36,8 +37,11 @@ class AssemblageTagSerializer(TransferSerializerMixin):
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
qs = self.Meta.model.objects.filter(**validated_data)
|
qs = self.Meta.model.objects.filter(**validated_data)
|
||||||
category = validated_data.get('category')
|
category = validated_data.get('category')
|
||||||
|
translations = validated_data.pop('label')
|
||||||
if not qs.exists() and category:
|
if not qs.exists() and category:
|
||||||
return super().create(validated_data)
|
instance = super().create(validated_data)
|
||||||
|
SiteInterfaceDictionary.objects.update_or_create_for_tag(instance, translations)
|
||||||
|
return instance
|
||||||
|
|
||||||
def get_tag_value(self, cepage, percent):
|
def get_tag_value(self, cepage, percent):
|
||||||
if cepage and percent:
|
if cepage and percent:
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
from django.contrib.postgres.fields import JSONField
|
from django.contrib.postgres.fields import JSONField
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.apps import apps
|
||||||
from utils.models import ProjectBaseMixin, LocaleManagerMixin
|
from utils.models import ProjectBaseMixin, LocaleManagerMixin
|
||||||
|
|
||||||
|
|
||||||
class LanguageQuerySet(models.QuerySet):
|
class LanguageQuerySet(models.QuerySet):
|
||||||
"""QuerySet for model Language"""
|
"""QuerySet for model Language"""
|
||||||
|
|
||||||
|
|
@ -50,6 +50,44 @@ class Language(models.Model):
|
||||||
class SiteInterfaceDictionaryManager(LocaleManagerMixin):
|
class SiteInterfaceDictionaryManager(LocaleManagerMixin):
|
||||||
"""Extended manager for SiteInterfaceDictionary model."""
|
"""Extended manager for SiteInterfaceDictionary model."""
|
||||||
|
|
||||||
|
def update_or_create_for_tag(self, tag, translations: dict):
|
||||||
|
Tag = apps.get_model('tag', 'Tag')
|
||||||
|
"""Creates or updates translation for EXISTING in DB Tag"""
|
||||||
|
if not tag.pk or not isinstance(tag, Tag):
|
||||||
|
raise NotImplementedError
|
||||||
|
if tag.translation:
|
||||||
|
tag.translation.text = translations
|
||||||
|
tag.translation.page = 'tag'
|
||||||
|
tag.translation.keywords = f'tag-{tag.pk}'
|
||||||
|
else:
|
||||||
|
trans = SiteInterfaceDictionary({
|
||||||
|
'text': translations,
|
||||||
|
'page': 'tag',
|
||||||
|
'keywords': f'tag-{tag.pk}'
|
||||||
|
})
|
||||||
|
trans.save()
|
||||||
|
tag.translation = trans
|
||||||
|
tag.save()
|
||||||
|
|
||||||
|
def update_or_create_for_tag_category(self, tag_category, translations: dict):
|
||||||
|
"""Creates or updates translation for EXISTING in DB TagCategory"""
|
||||||
|
TagCategory = apps.get_model('tag', 'TagCategory')
|
||||||
|
if not tag_category.pk or not isinstance(tag_category, TagCategory):
|
||||||
|
raise NotImplementedError
|
||||||
|
if tag_category.translation:
|
||||||
|
tag_category.translation.text = translations
|
||||||
|
tag_category.translation.page = 'tag'
|
||||||
|
tag_category.translation.keywords = f'tag_category-{tag_category.pk}'
|
||||||
|
else:
|
||||||
|
trans = SiteInterfaceDictionary({
|
||||||
|
'text': translations,
|
||||||
|
'page': 'tag',
|
||||||
|
'keywords': f'tag_category-{tag_category.pk}'
|
||||||
|
})
|
||||||
|
trans.save()
|
||||||
|
tag_category.translation = trans
|
||||||
|
tag_category.save()
|
||||||
|
|
||||||
|
|
||||||
class SiteInterfaceDictionary(ProjectBaseMixin):
|
class SiteInterfaceDictionary(ProjectBaseMixin):
|
||||||
"""Site interface dictionary model."""
|
"""Site interface dictionary model."""
|
||||||
|
|
|
||||||
|
|
@ -171,3 +171,11 @@ class RemovedBindingObjectNotFound(serializers.ValidationError):
|
||||||
"""The exception must be thrown if the object not found."""
|
"""The exception must be thrown if the object not found."""
|
||||||
|
|
||||||
default_detail = _('Removed binding object not found.')
|
default_detail = _('Removed binding object not found.')
|
||||||
|
|
||||||
|
|
||||||
|
class UnprocessableEntityError(exceptions.APIException):
|
||||||
|
"""
|
||||||
|
The exception should be thrown when executing data on server rise error.
|
||||||
|
"""
|
||||||
|
status_code = status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||||
|
default_detail = _('Unprocessable entity valid.')
|
||||||
|
|
|
||||||
115
apps/utils/export.py
Normal file
115
apps/utils/export.py
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
import csv
|
||||||
|
import xlsxwriter
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
from smtplib import SMTPException
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.mail import EmailMultiAlternatives
|
||||||
|
|
||||||
|
logging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SendExport:
|
||||||
|
|
||||||
|
def __init__(self, user, panel, file_type='csv'):
|
||||||
|
self.type_mapper = {
|
||||||
|
"csv": self.make_csv_file,
|
||||||
|
"xls": self.make_xls_file
|
||||||
|
}
|
||||||
|
self.file_type = file_type
|
||||||
|
self.user = user
|
||||||
|
self.panel = panel
|
||||||
|
self.email_from = settings.EMAIL_HOST_USER
|
||||||
|
self.email_subject = f'Export panel: {self.get_file_name()}'
|
||||||
|
self.email_body = 'Exported panel data'
|
||||||
|
self.get_file_method = self.type_mapper[file_type]
|
||||||
|
self.file_path = os.path.join(
|
||||||
|
settings.STATIC_ROOT,
|
||||||
|
'email', tempfile.gettempdir(),
|
||||||
|
self.get_file_name()
|
||||||
|
)
|
||||||
|
self.success = False
|
||||||
|
|
||||||
|
def get_file_name(self):
|
||||||
|
name = '_'.join(self.panel.name.split(' '))
|
||||||
|
return f'export_{name.lower()}.{self.file_type}'
|
||||||
|
|
||||||
|
def get_data(self):
|
||||||
|
return self.panel.get_data()
|
||||||
|
|
||||||
|
def get_headers(self):
|
||||||
|
try:
|
||||||
|
header = self.panel.get_headers()
|
||||||
|
self.success = True
|
||||||
|
return header
|
||||||
|
except Exception as err:
|
||||||
|
logger.info(f'HEADER:{err}')
|
||||||
|
|
||||||
|
def make_csv_file(self):
|
||||||
|
file_header = self.get_headers()
|
||||||
|
if not self.success:
|
||||||
|
return
|
||||||
|
with open(self.file_path, 'w') as f:
|
||||||
|
file_writer = csv.writer(f, quotechar='"', quoting=csv.QUOTE_MINIMAL)
|
||||||
|
# Write headers to CSV file
|
||||||
|
file_writer.writerow(file_header)
|
||||||
|
for row in self.get_data():
|
||||||
|
file_writer.writerow(row)
|
||||||
|
|
||||||
|
def make_xls_file(self):
|
||||||
|
headings = self.get_headers()
|
||||||
|
if not self.success:
|
||||||
|
return
|
||||||
|
with xlsxwriter.Workbook(self.file_path) as workbook:
|
||||||
|
worksheet = workbook.add_worksheet()
|
||||||
|
|
||||||
|
# Add a bold format to use to highlight cells.
|
||||||
|
bold = workbook.add_format({'bold': True})
|
||||||
|
|
||||||
|
# Add the worksheet data that the charts will refer to.
|
||||||
|
data = self.get_data()
|
||||||
|
|
||||||
|
worksheet.write_row('A1', headings, bold)
|
||||||
|
for n, row in enumerate(data):
|
||||||
|
worksheet.write_row(f'A{n+2}', [str(i) for i in row])
|
||||||
|
workbook.close()
|
||||||
|
|
||||||
|
def send(self):
|
||||||
|
self.get_file_method()
|
||||||
|
print(f'ok: {self.file_path}')
|
||||||
|
self.send_email()
|
||||||
|
|
||||||
|
def get_file(self):
|
||||||
|
if os.path.exists(self.file_path) and os.path.isfile(self.file_path):
|
||||||
|
with open(self.file_path, 'rb') as export_file:
|
||||||
|
return export_file
|
||||||
|
else:
|
||||||
|
logger.info('COMMUTATOR:image file not found dir: {path}')
|
||||||
|
|
||||||
|
def send_email(self):
|
||||||
|
|
||||||
|
msg = EmailMultiAlternatives(
|
||||||
|
subject=self.email_subject,
|
||||||
|
body=self.email_body,
|
||||||
|
from_email=self.email_from,
|
||||||
|
to=[
|
||||||
|
self.user.email,
|
||||||
|
'kuzmenko.da@gmail.com',
|
||||||
|
'sinapsit@yandex.ru'
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create an inline attachment
|
||||||
|
if self.file_path and self.success:
|
||||||
|
msg.attach_file(self.file_path)
|
||||||
|
else:
|
||||||
|
msg.body = 'An error occurred while executing the request.'
|
||||||
|
|
||||||
|
try:
|
||||||
|
msg.send()
|
||||||
|
logger.debug(f"COMMUTATOR:Email successfully sent")
|
||||||
|
except SMTPException as e:
|
||||||
|
logger.error(f"COMMUTATOR:Email connector: {e}")
|
||||||
|
|
@ -132,3 +132,12 @@ def namedtuplefetchall(cursor):
|
||||||
desc = cursor.description
|
desc = cursor.description
|
||||||
nt_result = namedtuple('Result', [col[0] for col in desc])
|
nt_result = namedtuple('Result', [col[0] for col in desc])
|
||||||
return [nt_result(*row) for row in cursor.fetchall()]
|
return [nt_result(*row) for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
|
||||||
|
def dictfetchall(cursor):
|
||||||
|
"Return all rows from a cursor as a dict"
|
||||||
|
columns = [col[0] for col in cursor.description]
|
||||||
|
return [
|
||||||
|
dict(zip(columns, row))
|
||||||
|
for row in cursor.fetchall()
|
||||||
|
]
|
||||||
|
|
@ -88,7 +88,6 @@ def translate_field(self, field_name, toggle_field_name=None):
|
||||||
return None
|
return None
|
||||||
return translate
|
return translate
|
||||||
|
|
||||||
|
|
||||||
# todo: refactor this
|
# todo: refactor this
|
||||||
class IndexJSON:
|
class IndexJSON:
|
||||||
|
|
||||||
|
|
@ -365,16 +364,12 @@ class GMTokenGenerator(PasswordResetTokenGenerator):
|
||||||
return self.get_fields(user, timestamp)
|
return self.get_fields(user, timestamp)
|
||||||
|
|
||||||
|
|
||||||
class GalleryModelMixin(models.Model):
|
class GalleryMixin:
|
||||||
"""Mixin for models that has gallery."""
|
"""Mixin for models that has gallery."""
|
||||||
|
|
||||||
class Meta:
|
|
||||||
"""Meta class."""
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def crop_gallery(self):
|
def crop_gallery(self):
|
||||||
if hasattr(self, 'gallery'):
|
if hasattr(self, 'gallery') and hasattr(self, '_meta'):
|
||||||
gallery = []
|
gallery = []
|
||||||
images = self.gallery.all()
|
images = self.gallery.all()
|
||||||
crop_parameters = [p for p in settings.SORL_THUMBNAIL_ALIASES
|
crop_parameters = [p for p in settings.SORL_THUMBNAIL_ALIASES
|
||||||
|
|
@ -394,7 +389,8 @@ class GalleryModelMixin(models.Model):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def crop_main_image(self):
|
def crop_main_image(self):
|
||||||
if hasattr(self, 'main_image') and self.main_image:
|
if hasattr(self, 'main_image') and hasattr(self, '_meta'):
|
||||||
|
if self.main_image:
|
||||||
image = self.main_image
|
image = self.main_image
|
||||||
image_property = {
|
image_property = {
|
||||||
'id': image.id,
|
'id': image.id,
|
||||||
|
|
@ -443,7 +439,8 @@ class HasTagsMixin(models.Model):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def visible_tags(self):
|
def visible_tags(self):
|
||||||
return self.tags.filter(category__public=True).prefetch_related('category')\
|
return self.tags.filter(category__public=True).prefetch_related('category',
|
||||||
|
'translation', 'category__translation')\
|
||||||
.exclude(category__value_type='bool')
|
.exclude(category__value_type='bool')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
@ -460,3 +457,13 @@ class FavoritesMixin:
|
||||||
|
|
||||||
|
|
||||||
timezone.datetime.now().date().isoformat()
|
timezone.datetime.now().date().isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
class TypeDefaultImageMixin:
|
||||||
|
"""Model mixin for default image."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_image_url(self):
|
||||||
|
"""Return image url."""
|
||||||
|
if hasattr(self, 'default_image') and self.default_image:
|
||||||
|
return self.default_image.image.url
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ from authorization.models import JWTRefreshToken
|
||||||
from utils.tokens import GMRefreshToken
|
from utils.tokens import GMRefreshToken
|
||||||
from establishment.models import EstablishmentSubType
|
from establishment.models import EstablishmentSubType
|
||||||
from location.models import Address
|
from location.models import Address
|
||||||
|
from product.models import Product, ProductType
|
||||||
|
|
||||||
|
|
||||||
class IsAuthenticatedAndTokenIsValid(permissions.BasePermission):
|
class IsAuthenticatedAndTokenIsValid(permissions.BasePermission):
|
||||||
"""
|
"""
|
||||||
|
|
@ -81,32 +83,20 @@ class IsStandardUser(IsGuest):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def has_permission(self, request, view):
|
def has_permission(self, request, view):
|
||||||
rules = [
|
|
||||||
super().has_permission(request, view)
|
|
||||||
]
|
|
||||||
|
|
||||||
# and request.user.email_confirmed,
|
rules = [super().has_permission(request, view),
|
||||||
if hasattr(request, 'user'):
|
|
||||||
rules = [
|
|
||||||
request.user.is_authenticated,
|
request.user.is_authenticated,
|
||||||
super().has_permission(request, view)
|
hasattr(request, 'user')
|
||||||
]
|
]
|
||||||
|
|
||||||
return any(rules)
|
return any(rules)
|
||||||
|
|
||||||
def has_object_permission(self, request, view, obj):
|
def has_object_permission(self, request, view, obj):
|
||||||
# Read permissions are allowed to any request
|
# Read permissions are allowed to any request
|
||||||
rules = [
|
|
||||||
super().has_object_permission(request, view, obj)
|
|
||||||
]
|
|
||||||
|
|
||||||
if hasattr(obj, 'user'):
|
rules = [super().has_object_permission(request, view, obj),
|
||||||
rules = [
|
request.user.is_authenticated,
|
||||||
obj.user == request.user
|
hasattr(request, 'user')
|
||||||
and obj.user.email_confirmed
|
|
||||||
and request.user.is_authenticated,
|
|
||||||
|
|
||||||
super().has_object_permission(request, view, obj)
|
|
||||||
]
|
]
|
||||||
|
|
||||||
return any(rules)
|
return any(rules)
|
||||||
|
|
@ -408,7 +398,7 @@ class IsWineryReviewer(IsStandardUser):
|
||||||
|
|
||||||
est = EstablishmentSubType.objects.filter(establishment_type_id=request.data['type_id'])
|
est = EstablishmentSubType.objects.filter(establishment_type_id=request.data['type_id'])
|
||||||
if est.exists():
|
if est.exists():
|
||||||
role = Role.objects.filter(establishment_subtype_id__in=[type.id for type in est],
|
role = Role.objects.filter(establishment_subtype_id__in=[est_type.id for est_type in est],
|
||||||
role=Role.WINERY_REVIEWER,
|
role=Role.WINERY_REVIEWER,
|
||||||
country_id__in=[country.id for country in countries]) \
|
country_id__in=[country.id for country in countries]) \
|
||||||
.first()
|
.first()
|
||||||
|
|
@ -433,7 +423,7 @@ class IsWineryReviewer(IsStandardUser):
|
||||||
|
|
||||||
est = EstablishmentSubType.objects.filter(establishment_type_id=type_id)
|
est = EstablishmentSubType.objects.filter(establishment_type_id=type_id)
|
||||||
role = Role.objects.filter(role=Role.WINERY_REVIEWER,
|
role = Role.objects.filter(role=Role.WINERY_REVIEWER,
|
||||||
establishment_subtype_id__in=[id for type.id in est],
|
establishment_subtype_id__in=[est_type.id for est_type in est],
|
||||||
country_id=obj.country_id).first()
|
country_id=obj.country_id).first()
|
||||||
|
|
||||||
object_id: int
|
object_id: int
|
||||||
|
|
@ -449,3 +439,159 @@ class IsWineryReviewer(IsStandardUser):
|
||||||
super().has_object_permission(request, view, obj)
|
super().has_object_permission(request, view, obj)
|
||||||
]
|
]
|
||||||
return any(rules)
|
return any(rules)
|
||||||
|
|
||||||
|
|
||||||
|
class IsWineryReviewer(IsStandardUser):
|
||||||
|
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
rules = [
|
||||||
|
super().has_permission(request, view)
|
||||||
|
]
|
||||||
|
|
||||||
|
if 'type_id' in request.data and 'address_id' in request.data and request.user:
|
||||||
|
countries = Address.objects.filter(id=request.data['address_id'])
|
||||||
|
|
||||||
|
est = EstablishmentSubType.objects.filter(establishment_type_id=request.data['type_id'])
|
||||||
|
if est.exists():
|
||||||
|
role = Role.objects.filter(establishment_subtype_id__in=[est_type.id for est_type in est],
|
||||||
|
role=Role.WINERY_REVIEWER,
|
||||||
|
country_id__in=[country.id for country in countries]) \
|
||||||
|
.first()
|
||||||
|
|
||||||
|
rules.append(
|
||||||
|
UserRole.objects.filter(user=request.user, role=role).exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
return any(rules)
|
||||||
|
|
||||||
|
def has_object_permission(self, request, view, obj):
|
||||||
|
rules = [
|
||||||
|
super().has_object_permission(request, view, obj)
|
||||||
|
]
|
||||||
|
|
||||||
|
if hasattr(obj, 'type_id') or hasattr(obj, 'establishment_type_id'):
|
||||||
|
type_id: int
|
||||||
|
if hasattr(obj, 'type_id'):
|
||||||
|
type_id = obj.type_id
|
||||||
|
else:
|
||||||
|
type_id = obj.establishment_type_id
|
||||||
|
|
||||||
|
est = EstablishmentSubType.objects.filter(establishment_type_id=type_id)
|
||||||
|
role = Role.objects.filter(role=Role.WINERY_REVIEWER,
|
||||||
|
establishment_subtype_id__in=[est_type.id for est_type in est],
|
||||||
|
country_id=obj.country_id).first()
|
||||||
|
|
||||||
|
object_id: int
|
||||||
|
if hasattr(obj, 'object_id'):
|
||||||
|
object_id = obj.object_id
|
||||||
|
else:
|
||||||
|
object_id = obj.establishment_id
|
||||||
|
|
||||||
|
rules = [
|
||||||
|
UserRole.objects.filter(user=request.user, role=role,
|
||||||
|
establishment_id=object_id
|
||||||
|
).exists(),
|
||||||
|
super().has_object_permission(request, view, obj)
|
||||||
|
]
|
||||||
|
return any(rules)
|
||||||
|
|
||||||
|
|
||||||
|
class IsProductReviewer(IsStandardUser):
|
||||||
|
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
rules = [
|
||||||
|
super().has_permission(request, view)
|
||||||
|
]
|
||||||
|
|
||||||
|
pk_object = None
|
||||||
|
roles = None
|
||||||
|
permission = False
|
||||||
|
|
||||||
|
if 'site_id' in request.data:
|
||||||
|
if request.data['site_id'] is not None:
|
||||||
|
roles = Role.objects.filter(role=Role.PRODUCT_REVIEWER,
|
||||||
|
site_id=request.data['site_id'])
|
||||||
|
|
||||||
|
if 'pk' in view.kwargs:
|
||||||
|
pk_object = view.kwargs['pk']
|
||||||
|
|
||||||
|
if pk_object is not None:
|
||||||
|
product = Product.objects.get(pk=pk_object)
|
||||||
|
if product.site_id is not None:
|
||||||
|
roles = Role.objects.filter(role=Role.PRODUCT_REVIEWER,
|
||||||
|
site_id=product.site_id)
|
||||||
|
|
||||||
|
if roles is not None:
|
||||||
|
permission = UserRole.objects.filter(user=request.user, role__in=[role for role in roles])\
|
||||||
|
.exists()
|
||||||
|
|
||||||
|
rules.append(permission)
|
||||||
|
return any(rules)
|
||||||
|
|
||||||
|
|
||||||
|
class IsLiquorReviewer(IsStandardUser):
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
rules = [
|
||||||
|
super().has_permission(request, view)
|
||||||
|
]
|
||||||
|
|
||||||
|
pk_object = None
|
||||||
|
roles = None
|
||||||
|
permission = False
|
||||||
|
|
||||||
|
if 'site_id' in request.data and 'product_type_id' in request.data:
|
||||||
|
if request.data['site_id'] is not None \
|
||||||
|
and request.data['product_type_id'] is not None:
|
||||||
|
|
||||||
|
product_types = ProductType.objects. \
|
||||||
|
filter(index_name=ProductType.LIQUOR,
|
||||||
|
id=request.data['product_type_id'])
|
||||||
|
|
||||||
|
if product_types.exists():
|
||||||
|
roles = Role.objects.filter(role=Role.LIQUOR_REVIEWER,
|
||||||
|
site_id=request.data['site_id'])
|
||||||
|
|
||||||
|
if 'pk' in view.kwargs:
|
||||||
|
pk_object = view.kwargs['pk']
|
||||||
|
|
||||||
|
if pk_object is not None:
|
||||||
|
product = Product.objects.get(pk=pk_object)
|
||||||
|
if product.site_id is not None \
|
||||||
|
and product.product_type_id is not None:
|
||||||
|
|
||||||
|
product_types = ProductType.objects. \
|
||||||
|
filter(index_name=ProductType.LIQUOR,
|
||||||
|
id=product.product_type_id)
|
||||||
|
|
||||||
|
if product_types.exists():
|
||||||
|
roles = Role.objects.filter(role=Role.LIQUOR_REVIEWER,
|
||||||
|
site_id=product.site_id)
|
||||||
|
|
||||||
|
if roles is not None:
|
||||||
|
permission = UserRole.objects.filter(user=request.user, role__in=[role for role in roles])\
|
||||||
|
.exists()
|
||||||
|
|
||||||
|
rules.append(permission)
|
||||||
|
return any(rules)
|
||||||
|
|
||||||
|
#
|
||||||
|
# def has_object_permission(self, request, view, obj):
|
||||||
|
# rules = [
|
||||||
|
# super().has_object_permission(request, view, obj)
|
||||||
|
# ]
|
||||||
|
# # pk_object = None
|
||||||
|
# # product = None
|
||||||
|
# # permission = False
|
||||||
|
# #
|
||||||
|
# # if 'pk' in view.kwargs:
|
||||||
|
# # pk_object = view.kwargs['pk']
|
||||||
|
# #
|
||||||
|
# # if pk_object is not None:
|
||||||
|
# # product = Product.objects.get(pk=pk_object)
|
||||||
|
# #
|
||||||
|
# # if product.sites.exists():
|
||||||
|
# # role = Role.objects.filter(role=Role.LIQUOR_REVIEWER, site__in=[site for site in product.sites])
|
||||||
|
# # permission = UserRole.objects.filter(user=request.user, role=role).exists()
|
||||||
|
# #
|
||||||
|
# # rules.append(permission)
|
||||||
|
# return any(rules)
|
||||||
|
|
@ -53,7 +53,6 @@ class TranslateFieldTests(BaseTestCase):
|
||||||
"ru-RU": "Тестовая новость"
|
"ru-RU": "Тестовая новость"
|
||||||
},
|
},
|
||||||
description={"en-GB": "Test description"},
|
description={"en-GB": "Test description"},
|
||||||
start=datetime.now(pytz.utc) + timedelta(hours=-13),
|
|
||||||
end=datetime.now(pytz.utc) + timedelta(hours=13),
|
end=datetime.now(pytz.utc) + timedelta(hours=13),
|
||||||
news_type=self.news_type,
|
news_type=self.news_type,
|
||||||
slugs={'en-GB': 'test'},
|
slugs={'en-GB': 'test'},
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ services:
|
||||||
MYSQL_ROOT_PASSWORD: rootPassword
|
MYSQL_ROOT_PASSWORD: rootPassword
|
||||||
volumes:
|
volumes:
|
||||||
- gm-mysql_db:/var/lib/mysql
|
- gm-mysql_db:/var/lib/mysql
|
||||||
- .:/code
|
|
||||||
|
|
||||||
|
|
||||||
# PostgreSQL database
|
# PostgreSQL database
|
||||||
|
|
@ -30,7 +29,6 @@ services:
|
||||||
- "5436:5432"
|
- "5436:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- gm-db:/var/lib/postgresql/data/
|
- gm-db:/var/lib/postgresql/data/
|
||||||
- .:/code
|
|
||||||
|
|
||||||
|
|
||||||
elasticsearch:
|
elasticsearch:
|
||||||
|
|
|
||||||
|
|
@ -516,8 +516,12 @@ PHONENUMBER_DEFAULT_REGION = "FR"
|
||||||
|
|
||||||
FALLBACK_LOCALE = 'en-GB'
|
FALLBACK_LOCALE = 'en-GB'
|
||||||
|
|
||||||
ESTABLISHMENT_CHOSEN_TAGS = ['gastronomic', 'en_vogue', 'terrace', 'streetfood', 'business', 'bar_cocktail', 'brunch', 'pop']
|
ESTABLISHMENT_CHOSEN_TAGS = ['gastronomic', 'terrace', 'streetfood', 'business', 'bar_cocktail', 'brunch', 'pop']
|
||||||
NEWS_CHOSEN_TAGS = ['eat', 'drink', 'cook', 'style', 'international', 'event', 'partnership']
|
NEWS_CHOSEN_TAGS = ['eat', 'drink', 'cook', 'style', 'international', 'event', 'partnership']
|
||||||
|
ARTISANS_CHOSEN_TAGS = ['butchery', 'bakery', 'patisserie', 'cheese_shop', 'fish_shop', 'ice-cream_maker',
|
||||||
|
'wine_merchant', 'coffe_shop']
|
||||||
|
RECIPES_CHOSEN_TAGS = ['cook', 'eat', 'drink']
|
||||||
|
|
||||||
INTERNATIONAL_COUNTRY_CODES = ['www', 'main', 'next']
|
INTERNATIONAL_COUNTRY_CODES = ['www', 'main', 'next']
|
||||||
|
|
||||||
THUMBNAIL_ENGINE = 'utils.thumbnail_engine.GMEngine'
|
THUMBNAIL_ENGINE = 'utils.thumbnail_engine.GMEngine'
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,7 @@ MEDIA_ROOT = os.path.join(PUBLIC_ROOT, MEDIA_LOCATION)
|
||||||
# SORL thumbnails
|
# SORL thumbnails
|
||||||
THUMBNAIL_DEBUG = True
|
THUMBNAIL_DEBUG = True
|
||||||
|
|
||||||
# ADDED TRANSFER APP
|
|
||||||
INSTALLED_APPS.append('transfer.apps.TransferConfig')
|
|
||||||
|
|
||||||
# DATABASES
|
# DATABASES
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
|
|
@ -86,11 +85,11 @@ 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,
|
||||||
# },
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,3 +66,6 @@ django-mptt==0.9.1
|
||||||
|
|
||||||
# slugify
|
# slugify
|
||||||
python-slugify==4.0.0
|
python-slugify==4.0.0
|
||||||
|
|
||||||
|
# Export to Excel
|
||||||
|
XlsxWriter==1.2.6
|
||||||
Loading…
Reference in New Issue
Block a user