Merge branch 'develop' into feature/collection-slugs

# Conflicts:
#	requirements/base.txt
This commit is contained in:
Dmitriy Kuzmenko 2019-12-23 16:57:24 +03:00
commit 2580b9f218
93 changed files with 2156 additions and 554 deletions

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

View 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 = [
]

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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),
),
]

View 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),
]

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

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

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

View 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 = [
]

View File

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

View File

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

View File

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

View File

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

View File

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

View 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(),
),
]

View File

@ -23,4 +23,4 @@ class Rating(models.Model):
class ViewCount(models.Model): class ViewCount(models.Model):
count = models.IntegerField() count = models.PositiveIntegerField()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

@ -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 - перенос языков
] ]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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