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