Merge branch 'develop' into feature/guides

# Conflicts:
#	apps/collection/models.py
#	apps/establishment/models.py
#	apps/establishment/views/web.py
#	apps/product/views/common.py
#	requirements/base.txt
This commit is contained in:
Anatoly 2019-12-23 13:11:05 +03:00
commit f576825a71
54 changed files with 1220 additions and 386 deletions

View File

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

View File

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

View File

@ -16,6 +16,7 @@ from utils.models import (
URLImageMixin,
)
from utils.querysets import RelatedObjectsCountMixin
from utils.models import IntermediateGalleryModelMixin, GalleryMixin
# Mixins
@ -122,22 +123,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)

View File

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

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2019-12-20 09:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('establishment', '0067_auto_20191122_1244'),
]
operations = [
migrations.AlterField(
model_name='establishment',
name='schedule',
field=models.ManyToManyField(blank=True, related_name='schedule', to='timetable.Timetable', verbose_name='Establishment schedule'),
),
]

View File

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

View File

@ -27,13 +27,13 @@ from main.models import Award, Currency
from review.models import Review
from 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)
@ -249,29 +258,26 @@ class EstablishmentQuerySet(models.QuerySet):
.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,
}
)
# 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')
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."""
@ -284,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):
"""
@ -314,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):
"""
@ -435,7 +433,7 @@ class EstablishmentQuerySet(models.QuerySet):
)
class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin,
class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin,
TranslatedFieldsMixin, HasTagsMixin, FavoritesMixin):
"""Establishment model."""
@ -485,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'),
@ -534,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

View File

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

View File

@ -46,6 +46,19 @@ class EstablishmentSimilarView(EstablishmentListView):
serializer_class = serializers.EstablishmentSimilarSerializer
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):
"""Resource for getting a establishment."""
@ -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'))[:settings.QUERY_OUTPUT_OBJECTS]
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'))[:settings.QUERY_OUTPUT_OBJECTS]
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'))[:settings.QUERY_OUTPUT_OBJECTS]
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):

View File

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

View File

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

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2019-12-20 10:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('location', '0031_establishmentwineoriginaddress_wineoriginaddress'),
]
operations = [
migrations.AlterField(
model_name='address',
name='number',
field=models.IntegerField(blank=True, default=0, verbose_name='number'),
),
]

View File

@ -12,7 +12,7 @@ from typing import List
from translation.models import Language
from 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'))

View File

@ -1,5 +1,4 @@
"""Location app views."""
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import generics
from location import models, serializers
@ -9,6 +8,7 @@ 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
@ -109,11 +109,8 @@ class RegionListCreateView(common.RegionViewMixin, generics.ListCreateAPIView):
pagination_class = None
serializer_class = serializers.RegionSerializer
permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
filter_backends = (DjangoFilterBackend,)
ordering_fields = '__all__'
filterset_fields = (
'country',
)
filter_class = RegionFilter
class RegionRUDView(common.RegionViewMixin, generics.RetrieveUpdateDestroyAPIView):

View File

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

View File

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

View File

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

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

View File

@ -23,9 +23,10 @@ urlpatterns = [
path('page-types/', views.PageTypeListCreateView.as_view(),
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')
]

View File

@ -1,8 +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
@ -106,4 +110,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
)

View File

@ -1,29 +0,0 @@
from django.core.management.base import BaseCommand
from django.db.models import F
from tqdm import tqdm
from account.models import User
from news.models import News
from transfer.models import PageTexts
class Command(BaseCommand):
help = 'Add author of News'
def handle(self, *args, **kwargs):
count = 0
news_list = News.objects.filter(created_by__isnull=True)
for news in tqdm(news_list, desc="Find author for exist news"):
old_news = PageTexts.objects.filter(id=news.old_id).annotate(
account_id=F('page__account_id'),
).first()
if old_news:
user = User.objects.filter(old_id=old_news.account_id).first()
if user:
news.created_by = user
news.modified_by = user
news.save()
count += 1
self.stdout.write(self.style.WARNING(f'Update {count} objects.'))

View File

@ -1,36 +0,0 @@
from django.core.management.base import BaseCommand
from news.models import News, NewsType
from tag.models import Tag, TagCategory
from transfer.models import PageMetadata, Pages, PageTexts
class Command(BaseCommand):
help = 'Remove old news from new bd'
def handle(self, *args, **kwargs):
count = 0
news_type, _ = NewsType.objects.get_or_create(name='News')
tag_cat, _ = TagCategory.objects.get_or_create(index_name='category')
news_type.tag_categories.add(tag_cat)
news_type.save()
old_news_tag = PageMetadata.objects.filter(key='category', page__pagetexts__isnull=False)
for old_tag in old_news_tag:
old_id_list = old_tag.page.pagetexts_set.all().values_list('id', flat=True)
# Make Tag
new_tag, created = Tag.objects.get_or_create(category=tag_cat, value=old_tag.value)
if created:
text_value = ' '.join(new_tag.value.split('_'))
new_tag.label = {'en-GB': text_value}
new_tag.save()
for id in old_id_list:
if isinstance(id, int):
news = News.objects.filter(old_id=id).first()
if news:
news.tags.add(new_tag)
news.save()
count += 1
self.stdout.write(self.style.WARNING(f'Create or update {count} objects.'))

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.7 on 2019-12-23 06:19
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('news', '0048_remove_news_must_of_the_week'),
]
operations = [
migrations.AlterField(
model_name='news',
name='views_count',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='news', to='rating.ViewCount'),
),
]

View File

@ -14,7 +14,7 @@ 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
@ -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."""
@ -105,10 +105,10 @@ class NewsQuerySet(TranslationQuerysetMixin):
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)
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?
@ -140,7 +140,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."""
@ -215,7 +215,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')

View File

@ -189,8 +189,12 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
'must_of_the_week',
'publication_date',
'publication_time',
'created',
'modified',
)
extra_kwargs = {
'created': {'read_only': True},
'modified': {'read_only': True},
'duplication_date': {'read_only': True},
'locale_to_description_is_active': {'allow_null': False},
'must_of_the_week': {'read_only': True},

View File

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

View File

@ -1,44 +1,61 @@
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()
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()
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 +65,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,
]
}

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

@ -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'
@ -37,6 +38,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."""
@ -45,7 +50,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 +67,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 +92,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 +204,7 @@ class ProductQuerySet(models.QuerySet):
return self.none()
class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes,
class Product(GalleryMixin, TranslatedFieldsMixin, BaseAttributes,
HasTagsMixin, FavoritesMixin):
"""Product models."""

View File

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

View File

@ -1,10 +1,12 @@
"""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
from django.conf import settings
@ -36,7 +38,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):
@ -96,7 +106,10 @@ class SimilarListView(ProductSimilarView):
def get_queryset(self):
"""Overridden get_queryset method."""
return super().get_queryset() \
.has_location() \
.similar(slug=self.kwargs.get('slug'))[:settings.QUERY_OUTPUT_OBJECTS]
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()

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2019-12-23 08:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('rating', '0004_auto_20191114_2041'),
]
operations = [
migrations.AlterField(
model_name='viewcount',
name='count',
field=models.PositiveIntegerField(),
),
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -132,3 +132,12 @@ def namedtuplefetchall(cursor):
desc = cursor.description
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()
]

View File

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

View File

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

View File

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

View File

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

View File

@ -64,5 +64,8 @@ pycountry==19.8.18
# sql-tree
django-mptt==0.9.1
# Export to Excel
XlsxWriter==1.2.6
# For recursive fields
djangorestframework-recursive==0.1.2