Merge remote-tracking branch 'origin/develop' into nes_transfer

This commit is contained in:
alex 2019-12-23 08:28:28 +03:00
commit 56b2b243da
28 changed files with 534 additions and 68 deletions

View File

@ -118,22 +118,23 @@ class Collection(ProjectBaseMixin, CollectionDateMixin,
instances = getattr(self, f'{related_object}') instances = getattr(self, f'{related_object}')
if instances.exists(): if instances.exists():
for instance in instances.all(): for instance in instances.all():
raw_object = (instance.id, instance.slug) if hasattr(instance, 'slug') else ( raw_object = (instance.id, instance.establishment_type.index_name,
instance.id, None instance.slug) if \
) hasattr(instance, 'slug') else (instance.id, None, None)
raw_objects.append(raw_object) raw_objects.append(raw_object)
# parse slugs # parse slugs
related_objects = [] related_objects = []
object_names = set() object_names = set()
re_pattern = r'[\w]+' re_pattern = r'[\w]+'
for object_id, raw_name, in raw_objects: for object_id, object_type, raw_name, in raw_objects:
result = re.findall(re_pattern, raw_name) result = re.findall(re_pattern, raw_name)
if result: if result:
name = ' '.join(result).capitalize() name = ' '.join(result).capitalize()
if name not in object_names: if name not in object_names:
related_objects.append({ related_objects.append({
'id': object_id, 'id': object_id,
'establishment_type': object_type,
'name': name 'name': name
}) })
object_names.add(name) object_names.add(name)

View File

@ -0,0 +1,68 @@
import boto3
from django.conf import settings
from django.core.management.base import BaseCommand
from establishment.models import EstablishmentSubType
from gallery.models import Image
class Command(BaseCommand):
help = """
Fill establishment type by index names.
Steps:
1 Upload default images into s3 bucket
2 Run command ./manage.py fill_artisan_default_image
"""
def add_arguments(self, parser):
parser.add_argument(
'--template_image_folder_name',
help='Template image folder in Amazon S3 bucket'
)
def handle(self, *args, **kwargs):
not_updated = 0
template_image_folder_name = kwargs.get('template_image_folder_name')
if (template_image_folder_name and
hasattr(settings, 'AWS_ACCESS_KEY_ID') and
hasattr(settings, 'AWS_SECRET_ACCESS_KEY') and
hasattr(settings, 'AWS_STORAGE_BUCKET_NAME')):
to_update = []
s3 = boto3.resource('s3')
s3_bucket = s3.Bucket(settings.AWS_STORAGE_BUCKET_NAME)
for object_summary in s3_bucket.objects.filter(Prefix=f'media/{template_image_folder_name}/'):
uri_path = object_summary.key
filename = uri_path.split('/')[-1:][0]
if filename:
artisan_index_slice = filename.split('.')[:-1][0] \
.split('_')[2:]
if len(artisan_index_slice) > 1:
artisan_index_name = '_'.join(artisan_index_slice)
else:
artisan_index_name = artisan_index_slice[0]
attachment_suffix_url = f'{template_image_folder_name}/{filename}'
# check artisan in db
artisan_qs = EstablishmentSubType.objects.filter(index_name__iexact=artisan_index_name,
establishment_type__index_name__iexact='artisan')
if artisan_qs.exists():
artisan = artisan_qs.first()
image, created = Image.objects.get_or_create(image=attachment_suffix_url,
defaults={
'image': attachment_suffix_url,
'orientation': Image.HORIZONTAL,
'title': f'{artisan.__str__()} '
f'{artisan.id} - '
f'{attachment_suffix_url}'})
if created:
# update artisan instance
artisan.default_image = image
to_update.append(artisan)
else:
not_updated += 1
EstablishmentSubType.objects.bulk_update(to_update, ['default_image', ])
self.stdout.write(self.style.WARNING(f'Updated {len(to_update)} objects.'))
self.stdout.write(self.style.WARNING(f'Not updated {not_updated} objects.'))

View File

@ -0,0 +1,25 @@
# Generated by Django 2.2.7 on 2019-12-20 10:07
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('gallery', '0007_auto_20191211_1528'),
('establishment', '0068_auto_20191220_0914'),
]
operations = [
migrations.AddField(
model_name='establishmentsubtype',
name='default_image',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='establishment_sub_types', to='gallery.Image', verbose_name='default image'),
),
migrations.AddField(
model_name='establishmenttype',
name='default_image',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='establishment_types', to='gallery.Image', verbose_name='default image'),
),
]

View File

@ -51,6 +51,10 @@ class EstablishmentType(TranslatedFieldsMixin, ProjectBaseMixin):
tag_categories = models.ManyToManyField('tag.TagCategory', tag_categories = models.ManyToManyField('tag.TagCategory',
related_name='establishment_types', related_name='establishment_types',
verbose_name=_('Tag')) verbose_name=_('Tag'))
default_image = models.ForeignKey('gallery.Image', on_delete=models.SET_NULL,
related_name='establishment_types',
blank=True, null=True, default=None,
verbose_name='default image')
class Meta: class Meta:
"""Meta class.""" """Meta class."""
@ -85,6 +89,10 @@ class EstablishmentSubType(ProjectBaseMixin, TranslatedFieldsMixin):
tag_categories = models.ManyToManyField('tag.TagCategory', tag_categories = models.ManyToManyField('tag.TagCategory',
related_name='establishment_subtypes', related_name='establishment_subtypes',
verbose_name=_('Tag')) verbose_name=_('Tag'))
default_image = models.ForeignKey('gallery.Image', on_delete=models.SET_NULL,
related_name='establishment_sub_types',
blank=True, null=True, default=None,
verbose_name='default image')
objects = EstablishmentSubTypeManager() objects = EstablishmentSubTypeManager()
@ -105,7 +113,7 @@ class EstablishmentQuerySet(models.QuerySet):
def with_base_related(self): def with_base_related(self):
"""Return qs with related objects.""" """Return qs with related objects."""
return self.select_related('address', 'establishment_type'). \ return self.select_related('address', 'establishment_type'). \
prefetch_related('tags') prefetch_related('tags', 'tags__translation')
def with_schedule(self): def with_schedule(self):
"""Return qs with related schedule.""" """Return qs with related schedule."""

View File

@ -97,6 +97,8 @@ class MenuRUDSerializers(ProjectModelSerializer):
class EstablishmentTypeBaseSerializer(serializers.ModelSerializer): class EstablishmentTypeBaseSerializer(serializers.ModelSerializer):
"""Serializer for EstablishmentType model.""" """Serializer for EstablishmentType model."""
name_translated = TranslatedField() name_translated = TranslatedField()
default_image_url = serializers.ImageField(source='default_image.image',
allow_null=True)
class Meta: class Meta:
"""Meta class.""" """Meta class."""
@ -107,6 +109,7 @@ class EstablishmentTypeBaseSerializer(serializers.ModelSerializer):
'name_translated', 'name_translated',
'use_subtypes', 'use_subtypes',
'index_name', 'index_name',
'default_image_url',
] ]
extra_kwargs = { extra_kwargs = {
'name': {'write_only': True}, 'name': {'write_only': True},
@ -129,8 +132,9 @@ class EstablishmentTypeGeoSerializer(EstablishmentTypeBaseSerializer):
class EstablishmentSubTypeBaseSerializer(serializers.ModelSerializer): class EstablishmentSubTypeBaseSerializer(serializers.ModelSerializer):
"""Serializer for EstablishmentSubType models.""" """Serializer for EstablishmentSubType models."""
name_translated = TranslatedField() name_translated = TranslatedField()
default_image_url = serializers.ImageField(source='default_image.image',
allow_null=True)
class Meta: class Meta:
"""Meta class.""" """Meta class."""
@ -141,6 +145,7 @@ class EstablishmentSubTypeBaseSerializer(serializers.ModelSerializer):
'name_translated', 'name_translated',
'establishment_type', 'establishment_type',
'index_name', 'index_name',
'default_image_url',
] ]
extra_kwargs = { extra_kwargs = {
'name': {'write_only': True}, 'name': {'write_only': True},

View File

@ -1,6 +1,6 @@
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from sorl.thumbnail import delete from sorl import thumbnail
from sorl.thumbnail.fields import ImageField as SORLImageField from sorl.thumbnail.fields import ImageField as SORLImageField
from utils.methods import image_path from utils.methods import image_path
@ -47,7 +47,7 @@ class Image(ProjectBaseMixin, SORLImageMixin, PlatformMixin):
""" """
try: try:
# Delete from remote storage # Delete from remote storage
delete(file_=self.image.file, delete_file=completely) thumbnail.delete(file_=self.image.file, delete_file=completely)
except FileNotFoundError: except FileNotFoundError:
pass pass
finally: finally:

View File

@ -481,6 +481,19 @@ class Panel(ProjectBaseMixin):
columns = [col[0] for col in cursor.description] columns = [col[0] for col in cursor.description]
return columns 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): def _raw_page(self, raw, request):
page = request.query_params.get('page', 0) page = request.query_params.get('page', 0)
page_size = request.query_params.get('page_size', 0) page_size = request.query_params.get('page_size', 0)

14
apps/main/tasks.py Normal file
View File

@ -0,0 +1,14 @@
"""Task methods for main app."""
from celery import shared_task
from account.models import User
from main.models import Panel
from utils.export import SendExport
@shared_task
def send_export_to_email(panel_id, user_id, file_type='csv'):
panel = Panel.objects.get(id=panel_id)
user = User.objects.get(id=user_id)
SendExport(user, panel, file_type).send()

View File

@ -24,8 +24,9 @@ urlpatterns = [
name='page-types-list-create'), name='page-types-list-create'),
path('panels/', views.PanelsListCreateView.as_view(), name='panels'), path('panels/', views.PanelsListCreateView.as_view(), name='panels'),
path('panels/<int:pk>/', views.PanelsRUDView.as_view(), name='panels-rud'), 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>/execute/', views.PanelsExecuteView.as_view(), name='panels-execute'),
path('panels/<int:pk>/csv/', views.PanelsExportCSVView.as_view(), name='panels-csv'),
path('panels/<int:pk>/xls/', views.PanelsExecuteXLSView.as_view(), name='panels-xls')
] ]

View File

@ -1,10 +1,12 @@
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import generics, permissions from rest_framework import generics, permissions, status
from rest_framework.generics import get_object_or_404 from rest_framework.generics import get_object_or_404
from rest_framework.response import Response from rest_framework.response import Response
from main import serializers from main import serializers
from main import tasks
from main.filters import AwardFilter from main.filters import AwardFilter
from main.models import Award, Footer, PageType, Panel from main.models import Award, Footer, PageType, Panel
from main.views import SiteSettingsView, SiteListView from main.views import SiteSettingsView, SiteListView
@ -121,3 +123,35 @@ class PanelsExecuteView(generics.ListAPIView):
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
panel = get_object_or_404(Panel, id=self.kwargs['pk']) panel = get_object_or_404(Panel, id=self.kwargs['pk'])
return Response(panel.execute_query(request)) return Response(panel.execute_query(request))
class PanelsExportCSVView(PanelsExecuteView):
"""Export panels via csv view."""
permission_classes = (permissions.IsAdminUser,)
queryset = Panel.objects.all()
def list(self, request, *args, **kwargs):
panel = get_object_or_404(Panel, id=self.kwargs['pk'])
# make task for celery
tasks.send_export_to_email.delay(
panel_id=panel.id, user_id=request.user.id)
return Response(
{"success": _('The file will be sent to your email.')},
status=status.HTTP_200_OK
)
class PanelsExecuteXLSView(PanelsExecuteView):
"""Export panels via xlsx view."""
permission_classes = (permissions.IsAdminUser,)
queryset = Panel.objects.all()
def list(self, request, *args, **kwargs):
panel = get_object_or_404(Panel, id=self.kwargs['pk'])
# make task for celery
tasks.send_export_to_email.delay(
panel_id=panel.id, user_id=request.user.id, file_type='xls')
return Response(
{"success": _('The file will be sent to your email.')},
status=status.HTTP_200_OK
)

View File

@ -74,7 +74,7 @@ class NewsQuerySet(TranslationQuerysetMixin):
def with_base_related(self): def with_base_related(self):
"""Return qs with related objects.""" """Return qs with related objects."""
return self.select_related('news_type', 'country').prefetch_related('tags') return self.select_related('news_type', 'country').prefetch_related('tags', 'tags__translation')
def with_extended_related(self): def with_extended_related(self):
"""Return qs with related objects.""" """Return qs with related objects."""

View File

@ -0,0 +1,25 @@
# Generated by Django 2.2.7 on 2019-12-20 10:07
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('gallery', '0007_auto_20191211_1528'),
('product', '0021_auto_20191212_0926'),
]
operations = [
migrations.AddField(
model_name='productsubtype',
name='default_image',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='product_sub_types', to='gallery.Image', verbose_name='default image'),
),
migrations.AddField(
model_name='producttype',
name='default_image',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='product_types', to='gallery.Image', verbose_name='default image'),
),
]

View File

@ -37,6 +37,10 @@ class ProductType(TranslatedFieldsMixin, ProjectBaseMixin):
tag_categories = models.ManyToManyField('tag.TagCategory', tag_categories = models.ManyToManyField('tag.TagCategory',
related_name='product_types', related_name='product_types',
verbose_name=_('Tag categories')) verbose_name=_('Tag categories'))
default_image = models.ForeignKey('gallery.Image', on_delete=models.SET_NULL,
related_name='product_types',
blank=True, null=True, default=None,
verbose_name='default image')
class Meta: class Meta:
"""Meta class.""" """Meta class."""
@ -62,6 +66,10 @@ class ProductSubType(TranslatedFieldsMixin, ProjectBaseMixin):
verbose_name=_('Name'), help_text='{"en-GB":"some text"}') verbose_name=_('Name'), help_text='{"en-GB":"some text"}')
index_name = models.CharField(max_length=50, unique=True, db_index=True, index_name = models.CharField(max_length=50, unique=True, db_index=True,
verbose_name=_('Index name')) verbose_name=_('Index name'))
default_image = models.ForeignKey('gallery.Image', on_delete=models.SET_NULL,
related_name='product_sub_types',
blank=True, null=True, default=None,
verbose_name='default image')
class Meta: class Meta:
"""Meta class.""" """Meta class."""
@ -83,7 +91,7 @@ class ProductQuerySet(models.QuerySet):
def with_base_related(self): def with_base_related(self):
return self.select_related('product_type', 'establishment') \ return self.select_related('product_type', 'establishment') \
.prefetch_related('product_type__subtypes') .prefetch_related('product_type__subtypes', 'tags', 'tags__translation')
def with_extended_related(self): def with_extended_related(self):
"""Returns qs with almost all related objects.""" """Returns qs with almost all related objects."""

View File

@ -34,6 +34,8 @@ class ProductSubTypeBaseSerializer(serializers.ModelSerializer):
name_translated = TranslatedField() name_translated = TranslatedField()
index_name_display = serializers.CharField(source='get_index_name_display', index_name_display = serializers.CharField(source='get_index_name_display',
read_only=True) read_only=True)
default_image_url = serializers.ImageField(source='default_image.image',
allow_null=True)
class Meta: class Meta:
model = models.ProductSubType model = models.ProductSubType
@ -41,12 +43,15 @@ class ProductSubTypeBaseSerializer(serializers.ModelSerializer):
'id', 'id',
'name_translated', 'name_translated',
'index_name_display', 'index_name_display',
'default_image_url',
] ]
class ProductTypeBaseSerializer(serializers.ModelSerializer): class ProductTypeBaseSerializer(serializers.ModelSerializer):
"""ProductType base serializer""" """ProductType base serializer"""
name_translated = TranslatedField() name_translated = TranslatedField()
default_image_url = serializers.ImageField(source='default_image.image',
allow_null=True)
class Meta: class Meta:
model = models.ProductType model = models.ProductType
@ -54,6 +59,7 @@ class ProductTypeBaseSerializer(serializers.ModelSerializer):
'id', 'id',
'name_translated', 'name_translated',
'index_name', 'index_name',
'default_image_url',
] ]

View File

@ -143,6 +143,8 @@ class EstablishmentDocument(Document):
'weekday_display': fields.KeywordField(attr='get_weekday_display'), 'weekday_display': fields.KeywordField(attr='get_weekday_display'),
'closed_at': fields.KeywordField(attr='closed_at_str'), 'closed_at': fields.KeywordField(attr='closed_at_str'),
'opening_at': fields.KeywordField(attr='opening_at_str'), 'opening_at': fields.KeywordField(attr='opening_at_str'),
'closed_at_indexing': fields.DateField(),
'opening_at_indexing': fields.DateField(),
} }
)) ))
address = fields.ObjectField( address = fields.ObjectField(

View File

@ -51,7 +51,7 @@ class TagCategoryFilterSet(TagsBaseFilterSet):
# todo: filter by establishment type # todo: filter by establishment type
def by_establishment_type(self, queryset, name, value): def by_establishment_type(self, queryset, name, value):
if value == EstablishmentType.ARTISAN: if value == EstablishmentType.ARTISAN:
qs = models.TagCategory.objects.filter(index_name='shop_category') qs = models.TagCategory.objects.with_base_related().filter(index_name='shop_category')
else: else:
qs = queryset.by_establishment_type(value) qs = queryset.by_establishment_type(value)
return qs return qs
@ -73,10 +73,10 @@ class TagsFilterSet(TagsBaseFilterSet):
def by_establishment_type(self, queryset, name, value): def by_establishment_type(self, queryset, name, value):
if value == EstablishmentType.ARTISAN: if value == EstablishmentType.ARTISAN:
qs = models.Tag.objects.by_category_index_name('shop_category') qs = models.Tag.objects.filter(index_name__in=settings.ARTISANS_CHOSEN_TAGS)
if self.request.country_code and self.request.country_code not in settings.INTERNATIONAL_COUNTRY_CODES: if self.request.country_code and self.request.country_code not in settings.INTERNATIONAL_COUNTRY_CODES:
qs = qs.filter(establishments__address__city__country__code=self.request.country_code).distinct('id') qs = qs.filter(establishments__address__city__country__code=self.request.country_code).distinct('id')
return qs.exclude(establishments__isnull=True)[0:8] return qs.exclude(establishments__isnull=True)
return queryset.by_establishment_type(value) return queryset.by_establishment_type(value)
# TMP TODO remove it later # TMP TODO remove it later

View File

@ -0,0 +1,46 @@
# Generated by Django 2.2.7 on 2019-12-20 12:24
from django.db import migrations, models
import django.db.models.deletion
def fill_translations(apps, schemaeditor):
Tag = apps.get_model('tag', 'Tag')
TagCategory = apps.get_model('tag', 'TagCategory')
SiteInterfaceDictionary = apps.get_model('translation', 'SiteInterfaceDictionary')
for tag_category in TagCategory.objects.all():
if tag_category.label:
t = SiteInterfaceDictionary(text=tag_category.label)
t.save()
tag_category.translation = t
tag_category.save()
for tag in Tag.objects.all():
if tag.label:
t = SiteInterfaceDictionary(text=tag.label)
t.save()
tag.translation = t
tag.save()
class Migration(migrations.Migration):
dependencies = [
('translation', '0007_language_is_active'),
('tag', '0015_auto_20191118_1210'),
]
operations = [
migrations.AddField(
model_name='tag',
name='translation',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tag', to='translation.SiteInterfaceDictionary', verbose_name='Translation'),
),
migrations.AddField(
model_name='tagcategory',
name='translation',
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tag_category', to='translation.SiteInterfaceDictionary', verbose_name='Translation'),
),
migrations.RunPython(fill_translations, migrations.RunPython.noop)
]

View File

@ -0,0 +1,21 @@
# Generated by Django 2.2.7 on 2019-12-20 16:23
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('tag', '0016_auto_20191220_1224'),
]
operations = [
migrations.RemoveField(
model_name='tag',
name='label',
),
migrations.RemoveField(
model_name='tagcategory',
name='label',
),
]

View File

@ -5,7 +5,7 @@ from django.utils.translation import gettext_lazy as _
from configuration.models import TranslationSettings from configuration.models import TranslationSettings
from location.models import Country from location.models import Country
from utils.models import TJSONField, TranslatedFieldsMixin from utils.models import IndexJSON
class TagQuerySet(models.QuerySet): class TagQuerySet(models.QuerySet):
@ -29,12 +29,9 @@ class TagQuerySet(models.QuerySet):
return self.filter(category__establishment_types__index_name=index_name) return self.filter(category__establishment_types__index_name=index_name)
class Tag(TranslatedFieldsMixin, models.Model): class Tag(models.Model):
"""Tag model.""" """Tag model."""
label = TJSONField(blank=True, null=True, default=None,
verbose_name=_('label'),
help_text='{"en-GB":"some text"}')
value = models.CharField(_('indexing name'), max_length=255, blank=True, db_index=True, value = models.CharField(_('indexing name'), max_length=255, blank=True, db_index=True,
null=True, default=None) null=True, default=None)
category = models.ForeignKey('TagCategory', on_delete=models.CASCADE, category = models.ForeignKey('TagCategory', on_delete=models.CASCADE,
@ -48,6 +45,16 @@ class Tag(TranslatedFieldsMixin, models.Model):
old_id_meta_product = models.PositiveIntegerField(_('old id metadata product'), old_id_meta_product = models.PositiveIntegerField(_('old id metadata product'),
blank=True, null=True, default=None) blank=True, null=True, default=None)
translation = models.ForeignKey('translation.SiteInterfaceDictionary', on_delete=models.SET_NULL,
null=True, related_name='tag', verbose_name=_('Translation'))
@property
def label_indexing(self):
base_dict = self.translation.text if self.translation and isinstance(self.translation.text, dict) else {}
index = IndexJSON()
for k, v in base_dict.items():
setattr(index, k, v)
return index
objects = TagQuerySet.as_manager() objects = TagQuerySet.as_manager()
@ -88,7 +95,7 @@ class TagCategoryQuerySet(models.QuerySet):
def with_base_related(self): def with_base_related(self):
"""Select related objects.""" """Select related objects."""
return self.prefetch_related('tags') return self.prefetch_related('tags', 'tags__translation').select_related('translation')
def with_extended_related(self): def with_extended_related(self):
"""Select related objects.""" """Select related objects."""
@ -119,7 +126,7 @@ class TagCategoryQuerySet(models.QuerySet):
return self.exclude(tags__isnull=switcher) return self.exclude(tags__isnull=switcher)
class TagCategory(TranslatedFieldsMixin, models.Model): class TagCategory(models.Model):
"""Tag base category model.""" """Tag base category model."""
STRING = 'string' STRING = 'string'
@ -137,10 +144,6 @@ class TagCategory(TranslatedFieldsMixin, models.Model):
(PERCENTAGE, _('percentage')), (PERCENTAGE, _('percentage')),
(BOOLEAN, _('boolean')), (BOOLEAN, _('boolean')),
) )
label = TJSONField(blank=True, null=True, default=None,
verbose_name=_('label'),
help_text='{"en-GB":"some text"}')
country = models.ForeignKey('location.Country', country = models.ForeignKey('location.Country',
on_delete=models.SET_NULL, null=True, on_delete=models.SET_NULL, null=True,
default=None) default=None)
@ -151,6 +154,16 @@ class TagCategory(TranslatedFieldsMixin, models.Model):
value_type = models.CharField(_('value type'), max_length=255, value_type = models.CharField(_('value type'), max_length=255,
choices=VALUE_TYPE_CHOICES, default=LIST, ) choices=VALUE_TYPE_CHOICES, default=LIST, )
old_id = models.IntegerField(blank=True, null=True) old_id = models.IntegerField(blank=True, null=True)
translation = models.OneToOneField('translation.SiteInterfaceDictionary', on_delete=models.SET_NULL,
null=True, related_name='tag_category', verbose_name=_('Translation'))
@property
def label_indexing(self):
base_dict = self.translation.text if self.translation and isinstance(self.translation.text, dict) else {}
index = IndexJSON()
for k, v in base_dict.items():
setattr(index, k, v)
return index
objects = TagCategoryQuerySet.as_manager() objects = TagCategoryQuerySet.as_manager()

View File

@ -2,15 +2,25 @@
from rest_framework import serializers from rest_framework import serializers
from rest_framework.fields import SerializerMethodField from rest_framework.fields import SerializerMethodField
from establishment.models import Establishment from establishment.models import Establishment, EstablishmentType
from establishment.models import EstablishmentType
from news.models import News from news.models import News
from news.models import NewsType from news.models import NewsType
from tag import models from tag import models
from utils.exceptions import BindingObjectNotFound from utils.exceptions import BindingObjectNotFound, ObjectAlreadyAdded, RemovedBindingObjectNotFound
from utils.exceptions import ObjectAlreadyAdded
from utils.exceptions import RemovedBindingObjectNotFound
from utils.serializers import TranslatedField from utils.serializers import TranslatedField
from utils.models import get_default_locale, get_language, to_locale
def translate_obj(obj):
if not obj.translation or not isinstance(obj.translation.text, dict):
return None
try:
field = obj.translation.text
return field.get(to_locale(get_language()),
field.get(get_default_locale(),
next(iter(field.values()))))
except StopIteration:
return None
class TagBaseSerializer(serializers.ModelSerializer): class TagBaseSerializer(serializers.ModelSerializer):
@ -19,8 +29,11 @@ class TagBaseSerializer(serializers.ModelSerializer):
def get_extra_kwargs(self): def get_extra_kwargs(self):
return super().get_extra_kwargs() return super().get_extra_kwargs()
label_translated = TranslatedField()
index_name = serializers.CharField(source='value', read_only=True, allow_null=True) index_name = serializers.CharField(source='value', read_only=True, allow_null=True)
label_translated = serializers.SerializerMethodField(read_only=True, allow_null=True)
def get_label_translated(self, obj):
return translate_obj(obj)
class Meta: class Meta:
"""Meta class.""" """Meta class."""
@ -47,8 +60,10 @@ class TagBackOfficeSerializer(TagBaseSerializer):
class TagCategoryProductSerializer(serializers.ModelSerializer): class TagCategoryProductSerializer(serializers.ModelSerializer):
"""SHORT Serializer for TagCategory""" """SHORT Serializer for TagCategory"""
label_translated = serializers.SerializerMethodField(read_only=True, allow_null=True)
label_translated = TranslatedField() def get_label_translated(self, obj):
return translate_obj(obj)
class Meta: class Meta:
"""Meta class.""" """Meta class."""
@ -56,7 +71,6 @@ class TagCategoryProductSerializer(serializers.ModelSerializer):
model = models.TagCategory model = models.TagCategory
fields = ( fields = (
'id', 'id',
'label_translated',
'index_name', 'index_name',
) )
@ -64,8 +78,8 @@ class TagCategoryProductSerializer(serializers.ModelSerializer):
class TagCategoryBaseSerializer(serializers.ModelSerializer): class TagCategoryBaseSerializer(serializers.ModelSerializer):
"""Serializer for model TagCategory.""" """Serializer for model TagCategory."""
label_translated = TranslatedField() tags = TagBaseSerializer(many=True, allow_null=True)
tags = SerializerMethodField() label_translated = serializers.SerializerMethodField(read_only=True, allow_null=True)
class Meta: class Meta:
"""Meta class.""" """Meta class."""
@ -78,33 +92,17 @@ class TagCategoryBaseSerializer(serializers.ModelSerializer):
'tags', 'tags',
) )
def get_tags(self, obj): def get_label_translated(self, obj):
query_params = dict(self.context['request'].query_params) return translate_obj(obj)
if len(query_params) > 1:
return []
params = {}
if 'establishment_type' in query_params:
params = {
'establishments__isnull': False,
}
elif 'product_type' in query_params:
params = {
'products__isnull': False,
}
tags = obj.tags.filter(**params).distinct()
return TagBaseSerializer(instance=tags, many=True, read_only=True).data
class FiltersTagCategoryBaseSerializer(serializers.ModelSerializer): class FiltersTagCategoryBaseSerializer(serializers.ModelSerializer):
"""Serializer for model TagCategory.""" """Serializer for model TagCategory."""
label_translated = TranslatedField()
filters = SerializerMethodField() filters = SerializerMethodField()
param_name = SerializerMethodField() param_name = SerializerMethodField()
type = SerializerMethodField() type = SerializerMethodField()
label_translated = serializers.SerializerMethodField(allow_null=True, read_only=True)
class Meta: class Meta:
"""Meta class.""" """Meta class."""
@ -127,6 +125,9 @@ class FiltersTagCategoryBaseSerializer(serializers.ModelSerializer):
return 'wine_colors_id__in' return 'wine_colors_id__in'
return 'tags_id__in' return 'tags_id__in'
def get_label_translated(self, obj):
return translate_obj(obj)
def get_fields(self, *args, **kwargs): def get_fields(self, *args, **kwargs):
fields = super(FiltersTagCategoryBaseSerializer, self).get_fields() fields = super(FiltersTagCategoryBaseSerializer, self).get_fields()
@ -157,10 +158,13 @@ class FiltersTagCategoryBaseSerializer(serializers.ModelSerializer):
class TagCategoryShortSerializer(serializers.ModelSerializer): class TagCategoryShortSerializer(serializers.ModelSerializer):
"""Serializer for model TagCategory.""" """Serializer for model TagCategory."""
label_translated = TranslatedField() label_translated = serializers.SerializerMethodField(allow_null=True, read_only=True)
value_type_display = serializers.CharField(source='get_value_type_display', value_type_display = serializers.CharField(source='get_value_type_display',
read_only=True) read_only=True)
def get_label_translated(self, obj):
return translate_obj(obj)
class Meta(TagCategoryBaseSerializer.Meta): class Meta(TagCategoryBaseSerializer.Meta):
"""Meta class.""" """Meta class."""
fields = [ fields = [

View File

@ -1,6 +1,6 @@
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from datetime import time from datetime import time, datetime
from utils.models import ProjectBaseMixin from utils.models import ProjectBaseMixin
@ -59,6 +59,14 @@ class Timetable(ProjectBaseMixin):
def opening_at_str(self): def opening_at_str(self):
return str(self.opening_at) if self.opening_at else None return str(self.opening_at) if self.opening_at else None
@property
def closed_at_indexing(self):
return datetime.combine(time=self.closed_at, date=datetime(1970, 1, 1 + self.weekday).date()) if self.closed_at else None
@property
def opening_at_indexing(self):
return datetime.combine(time=self.opening_at, date=datetime(1970, 1, 1 + self.weekday).date()) if self.opening_at else None
@property @property
def opening_time(self): def opening_time(self):
return self.opening_at or self.lunch_start or self.dinner_start return self.opening_at or self.lunch_start or self.dinner_start

View File

@ -3,6 +3,7 @@ from django.utils.text import slugify
from rest_framework import serializers from rest_framework import serializers
from tag.models import Tag from tag.models import Tag
from translation.models import SiteInterfaceDictionary
from transfer.mixins import TransferSerializerMixin from transfer.mixins import TransferSerializerMixin
from transfer.models import Cepages from transfer.models import Cepages
@ -36,8 +37,11 @@ class AssemblageTagSerializer(TransferSerializerMixin):
def create(self, validated_data): def create(self, validated_data):
qs = self.Meta.model.objects.filter(**validated_data) qs = self.Meta.model.objects.filter(**validated_data)
category = validated_data.get('category') category = validated_data.get('category')
translations = validated_data.pop('label')
if not qs.exists() and category: if not qs.exists() and category:
return super().create(validated_data) instance = super().create(validated_data)
SiteInterfaceDictionary.objects.update_or_create_for_tag(instance, translations)
return instance
def get_tag_value(self, cepage, percent): def get_tag_value(self, cepage, percent):
if cepage and percent: if cepage and percent:

View File

@ -2,9 +2,9 @@
from django.contrib.postgres.fields import JSONField from django.contrib.postgres.fields import JSONField
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.apps import apps
from utils.models import ProjectBaseMixin, LocaleManagerMixin from utils.models import ProjectBaseMixin, LocaleManagerMixin
class LanguageQuerySet(models.QuerySet): class LanguageQuerySet(models.QuerySet):
"""QuerySet for model Language""" """QuerySet for model Language"""
@ -50,6 +50,44 @@ class Language(models.Model):
class SiteInterfaceDictionaryManager(LocaleManagerMixin): class SiteInterfaceDictionaryManager(LocaleManagerMixin):
"""Extended manager for SiteInterfaceDictionary model.""" """Extended manager for SiteInterfaceDictionary model."""
def update_or_create_for_tag(self, tag, translations: dict):
Tag = apps.get_model('tag', 'Tag')
"""Creates or updates translation for EXISTING in DB Tag"""
if not tag.pk or not isinstance(tag, Tag):
raise NotImplementedError
if tag.translation:
tag.translation.text = translations
tag.translation.page = 'tag'
tag.translation.keywords = f'tag-{tag.pk}'
else:
trans = SiteInterfaceDictionary({
'text': translations,
'page': 'tag',
'keywords': f'tag-{tag.pk}'
})
trans.save()
tag.translation = trans
tag.save()
def update_or_create_for_tag_category(self, tag_category, translations: dict):
"""Creates or updates translation for EXISTING in DB TagCategory"""
TagCategory = apps.get_model('tag', 'TagCategory')
if not tag_category.pk or not isinstance(tag_category, TagCategory):
raise NotImplementedError
if tag_category.translation:
tag_category.translation.text = translations
tag_category.translation.page = 'tag'
tag_category.translation.keywords = f'tag_category-{tag_category.pk}'
else:
trans = SiteInterfaceDictionary({
'text': translations,
'page': 'tag',
'keywords': f'tag_category-{tag_category.pk}'
})
trans.save()
tag_category.translation = trans
tag_category.save()
class SiteInterfaceDictionary(ProjectBaseMixin): class SiteInterfaceDictionary(ProjectBaseMixin):
"""Site interface dictionary model.""" """Site interface dictionary model."""

115
apps/utils/export.py Normal file
View File

@ -0,0 +1,115 @@
import csv
import xlsxwriter
import logging
import os
import tempfile
from smtplib import SMTPException
from django.conf import settings
from django.core.mail import EmailMultiAlternatives
logging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.INFO)
logger = logging.getLogger(__name__)
class SendExport:
def __init__(self, user, panel, file_type='csv'):
self.type_mapper = {
"csv": self.make_csv_file,
"xls": self.make_xls_file
}
self.file_type = file_type
self.user = user
self.panel = panel
self.email_from = settings.EMAIL_HOST_USER
self.email_subject = f'Export panel: {self.get_file_name()}'
self.email_body = 'Exported panel data'
self.get_file_method = self.type_mapper[file_type]
self.file_path = os.path.join(
settings.STATIC_ROOT,
'email', tempfile.gettempdir(),
self.get_file_name()
)
self.success = False
def get_file_name(self):
name = '_'.join(self.panel.name.split(' '))
return f'export_{name.lower()}.{self.file_type}'
def get_data(self):
return self.panel.get_data()
def get_headers(self):
try:
header = self.panel.get_headers()
self.success = True
return header
except Exception as err:
logger.info(f'HEADER:{err}')
def make_csv_file(self):
file_header = self.get_headers()
if not self.success:
return
with open(self.file_path, 'w') as f:
file_writer = csv.writer(f, quotechar='"', quoting=csv.QUOTE_MINIMAL)
# Write headers to CSV file
file_writer.writerow(file_header)
for row in self.get_data():
file_writer.writerow(row)
def make_xls_file(self):
headings = self.get_headers()
if not self.success:
return
with xlsxwriter.Workbook(self.file_path) as workbook:
worksheet = workbook.add_worksheet()
# Add a bold format to use to highlight cells.
bold = workbook.add_format({'bold': True})
# Add the worksheet data that the charts will refer to.
data = self.get_data()
worksheet.write_row('A1', headings, bold)
for n, row in enumerate(data):
worksheet.write_row(f'A{n+2}', [str(i) for i in row])
workbook.close()
def send(self):
self.get_file_method()
print(f'ok: {self.file_path}')
self.send_email()
def get_file(self):
if os.path.exists(self.file_path) and os.path.isfile(self.file_path):
with open(self.file_path, 'rb') as export_file:
return export_file
else:
logger.info('COMMUTATOR:image file not found dir: {path}')
def send_email(self):
msg = EmailMultiAlternatives(
subject=self.email_subject,
body=self.email_body,
from_email=self.email_from,
to=[
self.user.email,
'kuzmenko.da@gmail.com',
'sinapsit@yandex.ru'
]
)
# Create an inline attachment
if self.file_path and self.success:
msg.attach_file(self.file_path)
else:
msg.body = 'An error occurred while executing the request.'
try:
msg.send()
logger.debug(f"COMMUTATOR:Email successfully sent")
except SMTPException as e:
logger.error(f"COMMUTATOR:Email connector: {e}")

View File

@ -88,7 +88,6 @@ def translate_field(self, field_name, toggle_field_name=None):
return None return None
return translate return translate
# todo: refactor this # todo: refactor this
class IndexJSON: class IndexJSON:
@ -443,7 +442,8 @@ class HasTagsMixin(models.Model):
@property @property
def visible_tags(self): def visible_tags(self):
return self.tags.filter(category__public=True).prefetch_related('category')\ return self.tags.filter(category__public=True).prefetch_related('category',
'translation', 'category__translation')\
.exclude(category__value_type='bool') .exclude(category__value_type='bool')
class Meta: class Meta:

View File

@ -516,8 +516,12 @@ PHONENUMBER_DEFAULT_REGION = "FR"
FALLBACK_LOCALE = 'en-GB' FALLBACK_LOCALE = 'en-GB'
ESTABLISHMENT_CHOSEN_TAGS = ['gastronomic', 'en_vogue', 'terrace', 'streetfood', 'business', 'bar_cocktail', 'brunch', 'pop'] ESTABLISHMENT_CHOSEN_TAGS = ['gastronomic', 'terrace', 'streetfood', 'business', 'bar_cocktail', 'brunch', 'pop']
NEWS_CHOSEN_TAGS = ['eat', 'drink', 'cook', 'style', 'international', 'event', 'partnership'] NEWS_CHOSEN_TAGS = ['eat', 'drink', 'cook', 'style', 'international', 'event', 'partnership']
ARTISANS_CHOSEN_TAGS = ['butchery', 'bakery', 'patisserie', 'cheese_shop', 'fish_shop', 'ice-cream_maker',
'wine_merchant', 'coffe_shop']
RECIPES_CHOSEN_TAGS = ['cook', 'eat', 'drink']
INTERNATIONAL_COUNTRY_CODES = ['www', 'main', 'next'] INTERNATIONAL_COUNTRY_CODES = ['www', 'main', 'next']
THUMBNAIL_ENGINE = 'utils.thumbnail_engine.GMEngine' THUMBNAIL_ENGINE = 'utils.thumbnail_engine.GMEngine'

View File

@ -86,11 +86,11 @@ LOGGING = {
'py.warnings': { 'py.warnings': {
'handlers': ['console'], 'handlers': ['console'],
}, },
# 'django.db.backends': { 'django.db.backends': {
# 'handlers': ['console', ], 'handlers': ['console', ],
# 'level': 'DEBUG', 'level': 'DEBUG',
# 'propagate': False, 'propagate': False,
# }, },
} }
} }

View File

@ -63,3 +63,6 @@ pycountry==19.8.18
# sql-tree # sql-tree
django-mptt==0.9.1 django-mptt==0.9.1
# Export to Excel
XlsxWriter==1.2.6