diff --git a/apps/collection/models.py b/apps/collection/models.py index 90837cd7..e3ca63be 100644 --- a/apps/collection/models.py +++ b/apps/collection/models.py @@ -118,22 +118,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) diff --git a/apps/establishment/management/commands/fill_artisan_default_image.py b/apps/establishment/management/commands/fill_artisan_default_image.py new file mode 100644 index 00000000..1231ba76 --- /dev/null +++ b/apps/establishment/management/commands/fill_artisan_default_image.py @@ -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.')) \ No newline at end of file diff --git a/apps/establishment/migrations/0069_auto_20191220_1007.py b/apps/establishment/migrations/0069_auto_20191220_1007.py new file mode 100644 index 00000000..6225c592 --- /dev/null +++ b/apps/establishment/migrations/0069_auto_20191220_1007.py @@ -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'), + ), + ] diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 2442d449..cde60618 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -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.""" @@ -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.""" diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 19b4b764..1e7a153a 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -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}, diff --git a/apps/gallery/models.py b/apps/gallery/models.py index 22c5b5e7..0cc8c60e 100644 --- a/apps/gallery/models.py +++ b/apps/gallery/models.py @@ -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: diff --git a/apps/main/models.py b/apps/main/models.py index 83109f40..d8860aee 100644 --- a/apps/main/models.py +++ b/apps/main/models.py @@ -481,6 +481,19 @@ class Panel(ProjectBaseMixin): 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) diff --git a/apps/main/tasks.py b/apps/main/tasks.py new file mode 100644 index 00000000..0231b83f --- /dev/null +++ b/apps/main/tasks.py @@ -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() diff --git a/apps/main/urls/back.py b/apps/main/urls/back.py index a2049a42..3d39f008 100644 --- a/apps/main/urls/back.py +++ b/apps/main/urls/back.py @@ -24,8 +24,9 @@ urlpatterns = [ name='page-types-list-create'), path('panels/', views.PanelsListCreateView.as_view(), name='panels'), path('panels//', views.PanelsRUDView.as_view(), name='panels-rud'), - path('panels//execute/', views.PanelsExecuteView.as_view(), name='panels-execute') - + path('panels//execute/', views.PanelsExecuteView.as_view(), name='panels-execute'), + path('panels//csv/', views.PanelsExportCSVView.as_view(), name='panels-csv'), + path('panels//xls/', views.PanelsExecuteXLSView.as_view(), name='panels-xls') ] diff --git a/apps/main/views/back.py b/apps/main/views/back.py index 98398f17..e819b71d 100644 --- a/apps/main/views/back.py +++ b/apps/main/views/back.py @@ -1,10 +1,12 @@ 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.views import SiteSettingsView, SiteListView @@ -121,3 +123,35 @@ class PanelsExecuteView(generics.ListAPIView): 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 + ) diff --git a/apps/news/models.py b/apps/news/models.py index ab65ed88..64b22ffb 100644 --- a/apps/news/models.py +++ b/apps/news/models.py @@ -74,7 +74,7 @@ class NewsQuerySet(TranslationQuerysetMixin): 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.""" diff --git a/apps/product/migrations/0022_auto_20191220_1007.py b/apps/product/migrations/0022_auto_20191220_1007.py new file mode 100644 index 00000000..c99b0e37 --- /dev/null +++ b/apps/product/migrations/0022_auto_20191220_1007.py @@ -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'), + ), + ] diff --git a/apps/product/models.py b/apps/product/models.py index 7aeacdf2..2c2081c9 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -37,6 +37,10 @@ class ProductType(TranslatedFieldsMixin, ProjectBaseMixin): 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.""" @@ -62,6 +66,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 +91,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.""" diff --git a/apps/product/serializers/common.py b/apps/product/serializers/common.py index 86344a36..f3678448 100644 --- a/apps/product/serializers/common.py +++ b/apps/product/serializers/common.py @@ -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', ] diff --git a/apps/search_indexes/documents/establishment.py b/apps/search_indexes/documents/establishment.py index 8ae26097..e9761393 100644 --- a/apps/search_indexes/documents/establishment.py +++ b/apps/search_indexes/documents/establishment.py @@ -143,6 +143,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( diff --git a/apps/tag/filters.py b/apps/tag/filters.py index 46470ca1..5b0ba43e 100644 --- a/apps/tag/filters.py +++ b/apps/tag/filters.py @@ -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 diff --git a/apps/tag/migrations/0016_auto_20191220_1224.py b/apps/tag/migrations/0016_auto_20191220_1224.py new file mode 100644 index 00000000..3d70ecb3 --- /dev/null +++ b/apps/tag/migrations/0016_auto_20191220_1224.py @@ -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) + ] diff --git a/apps/tag/migrations/0017_auto_20191220_1623.py b/apps/tag/migrations/0017_auto_20191220_1623.py new file mode 100644 index 00000000..f36f0a55 --- /dev/null +++ b/apps/tag/migrations/0017_auto_20191220_1623.py @@ -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', + ), + ] diff --git a/apps/tag/models.py b/apps/tag/models.py index b718d83c..4d0ab43a 100644 --- a/apps/tag/models.py +++ b/apps/tag/models.py @@ -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() diff --git a/apps/tag/serializers.py b/apps/tag/serializers.py index b5e5a267..2155de73 100644 --- a/apps/tag/serializers.py +++ b/apps/tag/serializers.py @@ -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.""" @@ -47,8 +60,10 @@ class TagBackOfficeSerializer(TagBaseSerializer): class TagCategoryProductSerializer(serializers.ModelSerializer): """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: """Meta class.""" @@ -56,7 +71,6 @@ class TagCategoryProductSerializer(serializers.ModelSerializer): model = models.TagCategory fields = ( 'id', - 'label_translated', 'index_name', ) @@ -64,8 +78,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 +92,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 +125,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 +158,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 = [ diff --git a/apps/timetable/models.py b/apps/timetable/models.py index 07e52807..c9295b3b 100644 --- a/apps/timetable/models.py +++ b/apps/timetable/models.py @@ -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 @@ -59,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 diff --git a/apps/transfer/serializers/tag.py b/apps/transfer/serializers/tag.py index c47ffafc..4f8ae862 100644 --- a/apps/transfer/serializers/tag.py +++ b/apps/transfer/serializers/tag.py @@ -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: diff --git a/apps/translation/models.py b/apps/translation/models.py index 1d9695fe..7b64dce0 100644 --- a/apps/translation/models.py +++ b/apps/translation/models.py @@ -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.""" diff --git a/apps/utils/export.py b/apps/utils/export.py new file mode 100644 index 00000000..e4756b09 --- /dev/null +++ b/apps/utils/export.py @@ -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}") \ No newline at end of file diff --git a/apps/utils/models.py b/apps/utils/models.py index 07891330..42e35bb1 100644 --- a/apps/utils/models.py +++ b/apps/utils/models.py @@ -88,7 +88,6 @@ def translate_field(self, field_name, toggle_field_name=None): return None return translate - # todo: refactor this class IndexJSON: @@ -443,7 +442,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: diff --git a/project/settings/base.py b/project/settings/base.py index 2dfe1ba1..a7d3274f 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -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' diff --git a/project/settings/local.py b/project/settings/local.py index d9c7cab8..b101d78e 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -86,11 +86,11 @@ LOGGING = { 'py.warnings': { 'handlers': ['console'], }, - # 'django.db.backends': { - # 'handlers': ['console', ], - # 'level': 'DEBUG', - # 'propagate': False, - # }, + 'django.db.backends': { + 'handlers': ['console', ], + 'level': 'DEBUG', + 'propagate': False, + }, } } diff --git a/requirements/base.txt b/requirements/base.txt index 90e5b2d5..18bb1bc5 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -63,3 +63,6 @@ pycountry==19.8.18 # sql-tree django-mptt==0.9.1 + +# Export to Excel +XlsxWriter==1.2.6 \ No newline at end of file