Merge branch 'develop' into feature/fix-country-region-city-transfer
# Conflicts: # apps/location/models.py # apps/transfer/serializers/news.py # apps/utils/models.py
This commit is contained in:
commit
858b6f3544
18
apps/account/migrations/0026_auto_20191210_1553.py
Normal file
18
apps/account/migrations/0026_auto_20191210_1553.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-10 15:53
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('account', '0025_auto_20191210_0623'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='role',
|
||||||
|
name='role',
|
||||||
|
field=models.PositiveIntegerField(choices=[(1, 'Standard user'), (2, 'Comments moderator'), (3, 'Country admin'), (4, 'Content page manager'), (5, 'Establishment manager'), (6, 'Reviewer manager'), (7, 'Restaurant reviewer'), (8, 'Sales man'), (9, 'Winery reviewer'), (10, 'Seller'), (11, 'Liquor reviewer'), (12, 'Product reviewer')], verbose_name='Role'),
|
||||||
|
),
|
||||||
|
]
|
||||||
14
apps/account/migrations/0028_merge_20191217_1127.py
Normal file
14
apps/account/migrations/0028_merge_20191217_1127.py
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-17 11:27
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('account', '0027_auto_20191211_1444'),
|
||||||
|
('account', '0026_auto_20191210_1553'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
]
|
||||||
|
|
@ -36,6 +36,8 @@ class Role(ProjectBaseMixin):
|
||||||
SALES_MAN = 8
|
SALES_MAN = 8
|
||||||
WINERY_REVIEWER = 9 # Establishments subtype "winery"
|
WINERY_REVIEWER = 9 # Establishments subtype "winery"
|
||||||
SELLER = 10
|
SELLER = 10
|
||||||
|
LIQUOR_REVIEWER = 11
|
||||||
|
PRODUCT_REVIEWER = 12
|
||||||
|
|
||||||
ROLE_CHOICES = (
|
ROLE_CHOICES = (
|
||||||
(STANDARD_USER, _('Standard user')),
|
(STANDARD_USER, _('Standard user')),
|
||||||
|
|
@ -47,7 +49,9 @@ class Role(ProjectBaseMixin):
|
||||||
(RESTAURANT_REVIEWER, 'Restaurant reviewer'),
|
(RESTAURANT_REVIEWER, 'Restaurant reviewer'),
|
||||||
(SALES_MAN, 'Sales man'),
|
(SALES_MAN, 'Sales man'),
|
||||||
(WINERY_REVIEWER, 'Winery reviewer'),
|
(WINERY_REVIEWER, 'Winery reviewer'),
|
||||||
(SELLER, 'Seller')
|
(SELLER, 'Seller'),
|
||||||
|
(LIQUOR_REVIEWER, 'Liquor reviewer'),
|
||||||
|
(PRODUCT_REVIEWER, 'Product reviewer'),
|
||||||
)
|
)
|
||||||
role = models.PositiveIntegerField(verbose_name=_('Role'), choices=ROLE_CHOICES,
|
role = models.PositiveIntegerField(verbose_name=_('Role'), choices=ROLE_CHOICES,
|
||||||
null=False, blank=False)
|
null=False, blank=False)
|
||||||
|
|
|
||||||
|
|
@ -10,5 +10,5 @@ urlpatterns = [
|
||||||
path('user-role/', views.UserRoleLstView.as_view(), name='user-role-list-create'),
|
path('user-role/', views.UserRoleLstView.as_view(), name='user-role-list-create'),
|
||||||
path('user/', views.UserLstView.as_view(), name='user-create-list'),
|
path('user/', views.UserLstView.as_view(), name='user-create-list'),
|
||||||
path('user/<int:id>/', views.UserRUDView.as_view(), name='user-rud'),
|
path('user/<int:id>/', views.UserRUDView.as_view(), name='user-rud'),
|
||||||
path('user/<int:id>/csv', views.get_user_csv, name='user-csv'),
|
path('user/<int:id>/csv/', views.get_user_csv, name='user-csv'),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ class UserRUDView(generics.RetrieveUpdateDestroyAPIView):
|
||||||
|
|
||||||
|
|
||||||
def get_user_csv(request, id):
|
def get_user_csv(request, id):
|
||||||
|
"""User CSV file download"""
|
||||||
# fields = ["id", "uuid", "nickname", "locale", "country_code", "city", "role", "consent_purpose", "consent_at",
|
# fields = ["id", "uuid", "nickname", "locale", "country_code", "city", "role", "consent_purpose", "consent_at",
|
||||||
# "last_seen_at", "created_at", "updated_at", "email", "is_admin", "ezuser_id", "ez_user_id",
|
# "last_seen_at", "created_at", "updated_at", "email", "is_admin", "ezuser_id", "ez_user_id",
|
||||||
# "encrypted_password", "reset_password_token", "reset_password_sent_at", "remember_created_at",
|
# "encrypted_password", "reset_password_token", "reset_password_sent_at", "remember_created_at",
|
||||||
|
|
|
||||||
18
apps/collection/migrations/0027_auto_20191217_1852.py
Normal file
18
apps/collection/migrations/0027_auto_20191217_1852.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-17 18:52
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('collection', '0026_merge_20191217_1151'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='collection',
|
||||||
|
name='slug',
|
||||||
|
field=models.SlugField(max_length=255, null=True, unique=True, verbose_name='Collection slug'),
|
||||||
|
),
|
||||||
|
]
|
||||||
14
apps/collection/migrations/0028_merge_20191223_1415.py
Normal file
14
apps/collection/migrations/0028_merge_20191223_1415.py
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-23 14:15
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('collection', '0027_auto_20191217_1852'),
|
||||||
|
('collection', '0027_auto_20191218_0753'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
]
|
||||||
|
|
@ -11,7 +11,9 @@ from utils.models import (
|
||||||
URLImageMixin,
|
URLImageMixin,
|
||||||
)
|
)
|
||||||
from utils.querysets import RelatedObjectsCountMixin
|
from utils.querysets import RelatedObjectsCountMixin
|
||||||
from utils.models import IntermediateGalleryModelMixin, GalleryModelMixin
|
from utils.models import IntermediateGalleryModelMixin, GalleryMixin
|
||||||
|
|
||||||
|
from slugify import slugify
|
||||||
|
|
||||||
|
|
||||||
# Mixins
|
# Mixins
|
||||||
|
|
@ -79,7 +81,7 @@ class Collection(ProjectBaseMixin, CollectionDateMixin,
|
||||||
description = TJSONField(
|
description = TJSONField(
|
||||||
_('description'), null=True, blank=True,
|
_('description'), null=True, blank=True,
|
||||||
default=None, help_text='{"en-GB":"some text"}')
|
default=None, help_text='{"en-GB":"some text"}')
|
||||||
slug = models.SlugField(max_length=50, unique=True,
|
slug = models.SlugField(max_length=255, unique=True,
|
||||||
verbose_name=_('Collection slug'), editable=True, null=True)
|
verbose_name=_('Collection slug'), editable=True, null=True)
|
||||||
old_id = models.IntegerField(null=True, blank=True)
|
old_id = models.IntegerField(null=True, blank=True)
|
||||||
|
|
||||||
|
|
@ -118,28 +120,38 @@ class Collection(ProjectBaseMixin, CollectionDateMixin,
|
||||||
instances = getattr(self, f'{related_object}')
|
instances = getattr(self, f'{related_object}')
|
||||||
if instances.exists():
|
if instances.exists():
|
||||||
for instance in instances.all():
|
for instance in instances.all():
|
||||||
raw_object = (instance.id, instance.slug) if hasattr(instance, 'slug') else (
|
raw_object = (instance.id, instance.establishment_type.index_name,
|
||||||
instance.id, None
|
instance.slug) if \
|
||||||
)
|
hasattr(instance, 'slug') else (instance.id, None, None)
|
||||||
raw_objects.append(raw_object)
|
raw_objects.append(raw_object)
|
||||||
|
|
||||||
# parse slugs
|
# parse slugs
|
||||||
related_objects = []
|
related_objects = []
|
||||||
object_names = set()
|
object_names = set()
|
||||||
re_pattern = r'[\w]+'
|
re_pattern = r'[\w]+'
|
||||||
for object_id, raw_name, in raw_objects:
|
for object_id, object_type, raw_name, in raw_objects:
|
||||||
result = re.findall(re_pattern, raw_name)
|
result = re.findall(re_pattern, raw_name)
|
||||||
if result:
|
if result:
|
||||||
name = ' '.join(result).capitalize()
|
name = ' '.join(result).capitalize()
|
||||||
if name not in object_names:
|
if name not in object_names:
|
||||||
related_objects.append({
|
related_objects.append({
|
||||||
'id': object_id,
|
'id': object_id,
|
||||||
|
'establishment_type': object_type,
|
||||||
'name': name
|
'name': name
|
||||||
})
|
})
|
||||||
object_names.add(name)
|
object_names.add(name)
|
||||||
|
|
||||||
return related_objects
|
return related_objects
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.pk:
|
||||||
|
slugify_slug = slugify(
|
||||||
|
next(iter(self.name.values())),
|
||||||
|
word_boundary=True
|
||||||
|
)
|
||||||
|
self.slug = slugify_slug
|
||||||
|
super(Collection, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class GuideTypeQuerySet(models.QuerySet):
|
class GuideTypeQuerySet(models.QuerySet):
|
||||||
"""QuerySet for model GuideType."""
|
"""QuerySet for model GuideType."""
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,9 @@ class CollectionBackOfficeSerializer(CollectionBaseSerializer):
|
||||||
'related_object_names',
|
'related_object_names',
|
||||||
'rank',
|
'rank',
|
||||||
]
|
]
|
||||||
|
extra_kwargs = {
|
||||||
|
'slug': {'read_only': True},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class CollectionBindObjectSerializer(serializers.Serializer):
|
class CollectionBindObjectSerializer(serializers.Serializer):
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
import boto3
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from establishment.models import EstablishmentSubType
|
||||||
|
from gallery.models import Image
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = """
|
||||||
|
Fill establishment type by index names.
|
||||||
|
Steps:
|
||||||
|
1 Upload default images into s3 bucket
|
||||||
|
2 Run command ./manage.py fill_artisan_default_image
|
||||||
|
"""
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
'--template_image_folder_name',
|
||||||
|
help='Template image folder in Amazon S3 bucket'
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
not_updated = 0
|
||||||
|
template_image_folder_name = kwargs.get('template_image_folder_name')
|
||||||
|
if (template_image_folder_name and
|
||||||
|
hasattr(settings, 'AWS_ACCESS_KEY_ID') and
|
||||||
|
hasattr(settings, 'AWS_SECRET_ACCESS_KEY') and
|
||||||
|
hasattr(settings, 'AWS_STORAGE_BUCKET_NAME')):
|
||||||
|
to_update = []
|
||||||
|
s3 = boto3.resource('s3')
|
||||||
|
s3_bucket = s3.Bucket(settings.AWS_STORAGE_BUCKET_NAME)
|
||||||
|
|
||||||
|
for object_summary in s3_bucket.objects.filter(Prefix=f'media/{template_image_folder_name}/'):
|
||||||
|
uri_path = object_summary.key
|
||||||
|
filename = uri_path.split('/')[-1:][0]
|
||||||
|
if filename:
|
||||||
|
artisan_index_slice = filename.split('.')[:-1][0] \
|
||||||
|
.split('_')[2:]
|
||||||
|
if len(artisan_index_slice) > 1:
|
||||||
|
artisan_index_name = '_'.join(artisan_index_slice)
|
||||||
|
else:
|
||||||
|
artisan_index_name = artisan_index_slice[0]
|
||||||
|
|
||||||
|
attachment_suffix_url = f'{template_image_folder_name}/{filename}'
|
||||||
|
|
||||||
|
# check artisan in db
|
||||||
|
artisan_qs = EstablishmentSubType.objects.filter(index_name__iexact=artisan_index_name,
|
||||||
|
establishment_type__index_name__iexact='artisan')
|
||||||
|
if artisan_qs.exists():
|
||||||
|
artisan = artisan_qs.first()
|
||||||
|
image, created = Image.objects.get_or_create(image=attachment_suffix_url,
|
||||||
|
defaults={
|
||||||
|
'image': attachment_suffix_url,
|
||||||
|
'orientation': Image.HORIZONTAL,
|
||||||
|
'title': f'{artisan.__str__()} '
|
||||||
|
f'{artisan.id} - '
|
||||||
|
f'{attachment_suffix_url}'})
|
||||||
|
if created:
|
||||||
|
# update artisan instance
|
||||||
|
artisan.default_image = image
|
||||||
|
to_update.append(artisan)
|
||||||
|
else:
|
||||||
|
not_updated += 1
|
||||||
|
|
||||||
|
EstablishmentSubType.objects.bulk_update(to_update, ['default_image', ])
|
||||||
|
self.stdout.write(self.style.WARNING(f'Updated {len(to_update)} objects.'))
|
||||||
|
self.stdout.write(self.style.WARNING(f'Not updated {not_updated} objects.'))
|
||||||
18
apps/establishment/migrations/0068_auto_20191220_0914.py
Normal file
18
apps/establishment/migrations/0068_auto_20191220_0914.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-20 09:14
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('establishment', '0067_auto_20191122_1244'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='establishment',
|
||||||
|
name='schedule',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='schedule', to='timetable.Timetable', verbose_name='Establishment schedule'),
|
||||||
|
),
|
||||||
|
]
|
||||||
25
apps/establishment/migrations/0069_auto_20191220_1007.py
Normal file
25
apps/establishment/migrations/0069_auto_20191220_1007.py
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-20 10:07
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('gallery', '0007_auto_20191211_1528'),
|
||||||
|
('establishment', '0068_auto_20191220_0914'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='establishmentsubtype',
|
||||||
|
name='default_image',
|
||||||
|
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='establishment_sub_types', to='gallery.Image', verbose_name='default image'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='establishmenttype',
|
||||||
|
name='default_image',
|
||||||
|
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='establishment_types', to='gallery.Image', verbose_name='default image'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -27,13 +27,13 @@ from main.models import Award, Currency
|
||||||
from review.models import Review
|
from review.models import Review
|
||||||
from tag.models import Tag
|
from tag.models import Tag
|
||||||
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
|
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
|
||||||
TranslatedFieldsMixin, BaseAttributes, GalleryModelMixin,
|
TranslatedFieldsMixin, BaseAttributes, GalleryMixin,
|
||||||
IntermediateGalleryModelMixin, HasTagsMixin,
|
IntermediateGalleryModelMixin, HasTagsMixin,
|
||||||
FavoritesMixin)
|
FavoritesMixin, TypeDefaultImageMixin)
|
||||||
|
|
||||||
|
|
||||||
# todo: establishment type&subtypes check
|
# todo: establishment type&subtypes check
|
||||||
class EstablishmentType(TranslatedFieldsMixin, ProjectBaseMixin):
|
class EstablishmentType(TypeDefaultImageMixin, TranslatedFieldsMixin, ProjectBaseMixin):
|
||||||
"""Establishment type model."""
|
"""Establishment type model."""
|
||||||
|
|
||||||
STR_FIELD_NAME = 'name'
|
STR_FIELD_NAME = 'name'
|
||||||
|
|
@ -51,6 +51,10 @@ class EstablishmentType(TranslatedFieldsMixin, ProjectBaseMixin):
|
||||||
tag_categories = models.ManyToManyField('tag.TagCategory',
|
tag_categories = models.ManyToManyField('tag.TagCategory',
|
||||||
related_name='establishment_types',
|
related_name='establishment_types',
|
||||||
verbose_name=_('Tag'))
|
verbose_name=_('Tag'))
|
||||||
|
default_image = models.ForeignKey('gallery.Image', on_delete=models.SET_NULL,
|
||||||
|
related_name='establishment_types',
|
||||||
|
blank=True, null=True, default=None,
|
||||||
|
verbose_name='default image')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
@ -69,7 +73,7 @@ class EstablishmentSubTypeManager(models.Manager):
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
class EstablishmentSubType(ProjectBaseMixin, TranslatedFieldsMixin):
|
class EstablishmentSubType(TypeDefaultImageMixin, TranslatedFieldsMixin, ProjectBaseMixin):
|
||||||
"""Establishment type model."""
|
"""Establishment type model."""
|
||||||
|
|
||||||
# EXAMPLE OF INDEX NAME CHOICES
|
# EXAMPLE OF INDEX NAME CHOICES
|
||||||
|
|
@ -85,6 +89,10 @@ class EstablishmentSubType(ProjectBaseMixin, TranslatedFieldsMixin):
|
||||||
tag_categories = models.ManyToManyField('tag.TagCategory',
|
tag_categories = models.ManyToManyField('tag.TagCategory',
|
||||||
related_name='establishment_subtypes',
|
related_name='establishment_subtypes',
|
||||||
verbose_name=_('Tag'))
|
verbose_name=_('Tag'))
|
||||||
|
default_image = models.ForeignKey('gallery.Image', on_delete=models.SET_NULL,
|
||||||
|
related_name='establishment_sub_types',
|
||||||
|
blank=True, null=True, default=None,
|
||||||
|
verbose_name='default image')
|
||||||
|
|
||||||
objects = EstablishmentSubTypeManager()
|
objects = EstablishmentSubTypeManager()
|
||||||
|
|
||||||
|
|
@ -105,7 +113,7 @@ class EstablishmentQuerySet(models.QuerySet):
|
||||||
def with_base_related(self):
|
def with_base_related(self):
|
||||||
"""Return qs with related objects."""
|
"""Return qs with related objects."""
|
||||||
return self.select_related('address', 'establishment_type'). \
|
return self.select_related('address', 'establishment_type'). \
|
||||||
prefetch_related('tags')
|
prefetch_related('tags', 'tags__translation')
|
||||||
|
|
||||||
def with_schedule(self):
|
def with_schedule(self):
|
||||||
"""Return qs with related schedule."""
|
"""Return qs with related schedule."""
|
||||||
|
|
@ -221,15 +229,16 @@ class EstablishmentQuerySet(models.QuerySet):
|
||||||
Return filtered QuerySet by base filters.
|
Return filtered QuerySet by base filters.
|
||||||
Filters including:
|
Filters including:
|
||||||
1 Filter by type (and subtype) establishment.
|
1 Filter by type (and subtype) establishment.
|
||||||
2 Filter by published Review.
|
2 With annotated distance.
|
||||||
3 With annotated distance.
|
3 By country
|
||||||
"""
|
"""
|
||||||
filters = {
|
filters = {
|
||||||
'reviews__status': Review.READY,
|
|
||||||
'establishment_type': establishment.establishment_type,
|
'establishment_type': establishment.establishment_type,
|
||||||
|
'address__city__country': establishment.address.city.country
|
||||||
}
|
}
|
||||||
if establishment.establishment_subtypes.exists():
|
if establishment.establishment_subtypes.exists():
|
||||||
filters.update({'establishment_subtypes__in': establishment.establishment_subtypes.all()})
|
filters.update({'establishment_subtypes__in': establishment.establishment_subtypes.all()})
|
||||||
|
|
||||||
return self.exclude(id=establishment.id) \
|
return self.exclude(id=establishment.id) \
|
||||||
.filter(**filters) \
|
.filter(**filters) \
|
||||||
.annotate_distance(point=establishment.location)
|
.annotate_distance(point=establishment.location)
|
||||||
|
|
@ -244,32 +253,31 @@ class EstablishmentQuerySet(models.QuerySet):
|
||||||
return Subquery(
|
return Subquery(
|
||||||
self.similar_base(establishment)
|
self.similar_base(establishment)
|
||||||
.filter(**filters)
|
.filter(**filters)
|
||||||
.order_by('distance')[:settings.LIMITING_QUERY_OBJECTS]
|
.order_by('distance')
|
||||||
.values('id')
|
.distinct()
|
||||||
|
.values_list('id', flat=True)[:settings.LIMITING_QUERY_OBJECTS]
|
||||||
)
|
)
|
||||||
|
|
||||||
def similar_restaurants(self, slug):
|
def similar_restaurants(self, restaurant):
|
||||||
"""
|
"""
|
||||||
Return QuerySet with objects that similar to Restaurant.
|
Return QuerySet with objects that similar to Restaurant.
|
||||||
:param slug: str restaurant slug
|
:param restaurant: Establishment instance.
|
||||||
"""
|
"""
|
||||||
restaurant_qs = self.filter(slug=slug)
|
|
||||||
if restaurant_qs.exists():
|
|
||||||
restaurant = restaurant_qs.first()
|
|
||||||
ids_by_subquery = self.similar_base_subquery(
|
ids_by_subquery = self.similar_base_subquery(
|
||||||
establishment=restaurant,
|
establishment=restaurant,
|
||||||
filters={
|
filters={
|
||||||
|
'reviews__status': Review.READY,
|
||||||
'public_mark__gte': 10,
|
'public_mark__gte': 10,
|
||||||
'establishment_gallery__is_main': True,
|
'establishment_gallery__is_main': True,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return self.filter(id__in=ids_by_subquery) \
|
# todo: fix this - replace ids_by_subquery.queryset on ids_by_subquery
|
||||||
|
return self.filter(id__in=ids_by_subquery.queryset) \
|
||||||
.annotate_intermediate_public_mark() \
|
.annotate_intermediate_public_mark() \
|
||||||
.annotate_mark_similarity(mark=restaurant.public_mark) \
|
.annotate_mark_similarity(mark=restaurant.public_mark) \
|
||||||
.order_by('mark_similarity') \
|
.order_by('mark_similarity') \
|
||||||
.distinct('mark_similarity', 'id')
|
.distinct('mark_similarity', 'id')
|
||||||
else:
|
|
||||||
return self.none()
|
|
||||||
|
|
||||||
def same_subtype(self, establishment):
|
def same_subtype(self, establishment):
|
||||||
"""Annotate flag same subtype."""
|
"""Annotate flag same subtype."""
|
||||||
|
|
@ -282,21 +290,17 @@ class EstablishmentQuerySet(models.QuerySet):
|
||||||
output_field=models.BooleanField(default=False)
|
output_field=models.BooleanField(default=False)
|
||||||
))
|
))
|
||||||
|
|
||||||
def similar_artisans_producers(self, slug):
|
def similar_artisans_producers(self, establishment):
|
||||||
"""
|
"""
|
||||||
Return QuerySet with objects that similar to Artisan/Producer(s).
|
Return QuerySet with objects that similar to Artisan/Producer(s).
|
||||||
:param slug: str artisan/producer slug
|
:param establishment: Establishment instance
|
||||||
"""
|
"""
|
||||||
establishment_qs = self.filter(slug=slug)
|
|
||||||
if establishment_qs.exists():
|
|
||||||
establishment = establishment_qs.first()
|
|
||||||
return self.similar_base(establishment) \
|
return self.similar_base(establishment) \
|
||||||
.same_subtype(establishment) \
|
.same_subtype(establishment) \
|
||||||
|
.has_published_reviews() \
|
||||||
.order_by(F('same_subtype').desc(),
|
.order_by(F('same_subtype').desc(),
|
||||||
F('distance').asc()) \
|
F('distance').asc()) \
|
||||||
.distinct('same_subtype', 'distance', 'id')
|
.distinct('same_subtype', 'distance', 'id')
|
||||||
else:
|
|
||||||
return self.none()
|
|
||||||
|
|
||||||
def by_wine_region(self, wine_region):
|
def by_wine_region(self, wine_region):
|
||||||
"""
|
"""
|
||||||
|
|
@ -312,23 +316,19 @@ class EstablishmentQuerySet(models.QuerySet):
|
||||||
"""
|
"""
|
||||||
return self.filter(wine_origin__wine_sub_region=wine_sub_region).distinct()
|
return self.filter(wine_origin__wine_sub_region=wine_sub_region).distinct()
|
||||||
|
|
||||||
def similar_wineries(self, slug: str):
|
def similar_wineries(self, winery):
|
||||||
"""
|
"""
|
||||||
Return QuerySet with objects that similar to Winery.
|
Return QuerySet with objects that similar to Winery.
|
||||||
:param establishment_slug: str Establishment slug
|
:param winery: Establishment instance
|
||||||
"""
|
"""
|
||||||
winery_qs = self.filter(slug=slug)
|
|
||||||
if winery_qs.exists():
|
|
||||||
winery = winery_qs.first()
|
|
||||||
return self.similar_base(winery) \
|
return self.similar_base(winery) \
|
||||||
.order_by(F('wine_origins__wine_region').asc(),
|
.order_by(F('wine_origins__wine_region').asc(),
|
||||||
F('wine_origins__wine_sub_region').asc()) \
|
F('wine_origins__wine_sub_region').asc(),
|
||||||
.annotate_distance(point=winery.location) \
|
F('distance').asc()) \
|
||||||
.order_by('distance') \
|
.distinct('wine_origins__wine_region',
|
||||||
.distinct('distance', 'wine_origins__wine_region',
|
'wine_origins__wine_sub_region',
|
||||||
'wine_origins__wine_sub_region', 'id')
|
'distance',
|
||||||
else:
|
'id')
|
||||||
return self.none()
|
|
||||||
|
|
||||||
def last_reviewed(self, point: Point):
|
def last_reviewed(self, point: Point):
|
||||||
"""
|
"""
|
||||||
|
|
@ -433,7 +433,7 @@ class EstablishmentQuerySet(models.QuerySet):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin,
|
class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin,
|
||||||
TranslatedFieldsMixin, HasTagsMixin, FavoritesMixin):
|
TranslatedFieldsMixin, HasTagsMixin, FavoritesMixin):
|
||||||
"""Establishment model."""
|
"""Establishment model."""
|
||||||
|
|
||||||
|
|
@ -483,7 +483,7 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin,
|
||||||
booking = models.URLField(blank=True, null=True, default=None, max_length=255,
|
booking = models.URLField(blank=True, null=True, default=None, max_length=255,
|
||||||
verbose_name=_('Booking URL'))
|
verbose_name=_('Booking URL'))
|
||||||
is_publish = models.BooleanField(default=False, verbose_name=_('Publish status'))
|
is_publish = models.BooleanField(default=False, verbose_name=_('Publish status'))
|
||||||
schedule = models.ManyToManyField(to='timetable.Timetable',
|
schedule = models.ManyToManyField(to='timetable.Timetable', blank=True,
|
||||||
verbose_name=_('Establishment schedule'),
|
verbose_name=_('Establishment schedule'),
|
||||||
related_name='schedule')
|
related_name='schedule')
|
||||||
# holidays_from = models.DateTimeField(verbose_name=_('Holidays from'),
|
# holidays_from = models.DateTimeField(verbose_name=_('Holidays from'),
|
||||||
|
|
@ -532,12 +532,6 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin,
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'id:{self.id}-{self.name}'
|
return f'id:{self.id}-{self.name}'
|
||||||
|
|
||||||
def clean_fields(self, exclude=None):
|
|
||||||
super().clean_fields(exclude)
|
|
||||||
if self.purchased_products.filter(product_type__index_name='souvenir').exists():
|
|
||||||
raise ValidationError(
|
|
||||||
_('Only souvenirs.'))
|
|
||||||
|
|
||||||
def delete(self, using=None, keep_parents=False):
|
def delete(self, using=None, keep_parents=False):
|
||||||
"""Overridden delete method"""
|
"""Overridden delete method"""
|
||||||
# Delete all related companies
|
# Delete all related companies
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,8 @@ class MenuRUDSerializers(ProjectModelSerializer):
|
||||||
class EstablishmentTypeBaseSerializer(serializers.ModelSerializer):
|
class EstablishmentTypeBaseSerializer(serializers.ModelSerializer):
|
||||||
"""Serializer for EstablishmentType model."""
|
"""Serializer for EstablishmentType model."""
|
||||||
name_translated = TranslatedField()
|
name_translated = TranslatedField()
|
||||||
|
default_image_url = serializers.ImageField(source='default_image.image',
|
||||||
|
allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
@ -107,6 +109,7 @@ class EstablishmentTypeBaseSerializer(serializers.ModelSerializer):
|
||||||
'name_translated',
|
'name_translated',
|
||||||
'use_subtypes',
|
'use_subtypes',
|
||||||
'index_name',
|
'index_name',
|
||||||
|
'default_image_url',
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'name': {'write_only': True},
|
'name': {'write_only': True},
|
||||||
|
|
@ -129,8 +132,9 @@ class EstablishmentTypeGeoSerializer(EstablishmentTypeBaseSerializer):
|
||||||
|
|
||||||
class EstablishmentSubTypeBaseSerializer(serializers.ModelSerializer):
|
class EstablishmentSubTypeBaseSerializer(serializers.ModelSerializer):
|
||||||
"""Serializer for EstablishmentSubType models."""
|
"""Serializer for EstablishmentSubType models."""
|
||||||
|
|
||||||
name_translated = TranslatedField()
|
name_translated = TranslatedField()
|
||||||
|
default_image_url = serializers.ImageField(source='default_image.image',
|
||||||
|
allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
@ -141,6 +145,7 @@ class EstablishmentSubTypeBaseSerializer(serializers.ModelSerializer):
|
||||||
'name_translated',
|
'name_translated',
|
||||||
'establishment_type',
|
'establishment_type',
|
||||||
'index_name',
|
'index_name',
|
||||||
|
'default_image_url',
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'name': {'write_only': True},
|
'name': {'write_only': True},
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,20 @@ class EstablishmentListView(EstablishmentMixinView, generics.ListAPIView):
|
||||||
class EstablishmentSimilarView(EstablishmentListView):
|
class EstablishmentSimilarView(EstablishmentListView):
|
||||||
"""Resource for getting a list of similar establishments."""
|
"""Resource for getting a list of similar establishments."""
|
||||||
serializer_class = serializers.EstablishmentSimilarSerializer
|
serializer_class = serializers.EstablishmentSimilarSerializer
|
||||||
pagination_class = PortionPagination
|
pagination_class = None
|
||||||
|
|
||||||
|
def get_base_object(self):
|
||||||
|
"""
|
||||||
|
Return base establishment instance for a getting list of similar establishments.
|
||||||
|
"""
|
||||||
|
establishment = get_object_or_404(models.Establishment.objects.all(),
|
||||||
|
slug=self.kwargs.get('slug'))
|
||||||
|
return establishment
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""Overridden get_queryset method."""
|
||||||
|
return EstablishmentMixinView.get_queryset(self) \
|
||||||
|
.has_location()
|
||||||
|
|
||||||
|
|
||||||
class EstablishmentRetrieveView(EstablishmentMixinView, generics.RetrieveAPIView):
|
class EstablishmentRetrieveView(EstablishmentMixinView, generics.RetrieveAPIView):
|
||||||
|
|
@ -88,9 +101,14 @@ class RestaurantSimilarListView(EstablishmentSimilarView):
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Overridden get_queryset method"""
|
"""Overridden get_queryset method"""
|
||||||
|
qs = super(RestaurantSimilarListView, self).get_queryset()
|
||||||
|
base_establishment = self.get_base_object()
|
||||||
|
|
||||||
|
if base_establishment:
|
||||||
|
return qs.similar_restaurants(base_establishment)[:settings.QUERY_OUTPUT_OBJECTS]
|
||||||
|
else:
|
||||||
return EstablishmentMixinView.get_queryset(self) \
|
return EstablishmentMixinView.get_queryset(self) \
|
||||||
.has_location() \
|
.none()
|
||||||
.similar_restaurants(slug=self.kwargs.get('slug'))
|
|
||||||
|
|
||||||
|
|
||||||
class WinerySimilarListView(EstablishmentSimilarView):
|
class WinerySimilarListView(EstablishmentSimilarView):
|
||||||
|
|
@ -98,9 +116,13 @@ class WinerySimilarListView(EstablishmentSimilarView):
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Overridden get_queryset method"""
|
"""Overridden get_queryset method"""
|
||||||
return EstablishmentMixinView.get_queryset(self) \
|
qs = EstablishmentSimilarView.get_queryset(self)
|
||||||
.has_location() \
|
base_establishment = self.get_base_object()
|
||||||
.similar_wineries(slug=self.kwargs.get('slug'))
|
|
||||||
|
if base_establishment:
|
||||||
|
return qs.similar_wineries(base_establishment)[:settings.QUERY_OUTPUT_OBJECTS]
|
||||||
|
else:
|
||||||
|
return qs.none()
|
||||||
|
|
||||||
|
|
||||||
class ArtisanProducerSimilarListView(EstablishmentSimilarView):
|
class ArtisanProducerSimilarListView(EstablishmentSimilarView):
|
||||||
|
|
@ -108,9 +130,13 @@ class ArtisanProducerSimilarListView(EstablishmentSimilarView):
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Overridden get_queryset method"""
|
"""Overridden get_queryset method"""
|
||||||
return EstablishmentMixinView.get_queryset(self) \
|
qs = super(ArtisanProducerSimilarListView, self).get_queryset()
|
||||||
.has_location() \
|
base_establishment = self.get_base_object()
|
||||||
.similar_artisans_producers(slug=self.kwargs.get('slug'))
|
|
||||||
|
if base_establishment:
|
||||||
|
return qs.similar_artisans_producers(base_establishment)[:settings.QUERY_OUTPUT_OBJECTS]
|
||||||
|
else:
|
||||||
|
return qs.none()
|
||||||
|
|
||||||
|
|
||||||
class EstablishmentTypeListView(generics.ListAPIView):
|
class EstablishmentTypeListView(generics.ListAPIView):
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,6 @@ class BaseTestCase(APITestCase):
|
||||||
title={"en-GB": "Test news"},
|
title={"en-GB": "Test news"},
|
||||||
news_type=self.test_news_type,
|
news_type=self.test_news_type,
|
||||||
description={"en-GB": "Description test news"},
|
description={"en-GB": "Description test news"},
|
||||||
start=datetime.fromisoformat("2020-12-03 12:00:00"),
|
|
||||||
end=datetime.fromisoformat("2020-12-03 12:00:00"),
|
end=datetime.fromisoformat("2020-12-03 12:00:00"),
|
||||||
state=News.PUBLISHED,
|
state=News.PUBLISHED,
|
||||||
slugs={'en-GB': 'test-news'}
|
slugs={'en-GB': 'test-news'}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from sorl.thumbnail import delete
|
from sorl import thumbnail
|
||||||
from sorl.thumbnail.fields import ImageField as SORLImageField
|
from sorl.thumbnail.fields import ImageField as SORLImageField
|
||||||
|
|
||||||
from utils.methods import image_path
|
from utils.methods import image_path
|
||||||
|
|
@ -47,7 +47,7 @@ class Image(ProjectBaseMixin, SORLImageMixin, PlatformMixin):
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Delete from remote storage
|
# Delete from remote storage
|
||||||
delete(file_=self.image.file, delete_file=completely)
|
thumbnail.delete(file_=self.image.file, delete_file=completely)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
from django.conf import settings
|
|
||||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from sorl.thumbnail import get_thumbnail
|
from sorl.thumbnail import get_thumbnail
|
||||||
from sorl.thumbnail.parsers import parse_crop, ThumbnailParseError
|
from sorl.thumbnail.parsers import parse_crop, ThumbnailParseError
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.conf import settings
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -36,6 +35,15 @@ class ImageSerializer(serializers.ModelSerializer):
|
||||||
'orientation': {'write_only': True}
|
'orientation': {'write_only': True}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
"""Overridden validate method."""
|
||||||
|
image = attrs.get('image')
|
||||||
|
|
||||||
|
if image and image.size >= settings.FILE_UPLOAD_MAX_MEMORY_SIZE:
|
||||||
|
raise serializers.ValidationError({'detail': _('File size too large: %s bytes') % image.size})
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
class CropImageSerializer(ImageSerializer):
|
class CropImageSerializer(ImageSerializer):
|
||||||
"""Serializers for image crops."""
|
"""Serializers for image crops."""
|
||||||
|
|
|
||||||
18
apps/location/migrations/0032_auto_20191220_1019.py
Normal file
18
apps/location/migrations/0032_auto_20191220_1019.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-20 10:19
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('location', '0031_establishmentwineoriginaddress_wineoriginaddress'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='address',
|
||||||
|
name='number',
|
||||||
|
field=models.IntegerField(blank=True, default=0, verbose_name='number'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -14,8 +14,7 @@ from django.contrib.postgres.fields import ArrayField
|
||||||
from translation.models import Language
|
from translation.models import Language
|
||||||
from utils.models import (ProjectBaseMixin, SVGImageMixin, TJSONField,
|
from utils.models import (ProjectBaseMixin, SVGImageMixin, TJSONField,
|
||||||
TranslatedFieldsMixin, get_current_locale,
|
TranslatedFieldsMixin, get_current_locale,
|
||||||
IntermediateGalleryModelMixin, GalleryModelMixin,
|
IntermediateGalleryModelMixin, GalleryMixin)
|
||||||
RelatedInstanceMixin)
|
|
||||||
|
|
||||||
|
|
||||||
class CountryQuerySet(models.QuerySet):
|
class CountryQuerySet(models.QuerySet):
|
||||||
|
|
@ -25,8 +24,7 @@ class CountryQuerySet(models.QuerySet):
|
||||||
return self.filter(is_active=switcher)
|
return self.filter(is_active=switcher)
|
||||||
|
|
||||||
|
|
||||||
class Country(RelatedInstanceMixin, TranslatedFieldsMixin,
|
class Country(TranslatedFieldsMixin, SVGImageMixin, ProjectBaseMixin):
|
||||||
SVGImageMixin, ProjectBaseMixin):
|
|
||||||
"""Country model."""
|
"""Country model."""
|
||||||
|
|
||||||
STR_FIELD_NAME = 'name'
|
STR_FIELD_NAME = 'name'
|
||||||
|
|
@ -142,12 +140,9 @@ class CityQuerySet(models.QuerySet):
|
||||||
return self.filter(country__code=code)
|
return self.filter(country__code=code)
|
||||||
|
|
||||||
|
|
||||||
class City(RelatedInstanceMixin, GalleryModelMixin):
|
class City(GalleryMixin, models.Model):
|
||||||
"""Region model."""
|
"""Region model."""
|
||||||
name = models.CharField(_('name'), max_length=250)
|
name = models.CharField(_('name'), max_length=250)
|
||||||
name_translated = TJSONField(blank=True, null=True, default=None,
|
|
||||||
verbose_name=_('Translated name'),
|
|
||||||
help_text='{"en-GB":"some text"}')
|
|
||||||
code = models.CharField(_('code'), max_length=250)
|
code = models.CharField(_('code'), max_length=250)
|
||||||
region = models.ForeignKey(Region, on_delete=models.CASCADE,
|
region = models.ForeignKey(Region, on_delete=models.CASCADE,
|
||||||
blank=True, null=True,
|
blank=True, null=True,
|
||||||
|
|
@ -205,7 +200,7 @@ class Address(models.Model):
|
||||||
_('street name 1'), max_length=500, blank=True, default='')
|
_('street name 1'), max_length=500, blank=True, default='')
|
||||||
street_name_2 = models.CharField(
|
street_name_2 = models.CharField(
|
||||||
_('street name 2'), max_length=500, blank=True, default='')
|
_('street name 2'), max_length=500, blank=True, default='')
|
||||||
number = models.IntegerField(_('number'))
|
number = models.IntegerField(_('number'), blank=True, default=0)
|
||||||
postal_code = models.CharField(
|
postal_code = models.CharField(
|
||||||
_('postal code'), max_length=10, blank=True,
|
_('postal code'), max_length=10, blank=True,
|
||||||
default='', help_text=_('Ex.: 350018'))
|
default='', help_text=_('Ex.: 350018'))
|
||||||
|
|
|
||||||
|
|
@ -56,10 +56,17 @@ class PageAdmin(admin.ModelAdmin):
|
||||||
date_hierarchy = 'created'
|
date_hierarchy = 'created'
|
||||||
|
|
||||||
|
|
||||||
|
class FooterLinkInline(admin.TabularInline):
|
||||||
|
model = models.Footer.links.through
|
||||||
|
extra = 1
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.Footer)
|
@admin.register(models.Footer)
|
||||||
class FooterAdmin(admin.ModelAdmin):
|
class FooterAdmin(admin.ModelAdmin):
|
||||||
"""Footer admin."""
|
"""Footer admin."""
|
||||||
list_display = ('id', 'site', )
|
list_display = ('id', 'site',)
|
||||||
|
exclude = ('links',)
|
||||||
|
inlines = [FooterLinkInline, ]
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.FooterLink)
|
@admin.register(models.FooterLink)
|
||||||
|
|
@ -70,7 +77,6 @@ class FooterLinkAdmin(admin.ModelAdmin):
|
||||||
@admin.register(models.Panel)
|
@admin.register(models.Panel)
|
||||||
class PanelAdmin(admin.ModelAdmin):
|
class PanelAdmin(admin.ModelAdmin):
|
||||||
"""Panel admin."""
|
"""Panel admin."""
|
||||||
list_display = ('id', 'name', 'user', 'created', )
|
list_display = ('id', 'name', 'user', 'created',)
|
||||||
raw_id_fields = ('user', )
|
raw_id_fields = ('user',)
|
||||||
list_display_links = ('id', 'name', )
|
list_display_links = ('id', 'name',)
|
||||||
|
|
||||||
|
|
|
||||||
18
apps/main/migrations/0045_carousel_is_international.py
Normal file
18
apps/main/migrations/0045_carousel_is_international.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-23 14:34
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('main', '0044_auto_20191217_1125'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='carousel',
|
||||||
|
name='is_international',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='is international'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -214,6 +214,9 @@ class CarouselQuerySet(models.QuerySet):
|
||||||
"""Filter collection by country code."""
|
"""Filter collection by country code."""
|
||||||
return self.filter(country__code=code)
|
return self.filter(country__code=code)
|
||||||
|
|
||||||
|
def get_international(self):
|
||||||
|
return self.filter(is_international=True)
|
||||||
|
|
||||||
|
|
||||||
class Carousel(models.Model):
|
class Carousel(models.Model):
|
||||||
"""Carousel model."""
|
"""Carousel model."""
|
||||||
|
|
@ -221,6 +224,7 @@ class Carousel(models.Model):
|
||||||
object_id = models.PositiveIntegerField(blank=True, null=True, default=None)
|
object_id = models.PositiveIntegerField(blank=True, null=True, default=None)
|
||||||
content_object = generic.GenericForeignKey('content_type', 'object_id')
|
content_object = generic.GenericForeignKey('content_type', 'object_id')
|
||||||
|
|
||||||
|
is_international = models.BooleanField(default=False, verbose_name=_('is international'))
|
||||||
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
|
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
|
||||||
title = models.CharField(_('old title'), max_length=255, blank=True, null=True, default=None)
|
title = models.CharField(_('old title'), max_length=255, blank=True, null=True, default=None)
|
||||||
link = models.CharField(_('old link'), max_length=255, blank=True, null=True, default=None)
|
link = models.CharField(_('old link'), max_length=255, blank=True, null=True, default=None)
|
||||||
|
|
@ -481,6 +485,19 @@ class Panel(ProjectBaseMixin):
|
||||||
columns = [col[0] for col in cursor.description]
|
columns = [col[0] for col in cursor.description]
|
||||||
return columns
|
return columns
|
||||||
|
|
||||||
|
def get_headers(self):
|
||||||
|
with connections['default'].cursor() as cursor:
|
||||||
|
try:
|
||||||
|
cursor.execute(self.query)
|
||||||
|
except Exception as er:
|
||||||
|
raise UnprocessableEntityError()
|
||||||
|
return self._raw_columns(cursor)
|
||||||
|
|
||||||
|
def get_data(self):
|
||||||
|
with connections['default'].cursor() as cursor:
|
||||||
|
cursor.execute(self.query)
|
||||||
|
return cursor.fetchall()
|
||||||
|
|
||||||
def _raw_page(self, raw, request):
|
def _raw_page(self, raw, request):
|
||||||
page = request.query_params.get('page', 0)
|
page = request.query_params.get('page', 0)
|
||||||
page_size = request.query_params.get('page_size', 0)
|
page_size = request.query_params.get('page_size', 0)
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,17 @@ class CurrencySerializer(ProjectModelSerializer):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class _FooterLinkSerializer(serializers.ModelSerializer):
|
||||||
|
"""FooterLink serializer"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.FooterLink
|
||||||
|
fields = [
|
||||||
|
'title',
|
||||||
|
'link',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class FooterSerializer(serializers.ModelSerializer):
|
class FooterSerializer(serializers.ModelSerializer):
|
||||||
"""Footer serializer."""
|
"""Footer serializer."""
|
||||||
|
|
||||||
|
|
@ -50,9 +61,15 @@ class FooterSerializer(serializers.ModelSerializer):
|
||||||
'copyright',
|
'copyright',
|
||||||
'created',
|
'created',
|
||||||
'modified',
|
'modified',
|
||||||
|
'links',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class _FooterSerializer(FooterSerializer):
|
||||||
|
"""Footer serializer."""
|
||||||
|
links = _FooterLinkSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
|
|
||||||
class FooterBackSerializer(FooterSerializer):
|
class FooterBackSerializer(FooterSerializer):
|
||||||
site_id = serializers.PrimaryKeyRelatedField(
|
site_id = serializers.PrimaryKeyRelatedField(
|
||||||
queryset=models.SiteSettings.objects.all(),
|
queryset=models.SiteSettings.objects.all(),
|
||||||
|
|
@ -98,7 +115,7 @@ class SiteSettingsSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
country_name = serializers.CharField(source='country.name_translated', read_only=True)
|
country_name = serializers.CharField(source='country.name_translated', read_only=True)
|
||||||
time_format = serializers.CharField(source='country.time_format', read_only=True)
|
time_format = serializers.CharField(source='country.time_format', read_only=True)
|
||||||
footers = FooterSerializer(many=True, read_only=True)
|
footers = _FooterSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
@ -300,6 +317,7 @@ class PanelSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class PanelExecuteSerializer(serializers.ModelSerializer):
|
class PanelExecuteSerializer(serializers.ModelSerializer):
|
||||||
"""Panel execute serializer."""
|
"""Panel execute serializer."""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Panel
|
model = models.Panel
|
||||||
fields = [
|
fields = [
|
||||||
|
|
|
||||||
14
apps/main/tasks.py
Normal file
14
apps/main/tasks.py
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
"""Task methods for main app."""
|
||||||
|
|
||||||
|
from celery import shared_task
|
||||||
|
|
||||||
|
from account.models import User
|
||||||
|
from main.models import Panel
|
||||||
|
from utils.export import SendExport
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def send_export_to_email(panel_id, user_id, file_type='csv'):
|
||||||
|
panel = Panel.objects.get(id=panel_id)
|
||||||
|
user = User.objects.get(id=user_id)
|
||||||
|
SendExport(user, panel, file_type).send()
|
||||||
|
|
@ -24,8 +24,9 @@ urlpatterns = [
|
||||||
name='page-types-list-create'),
|
name='page-types-list-create'),
|
||||||
path('panels/', views.PanelsListCreateView.as_view(), name='panels'),
|
path('panels/', views.PanelsListCreateView.as_view(), name='panels'),
|
||||||
path('panels/<int:pk>/', views.PanelsRUDView.as_view(), name='panels-rud'),
|
path('panels/<int:pk>/', views.PanelsRUDView.as_view(), name='panels-rud'),
|
||||||
path('panels/<int:pk>/execute/', views.PanelsExecuteView.as_view(), name='panels-execute')
|
path('panels/<int:pk>/execute/', views.PanelsExecuteView.as_view(), name='panels-execute'),
|
||||||
|
path('panels/<int:pk>/csv/', views.PanelsExportCSVView.as_view(), name='panels-csv'),
|
||||||
|
path('panels/<int:pk>/xls/', views.PanelsExecuteXLSView.as_view(), name='panels-xls')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from rest_framework import generics, permissions
|
from rest_framework import generics, permissions, status
|
||||||
from rest_framework.generics import get_object_or_404
|
from rest_framework.generics import get_object_or_404
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from main import serializers
|
from main import serializers
|
||||||
|
from main import tasks
|
||||||
from main.filters import AwardFilter
|
from main.filters import AwardFilter
|
||||||
from main.models import Award, Footer, PageType, Panel
|
from main.models import Award, Footer, PageType, Panel, SiteFeature, Feature
|
||||||
from main.views import SiteSettingsView, SiteListView
|
from main.views import SiteSettingsView, SiteListView
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -44,21 +46,29 @@ class ContentTypeView(generics.ListAPIView):
|
||||||
class FeatureBackView(generics.ListCreateAPIView):
|
class FeatureBackView(generics.ListCreateAPIView):
|
||||||
"""Feature list or create View."""
|
"""Feature list or create View."""
|
||||||
serializer_class = serializers.FeatureSerializer
|
serializer_class = serializers.FeatureSerializer
|
||||||
|
queryset = Feature.objects.all()
|
||||||
|
|
||||||
|
|
||||||
class SiteFeatureBackView(generics.ListCreateAPIView):
|
class SiteFeatureBackView(generics.ListCreateAPIView):
|
||||||
"""Feature list or create View."""
|
"""Feature list or create View."""
|
||||||
serializer_class = serializers.SiteFeatureSerializer
|
serializer_class = serializers.SiteFeatureSerializer
|
||||||
|
queryset = SiteFeature.objects.all()
|
||||||
|
pagination_class = None
|
||||||
|
permission_classes = [permissions.IsAdminUser]
|
||||||
|
|
||||||
|
|
||||||
class FeatureRUDBackView(generics.RetrieveUpdateDestroyAPIView):
|
class FeatureRUDBackView(generics.RetrieveUpdateDestroyAPIView):
|
||||||
"""Feature RUD View."""
|
"""Feature RUD View."""
|
||||||
serializer_class = serializers.FeatureSerializer
|
serializer_class = serializers.FeatureSerializer
|
||||||
|
queryset = SiteFeature.objects.all()
|
||||||
|
permission_classes = [permissions.IsAdminUser]
|
||||||
|
|
||||||
|
|
||||||
class SiteFeatureRUDBackView(generics.RetrieveUpdateDestroyAPIView):
|
class SiteFeatureRUDBackView(generics.RetrieveUpdateDestroyAPIView):
|
||||||
"""Feature RUD View."""
|
"""Feature RUD View."""
|
||||||
serializer_class = serializers.SiteFeatureSerializer
|
serializer_class = serializers.SiteFeatureSerializer
|
||||||
|
queryset = SiteFeature.objects.all()
|
||||||
|
permission_classes = [permissions.IsAdminUser]
|
||||||
|
|
||||||
|
|
||||||
class SiteSettingsBackOfficeView(SiteSettingsView):
|
class SiteSettingsBackOfficeView(SiteSettingsView):
|
||||||
|
|
@ -121,3 +131,35 @@ class PanelsExecuteView(generics.ListAPIView):
|
||||||
def list(self, request, *args, **kwargs):
|
def list(self, request, *args, **kwargs):
|
||||||
panel = get_object_or_404(Panel, id=self.kwargs['pk'])
|
panel = get_object_or_404(Panel, id=self.kwargs['pk'])
|
||||||
return Response(panel.execute_query(request))
|
return Response(panel.execute_query(request))
|
||||||
|
|
||||||
|
|
||||||
|
class PanelsExportCSVView(PanelsExecuteView):
|
||||||
|
"""Export panels via csv view."""
|
||||||
|
permission_classes = (permissions.IsAdminUser,)
|
||||||
|
queryset = Panel.objects.all()
|
||||||
|
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
panel = get_object_or_404(Panel, id=self.kwargs['pk'])
|
||||||
|
# make task for celery
|
||||||
|
tasks.send_export_to_email.delay(
|
||||||
|
panel_id=panel.id, user_id=request.user.id)
|
||||||
|
return Response(
|
||||||
|
{"success": _('The file will be sent to your email.')},
|
||||||
|
status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PanelsExecuteXLSView(PanelsExecuteView):
|
||||||
|
"""Export panels via xlsx view."""
|
||||||
|
permission_classes = (permissions.IsAdminUser,)
|
||||||
|
queryset = Panel.objects.all()
|
||||||
|
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
panel = get_object_or_404(Panel, id=self.kwargs['pk'])
|
||||||
|
# make task for celery
|
||||||
|
tasks.send_export_to_email.delay(
|
||||||
|
panel_id=panel.id, user_id=request.user.id, file_type='xls')
|
||||||
|
return Response(
|
||||||
|
{"success": _('The file will be sent to your email.')},
|
||||||
|
status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
from django.core.management.base import BaseCommand
|
|
||||||
from django.db.models import F
|
|
||||||
from tqdm import tqdm
|
|
||||||
|
|
||||||
from account.models import User
|
|
||||||
from news.models import News
|
|
||||||
from transfer.models import PageTexts
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
help = 'Add author of News'
|
|
||||||
|
|
||||||
def handle(self, *args, **kwargs):
|
|
||||||
count = 0
|
|
||||||
news_list = News.objects.filter(created_by__isnull=True)
|
|
||||||
|
|
||||||
for news in tqdm(news_list, desc="Find author for exist news"):
|
|
||||||
old_news = PageTexts.objects.filter(id=news.old_id).annotate(
|
|
||||||
account_id=F('page__account_id'),
|
|
||||||
).first()
|
|
||||||
if old_news:
|
|
||||||
user = User.objects.filter(old_id=old_news.account_id).first()
|
|
||||||
if user:
|
|
||||||
news.created_by = user
|
|
||||||
news.modified_by = user
|
|
||||||
news.save()
|
|
||||||
count += 1
|
|
||||||
|
|
||||||
self.stdout.write(self.style.WARNING(f'Update {count} objects.'))
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
from django.core.management.base import BaseCommand
|
|
||||||
|
|
||||||
from news.models import News, NewsType
|
|
||||||
from tag.models import Tag, TagCategory
|
|
||||||
from transfer.models import PageMetadata, Pages, PageTexts
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
help = 'Remove old news from new bd'
|
|
||||||
|
|
||||||
def handle(self, *args, **kwargs):
|
|
||||||
count = 0
|
|
||||||
news_type, _ = NewsType.objects.get_or_create(name='News')
|
|
||||||
tag_cat, _ = TagCategory.objects.get_or_create(index_name='category')
|
|
||||||
news_type.tag_categories.add(tag_cat)
|
|
||||||
news_type.save()
|
|
||||||
|
|
||||||
old_news_tag = PageMetadata.objects.filter(key='category', page__pagetexts__isnull=False)
|
|
||||||
for old_tag in old_news_tag:
|
|
||||||
old_id_list = old_tag.page.pagetexts_set.all().values_list('id', flat=True)
|
|
||||||
|
|
||||||
# Make Tag
|
|
||||||
new_tag, created = Tag.objects.get_or_create(category=tag_cat, value=old_tag.value)
|
|
||||||
if created:
|
|
||||||
text_value = ' '.join(new_tag.value.split('_'))
|
|
||||||
new_tag.label = {'en-GB': text_value}
|
|
||||||
new_tag.save()
|
|
||||||
for id in old_id_list:
|
|
||||||
if isinstance(id, int):
|
|
||||||
news = News.objects.filter(old_id=id).first()
|
|
||||||
if news:
|
|
||||||
news.tags.add(new_tag)
|
|
||||||
news.save()
|
|
||||||
count += 1
|
|
||||||
|
|
||||||
self.stdout.write(self.style.WARNING(f'Create or update {count} objects.'))
|
|
||||||
19
apps/news/migrations/0049_auto_20191223_0619.py
Normal file
19
apps/news/migrations/0049_auto_20191223_0619.py
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-23 06:19
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('news', '0048_remove_news_must_of_the_week'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='news',
|
||||||
|
name='views_count',
|
||||||
|
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='news', to='rating.ViewCount'),
|
||||||
|
),
|
||||||
|
]
|
||||||
34
apps/news/migrations/0050_auto_20191223_1148.py
Normal file
34
apps/news/migrations/0050_auto_20191223_1148.py
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-23 11:48
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.utils.timezone
|
||||||
|
import utils.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('news', '0049_auto_20191223_0619'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='agenda',
|
||||||
|
name='event_datetime',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='agenda',
|
||||||
|
name='end_datetime',
|
||||||
|
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='End datetime'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='agenda',
|
||||||
|
name='event_name',
|
||||||
|
field=utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='event name'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='agenda',
|
||||||
|
name='start_datetime',
|
||||||
|
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Start datetime'),
|
||||||
|
),
|
||||||
|
]
|
||||||
19
apps/news/migrations/0050_auto_20191223_1238.py
Normal file
19
apps/news/migrations/0050_auto_20191223_1238.py
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-23 12:38
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('news', '0049_auto_20191223_0619'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='news',
|
||||||
|
name='news_type',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='news', to='news.NewsType', verbose_name='news type'),
|
||||||
|
),
|
||||||
|
]
|
||||||
14
apps/news/migrations/0051_merge_20191223_1405.py
Normal file
14
apps/news/migrations/0051_merge_20191223_1405.py
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-23 14:05
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('news', '0050_auto_20191223_1148'),
|
||||||
|
('news', '0050_auto_20191223_1238'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
]
|
||||||
|
|
@ -14,7 +14,7 @@ from rest_framework.reverse import reverse
|
||||||
from main.models import Carousel
|
from main.models import Carousel
|
||||||
from rating.models import Rating, ViewCount
|
from rating.models import Rating, ViewCount
|
||||||
from utils.models import (BaseAttributes, TJSONField, TranslatedFieldsMixin, HasTagsMixin,
|
from utils.models import (BaseAttributes, TJSONField, TranslatedFieldsMixin, HasTagsMixin,
|
||||||
ProjectBaseMixin, GalleryModelMixin, IntermediateGalleryModelMixin,
|
ProjectBaseMixin, GalleryMixin, IntermediateGalleryModelMixin,
|
||||||
FavoritesMixin)
|
FavoritesMixin)
|
||||||
from utils.querysets import TranslationQuerysetMixin
|
from utils.querysets import TranslationQuerysetMixin
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
@ -22,12 +22,16 @@ from datetime import datetime
|
||||||
|
|
||||||
class Agenda(ProjectBaseMixin, TranslatedFieldsMixin):
|
class Agenda(ProjectBaseMixin, TranslatedFieldsMixin):
|
||||||
"""News agenda model"""
|
"""News agenda model"""
|
||||||
|
start_datetime = models.DateTimeField(default=timezone.now, editable=True,
|
||||||
event_datetime = models.DateTimeField(default=timezone.now, editable=False,
|
verbose_name=_('Start datetime'))
|
||||||
verbose_name=_('Event datetime'))
|
end_datetime = models.DateTimeField(default=timezone.now, editable=True,
|
||||||
|
verbose_name=_('End datetime'))
|
||||||
address = models.ForeignKey('location.Address', blank=True, null=True,
|
address = models.ForeignKey('location.Address', blank=True, null=True,
|
||||||
default=None, verbose_name=_('address'),
|
default=None, verbose_name=_('address'),
|
||||||
on_delete=models.SET_NULL)
|
on_delete=models.SET_NULL)
|
||||||
|
event_name = TJSONField(blank=True, null=True, default=None,
|
||||||
|
verbose_name=_('event name'),
|
||||||
|
help_text='{"en-GB":"some text"}')
|
||||||
content = TJSONField(blank=True, null=True, default=None,
|
content = TJSONField(blank=True, null=True, default=None,
|
||||||
verbose_name=_('content'),
|
verbose_name=_('content'),
|
||||||
help_text='{"en-GB":"some text"}')
|
help_text='{"en-GB":"some text"}')
|
||||||
|
|
@ -74,7 +78,7 @@ class NewsQuerySet(TranslationQuerysetMixin):
|
||||||
|
|
||||||
def with_base_related(self):
|
def with_base_related(self):
|
||||||
"""Return qs with related objects."""
|
"""Return qs with related objects."""
|
||||||
return self.select_related('news_type', 'country').prefetch_related('tags')
|
return self.select_related('news_type', 'country').prefetch_related('tags', 'tags__translation')
|
||||||
|
|
||||||
def with_extended_related(self):
|
def with_extended_related(self):
|
||||||
"""Return qs with related objects."""
|
"""Return qs with related objects."""
|
||||||
|
|
@ -140,7 +144,7 @@ class NewsQuerySet(TranslationQuerysetMixin):
|
||||||
return self.filter(title__icontains=locale)
|
return self.filter(title__icontains=locale)
|
||||||
|
|
||||||
|
|
||||||
class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin,
|
class News(GalleryMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin,
|
||||||
FavoritesMixin):
|
FavoritesMixin):
|
||||||
"""News model."""
|
"""News model."""
|
||||||
|
|
||||||
|
|
@ -177,7 +181,7 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
|
||||||
|
|
||||||
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
|
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
|
||||||
news_type = models.ForeignKey(NewsType, on_delete=models.PROTECT,
|
news_type = models.ForeignKey(NewsType, on_delete=models.PROTECT,
|
||||||
verbose_name=_('news type'))
|
verbose_name=_('news type'), related_name='news')
|
||||||
title = TJSONField(blank=True, null=True, default=None,
|
title = TJSONField(blank=True, null=True, default=None,
|
||||||
verbose_name=_('title'),
|
verbose_name=_('title'),
|
||||||
help_text='{"en-GB":"some text"}')
|
help_text='{"en-GB":"some text"}')
|
||||||
|
|
@ -215,7 +219,8 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
|
||||||
tags = models.ManyToManyField('tag.Tag', related_name='news',
|
tags = models.ManyToManyField('tag.Tag', related_name='news',
|
||||||
verbose_name=_('Tags'))
|
verbose_name=_('Tags'))
|
||||||
gallery = models.ManyToManyField('gallery.Image', through='news.NewsGallery')
|
gallery = models.ManyToManyField('gallery.Image', through='news.NewsGallery')
|
||||||
views_count = models.OneToOneField('rating.ViewCount', blank=True, null=True, on_delete=models.SET_NULL)
|
views_count = models.OneToOneField('rating.ViewCount', blank=True, null=True, on_delete=models.SET_NULL,
|
||||||
|
related_name='news')
|
||||||
ratings = generic.GenericRelation(Rating)
|
ratings = generic.GenericRelation(Rating)
|
||||||
favorites = generic.GenericRelation(to='favorites.Favorites')
|
favorites = generic.GenericRelation(to='favorites.Favorites')
|
||||||
carousels = generic.GenericRelation(to='main.Carousel')
|
carousels = generic.GenericRelation(to='main.Carousel')
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,29 @@
|
||||||
"""News app common serializers."""
|
"""News app common serializers."""
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.fields import SerializerMethodField
|
from rest_framework.fields import SerializerMethodField
|
||||||
|
|
||||||
from account.serializers.common import UserBaseSerializer
|
from account.serializers.common import UserBaseSerializer
|
||||||
from gallery.models import Image
|
from gallery.models import Image
|
||||||
from main.models import SiteSettings, Carousel
|
|
||||||
from location import models as location_models
|
from location import models as location_models
|
||||||
from location.serializers import CountrySimpleSerializer, AddressBaseSerializer
|
from location.serializers import AddressBaseSerializer, CountrySimpleSerializer
|
||||||
|
from main.models import SiteSettings
|
||||||
from news import models
|
from news import models
|
||||||
|
from rating import models as rating_models
|
||||||
from tag.serializers import TagBaseSerializer
|
from tag.serializers import TagBaseSerializer
|
||||||
from utils import exceptions as utils_exceptions
|
from utils import exceptions as utils_exceptions
|
||||||
from utils.serializers import (TranslatedField, ProjectModelSerializer,
|
|
||||||
FavoritesCreateSerializer, ImageBaseSerializer, CarouselCreateSerializer)
|
|
||||||
from rating import models as rating_models
|
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
from utils.models import get_current_locale, get_default_locale
|
from utils.models import get_current_locale, get_default_locale
|
||||||
|
from utils.serializers import (
|
||||||
|
CarouselCreateSerializer, FavoritesCreateSerializer, ImageBaseSerializer, ProjectModelSerializer, TranslatedField,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AgendaSerializer(ProjectModelSerializer):
|
class AgendaSerializer(ProjectModelSerializer):
|
||||||
event_datetime = serializers.DateTimeField()
|
start_datetime = serializers.DateTimeField()
|
||||||
|
end_datetime = serializers.DateTimeField()
|
||||||
address = AddressBaseSerializer()
|
address = AddressBaseSerializer()
|
||||||
|
event_name_translated = TranslatedField()
|
||||||
content_translated = TranslatedField()
|
content_translated = TranslatedField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
@ -29,9 +32,11 @@ class AgendaSerializer(ProjectModelSerializer):
|
||||||
model = models.Agenda
|
model = models.Agenda
|
||||||
fields = (
|
fields = (
|
||||||
'id',
|
'id',
|
||||||
'event_datetime',
|
'start_datetime',
|
||||||
|
'end_datetime',
|
||||||
'address',
|
'address',
|
||||||
'content_translated'
|
'content_translated',
|
||||||
|
'event_name_translated'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -125,8 +130,7 @@ class NewsDetailSerializer(NewsBaseSerializer):
|
||||||
description_translated = TranslatedField()
|
description_translated = TranslatedField()
|
||||||
country = CountrySimpleSerializer(read_only=True)
|
country = CountrySimpleSerializer(read_only=True)
|
||||||
author = UserBaseSerializer(source='created_by', read_only=True)
|
author = UserBaseSerializer(source='created_by', read_only=True)
|
||||||
state_display = serializers.CharField(source='get_state_display',
|
state_display = serializers.CharField(source='get_state_display', read_only=True)
|
||||||
read_only=True)
|
|
||||||
gallery = ImageBaseSerializer(read_only=True, source='crop_gallery', many=True)
|
gallery = ImageBaseSerializer(read_only=True, source='crop_gallery', many=True)
|
||||||
start = serializers.DateTimeField(source='publication_datetime', read_only=True)
|
start = serializers.DateTimeField(source='publication_datetime', read_only=True)
|
||||||
|
|
||||||
|
|
@ -189,8 +193,12 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
|
||||||
'must_of_the_week',
|
'must_of_the_week',
|
||||||
'publication_date',
|
'publication_date',
|
||||||
'publication_time',
|
'publication_time',
|
||||||
|
'created',
|
||||||
|
'modified',
|
||||||
)
|
)
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
|
'created': {'read_only': True},
|
||||||
|
'modified': {'read_only': True},
|
||||||
'duplication_date': {'read_only': True},
|
'duplication_date': {'read_only': True},
|
||||||
'locale_to_description_is_active': {'allow_null': False},
|
'locale_to_description_is_active': {'allow_null': False},
|
||||||
'must_of_the_week': {'read_only': True},
|
'must_of_the_week': {'read_only': True},
|
||||||
|
|
@ -198,11 +206,18 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
slugs = validated_data.get('slugs')
|
slugs = validated_data.get('slugs')
|
||||||
|
|
||||||
if slugs:
|
if slugs:
|
||||||
if models.News.objects.filter(
|
if models.News.objects.filter(
|
||||||
slugs__values__contains=list(slugs.values())
|
slugs__values__contains=list(slugs.values())
|
||||||
).exists():
|
).exists():
|
||||||
raise serializers.ValidationError({'slugs': _('News with this slug already exists.')})
|
raise serializers.ValidationError({'slugs': _('News with this slug already exists.')})
|
||||||
|
|
||||||
|
request = self.context.get("request")
|
||||||
|
if request and hasattr(request, "user"):
|
||||||
|
user = request.user
|
||||||
|
validated_data['created_by'] = user
|
||||||
|
|
||||||
return super().create(validated_data)
|
return super().create(validated_data)
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
|
|
@ -376,6 +391,7 @@ class NewsCloneCreateSerializer(NewsBackOfficeBaseSerializer,
|
||||||
template_display = serializers.CharField(source='get_template_display',
|
template_display = serializers.CharField(source='get_template_display',
|
||||||
read_only=True)
|
read_only=True)
|
||||||
duplicates = NewsBackOfficeDuplicationInfoSerializer(many=True, allow_null=True, read_only=True)
|
duplicates = NewsBackOfficeDuplicationInfoSerializer(many=True, allow_null=True, read_only=True)
|
||||||
|
|
||||||
class Meta(NewsBackOfficeBaseSerializer.Meta, NewsDetailSerializer.Meta):
|
class Meta(NewsBackOfficeBaseSerializer.Meta, NewsDetailSerializer.Meta):
|
||||||
fields = NewsBackOfficeBaseSerializer.Meta.fields + NewsDetailSerializer.Meta.fields + (
|
fields = NewsBackOfficeBaseSerializer.Meta.fields + NewsDetailSerializer.Meta.fields + (
|
||||||
'template_display',
|
'template_display',
|
||||||
|
|
@ -390,4 +406,3 @@ class NewsCloneCreateSerializer(NewsBackOfficeBaseSerializer,
|
||||||
view_count_model = rating_models.ViewCount.objects.create(count=0)
|
view_count_model = rating_models.ViewCount.objects.create(count=0)
|
||||||
instance.create_duplicate(new_country, view_count_model)
|
instance.create_duplicate(new_country, view_count_model)
|
||||||
return get_object_or_404(models.News, pk=kwargs['pk'])
|
return get_object_or_404(models.News, pk=kwargs['pk'])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ class BaseTestCase(APITestCase):
|
||||||
'refresh_token': tokens.get('refresh_token')})
|
'refresh_token': tokens.get('refresh_token')})
|
||||||
self.test_news_type = NewsType.objects.create(name="Test news type")
|
self.test_news_type = NewsType.objects.create(name="Test news type")
|
||||||
|
|
||||||
|
|
||||||
self.lang, created = Language.objects.get_or_create(
|
self.lang, created = Language.objects.get_or_create(
|
||||||
title='Russia',
|
title='Russia',
|
||||||
locale='ru-RU'
|
locale='ru-RU'
|
||||||
|
|
@ -57,13 +56,11 @@ class BaseTestCase(APITestCase):
|
||||||
)
|
)
|
||||||
user_role.save()
|
user_role.save()
|
||||||
|
|
||||||
|
|
||||||
self.test_news = News.objects.create(
|
self.test_news = News.objects.create(
|
||||||
created_by=self.user, modified_by=self.user,
|
created_by=self.user, modified_by=self.user,
|
||||||
title={"ru-RU": "Test news"},
|
title={"ru-RU": "Test news"},
|
||||||
news_type=self.test_news_type,
|
news_type=self.test_news_type,
|
||||||
description={"ru-RU": "Description test news"},
|
description={"ru-RU": "Description test news"},
|
||||||
start=datetime.now() + timedelta(hours=-2),
|
|
||||||
end=datetime.now() + timedelta(hours=2),
|
end=datetime.now() + timedelta(hours=2),
|
||||||
state=News.PUBLISHED,
|
state=News.PUBLISHED,
|
||||||
slugs={'en-GB': 'test-news-slug'},
|
slugs={'en-GB': 'test-news-slug'},
|
||||||
|
|
@ -82,7 +79,6 @@ class NewsTestCase(BaseTestCase):
|
||||||
"title": {"ru-RU": "Test news POST"},
|
"title": {"ru-RU": "Test news POST"},
|
||||||
"news_type_id": self.test_news_type.id,
|
"news_type_id": self.test_news_type.id,
|
||||||
"description": {"ru-RU": "Description test news"},
|
"description": {"ru-RU": "Description test news"},
|
||||||
"start": datetime.now() + timedelta(hours=-2),
|
|
||||||
"end": datetime.now() + timedelta(hours=2),
|
"end": datetime.now() + timedelta(hours=2),
|
||||||
"state": News.PUBLISHED,
|
"state": News.PUBLISHED,
|
||||||
"slugs": {'en-GB': 'test-news-slug_post'},
|
"slugs": {'en-GB': 'test-news-slug_post'},
|
||||||
|
|
@ -119,7 +115,6 @@ class NewsTestCase(BaseTestCase):
|
||||||
'id': self.test_news.id,
|
'id': self.test_news.id,
|
||||||
'description': {"ru-RU": "Description test news!"},
|
'description': {"ru-RU": "Description test news!"},
|
||||||
'slugs': self.test_news.slugs,
|
'slugs': self.test_news.slugs,
|
||||||
'start': self.test_news.start,
|
|
||||||
'news_type_id': self.test_news.news_type_id,
|
'news_type_id': self.test_news.news_type_id,
|
||||||
'country_id': self.country_ru.id,
|
'country_id': self.country_ru.id,
|
||||||
"site_id": self.site_ru.id
|
"site_id": self.site_ru.id
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,62 @@
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
|
|
||||||
from django.db.models import Aggregate, CharField, Value
|
|
||||||
from django.db.models import IntegerField, F
|
from django.db.models import IntegerField, F
|
||||||
|
from django.db.models import Value
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
from news.models import NewsType
|
from gallery.models import Image
|
||||||
from tag.models import TagCategory
|
from news.models import NewsType, News
|
||||||
from transfer.models import PageTexts
|
from rating.models import ViewCount
|
||||||
|
from tag.models import TagCategory, Tag
|
||||||
|
from transfer.models import PageTexts, PageCounters, PageMetadata
|
||||||
from transfer.serializers.news import NewsSerializer
|
from transfer.serializers.news import NewsSerializer
|
||||||
|
|
||||||
|
|
||||||
class GroupConcat(Aggregate):
|
def add_locale(locale, data):
|
||||||
function = 'GROUP_CONCAT'
|
if isinstance(data, dict) and locale not in data:
|
||||||
template = '%(function)s(%(expressions)s)'
|
data.update({
|
||||||
|
locale: next(iter(data.values()))
|
||||||
|
})
|
||||||
|
return data
|
||||||
|
|
||||||
def __init__(self, expression, **extra):
|
|
||||||
output_field = extra.pop('output_field', CharField())
|
|
||||||
super().__init__(expression, output_field=output_field, **extra)
|
|
||||||
|
|
||||||
def as_postgresql(self, compiler, connection):
|
def clear_old_news():
|
||||||
self.function = 'STRING_AGG'
|
"""
|
||||||
return super().as_sql(compiler, connection)
|
Clear lod news and news images
|
||||||
|
"""
|
||||||
|
images = Image.objects.filter(
|
||||||
|
news_gallery__isnull=False,
|
||||||
|
news__gallery__news__old_id__isnull=False
|
||||||
|
)
|
||||||
|
img_num = images.count()
|
||||||
|
|
||||||
|
news = News.objects.filter(old_id__isnull=False)
|
||||||
|
news_num = news.count()
|
||||||
|
|
||||||
|
images.delete()
|
||||||
|
news.delete()
|
||||||
|
# NewsType.objects.all().delete()
|
||||||
|
|
||||||
|
print(f'Deleted {img_num} images')
|
||||||
|
print(f'Deleted {news_num} news')
|
||||||
|
|
||||||
|
|
||||||
def transfer_news():
|
def transfer_news():
|
||||||
news_type, _ = NewsType.objects.get_or_create(name='News')
|
news_type, _ = NewsType.objects.get_or_create(name='news')
|
||||||
tag_cat, _ = TagCategory.objects.get_or_create(index_name='tag')
|
|
||||||
news_type.tag_categories.add(tag_cat)
|
|
||||||
news_type.save()
|
|
||||||
|
|
||||||
queryset = PageTexts.objects.filter(
|
queryset = PageTexts.objects.filter(
|
||||||
page__type='News',
|
page__type='News',
|
||||||
).annotate(
|
).annotate(
|
||||||
tag_cat_id=Value(tag_cat.id, output_field=IntegerField()),
|
page__id=F('page__id'),
|
||||||
news_type_id=Value(news_type.id, output_field=IntegerField()),
|
news_type_id=Value(news_type.id, output_field=IntegerField()),
|
||||||
country_code=F('page__site__country_code_2'),
|
page__created_at=F('page__created_at'),
|
||||||
news_title=F('page__root_title'),
|
page__account_id=F('page__account_id'),
|
||||||
image=F('page__attachment_suffix_url'),
|
page__state=F('page__state'),
|
||||||
template=F('page__template'),
|
page__template=F('page__template'),
|
||||||
tags=GroupConcat('page__tags__id'),
|
page__site__country_code_2=F('page__site__country_code_2'),
|
||||||
account_id=F('page__account_id'),
|
page__root_title=F('page__root_title'),
|
||||||
|
page__attachment_suffix_url=F('page__attachment_suffix_url'),
|
||||||
|
page__published_at=F('page__published_at'),
|
||||||
)
|
)
|
||||||
|
|
||||||
serialized_data = NewsSerializer(data=list(queryset.values()), many=True)
|
serialized_data = NewsSerializer(data=list(queryset.values()), many=True)
|
||||||
|
|
@ -48,6 +66,102 @@ def transfer_news():
|
||||||
pprint(f'News serializer errors: {serialized_data.errors}')
|
pprint(f'News serializer errors: {serialized_data.errors}')
|
||||||
|
|
||||||
|
|
||||||
|
def update_en_gb_locales():
|
||||||
|
"""
|
||||||
|
Update default locales (en-GB)
|
||||||
|
"""
|
||||||
|
news = News.objects.filter(old_id__isnull=False)
|
||||||
|
|
||||||
|
update_news = []
|
||||||
|
for news_item in tqdm(news):
|
||||||
|
news_item.slugs = add_locale('en-GB', news_item.slugs)
|
||||||
|
news_item.title = add_locale('en-GB', news_item.title)
|
||||||
|
news_item.locale_to_description_is_active = add_locale('en-GB', news_item.locale_to_description_is_active)
|
||||||
|
news_item.description = add_locale('en-GB', news_item.description)
|
||||||
|
news_item.subtitle = add_locale('en-GB', news_item.subtitle)
|
||||||
|
update_news.append(news_item)
|
||||||
|
News.objects.bulk_update(update_news, [
|
||||||
|
'slugs',
|
||||||
|
'title',
|
||||||
|
'locale_to_description_is_active',
|
||||||
|
'description',
|
||||||
|
'subtitle',
|
||||||
|
])
|
||||||
|
print(f'Updated {len(update_news)} news locales')
|
||||||
|
|
||||||
|
|
||||||
|
def add_views_count():
|
||||||
|
"""
|
||||||
|
Add views count to news from page_counters
|
||||||
|
"""
|
||||||
|
|
||||||
|
news = News.objects.filter(old_id__isnull=False).values_list('old_id', flat=True)
|
||||||
|
counters = PageCounters.objects.filter(page_id__in=list(news))
|
||||||
|
|
||||||
|
update_counters = []
|
||||||
|
for counter in tqdm(counters):
|
||||||
|
news_item = News.objects.filter(old_id=counter.page_id).first()
|
||||||
|
if news_item:
|
||||||
|
obj, _ = ViewCount.objects.update_or_create(
|
||||||
|
news=news_item,
|
||||||
|
defaults={'count': counter.count},
|
||||||
|
)
|
||||||
|
news_item.views_count = obj
|
||||||
|
update_counters.append(news_item)
|
||||||
|
News.objects.bulk_update(update_counters, ['views_count', ])
|
||||||
|
print(f'Updated {len(update_counters)} news counters')
|
||||||
|
|
||||||
|
|
||||||
|
def add_tags():
|
||||||
|
"""
|
||||||
|
Add news tags
|
||||||
|
"""
|
||||||
|
|
||||||
|
news_type, _ = NewsType.objects.get_or_create(name='news')
|
||||||
|
tag_category, _ = TagCategory.objects.get_or_create(index_name='category')
|
||||||
|
tag_tag, _ = TagCategory.objects.get_or_create(index_name='tag')
|
||||||
|
news_type.tag_categories.add(tag_category)
|
||||||
|
news_type.tag_categories.add(tag_tag)
|
||||||
|
news_type.save()
|
||||||
|
|
||||||
|
tag_cat = {
|
||||||
|
'category': tag_category,
|
||||||
|
'tag': tag_tag,
|
||||||
|
}
|
||||||
|
|
||||||
|
news = News.objects.filter(old_id__isnull=False).values_list('old_id', flat=True)
|
||||||
|
old_news_tag = PageMetadata.objects.filter(
|
||||||
|
key__in=('category', 'tag'),
|
||||||
|
page_id__in=list(news),
|
||||||
|
)
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for old_tag in tqdm(old_news_tag):
|
||||||
|
old_id = old_tag.page.id
|
||||||
|
new_tag, created = Tag.objects.get_or_create(
|
||||||
|
category=tag_cat.get(old_tag.key),
|
||||||
|
value=old_tag.value,
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
text_value = ' '.join(new_tag.value.split('_'))
|
||||||
|
new_tag.label = {'en-GB': text_value}
|
||||||
|
new_tag.save()
|
||||||
|
|
||||||
|
news = News.objects.filter(old_id=old_id).first()
|
||||||
|
if news:
|
||||||
|
news.tags.add(new_tag)
|
||||||
|
news.save()
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
print(f'Updated {count} tags')
|
||||||
|
|
||||||
|
|
||||||
data_types = {
|
data_types = {
|
||||||
'news': [transfer_news]
|
'news': [
|
||||||
|
clear_old_news,
|
||||||
|
transfer_news,
|
||||||
|
update_en_gb_locales,
|
||||||
|
add_views_count,
|
||||||
|
add_tags,
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView,
|
||||||
_ = super().create(request, *args, **kwargs)
|
_ = super().create(request, *args, **kwargs)
|
||||||
news_qs = self.filter_queryset(self.get_queryset())
|
news_qs = self.filter_queryset(self.get_queryset())
|
||||||
return response.Response(
|
return response.Response(
|
||||||
data=serializers.NewsDetailSerializer(get_object_or_404(news_qs, pk=kwargs.get('pk'))).data
|
data=serializers.NewsBackOfficeDetailSerializer(get_object_or_404(news_qs, pk=kwargs.get('pk'))).data
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_object(self):
|
def get_object(self):
|
||||||
|
|
|
||||||
35
apps/notification/migrations/0004_auto_20191118_1307.py
Normal file
35
apps/notification/migrations/0004_auto_20191118_1307.py
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-11-18 13:07
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
import utils.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('notification', '0003_auto_20191116_1248'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SubscriptionType',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')),
|
||||||
|
('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||||
|
('index_name', models.CharField(max_length=255, unique=True, verbose_name='Index name')),
|
||||||
|
('name', utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='name')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
bases=(models.Model, utils.models.TranslatedFieldsMixin),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='subscriber',
|
||||||
|
name='subscription_type',
|
||||||
|
field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='notification.SubscriptionType'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -4,7 +4,14 @@ from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from account.models import User
|
from account.models import User
|
||||||
from utils.methods import generate_string_code
|
from utils.methods import generate_string_code
|
||||||
from utils.models import ProjectBaseMixin
|
from utils.models import ProjectBaseMixin, TranslatedFieldsMixin, TJSONField
|
||||||
|
|
||||||
|
|
||||||
|
class SubscriptionType(ProjectBaseMixin, TranslatedFieldsMixin):
|
||||||
|
index_name = models.CharField(max_length=255, verbose_name=_('Index name'), unique=True)
|
||||||
|
name = TJSONField(blank=True, null=True, default=None,
|
||||||
|
verbose_name=_('name'),
|
||||||
|
help_text='{"en-GB":"some text"}')
|
||||||
|
|
||||||
|
|
||||||
# todo: associate user & subscriber after users registration
|
# todo: associate user & subscriber after users registration
|
||||||
|
|
@ -12,7 +19,7 @@ class SubscriberManager(models.Manager):
|
||||||
"""Extended manager for Subscriber model."""
|
"""Extended manager for Subscriber model."""
|
||||||
|
|
||||||
def make_subscriber(self, email=None, user=None, ip_address=None, country_code=None,
|
def make_subscriber(self, email=None, user=None, ip_address=None, country_code=None,
|
||||||
locale=None, *args, **kwargs):
|
locale=None, subscription_type=None, *args, **kwargs):
|
||||||
"""Make subscriber and update info."""
|
"""Make subscriber and update info."""
|
||||||
# search existing object
|
# search existing object
|
||||||
if not user:
|
if not user:
|
||||||
|
|
@ -35,10 +42,12 @@ class SubscriberManager(models.Manager):
|
||||||
obj.locale = locale
|
obj.locale = locale
|
||||||
obj.state = self.model.USABLE
|
obj.state = self.model.USABLE
|
||||||
obj.update_code = generate_string_code()
|
obj.update_code = generate_string_code()
|
||||||
|
obj.subscription_type = subscription_type
|
||||||
obj.save()
|
obj.save()
|
||||||
else:
|
else:
|
||||||
obj = self.model.objects.create(user=user, email=email, ip_address=ip_address,
|
obj = self.model.objects.create(user=user, email=email, ip_address=ip_address,
|
||||||
country_code=country_code, locale=locale)
|
country_code=country_code, locale=locale,
|
||||||
|
subscription_type=subscription_type)
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def associate_user(self, user):
|
def associate_user(self, user):
|
||||||
|
|
@ -98,6 +107,8 @@ class Subscriber(ProjectBaseMixin):
|
||||||
)
|
)
|
||||||
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
|
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
|
||||||
|
|
||||||
|
subscription_type = models.ForeignKey(SubscriptionType, on_delete=models.CASCADE, null=True, default=None)
|
||||||
|
|
||||||
objects = SubscriberManager.from_queryset(SubscriberQuerySet)()
|
objects = SubscriberManager.from_queryset(SubscriberQuerySet)()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,39 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from notification import models
|
from notification import models
|
||||||
from utils.methods import get_user_ip
|
from utils.methods import get_user_ip
|
||||||
|
from utils.serializers import TranslatedField
|
||||||
|
|
||||||
|
|
||||||
|
class SubscriptionTypeSerializer(serializers.ModelSerializer):
|
||||||
|
"""Subscription type serializer."""
|
||||||
|
|
||||||
|
name_translated = TranslatedField()
|
||||||
|
class Meta:
|
||||||
|
"""Meta class."""
|
||||||
|
|
||||||
|
model = models.SubscriptionType
|
||||||
|
fields = (
|
||||||
|
'id',
|
||||||
|
'index_name',
|
||||||
|
'name_translated',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SubscribeSerializer(serializers.ModelSerializer):
|
class SubscribeSerializer(serializers.ModelSerializer):
|
||||||
"""Subscribe serializer."""
|
"""Subscribe serializer."""
|
||||||
|
|
||||||
email = serializers.EmailField(required=False, source='send_to')
|
email = serializers.EmailField(required=False, source='send_to')
|
||||||
|
subscription_type = SubscriptionTypeSerializer(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
||||||
model = models.Subscriber
|
model = models.Subscriber
|
||||||
fields = ('email', 'state',)
|
fields = (
|
||||||
|
'email',
|
||||||
|
'subscription_type',
|
||||||
|
'state',
|
||||||
|
)
|
||||||
read_only_fields = ('state',)
|
read_only_fields = ('state',)
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
|
|
@ -38,9 +59,16 @@ class SubscribeSerializer(serializers.ModelSerializer):
|
||||||
attrs['ip_address'] = get_user_ip(request)
|
attrs['ip_address'] = get_user_ip(request)
|
||||||
if user.is_authenticated:
|
if user.is_authenticated:
|
||||||
attrs['user'] = user
|
attrs['user'] = user
|
||||||
|
|
||||||
|
subscription_type_id = self.context.get('request').parser_context.get('kwargs').get("subscription_type_pk")
|
||||||
|
subscription_type_qs = models.SubscriptionType.objects.filter(id=subscription_type_id)
|
||||||
|
if not subscription_type_qs.exists():
|
||||||
|
raise serializers.ValidationError({'detail': _(f'SubscriptionType not found.')})
|
||||||
|
attrs["subscription_type"] = subscription_type_qs.first()
|
||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
"""Create obj."""
|
"""Create obj."""
|
||||||
obj = models.Subscriber.objects.make_subscriber(**validated_data)
|
subscriber = models.Subscriber.objects.make_subscriber(**validated_data)
|
||||||
return obj
|
return subscriber
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,10 @@ from notification.views import common
|
||||||
app_name = "notification"
|
app_name = "notification"
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('subscribe/', common.SubscribeView.as_view(), name='subscribe'),
|
path('subscribe/<int:subscription_type_pk>', common.SubscribeView.as_view(), name='subscribe'),
|
||||||
path('subscribe-info/', common.SubscribeInfoAuthUserView.as_view(), name='check-code-auth'),
|
path('subscribe-info/', common.SubscribeInfoAuthUserView.as_view(), name='check-code-auth'),
|
||||||
path('subscribe-info/<code>/', common.SubscribeInfoView.as_view(), name='check-code'),
|
path('subscribe-info/<code>/', common.SubscribeInfoView.as_view(), name='check-code'),
|
||||||
path('unsubscribe/', common.UnsubscribeAuthUserView.as_view(), name='unsubscribe-auth'),
|
path('unsubscribe/', common.UnsubscribeAuthUserView.as_view(), name='unsubscribe-auth'),
|
||||||
path('unsubscribe/<code>/', common.UnsubscribeView.as_view(), name='unsubscribe'),
|
path('unsubscribe/<code>/', common.UnsubscribeView.as_view(), name='unsubscribe'),
|
||||||
|
path('subscription-types/', common.SubscriptionTypesView.as_view(), name='subscription-types'),
|
||||||
]
|
]
|
||||||
|
|
@ -30,20 +30,16 @@ class SubscribeInfoView(generics.RetrieveAPIView):
|
||||||
serializer_class = serializers.SubscribeSerializer
|
serializer_class = serializers.SubscribeSerializer
|
||||||
|
|
||||||
|
|
||||||
class SubscribeInfoAuthUserView(generics.RetrieveAPIView):
|
class SubscribeInfoAuthUserView(generics.ListAPIView):
|
||||||
"""Subscribe info auth user view."""
|
"""Subscribe info auth user view."""
|
||||||
|
|
||||||
permission_classes = (permissions.IsAuthenticated, )
|
permission_classes = (permissions.IsAuthenticated, )
|
||||||
queryset = models.Subscriber.objects.all()
|
|
||||||
serializer_class = serializers.SubscribeSerializer
|
serializer_class = serializers.SubscribeSerializer
|
||||||
|
|
||||||
def get_object(self):
|
def get_queryset(self):
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
queryset = self.filter_queryset(self.get_queryset())
|
queryset = self.filter_queryset(models.Subscriber.objects.all())
|
||||||
filter_kwargs = {'user': user}
|
return queryset.filter(user=user)
|
||||||
obj = get_object_or_404(queryset, **filter_kwargs)
|
|
||||||
self.check_object_permissions(self.request, obj)
|
|
||||||
return obj
|
|
||||||
|
|
||||||
|
|
||||||
class UnsubscribeView(generics.GenericAPIView):
|
class UnsubscribeView(generics.GenericAPIView):
|
||||||
|
|
@ -76,3 +72,10 @@ class UnsubscribeAuthUserView(generics.GenericAPIView):
|
||||||
serializer = self.get_serializer(instance=obj)
|
serializer = self.get_serializer(instance=obj)
|
||||||
return Response(data=serializer.data)
|
return Response(data=serializer.data)
|
||||||
|
|
||||||
|
|
||||||
|
class SubscriptionTypesView(generics.ListAPIView):
|
||||||
|
pagination_class = None
|
||||||
|
permission_classes = (permissions.AllowAny,)
|
||||||
|
queryset = models.SubscriptionType.objects.all()
|
||||||
|
serializer_class = serializers.SubscriptionTypeSerializer
|
||||||
|
|
||||||
|
|
|
||||||
20
apps/product/migrations/0021_product_site.py
Normal file
20
apps/product/migrations/0021_product_site.py
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-10 14:13
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('main', '0040_footer'),
|
||||||
|
('product', '0020_merge_20191209_0911'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='site',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='main.SiteSettings'),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
apps/product/migrations/0022_auto_20191210_1517.py
Normal file
18
apps/product/migrations/0022_auto_20191210_1517.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-10 15:17
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('product', '0021_product_site'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='producttype',
|
||||||
|
name='index_name',
|
||||||
|
field=models.CharField(choices=[('food', 'food'), ('wine', 'wine'), ('liquor', 'liquor'), ('souvenir', 'souvenir'), ('book', 'book')], db_index=True, max_length=50, unique=True, verbose_name='Index name'),
|
||||||
|
),
|
||||||
|
]
|
||||||
25
apps/product/migrations/0022_auto_20191220_1007.py
Normal file
25
apps/product/migrations/0022_auto_20191220_1007.py
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-20 10:07
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('gallery', '0007_auto_20191211_1528'),
|
||||||
|
('product', '0021_auto_20191212_0926'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='productsubtype',
|
||||||
|
name='default_image',
|
||||||
|
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='product_sub_types', to='gallery.Image', verbose_name='default image'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='producttype',
|
||||||
|
name='default_image',
|
||||||
|
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='product_types', to='gallery.Image', verbose_name='default image'),
|
||||||
|
),
|
||||||
|
]
|
||||||
14
apps/product/migrations/0023_merge_20191217_1127.py
Normal file
14
apps/product/migrations/0023_merge_20191217_1127.py
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-17 11:27
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('product', '0022_auto_20191210_1517'),
|
||||||
|
('product', '0021_auto_20191212_0926'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
]
|
||||||
14
apps/product/migrations/0024_merge_20191223_1405.py
Normal file
14
apps/product/migrations/0024_merge_20191223_1405.py
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-23 14:05
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('product', '0023_merge_20191217_1127'),
|
||||||
|
('product', '0022_auto_20191220_1007'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
]
|
||||||
|
|
@ -14,10 +14,11 @@ from location.models import WineOriginAddressMixin
|
||||||
from review.models import Review
|
from review.models import Review
|
||||||
from utils.models import (BaseAttributes, ProjectBaseMixin, HasTagsMixin,
|
from utils.models import (BaseAttributes, ProjectBaseMixin, HasTagsMixin,
|
||||||
TranslatedFieldsMixin, TJSONField, FavoritesMixin,
|
TranslatedFieldsMixin, TJSONField, FavoritesMixin,
|
||||||
GalleryModelMixin, IntermediateGalleryModelMixin)
|
GalleryMixin, IntermediateGalleryModelMixin,
|
||||||
|
TypeDefaultImageMixin)
|
||||||
|
|
||||||
|
|
||||||
class ProductType(TranslatedFieldsMixin, ProjectBaseMixin):
|
class ProductType(TypeDefaultImageMixin, TranslatedFieldsMixin, ProjectBaseMixin):
|
||||||
"""ProductType model."""
|
"""ProductType model."""
|
||||||
|
|
||||||
STR_FIELD_NAME = 'name'
|
STR_FIELD_NAME = 'name'
|
||||||
|
|
@ -29,14 +30,25 @@ class ProductType(TranslatedFieldsMixin, ProjectBaseMixin):
|
||||||
SOUVENIR = 'souvenir'
|
SOUVENIR = 'souvenir'
|
||||||
BOOK = 'book'
|
BOOK = 'book'
|
||||||
|
|
||||||
|
INDEX_CHOICES = (
|
||||||
|
(FOOD, 'food'),
|
||||||
|
(WINE, 'wine'),
|
||||||
|
(LIQUOR, 'liquor'),
|
||||||
|
(SOUVENIR, 'souvenir'),
|
||||||
|
(BOOK, 'book')
|
||||||
|
)
|
||||||
name = TJSONField(blank=True, null=True, default=None,
|
name = TJSONField(blank=True, null=True, default=None,
|
||||||
verbose_name=_('Name'), help_text='{"en-GB":"some text"}')
|
verbose_name=_('Name'), help_text='{"en-GB":"some text"}')
|
||||||
index_name = models.CharField(max_length=50, unique=True, db_index=True,
|
index_name = models.CharField(max_length=50, unique=True, db_index=True,
|
||||||
verbose_name=_('Index name'))
|
verbose_name=_('Index name'), choices=INDEX_CHOICES)
|
||||||
use_subtypes = models.BooleanField(_('Use subtypes'), default=True)
|
use_subtypes = models.BooleanField(_('Use subtypes'), default=True)
|
||||||
tag_categories = models.ManyToManyField('tag.TagCategory',
|
tag_categories = models.ManyToManyField('tag.TagCategory',
|
||||||
related_name='product_types',
|
related_name='product_types',
|
||||||
verbose_name=_('Tag categories'))
|
verbose_name=_('Tag categories'))
|
||||||
|
default_image = models.ForeignKey('gallery.Image', on_delete=models.SET_NULL,
|
||||||
|
related_name='product_types',
|
||||||
|
blank=True, null=True, default=None,
|
||||||
|
verbose_name='default image')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
@ -45,7 +57,7 @@ class ProductType(TranslatedFieldsMixin, ProjectBaseMixin):
|
||||||
verbose_name_plural = _('Product types')
|
verbose_name_plural = _('Product types')
|
||||||
|
|
||||||
|
|
||||||
class ProductSubType(TranslatedFieldsMixin, ProjectBaseMixin):
|
class ProductSubType(TypeDefaultImageMixin, TranslatedFieldsMixin, ProjectBaseMixin):
|
||||||
"""ProductSubtype model."""
|
"""ProductSubtype model."""
|
||||||
|
|
||||||
STR_FIELD_NAME = 'name'
|
STR_FIELD_NAME = 'name'
|
||||||
|
|
@ -62,6 +74,10 @@ class ProductSubType(TranslatedFieldsMixin, ProjectBaseMixin):
|
||||||
verbose_name=_('Name'), help_text='{"en-GB":"some text"}')
|
verbose_name=_('Name'), help_text='{"en-GB":"some text"}')
|
||||||
index_name = models.CharField(max_length=50, unique=True, db_index=True,
|
index_name = models.CharField(max_length=50, unique=True, db_index=True,
|
||||||
verbose_name=_('Index name'))
|
verbose_name=_('Index name'))
|
||||||
|
default_image = models.ForeignKey('gallery.Image', on_delete=models.SET_NULL,
|
||||||
|
related_name='product_sub_types',
|
||||||
|
blank=True, null=True, default=None,
|
||||||
|
verbose_name='default image')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
@ -83,7 +99,7 @@ class ProductQuerySet(models.QuerySet):
|
||||||
|
|
||||||
def with_base_related(self):
|
def with_base_related(self):
|
||||||
return self.select_related('product_type', 'establishment') \
|
return self.select_related('product_type', 'establishment') \
|
||||||
.prefetch_related('product_type__subtypes')
|
.prefetch_related('product_type__subtypes', 'tags', 'tags__translation')
|
||||||
|
|
||||||
def with_extended_related(self):
|
def with_extended_related(self):
|
||||||
"""Returns qs with almost all related objects."""
|
"""Returns qs with almost all related objects."""
|
||||||
|
|
@ -195,7 +211,7 @@ class ProductQuerySet(models.QuerySet):
|
||||||
return self.none()
|
return self.none()
|
||||||
|
|
||||||
|
|
||||||
class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes,
|
class Product(GalleryMixin, TranslatedFieldsMixin, BaseAttributes,
|
||||||
HasTagsMixin, FavoritesMixin):
|
HasTagsMixin, FavoritesMixin):
|
||||||
"""Product models."""
|
"""Product models."""
|
||||||
|
|
||||||
|
|
@ -280,6 +296,8 @@ class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes,
|
||||||
default=None, null=True,
|
default=None, null=True,
|
||||||
verbose_name=_('Serial number'))
|
verbose_name=_('Serial number'))
|
||||||
|
|
||||||
|
site = models.ForeignKey(to='main.SiteSettings', null=True, blank=True, on_delete=models.CASCADE)
|
||||||
|
|
||||||
objects = ProductManager.from_queryset(ProductQuerySet)()
|
objects = ProductManager.from_queryset(ProductQuerySet)()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ from product.serializers import ProductDetailSerializer, ProductTypeBaseSerializ
|
||||||
ProductSubTypeBaseSerializer
|
ProductSubTypeBaseSerializer
|
||||||
from tag.models import TagCategory
|
from tag.models import TagCategory
|
||||||
from account.serializers.common import UserShortSerializer
|
from account.serializers.common import UserShortSerializer
|
||||||
|
from main.serializers import SiteSettingsSerializer
|
||||||
|
|
||||||
class ProductBackOfficeGallerySerializer(serializers.ModelSerializer):
|
class ProductBackOfficeGallerySerializer(serializers.ModelSerializer):
|
||||||
"""Serializer class for model ProductGallery."""
|
"""Serializer class for model ProductGallery."""
|
||||||
|
|
@ -55,6 +55,7 @@ class ProductBackOfficeGallerySerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class ProductBackOfficeDetailSerializer(ProductDetailSerializer):
|
class ProductBackOfficeDetailSerializer(ProductDetailSerializer):
|
||||||
"""Product back-office detail serializer."""
|
"""Product back-office detail serializer."""
|
||||||
|
in_favorites = serializers.BooleanField(allow_null=True, read_only=True)
|
||||||
|
|
||||||
class Meta(ProductDetailSerializer.Meta):
|
class Meta(ProductDetailSerializer.Meta):
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
@ -68,9 +69,10 @@ class ProductBackOfficeDetailSerializer(ProductDetailSerializer):
|
||||||
# 'wine_sub_region',
|
# 'wine_sub_region',
|
||||||
'wine_village',
|
'wine_village',
|
||||||
'state',
|
'state',
|
||||||
|
'site',
|
||||||
|
'product_type'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ProductTypeBackOfficeDetailSerializer(ProductTypeBaseSerializer):
|
class ProductTypeBackOfficeDetailSerializer(ProductTypeBaseSerializer):
|
||||||
"""Product type back-office detail serializer."""
|
"""Product type back-office detail serializer."""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,8 @@ class ProductSubTypeBaseSerializer(serializers.ModelSerializer):
|
||||||
name_translated = TranslatedField()
|
name_translated = TranslatedField()
|
||||||
index_name_display = serializers.CharField(source='get_index_name_display',
|
index_name_display = serializers.CharField(source='get_index_name_display',
|
||||||
read_only=True)
|
read_only=True)
|
||||||
|
default_image_url = serializers.ImageField(source='default_image.image',
|
||||||
|
allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.ProductSubType
|
model = models.ProductSubType
|
||||||
|
|
@ -41,12 +43,15 @@ class ProductSubTypeBaseSerializer(serializers.ModelSerializer):
|
||||||
'id',
|
'id',
|
||||||
'name_translated',
|
'name_translated',
|
||||||
'index_name_display',
|
'index_name_display',
|
||||||
|
'default_image_url',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ProductTypeBaseSerializer(serializers.ModelSerializer):
|
class ProductTypeBaseSerializer(serializers.ModelSerializer):
|
||||||
"""ProductType base serializer"""
|
"""ProductType base serializer"""
|
||||||
name_translated = TranslatedField()
|
name_translated = TranslatedField()
|
||||||
|
default_image_url = serializers.ImageField(source='default_image.image',
|
||||||
|
allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.ProductType
|
model = models.ProductType
|
||||||
|
|
@ -54,6 +59,7 @@ class ProductTypeBaseSerializer(serializers.ModelSerializer):
|
||||||
'id',
|
'id',
|
||||||
'name_translated',
|
'name_translated',
|
||||||
'index_name',
|
'index_name',
|
||||||
|
'default_image_url',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -95,7 +101,7 @@ class ProductBaseSerializer(serializers.ModelSerializer):
|
||||||
wine_colors = TagBaseSerializer(many=True, read_only=True)
|
wine_colors = TagBaseSerializer(many=True, read_only=True)
|
||||||
preview_image_url = serializers.URLField(allow_null=True,
|
preview_image_url = serializers.URLField(allow_null=True,
|
||||||
read_only=True)
|
read_only=True)
|
||||||
in_favorites = serializers.BooleanField(allow_null=True)
|
in_favorites = serializers.BooleanField(allow_null=True, read_only=True)
|
||||||
wine_origins = EstablishmentWineOriginBaseSerializer(many=True, read_only=True)
|
wine_origins = EstablishmentWineOriginBaseSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
||||||
121
apps/product/tests.py
Normal file
121
apps/product/tests.py
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
from rest_framework import status
|
||||||
|
from account.models import User
|
||||||
|
from http.cookies import SimpleCookie
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
|
from translation.models import Language
|
||||||
|
from account.models import Role, UserRole
|
||||||
|
from location.models import Country, Address, City, Region
|
||||||
|
from main.models import SiteSettings
|
||||||
|
from product.models import Product, ProductType
|
||||||
|
|
||||||
|
class BaseTestCase(APITestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.username = 'sedragurda'
|
||||||
|
self.password = 'sedragurdaredips19'
|
||||||
|
self.email = 'sedragurda@desoz.com'
|
||||||
|
self.newsletter = True
|
||||||
|
self.user = User.objects.create_user(
|
||||||
|
username=self.username,
|
||||||
|
email=self.email,
|
||||||
|
password=self.password,
|
||||||
|
is_staff=True,
|
||||||
|
)
|
||||||
|
# get tokens
|
||||||
|
tokens = User.create_jwt_tokens(self.user)
|
||||||
|
self.client.cookies = SimpleCookie(
|
||||||
|
{'access_token': tokens.get('access_token'),
|
||||||
|
'refresh_token': tokens.get('refresh_token')})
|
||||||
|
|
||||||
|
|
||||||
|
self.lang = Language.objects.create(
|
||||||
|
title='Russia',
|
||||||
|
locale='ru-RU'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.country_ru = Country.objects.create(
|
||||||
|
name={'en-GB': 'Russian'},
|
||||||
|
code='RU',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.region = Region.objects.create(name='Moscow area', code='01',
|
||||||
|
country=self.country_ru)
|
||||||
|
self.region.save()
|
||||||
|
|
||||||
|
self.city = City.objects.create(
|
||||||
|
name='Mosocow', code='01',
|
||||||
|
region=self.region,
|
||||||
|
country=self.country_ru)
|
||||||
|
self.city.save()
|
||||||
|
|
||||||
|
self.address = Address.objects.create(
|
||||||
|
city=self.city, street_name_1='Krasnaya',
|
||||||
|
number=2, postal_code='010100')
|
||||||
|
self.address.save()
|
||||||
|
|
||||||
|
self.site = SiteSettings.objects.create(
|
||||||
|
subdomain='ru',
|
||||||
|
country=self.country_ru
|
||||||
|
)
|
||||||
|
|
||||||
|
self.site.save()
|
||||||
|
|
||||||
|
self.role = Role.objects.create(role=Role.LIQUOR_REVIEWER,
|
||||||
|
site=self.site)
|
||||||
|
self.role.save()
|
||||||
|
|
||||||
|
self.user_role = UserRole.objects.create(
|
||||||
|
user=self.user, role=self.role)
|
||||||
|
|
||||||
|
self.user_role.save()
|
||||||
|
|
||||||
|
self.product_type = ProductType.objects.create(index_name=ProductType.LIQUOR)
|
||||||
|
self.product_type.save()
|
||||||
|
|
||||||
|
self.product = Product.objects.create(name='Product')
|
||||||
|
self.product.save()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class LiquorReviewerTests(BaseTestCase):
|
||||||
|
def test_get(self):
|
||||||
|
self.product.product_type = self.product_type
|
||||||
|
self.product.save()
|
||||||
|
|
||||||
|
url = reverse("back:product:list-create")
|
||||||
|
response = self.client.get(url, format='json')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
url = reverse("back:product:rud", kwargs={'pk': self.product.id})
|
||||||
|
response = self.client.get(url, format='json')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def test_post_patch_put_delete(self):
|
||||||
|
data_post = {
|
||||||
|
"slug": None,
|
||||||
|
"public_mark": None,
|
||||||
|
"vintage": None,
|
||||||
|
"average_price": None,
|
||||||
|
"description": None,
|
||||||
|
"available": False,
|
||||||
|
"establishment": None,
|
||||||
|
"wine_village": None,
|
||||||
|
"state": Product.PUBLISHED,
|
||||||
|
"site_id": self.site.id,
|
||||||
|
"product_type_id": self.product_type.id
|
||||||
|
}
|
||||||
|
url = reverse("back:product:list-create")
|
||||||
|
response = self.client.post(url, data=data_post, format='json')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
data_patch = {
|
||||||
|
'name': 'Test product'
|
||||||
|
}
|
||||||
|
url = reverse("back:product:rud", kwargs={'pk': self.product.id})
|
||||||
|
response = self.client.patch(url, data=data_patch, format='json')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -7,6 +7,7 @@ from product import serializers, models
|
||||||
from product.views import ProductBaseView
|
from product.views import ProductBaseView
|
||||||
from utils.serializers import ImageBaseSerializer
|
from utils.serializers import ImageBaseSerializer
|
||||||
from utils.views import CreateDestroyGalleryViewMixin
|
from utils.views import CreateDestroyGalleryViewMixin
|
||||||
|
from utils.permissions import IsLiquorReviewer, IsProductReviewer
|
||||||
|
|
||||||
|
|
||||||
class ProductBackOfficeMixinView(ProductBaseView):
|
class ProductBackOfficeMixinView(ProductBaseView):
|
||||||
|
|
@ -91,12 +92,14 @@ class ProductDetailBackOfficeView(ProductBackOfficeMixinView,
|
||||||
generics.RetrieveUpdateDestroyAPIView):
|
generics.RetrieveUpdateDestroyAPIView):
|
||||||
"""Product back-office R/U/D view."""
|
"""Product back-office R/U/D view."""
|
||||||
serializer_class = serializers.ProductBackOfficeDetailSerializer
|
serializer_class = serializers.ProductBackOfficeDetailSerializer
|
||||||
|
permission_classes = [IsLiquorReviewer | IsProductReviewer]
|
||||||
|
|
||||||
|
|
||||||
class ProductListCreateBackOfficeView(BackOfficeListCreateMixin, ProductBackOfficeMixinView,
|
class ProductListCreateBackOfficeView(BackOfficeListCreateMixin, ProductBackOfficeMixinView,
|
||||||
generics.ListCreateAPIView):
|
generics.ListCreateAPIView):
|
||||||
"""Product back-office list-create view."""
|
"""Product back-office list-create view."""
|
||||||
serializer_class = serializers.ProductBackOfficeDetailSerializer
|
serializer_class = serializers.ProductBackOfficeDetailSerializer
|
||||||
|
permission_classes = [IsLiquorReviewer | IsProductReviewer]
|
||||||
|
|
||||||
|
|
||||||
class ProductTypeListCreateBackOfficeView(BackOfficeListCreateMixin,
|
class ProductTypeListCreateBackOfficeView(BackOfficeListCreateMixin,
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
"""Product app views."""
|
"""Product app views."""
|
||||||
from rest_framework import generics, permissions
|
from django.conf import settings
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from product.models import Product
|
from rest_framework import generics, permissions
|
||||||
|
|
||||||
from comment.models import Comment
|
from comment.models import Comment
|
||||||
from product import filters, serializers
|
|
||||||
from comment.serializers import CommentRUDSerializer
|
from comment.serializers import CommentRUDSerializer
|
||||||
|
from product import filters, serializers
|
||||||
|
from product.models import Product
|
||||||
from utils.views import FavoritesCreateDestroyMixinView
|
from utils.views import FavoritesCreateDestroyMixinView
|
||||||
from utils.pagination import PortionPagination
|
|
||||||
|
|
||||||
|
|
||||||
class ProductBaseView(generics.GenericAPIView):
|
class ProductBaseView(generics.GenericAPIView):
|
||||||
|
|
@ -35,7 +36,15 @@ class ProductListView(ProductBaseView, generics.ListAPIView):
|
||||||
class ProductSimilarView(ProductListView):
|
class ProductSimilarView(ProductListView):
|
||||||
"""Resource for getting a list of similar product."""
|
"""Resource for getting a list of similar product."""
|
||||||
serializer_class = serializers.ProductBaseSerializer
|
serializer_class = serializers.ProductBaseSerializer
|
||||||
pagination_class = PortionPagination
|
pagination_class = None
|
||||||
|
|
||||||
|
def get_base_object(self):
|
||||||
|
"""
|
||||||
|
Return base product instance for a getting list of similar products.
|
||||||
|
"""
|
||||||
|
product = get_object_or_404(Product.objects.all(),
|
||||||
|
slug=self.kwargs.get('slug'))
|
||||||
|
return product
|
||||||
|
|
||||||
|
|
||||||
class ProductDetailView(ProductBaseView, generics.RetrieveAPIView):
|
class ProductDetailView(ProductBaseView, generics.RetrieveAPIView):
|
||||||
|
|
@ -95,7 +104,10 @@ class SimilarListView(ProductSimilarView):
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Overridden get_queryset method."""
|
"""Overridden get_queryset method."""
|
||||||
return super().get_queryset() \
|
qs = super(SimilarListView, self).get_queryset()
|
||||||
.has_location() \
|
base_product = self.get_base_object()
|
||||||
.similar(slug=self.kwargs.get('slug'))
|
|
||||||
|
|
||||||
|
if base_product:
|
||||||
|
return qs.has_location().similar(slug=self.kwargs.get('slug'))[:settings.QUERY_OUTPUT_OBJECTS]
|
||||||
|
else:
|
||||||
|
return qs.none()
|
||||||
|
|
|
||||||
18
apps/rating/migrations/0005_auto_20191223_0850.py
Normal file
18
apps/rating/migrations/0005_auto_20191223_0850.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-23 08:50
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('rating', '0004_auto_20191114_2041'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='viewcount',
|
||||||
|
name='count',
|
||||||
|
field=models.PositiveIntegerField(),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -23,4 +23,4 @@ class Rating(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class ViewCount(models.Model):
|
class ViewCount(models.Model):
|
||||||
count = models.IntegerField()
|
count = models.PositiveIntegerField()
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ class RecipeQuerySet(models.QuerySet):
|
||||||
|
|
||||||
# todo: what records are considered published?
|
# todo: what records are considered published?
|
||||||
def published(self):
|
def published(self):
|
||||||
|
# TODO: проверка по полю published_at
|
||||||
return self.filter(state__in=[self.model.PUBLISHED,
|
return self.filter(state__in=[self.model.PUBLISHED,
|
||||||
self.model.PUBLISHED_EXCLUSIVE])
|
self.model.PUBLISHED_EXCLUSIVE])
|
||||||
|
|
||||||
|
|
@ -67,3 +68,6 @@ class Recipe(TranslatedFieldsMixin, ImageMixin, BaseAttributes):
|
||||||
|
|
||||||
verbose_name = _('Recipe')
|
verbose_name = _('Recipe')
|
||||||
verbose_name_plural = _('Recipes')
|
verbose_name_plural = _('Recipes')
|
||||||
|
|
||||||
|
# TODO: в save добавить обновление published_at если state в PUBLISHED или PUBLISHED_EXCLUSIVE
|
||||||
|
# TODO: в save добавить обновление published_at в None если state в WAITING или HIDDEN
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from utils.models import (BaseAttributes, TranslatedFieldsMixin,
|
from utils.models import (BaseAttributes, TranslatedFieldsMixin,
|
||||||
ProjectBaseMixin, GalleryModelMixin,
|
ProjectBaseMixin, GalleryMixin,
|
||||||
TJSONField, IntermediateGalleryModelMixin)
|
TJSONField, IntermediateGalleryModelMixin)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -93,7 +93,7 @@ class Review(BaseAttributes, TranslatedFieldsMixin):
|
||||||
verbose_name_plural = _('Reviews')
|
verbose_name_plural = _('Reviews')
|
||||||
|
|
||||||
|
|
||||||
class Inquiries(GalleryModelMixin, ProjectBaseMixin):
|
class Inquiries(GalleryMixin, ProjectBaseMixin):
|
||||||
NONE = 0
|
NONE = 0
|
||||||
DINER = 1
|
DINER = 1
|
||||||
LUNCH = 2
|
LUNCH = 2
|
||||||
|
|
|
||||||
|
|
@ -23,12 +23,14 @@ class EstablishmentDocument(Document):
|
||||||
'name': fields.ObjectField(attr='name_indexing',
|
'name': fields.ObjectField(attr='name_indexing',
|
||||||
properties=OBJECT_FIELD_PROPERTIES),
|
properties=OBJECT_FIELD_PROPERTIES),
|
||||||
'index_name': fields.KeywordField(attr='index_name'),
|
'index_name': fields.KeywordField(attr='index_name'),
|
||||||
|
'default_image': fields.KeywordField(attr='default_image_url'),
|
||||||
})
|
})
|
||||||
establishment_subtypes = fields.ObjectField(
|
establishment_subtypes = fields.ObjectField(
|
||||||
properties={
|
properties={
|
||||||
'id': fields.IntegerField(),
|
'id': fields.IntegerField(),
|
||||||
'name': fields.ObjectField(attr='name_indexing'),
|
'name': fields.ObjectField(attr='name_indexing'),
|
||||||
'index_name': fields.KeywordField(attr='index_name'),
|
'index_name': fields.KeywordField(attr='index_name'),
|
||||||
|
'default_image': fields.KeywordField(attr='default_image_url'),
|
||||||
},
|
},
|
||||||
multi=True)
|
multi=True)
|
||||||
works_evening = fields.ListField(fields.IntegerField(
|
works_evening = fields.ListField(fields.IntegerField(
|
||||||
|
|
@ -143,6 +145,8 @@ class EstablishmentDocument(Document):
|
||||||
'weekday_display': fields.KeywordField(attr='get_weekday_display'),
|
'weekday_display': fields.KeywordField(attr='get_weekday_display'),
|
||||||
'closed_at': fields.KeywordField(attr='closed_at_str'),
|
'closed_at': fields.KeywordField(attr='closed_at_str'),
|
||||||
'opening_at': fields.KeywordField(attr='opening_at_str'),
|
'opening_at': fields.KeywordField(attr='opening_at_str'),
|
||||||
|
'closed_at_indexing': fields.DateField(),
|
||||||
|
'opening_at_indexing': fields.DateField(),
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
address = fields.ObjectField(
|
address = fields.ObjectField(
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ class ProductDocument(Document):
|
||||||
'id': fields.IntegerField(),
|
'id': fields.IntegerField(),
|
||||||
'name': fields.ObjectField(attr='name_indexing', properties=OBJECT_FIELD_PROPERTIES),
|
'name': fields.ObjectField(attr='name_indexing', properties=OBJECT_FIELD_PROPERTIES),
|
||||||
'index_name': fields.KeywordField(),
|
'index_name': fields.KeywordField(),
|
||||||
|
'default_image': fields.KeywordField(attr='default_image_url'),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
subtypes = fields.ObjectField(
|
subtypes = fields.ObjectField(
|
||||||
|
|
@ -26,6 +27,7 @@ class ProductDocument(Document):
|
||||||
'id': fields.IntegerField(),
|
'id': fields.IntegerField(),
|
||||||
'name': fields.ObjectField(attr='name_indexing', properties=OBJECT_FIELD_PROPERTIES),
|
'name': fields.ObjectField(attr='name_indexing', properties=OBJECT_FIELD_PROPERTIES),
|
||||||
'index_name': fields.KeywordField(),
|
'index_name': fields.KeywordField(),
|
||||||
|
'default_image': fields.KeywordField(attr='default_image_url'),
|
||||||
},
|
},
|
||||||
multi=True
|
multi=True
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ class TagCategoryFilterSet(TagsBaseFilterSet):
|
||||||
# todo: filter by establishment type
|
# todo: filter by establishment type
|
||||||
def by_establishment_type(self, queryset, name, value):
|
def by_establishment_type(self, queryset, name, value):
|
||||||
if value == EstablishmentType.ARTISAN:
|
if value == EstablishmentType.ARTISAN:
|
||||||
qs = models.TagCategory.objects.filter(index_name='shop_category')
|
qs = models.TagCategory.objects.with_base_related().filter(index_name='shop_category')
|
||||||
else:
|
else:
|
||||||
qs = queryset.by_establishment_type(value)
|
qs = queryset.by_establishment_type(value)
|
||||||
return qs
|
return qs
|
||||||
|
|
@ -73,10 +73,10 @@ class TagsFilterSet(TagsBaseFilterSet):
|
||||||
|
|
||||||
def by_establishment_type(self, queryset, name, value):
|
def by_establishment_type(self, queryset, name, value):
|
||||||
if value == EstablishmentType.ARTISAN:
|
if value == EstablishmentType.ARTISAN:
|
||||||
qs = models.Tag.objects.by_category_index_name('shop_category')
|
qs = models.Tag.objects.filter(index_name__in=settings.ARTISANS_CHOSEN_TAGS)
|
||||||
if self.request.country_code and self.request.country_code not in settings.INTERNATIONAL_COUNTRY_CODES:
|
if self.request.country_code and self.request.country_code not in settings.INTERNATIONAL_COUNTRY_CODES:
|
||||||
qs = qs.filter(establishments__address__city__country__code=self.request.country_code).distinct('id')
|
qs = qs.filter(establishments__address__city__country__code=self.request.country_code).distinct('id')
|
||||||
return qs.exclude(establishments__isnull=True)[0:8]
|
return qs.exclude(establishments__isnull=True)
|
||||||
return queryset.by_establishment_type(value)
|
return queryset.by_establishment_type(value)
|
||||||
|
|
||||||
# TMP TODO remove it later
|
# TMP TODO remove it later
|
||||||
|
|
|
||||||
46
apps/tag/migrations/0016_auto_20191220_1224.py
Normal file
46
apps/tag/migrations/0016_auto_20191220_1224.py
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-20 12:24
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
def fill_translations(apps, schemaeditor):
|
||||||
|
Tag = apps.get_model('tag', 'Tag')
|
||||||
|
TagCategory = apps.get_model('tag', 'TagCategory')
|
||||||
|
SiteInterfaceDictionary = apps.get_model('translation', 'SiteInterfaceDictionary')
|
||||||
|
|
||||||
|
for tag_category in TagCategory.objects.all():
|
||||||
|
if tag_category.label:
|
||||||
|
t = SiteInterfaceDictionary(text=tag_category.label)
|
||||||
|
t.save()
|
||||||
|
tag_category.translation = t
|
||||||
|
tag_category.save()
|
||||||
|
|
||||||
|
for tag in Tag.objects.all():
|
||||||
|
if tag.label:
|
||||||
|
t = SiteInterfaceDictionary(text=tag.label)
|
||||||
|
t.save()
|
||||||
|
tag.translation = t
|
||||||
|
tag.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('translation', '0007_language_is_active'),
|
||||||
|
('tag', '0015_auto_20191118_1210'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='tag',
|
||||||
|
name='translation',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tag', to='translation.SiteInterfaceDictionary', verbose_name='Translation'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='tagcategory',
|
||||||
|
name='translation',
|
||||||
|
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tag_category', to='translation.SiteInterfaceDictionary', verbose_name='Translation'),
|
||||||
|
),
|
||||||
|
migrations.RunPython(fill_translations, migrations.RunPython.noop)
|
||||||
|
]
|
||||||
21
apps/tag/migrations/0017_auto_20191220_1623.py
Normal file
21
apps/tag/migrations/0017_auto_20191220_1623.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-20 16:23
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('tag', '0016_auto_20191220_1224'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='tag',
|
||||||
|
name='label',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='tagcategory',
|
||||||
|
name='label',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -5,7 +5,7 @@ from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from configuration.models import TranslationSettings
|
from configuration.models import TranslationSettings
|
||||||
from location.models import Country
|
from location.models import Country
|
||||||
from utils.models import TJSONField, TranslatedFieldsMixin
|
from utils.models import IndexJSON
|
||||||
|
|
||||||
|
|
||||||
class TagQuerySet(models.QuerySet):
|
class TagQuerySet(models.QuerySet):
|
||||||
|
|
@ -29,12 +29,9 @@ class TagQuerySet(models.QuerySet):
|
||||||
return self.filter(category__establishment_types__index_name=index_name)
|
return self.filter(category__establishment_types__index_name=index_name)
|
||||||
|
|
||||||
|
|
||||||
class Tag(TranslatedFieldsMixin, models.Model):
|
class Tag(models.Model):
|
||||||
"""Tag model."""
|
"""Tag model."""
|
||||||
|
|
||||||
label = TJSONField(blank=True, null=True, default=None,
|
|
||||||
verbose_name=_('label'),
|
|
||||||
help_text='{"en-GB":"some text"}')
|
|
||||||
value = models.CharField(_('indexing name'), max_length=255, blank=True, db_index=True,
|
value = models.CharField(_('indexing name'), max_length=255, blank=True, db_index=True,
|
||||||
null=True, default=None)
|
null=True, default=None)
|
||||||
category = models.ForeignKey('TagCategory', on_delete=models.CASCADE,
|
category = models.ForeignKey('TagCategory', on_delete=models.CASCADE,
|
||||||
|
|
@ -48,6 +45,16 @@ class Tag(TranslatedFieldsMixin, models.Model):
|
||||||
|
|
||||||
old_id_meta_product = models.PositiveIntegerField(_('old id metadata product'),
|
old_id_meta_product = models.PositiveIntegerField(_('old id metadata product'),
|
||||||
blank=True, null=True, default=None)
|
blank=True, null=True, default=None)
|
||||||
|
translation = models.ForeignKey('translation.SiteInterfaceDictionary', on_delete=models.SET_NULL,
|
||||||
|
null=True, related_name='tag', verbose_name=_('Translation'))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def label_indexing(self):
|
||||||
|
base_dict = self.translation.text if self.translation and isinstance(self.translation.text, dict) else {}
|
||||||
|
index = IndexJSON()
|
||||||
|
for k, v in base_dict.items():
|
||||||
|
setattr(index, k, v)
|
||||||
|
return index
|
||||||
|
|
||||||
objects = TagQuerySet.as_manager()
|
objects = TagQuerySet.as_manager()
|
||||||
|
|
||||||
|
|
@ -88,7 +95,7 @@ class TagCategoryQuerySet(models.QuerySet):
|
||||||
|
|
||||||
def with_base_related(self):
|
def with_base_related(self):
|
||||||
"""Select related objects."""
|
"""Select related objects."""
|
||||||
return self.prefetch_related('tags')
|
return self.prefetch_related('tags', 'tags__translation').select_related('translation')
|
||||||
|
|
||||||
def with_extended_related(self):
|
def with_extended_related(self):
|
||||||
"""Select related objects."""
|
"""Select related objects."""
|
||||||
|
|
@ -119,7 +126,7 @@ class TagCategoryQuerySet(models.QuerySet):
|
||||||
return self.exclude(tags__isnull=switcher)
|
return self.exclude(tags__isnull=switcher)
|
||||||
|
|
||||||
|
|
||||||
class TagCategory(TranslatedFieldsMixin, models.Model):
|
class TagCategory(models.Model):
|
||||||
"""Tag base category model."""
|
"""Tag base category model."""
|
||||||
|
|
||||||
STRING = 'string'
|
STRING = 'string'
|
||||||
|
|
@ -137,10 +144,6 @@ class TagCategory(TranslatedFieldsMixin, models.Model):
|
||||||
(PERCENTAGE, _('percentage')),
|
(PERCENTAGE, _('percentage')),
|
||||||
(BOOLEAN, _('boolean')),
|
(BOOLEAN, _('boolean')),
|
||||||
)
|
)
|
||||||
|
|
||||||
label = TJSONField(blank=True, null=True, default=None,
|
|
||||||
verbose_name=_('label'),
|
|
||||||
help_text='{"en-GB":"some text"}')
|
|
||||||
country = models.ForeignKey('location.Country',
|
country = models.ForeignKey('location.Country',
|
||||||
on_delete=models.SET_NULL, null=True,
|
on_delete=models.SET_NULL, null=True,
|
||||||
default=None)
|
default=None)
|
||||||
|
|
@ -151,6 +154,16 @@ class TagCategory(TranslatedFieldsMixin, models.Model):
|
||||||
value_type = models.CharField(_('value type'), max_length=255,
|
value_type = models.CharField(_('value type'), max_length=255,
|
||||||
choices=VALUE_TYPE_CHOICES, default=LIST, )
|
choices=VALUE_TYPE_CHOICES, default=LIST, )
|
||||||
old_id = models.IntegerField(blank=True, null=True)
|
old_id = models.IntegerField(blank=True, null=True)
|
||||||
|
translation = models.OneToOneField('translation.SiteInterfaceDictionary', on_delete=models.SET_NULL,
|
||||||
|
null=True, related_name='tag_category', verbose_name=_('Translation'))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def label_indexing(self):
|
||||||
|
base_dict = self.translation.text if self.translation and isinstance(self.translation.text, dict) else {}
|
||||||
|
index = IndexJSON()
|
||||||
|
for k, v in base_dict.items():
|
||||||
|
setattr(index, k, v)
|
||||||
|
return index
|
||||||
|
|
||||||
objects = TagCategoryQuerySet.as_manager()
|
objects = TagCategoryQuerySet.as_manager()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,25 @@
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.fields import SerializerMethodField
|
from rest_framework.fields import SerializerMethodField
|
||||||
|
|
||||||
from establishment.models import Establishment
|
from establishment.models import Establishment, EstablishmentType
|
||||||
from establishment.models import EstablishmentType
|
|
||||||
from news.models import News
|
from news.models import News
|
||||||
from news.models import NewsType
|
from news.models import NewsType
|
||||||
from tag import models
|
from tag import models
|
||||||
from utils.exceptions import BindingObjectNotFound
|
from utils.exceptions import BindingObjectNotFound, ObjectAlreadyAdded, RemovedBindingObjectNotFound
|
||||||
from utils.exceptions import ObjectAlreadyAdded
|
|
||||||
from utils.exceptions import RemovedBindingObjectNotFound
|
|
||||||
from utils.serializers import TranslatedField
|
from utils.serializers import TranslatedField
|
||||||
|
from utils.models import get_default_locale, get_language, to_locale
|
||||||
|
|
||||||
|
|
||||||
|
def translate_obj(obj):
|
||||||
|
if not obj.translation or not isinstance(obj.translation.text, dict):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
field = obj.translation.text
|
||||||
|
return field.get(to_locale(get_language()),
|
||||||
|
field.get(get_default_locale(),
|
||||||
|
next(iter(field.values()))))
|
||||||
|
except StopIteration:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class TagBaseSerializer(serializers.ModelSerializer):
|
class TagBaseSerializer(serializers.ModelSerializer):
|
||||||
|
|
@ -19,8 +29,11 @@ class TagBaseSerializer(serializers.ModelSerializer):
|
||||||
def get_extra_kwargs(self):
|
def get_extra_kwargs(self):
|
||||||
return super().get_extra_kwargs()
|
return super().get_extra_kwargs()
|
||||||
|
|
||||||
label_translated = TranslatedField()
|
|
||||||
index_name = serializers.CharField(source='value', read_only=True, allow_null=True)
|
index_name = serializers.CharField(source='value', read_only=True, allow_null=True)
|
||||||
|
label_translated = serializers.SerializerMethodField(read_only=True, allow_null=True)
|
||||||
|
|
||||||
|
def get_label_translated(self, obj):
|
||||||
|
return translate_obj(obj)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
@ -36,6 +49,8 @@ class TagBaseSerializer(serializers.ModelSerializer):
|
||||||
class TagBackOfficeSerializer(TagBaseSerializer):
|
class TagBackOfficeSerializer(TagBaseSerializer):
|
||||||
"""Serializer for Tag model for Back office users."""
|
"""Serializer for Tag model for Back office users."""
|
||||||
|
|
||||||
|
label = serializers.DictField(source='translation.text')
|
||||||
|
|
||||||
class Meta(TagBaseSerializer.Meta):
|
class Meta(TagBaseSerializer.Meta):
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
||||||
|
|
@ -48,7 +63,8 @@ class TagBackOfficeSerializer(TagBaseSerializer):
|
||||||
class TagCategoryProductSerializer(serializers.ModelSerializer):
|
class TagCategoryProductSerializer(serializers.ModelSerializer):
|
||||||
"""SHORT Serializer for TagCategory"""
|
"""SHORT Serializer for TagCategory"""
|
||||||
|
|
||||||
label_translated = TranslatedField()
|
def get_label_translated(self, obj):
|
||||||
|
return translate_obj(obj)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
@ -56,7 +72,6 @@ class TagCategoryProductSerializer(serializers.ModelSerializer):
|
||||||
model = models.TagCategory
|
model = models.TagCategory
|
||||||
fields = (
|
fields = (
|
||||||
'id',
|
'id',
|
||||||
'label_translated',
|
|
||||||
'index_name',
|
'index_name',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -64,8 +79,8 @@ class TagCategoryProductSerializer(serializers.ModelSerializer):
|
||||||
class TagCategoryBaseSerializer(serializers.ModelSerializer):
|
class TagCategoryBaseSerializer(serializers.ModelSerializer):
|
||||||
"""Serializer for model TagCategory."""
|
"""Serializer for model TagCategory."""
|
||||||
|
|
||||||
label_translated = TranslatedField()
|
tags = TagBaseSerializer(many=True, allow_null=True)
|
||||||
tags = SerializerMethodField()
|
label_translated = serializers.SerializerMethodField(read_only=True, allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
@ -78,33 +93,17 @@ class TagCategoryBaseSerializer(serializers.ModelSerializer):
|
||||||
'tags',
|
'tags',
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_tags(self, obj):
|
def get_label_translated(self, obj):
|
||||||
query_params = dict(self.context['request'].query_params)
|
return translate_obj(obj)
|
||||||
|
|
||||||
if len(query_params) > 1:
|
|
||||||
return []
|
|
||||||
|
|
||||||
params = {}
|
|
||||||
if 'establishment_type' in query_params:
|
|
||||||
params = {
|
|
||||||
'establishments__isnull': False,
|
|
||||||
}
|
|
||||||
elif 'product_type' in query_params:
|
|
||||||
params = {
|
|
||||||
'products__isnull': False,
|
|
||||||
}
|
|
||||||
|
|
||||||
tags = obj.tags.filter(**params).distinct()
|
|
||||||
return TagBaseSerializer(instance=tags, many=True, read_only=True).data
|
|
||||||
|
|
||||||
|
|
||||||
class FiltersTagCategoryBaseSerializer(serializers.ModelSerializer):
|
class FiltersTagCategoryBaseSerializer(serializers.ModelSerializer):
|
||||||
"""Serializer for model TagCategory."""
|
"""Serializer for model TagCategory."""
|
||||||
|
|
||||||
label_translated = TranslatedField()
|
|
||||||
filters = SerializerMethodField()
|
filters = SerializerMethodField()
|
||||||
param_name = SerializerMethodField()
|
param_name = SerializerMethodField()
|
||||||
type = SerializerMethodField()
|
type = SerializerMethodField()
|
||||||
|
label_translated = serializers.SerializerMethodField(allow_null=True, read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
@ -127,6 +126,9 @@ class FiltersTagCategoryBaseSerializer(serializers.ModelSerializer):
|
||||||
return 'wine_colors_id__in'
|
return 'wine_colors_id__in'
|
||||||
return 'tags_id__in'
|
return 'tags_id__in'
|
||||||
|
|
||||||
|
def get_label_translated(self, obj):
|
||||||
|
return translate_obj(obj)
|
||||||
|
|
||||||
def get_fields(self, *args, **kwargs):
|
def get_fields(self, *args, **kwargs):
|
||||||
fields = super(FiltersTagCategoryBaseSerializer, self).get_fields()
|
fields = super(FiltersTagCategoryBaseSerializer, self).get_fields()
|
||||||
|
|
||||||
|
|
@ -157,10 +159,13 @@ class FiltersTagCategoryBaseSerializer(serializers.ModelSerializer):
|
||||||
class TagCategoryShortSerializer(serializers.ModelSerializer):
|
class TagCategoryShortSerializer(serializers.ModelSerializer):
|
||||||
"""Serializer for model TagCategory."""
|
"""Serializer for model TagCategory."""
|
||||||
|
|
||||||
label_translated = TranslatedField()
|
label_translated = serializers.SerializerMethodField(allow_null=True, read_only=True)
|
||||||
value_type_display = serializers.CharField(source='get_value_type_display',
|
value_type_display = serializers.CharField(source='get_value_type_display',
|
||||||
read_only=True)
|
read_only=True)
|
||||||
|
|
||||||
|
def get_label_translated(self, obj):
|
||||||
|
return translate_obj(obj)
|
||||||
|
|
||||||
class Meta(TagCategoryBaseSerializer.Meta):
|
class Meta(TagCategoryBaseSerializer.Meta):
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
fields = [
|
fields = [
|
||||||
|
|
@ -173,6 +178,7 @@ class TagCategoryBackOfficeDetailSerializer(TagCategoryBaseSerializer):
|
||||||
"""Tag Category detail serializer for back-office users."""
|
"""Tag Category detail serializer for back-office users."""
|
||||||
|
|
||||||
country_translated = TranslatedField(source='country.name_translated')
|
country_translated = TranslatedField(source='country.name_translated')
|
||||||
|
label = serializers.DictField(source='translation.text')
|
||||||
|
|
||||||
class Meta(TagCategoryBaseSerializer.Meta):
|
class Meta(TagCategoryBaseSerializer.Meta):
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from datetime import time
|
from datetime import time, datetime
|
||||||
|
|
||||||
from utils.models import ProjectBaseMixin
|
from utils.models import ProjectBaseMixin
|
||||||
|
|
||||||
|
|
@ -35,6 +35,22 @@ class Timetable(ProjectBaseMixin):
|
||||||
opening_at = models.TimeField(verbose_name=_('Opening time'), null=True)
|
opening_at = models.TimeField(verbose_name=_('Opening time'), null=True)
|
||||||
closed_at = models.TimeField(verbose_name=_('Closed time'), null=True)
|
closed_at = models.TimeField(verbose_name=_('Closed time'), null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta class."""
|
||||||
|
verbose_name = _('Timetable')
|
||||||
|
verbose_name_plural = _('Timetables')
|
||||||
|
ordering = ['weekday']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Overridden str dunder."""
|
||||||
|
return f'{self.get_weekday_display()} ' \
|
||||||
|
f'(closed_at - {self.closed_at_str}, ' \
|
||||||
|
f'opening_at - {self.opening_at_str}, ' \
|
||||||
|
f'opening_time - {self.opening_time}, ' \
|
||||||
|
f'ending_time - {self.ending_time}, ' \
|
||||||
|
f'works_at_noon - {self.works_at_noon}, ' \
|
||||||
|
f'works_at_afternoon: {self.works_at_afternoon})'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def closed_at_str(self):
|
def closed_at_str(self):
|
||||||
return str(self.closed_at) if self.closed_at else None
|
return str(self.closed_at) if self.closed_at else None
|
||||||
|
|
@ -43,6 +59,14 @@ class Timetable(ProjectBaseMixin):
|
||||||
def opening_at_str(self):
|
def opening_at_str(self):
|
||||||
return str(self.opening_at) if self.opening_at else None
|
return str(self.opening_at) if self.opening_at else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def closed_at_indexing(self):
|
||||||
|
return datetime.combine(time=self.closed_at, date=datetime(1970, 1, 1 + self.weekday).date()) if self.closed_at else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def opening_at_indexing(self):
|
||||||
|
return datetime.combine(time=self.opening_at, date=datetime(1970, 1, 1 + self.weekday).date()) if self.opening_at else None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def opening_time(self):
|
def opening_time(self):
|
||||||
return self.opening_at or self.lunch_start or self.dinner_start
|
return self.opening_at or self.lunch_start or self.dinner_start
|
||||||
|
|
@ -58,9 +82,3 @@ class Timetable(ProjectBaseMixin):
|
||||||
@property
|
@property
|
||||||
def works_at_afternoon(self):
|
def works_at_afternoon(self):
|
||||||
return bool(self.ending_time and self.ending_time > self.NOON)
|
return bool(self.ending_time and self.ending_time > self.NOON)
|
||||||
|
|
||||||
class Meta:
|
|
||||||
"""Meta class."""
|
|
||||||
verbose_name = _('Timetable')
|
|
||||||
verbose_name_plural = _('Timetables')
|
|
||||||
ordering = ['weekday']
|
|
||||||
|
|
|
||||||
|
|
@ -4,101 +4,89 @@ from account.models import User
|
||||||
from gallery.models import Image
|
from gallery.models import Image
|
||||||
from location.models import Country
|
from location.models import Country
|
||||||
from news.models import News, NewsGallery
|
from news.models import News, NewsGallery
|
||||||
from tag.models import Tag
|
|
||||||
from transfer.models import PageMetadata
|
|
||||||
from utils.legacy_parser import parse_legacy_news_content
|
from utils.legacy_parser import parse_legacy_news_content
|
||||||
|
|
||||||
|
|
||||||
class NewsSerializer(serializers.Serializer):
|
class NewsSerializer(serializers.Serializer):
|
||||||
id = serializers.IntegerField()
|
|
||||||
account_id = serializers.IntegerField(allow_null=True)
|
|
||||||
tag_cat_id = serializers.IntegerField()
|
|
||||||
news_type_id = serializers.IntegerField()
|
|
||||||
news_title = serializers.CharField()
|
|
||||||
title = serializers.CharField()
|
|
||||||
summary = serializers.CharField(allow_null=True, allow_blank=True)
|
|
||||||
body = serializers.CharField(allow_null=True)
|
|
||||||
created_at = serializers.DateTimeField(format='%m-%d-%Y %H:%M:%S')
|
|
||||||
slug = serializers.CharField()
|
|
||||||
state = serializers.CharField()
|
|
||||||
template = serializers.CharField()
|
|
||||||
country_code = serializers.CharField(allow_null=True)
|
|
||||||
locale = serializers.CharField()
|
locale = serializers.CharField()
|
||||||
image = serializers.CharField()
|
page__id = serializers.IntegerField()
|
||||||
tags = serializers.CharField(allow_null=True)
|
news_type_id = serializers.IntegerField()
|
||||||
|
page__created_at = serializers.DateTimeField(format='%m-%d-%Y %H:%M:%S')
|
||||||
def create(self, validated_data):
|
page__account_id = serializers.IntegerField(allow_null=True)
|
||||||
|
page__state = serializers.CharField()
|
||||||
|
page__template = serializers.CharField()
|
||||||
|
page__site__country_code_2 = serializers.CharField(allow_null=True)
|
||||||
|
slug = serializers.CharField()
|
||||||
|
body = serializers.CharField(allow_null=True)
|
||||||
|
title = serializers.CharField()
|
||||||
|
page__root_title = serializers.CharField()
|
||||||
|
summary = serializers.CharField(allow_null=True, allow_blank=True)
|
||||||
|
page__attachment_suffix_url = serializers.CharField()
|
||||||
|
page__published_at = serializers.DateTimeField(format='%m-%d-%Y %H:%M:%S', allow_null=True)
|
||||||
|
|
||||||
|
def create(self, data):
|
||||||
|
account = self.get_account(data)
|
||||||
payload = {
|
payload = {
|
||||||
'old_id': validated_data['id'],
|
'old_id': data['page__id'],
|
||||||
'news_type_id': validated_data['news_type_id'],
|
'news_type_id': data['news_type_id'],
|
||||||
'title': {validated_data['locale']: validated_data['news_title']},
|
'created': data['page__created_at'],
|
||||||
'subtitle': self.get_subtitle(validated_data),
|
'created_by': account,
|
||||||
'description': self.get_description(validated_data),
|
'modified_by': account,
|
||||||
'start': validated_data['created_at'],
|
'state': self.get_state(data),
|
||||||
'slugs': self.get_slugs(validated_data),
|
'template': self.get_template(data),
|
||||||
'state': self.get_state(validated_data),
|
'country': self.get_country(data),
|
||||||
'template': self.get_template(validated_data),
|
'slugs': {data['locale']: data['slug']},
|
||||||
'country': self.get_country(validated_data),
|
'description': self.get_description(data),
|
||||||
'created_by': self.get_account(validated_data),
|
'title': {data['locale']: data['title']},
|
||||||
'modified_by': self.get_account(validated_data),
|
'backoffice_title': data['page__root_title'],
|
||||||
|
'subtitle': self.get_subtitle(data),
|
||||||
|
'locale_to_description_is_active': {data['locale']: True},
|
||||||
|
'publication_date': self.get_publication_date(data),
|
||||||
|
'publication_time': self.get_publication_time(data),
|
||||||
}
|
}
|
||||||
|
|
||||||
obj, _ = News.objects.update_or_create(
|
obj, created = News.objects.get_or_create(
|
||||||
old_id=validated_data['id'],
|
old_id=payload['old_id'],
|
||||||
defaults=payload,
|
defaults=payload,
|
||||||
)
|
)
|
||||||
|
if not created:
|
||||||
|
obj.slugs.update(payload['slugs'])
|
||||||
|
obj.title.update(payload['title'])
|
||||||
|
obj.locale_to_description_is_active.update(payload['locale_to_description_is_active'])
|
||||||
|
|
||||||
|
if obj.description and payload['description']:
|
||||||
|
obj.description.update(payload['description'])
|
||||||
|
else:
|
||||||
|
obj.description = payload['description']
|
||||||
|
|
||||||
|
if obj.subtitle and payload['subtitle']:
|
||||||
|
obj.subtitle.update(payload['subtitle'])
|
||||||
|
else:
|
||||||
|
obj.subtitle = payload['subtitle']
|
||||||
|
|
||||||
tags = self.get_tags(validated_data)
|
|
||||||
for tag in tags:
|
|
||||||
obj.tags.add(tag)
|
|
||||||
obj.save()
|
obj.save()
|
||||||
|
|
||||||
self.make_gallery(validated_data, obj)
|
self.make_gallery(data, obj)
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def make_gallery(data, obj):
|
def get_publication_date(data):
|
||||||
if not data['image'] or data['image'] == 'default/missing.png':
|
published_at = data.get('page__published_at')
|
||||||
return
|
if published_at:
|
||||||
|
return published_at.date()
|
||||||
img = Image.objects.create(
|
|
||||||
image=data['image'],
|
|
||||||
title=data['news_title'],
|
|
||||||
)
|
|
||||||
NewsGallery.objects.create(
|
|
||||||
news=obj,
|
|
||||||
image=img,
|
|
||||||
is_main=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_tags(data):
|
|
||||||
results = []
|
|
||||||
if not data['tags']:
|
|
||||||
return results
|
|
||||||
|
|
||||||
meta_ids = (int(_id) for _id in data['tags'].split(','))
|
|
||||||
tags = PageMetadata.objects.filter(
|
|
||||||
id__in=meta_ids,
|
|
||||||
key='tag',
|
|
||||||
value__isnull=False,
|
|
||||||
)
|
|
||||||
for old_tag in tags:
|
|
||||||
tag, _ = Tag.objects.get_or_create(
|
|
||||||
category_id=data['tag_cat_id'],
|
|
||||||
label={data['locale']: old_tag.value},
|
|
||||||
)
|
|
||||||
results.append(tag)
|
|
||||||
return results
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_description(data):
|
|
||||||
if data['body']:
|
|
||||||
content = parse_legacy_news_content(data['body'])
|
|
||||||
return {data['locale']: content}
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_publication_time(data):
|
||||||
|
published_at = data.get('page__published_at')
|
||||||
|
if published_at:
|
||||||
|
return published_at.time()
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_account(data):
|
||||||
|
return User.objects.filter(old_id=data['page__account_id']).first()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_state(data):
|
def get_state(data):
|
||||||
states = {
|
states = {
|
||||||
|
|
@ -108,37 +96,47 @@ class NewsSerializer(serializers.Serializer):
|
||||||
'published_exclusive': News.PUBLISHED_EXCLUSIVE,
|
'published_exclusive': News.PUBLISHED_EXCLUSIVE,
|
||||||
'scheduled_exclusively': News.WAITING,
|
'scheduled_exclusively': News.WAITING,
|
||||||
}
|
}
|
||||||
return states.get(data['state'], News.WAITING)
|
return states.get(data['page__state'], News.WAITING)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_template(data):
|
def get_template(data):
|
||||||
templates = {
|
templates = {
|
||||||
'main': News.MAIN,
|
'main': News.MAIN,
|
||||||
'main.pdf.erb': News.MAIN_PDF_ERB,
|
'main.pdf.erb': News.MAIN_PDF_ERB,
|
||||||
|
'newspaper': News.NEWSPAPER,
|
||||||
}
|
}
|
||||||
return templates.get(data['template'], News.MAIN)
|
return templates.get(data['page__template'], News.MAIN)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_country(data):
|
def get_country(data):
|
||||||
return Country.objects.filter(code__iexact=data['country_code']).first()
|
return Country.objects.filter(code__iexact=data['page__site__country_code_2']).first()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_title(data):
|
def get_description(data):
|
||||||
return {data['locale']: data['title']}
|
if data['body']:
|
||||||
|
content = parse_legacy_news_content(data['body'])
|
||||||
|
return {data['locale']: content}
|
||||||
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_subtitle(data):
|
def get_subtitle(data):
|
||||||
if data.get('summary'):
|
if data.get('summary'):
|
||||||
content = {data['locale']: data['summary']}
|
return {data['locale']: data['summary']}
|
||||||
else:
|
return None
|
||||||
content = {data['locale']: data['title']}
|
|
||||||
return content
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_slugs(data):
|
def make_gallery(data, obj):
|
||||||
return {data['locale']: data['slug']}
|
if not data['page__attachment_suffix_url'] or data['page__attachment_suffix_url'] == 'default/missing.png':
|
||||||
|
return
|
||||||
|
|
||||||
@staticmethod
|
img, _ = Image.objects.get_or_create(
|
||||||
def get_account(data):
|
image=data['page__attachment_suffix_url'],
|
||||||
"""Get account"""
|
title=data['page__root_title'],
|
||||||
return User.objects.filter(old_id=data['account_id']).first()
|
created=data['page__created_at']
|
||||||
|
)
|
||||||
|
|
||||||
|
gal, _ = NewsGallery.objects.get_or_create(
|
||||||
|
news=obj,
|
||||||
|
image=img,
|
||||||
|
is_main=True,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ from django.utils.text import slugify
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from tag.models import Tag
|
from tag.models import Tag
|
||||||
|
from translation.models import SiteInterfaceDictionary
|
||||||
from transfer.mixins import TransferSerializerMixin
|
from transfer.mixins import TransferSerializerMixin
|
||||||
from transfer.models import Cepages
|
from transfer.models import Cepages
|
||||||
|
|
||||||
|
|
@ -36,8 +37,11 @@ class AssemblageTagSerializer(TransferSerializerMixin):
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
qs = self.Meta.model.objects.filter(**validated_data)
|
qs = self.Meta.model.objects.filter(**validated_data)
|
||||||
category = validated_data.get('category')
|
category = validated_data.get('category')
|
||||||
|
translations = validated_data.pop('label')
|
||||||
if not qs.exists() and category:
|
if not qs.exists() and category:
|
||||||
return super().create(validated_data)
|
instance = super().create(validated_data)
|
||||||
|
SiteInterfaceDictionary.objects.update_or_create_for_tag(instance, translations)
|
||||||
|
return instance
|
||||||
|
|
||||||
def get_tag_value(self, cepage, percent):
|
def get_tag_value(self, cepage, percent):
|
||||||
if cepage and percent:
|
if cepage and percent:
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
from django.contrib.postgres.fields import JSONField
|
from django.contrib.postgres.fields import JSONField
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.apps import apps
|
||||||
from utils.models import ProjectBaseMixin, LocaleManagerMixin
|
from utils.models import ProjectBaseMixin, LocaleManagerMixin
|
||||||
|
|
||||||
|
|
||||||
class LanguageQuerySet(models.QuerySet):
|
class LanguageQuerySet(models.QuerySet):
|
||||||
"""QuerySet for model Language"""
|
"""QuerySet for model Language"""
|
||||||
|
|
||||||
|
|
@ -50,6 +50,44 @@ class Language(models.Model):
|
||||||
class SiteInterfaceDictionaryManager(LocaleManagerMixin):
|
class SiteInterfaceDictionaryManager(LocaleManagerMixin):
|
||||||
"""Extended manager for SiteInterfaceDictionary model."""
|
"""Extended manager for SiteInterfaceDictionary model."""
|
||||||
|
|
||||||
|
def update_or_create_for_tag(self, tag, translations: dict):
|
||||||
|
Tag = apps.get_model('tag', 'Tag')
|
||||||
|
"""Creates or updates translation for EXISTING in DB Tag"""
|
||||||
|
if not tag.pk or not isinstance(tag, Tag):
|
||||||
|
raise NotImplementedError
|
||||||
|
if tag.translation:
|
||||||
|
tag.translation.text = translations
|
||||||
|
tag.translation.page = 'tag'
|
||||||
|
tag.translation.keywords = f'tag-{tag.pk}'
|
||||||
|
else:
|
||||||
|
trans = SiteInterfaceDictionary({
|
||||||
|
'text': translations,
|
||||||
|
'page': 'tag',
|
||||||
|
'keywords': f'tag-{tag.pk}'
|
||||||
|
})
|
||||||
|
trans.save()
|
||||||
|
tag.translation = trans
|
||||||
|
tag.save()
|
||||||
|
|
||||||
|
def update_or_create_for_tag_category(self, tag_category, translations: dict):
|
||||||
|
"""Creates or updates translation for EXISTING in DB TagCategory"""
|
||||||
|
TagCategory = apps.get_model('tag', 'TagCategory')
|
||||||
|
if not tag_category.pk or not isinstance(tag_category, TagCategory):
|
||||||
|
raise NotImplementedError
|
||||||
|
if tag_category.translation:
|
||||||
|
tag_category.translation.text = translations
|
||||||
|
tag_category.translation.page = 'tag'
|
||||||
|
tag_category.translation.keywords = f'tag_category-{tag_category.pk}'
|
||||||
|
else:
|
||||||
|
trans = SiteInterfaceDictionary({
|
||||||
|
'text': translations,
|
||||||
|
'page': 'tag',
|
||||||
|
'keywords': f'tag_category-{tag_category.pk}'
|
||||||
|
})
|
||||||
|
trans.save()
|
||||||
|
tag_category.translation = trans
|
||||||
|
tag_category.save()
|
||||||
|
|
||||||
|
|
||||||
class SiteInterfaceDictionary(ProjectBaseMixin):
|
class SiteInterfaceDictionary(ProjectBaseMixin):
|
||||||
"""Site interface dictionary model."""
|
"""Site interface dictionary model."""
|
||||||
|
|
|
||||||
115
apps/utils/export.py
Normal file
115
apps/utils/export.py
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
import csv
|
||||||
|
import xlsxwriter
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
from smtplib import SMTPException
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.mail import EmailMultiAlternatives
|
||||||
|
|
||||||
|
logging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SendExport:
|
||||||
|
|
||||||
|
def __init__(self, user, panel, file_type='csv'):
|
||||||
|
self.type_mapper = {
|
||||||
|
"csv": self.make_csv_file,
|
||||||
|
"xls": self.make_xls_file
|
||||||
|
}
|
||||||
|
self.file_type = file_type
|
||||||
|
self.user = user
|
||||||
|
self.panel = panel
|
||||||
|
self.email_from = settings.EMAIL_HOST_USER
|
||||||
|
self.email_subject = f'Export panel: {self.get_file_name()}'
|
||||||
|
self.email_body = 'Exported panel data'
|
||||||
|
self.get_file_method = self.type_mapper[file_type]
|
||||||
|
self.file_path = os.path.join(
|
||||||
|
settings.STATIC_ROOT,
|
||||||
|
'email', tempfile.gettempdir(),
|
||||||
|
self.get_file_name()
|
||||||
|
)
|
||||||
|
self.success = False
|
||||||
|
|
||||||
|
def get_file_name(self):
|
||||||
|
name = '_'.join(self.panel.name.split(' '))
|
||||||
|
return f'export_{name.lower()}.{self.file_type}'
|
||||||
|
|
||||||
|
def get_data(self):
|
||||||
|
return self.panel.get_data()
|
||||||
|
|
||||||
|
def get_headers(self):
|
||||||
|
try:
|
||||||
|
header = self.panel.get_headers()
|
||||||
|
self.success = True
|
||||||
|
return header
|
||||||
|
except Exception as err:
|
||||||
|
logger.info(f'HEADER:{err}')
|
||||||
|
|
||||||
|
def make_csv_file(self):
|
||||||
|
file_header = self.get_headers()
|
||||||
|
if not self.success:
|
||||||
|
return
|
||||||
|
with open(self.file_path, 'w') as f:
|
||||||
|
file_writer = csv.writer(f, quotechar='"', quoting=csv.QUOTE_MINIMAL)
|
||||||
|
# Write headers to CSV file
|
||||||
|
file_writer.writerow(file_header)
|
||||||
|
for row in self.get_data():
|
||||||
|
file_writer.writerow(row)
|
||||||
|
|
||||||
|
def make_xls_file(self):
|
||||||
|
headings = self.get_headers()
|
||||||
|
if not self.success:
|
||||||
|
return
|
||||||
|
with xlsxwriter.Workbook(self.file_path) as workbook:
|
||||||
|
worksheet = workbook.add_worksheet()
|
||||||
|
|
||||||
|
# Add a bold format to use to highlight cells.
|
||||||
|
bold = workbook.add_format({'bold': True})
|
||||||
|
|
||||||
|
# Add the worksheet data that the charts will refer to.
|
||||||
|
data = self.get_data()
|
||||||
|
|
||||||
|
worksheet.write_row('A1', headings, bold)
|
||||||
|
for n, row in enumerate(data):
|
||||||
|
worksheet.write_row(f'A{n+2}', [str(i) for i in row])
|
||||||
|
workbook.close()
|
||||||
|
|
||||||
|
def send(self):
|
||||||
|
self.get_file_method()
|
||||||
|
print(f'ok: {self.file_path}')
|
||||||
|
self.send_email()
|
||||||
|
|
||||||
|
def get_file(self):
|
||||||
|
if os.path.exists(self.file_path) and os.path.isfile(self.file_path):
|
||||||
|
with open(self.file_path, 'rb') as export_file:
|
||||||
|
return export_file
|
||||||
|
else:
|
||||||
|
logger.info('COMMUTATOR:image file not found dir: {path}')
|
||||||
|
|
||||||
|
def send_email(self):
|
||||||
|
|
||||||
|
msg = EmailMultiAlternatives(
|
||||||
|
subject=self.email_subject,
|
||||||
|
body=self.email_body,
|
||||||
|
from_email=self.email_from,
|
||||||
|
to=[
|
||||||
|
self.user.email,
|
||||||
|
'kuzmenko.da@gmail.com',
|
||||||
|
'sinapsit@yandex.ru'
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create an inline attachment
|
||||||
|
if self.file_path and self.success:
|
||||||
|
msg.attach_file(self.file_path)
|
||||||
|
else:
|
||||||
|
msg.body = 'An error occurred while executing the request.'
|
||||||
|
|
||||||
|
try:
|
||||||
|
msg.send()
|
||||||
|
logger.debug(f"COMMUTATOR:Email successfully sent")
|
||||||
|
except SMTPException as e:
|
||||||
|
logger.error(f"COMMUTATOR:Email connector: {e}")
|
||||||
|
|
@ -365,16 +365,12 @@ class GMTokenGenerator(PasswordResetTokenGenerator):
|
||||||
return self.get_fields(user, timestamp)
|
return self.get_fields(user, timestamp)
|
||||||
|
|
||||||
|
|
||||||
class GalleryModelMixin(models.Model):
|
class GalleryMixin:
|
||||||
"""Mixin for models that has gallery."""
|
"""Mixin for models that has gallery."""
|
||||||
|
|
||||||
class Meta:
|
|
||||||
"""Meta class."""
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def crop_gallery(self):
|
def crop_gallery(self):
|
||||||
if hasattr(self, 'gallery'):
|
if hasattr(self, 'gallery') and hasattr(self, '_meta'):
|
||||||
gallery = []
|
gallery = []
|
||||||
images = self.gallery.all()
|
images = self.gallery.all()
|
||||||
crop_parameters = [p for p in settings.SORL_THUMBNAIL_ALIASES
|
crop_parameters = [p for p in settings.SORL_THUMBNAIL_ALIASES
|
||||||
|
|
@ -394,7 +390,8 @@ class GalleryModelMixin(models.Model):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def crop_main_image(self):
|
def crop_main_image(self):
|
||||||
if hasattr(self, 'main_image') and self.main_image:
|
if hasattr(self, 'main_image') and hasattr(self, '_meta'):
|
||||||
|
if self.main_image:
|
||||||
image = self.main_image
|
image = self.main_image
|
||||||
image_property = {
|
image_property = {
|
||||||
'id': image.id,
|
'id': image.id,
|
||||||
|
|
@ -443,7 +440,8 @@ class HasTagsMixin(models.Model):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def visible_tags(self):
|
def visible_tags(self):
|
||||||
return self.tags.filter(category__public=True).prefetch_related('category')\
|
return self.tags.filter(category__public=True).prefetch_related('category',
|
||||||
|
'translation', 'category__translation')\
|
||||||
.exclude(category__value_type='bool')
|
.exclude(category__value_type='bool')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
@ -459,40 +457,14 @@ class FavoritesMixin:
|
||||||
return self.favorites.aggregate(arr=ArrayAgg('user_id')).get('arr')
|
return self.favorites.aggregate(arr=ArrayAgg('user_id')).get('arr')
|
||||||
|
|
||||||
|
|
||||||
class RelatedInstanceMixin:
|
|
||||||
"""Mixin for getting related objects."""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _related_objects(self) -> list:
|
|
||||||
"""Return list of related objects."""
|
|
||||||
if hasattr(self, '_meta'):
|
|
||||||
related_objects = []
|
|
||||||
for related_object in self._meta.related_objects:
|
|
||||||
related_objects.append(related_object)
|
|
||||||
return related_objects
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _related_instances(self) -> set:
|
|
||||||
"""Return list of related instances."""
|
|
||||||
if hasattr(self, '_related_objects'):
|
|
||||||
related_instances = []
|
|
||||||
related_names = [related_object.related_name or f'{related_object.name}_set'
|
|
||||||
if related_object.multiple
|
|
||||||
else f'{related_object.name}'
|
|
||||||
for related_object in self._related_objects]
|
|
||||||
for related_object in related_names:
|
|
||||||
try:
|
|
||||||
instances = getattr(self, f'{related_object}')
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
if hasattr(instances, 'exists') and instances.exists():
|
|
||||||
for instance in instances.all():
|
|
||||||
related_instances.append(instance)
|
|
||||||
else:
|
|
||||||
# if one object put it in list.
|
|
||||||
related_instances.append(instances)
|
|
||||||
|
|
||||||
return set(related_instances)
|
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ from authorization.models import JWTRefreshToken
|
||||||
from utils.tokens import GMRefreshToken
|
from utils.tokens import GMRefreshToken
|
||||||
from establishment.models import EstablishmentSubType
|
from establishment.models import EstablishmentSubType
|
||||||
from location.models import Address
|
from location.models import Address
|
||||||
|
from product.models import Product, ProductType
|
||||||
|
|
||||||
|
|
||||||
class IsAuthenticatedAndTokenIsValid(permissions.BasePermission):
|
class IsAuthenticatedAndTokenIsValid(permissions.BasePermission):
|
||||||
"""
|
"""
|
||||||
|
|
@ -81,32 +83,20 @@ class IsStandardUser(IsGuest):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def has_permission(self, request, view):
|
def has_permission(self, request, view):
|
||||||
rules = [
|
|
||||||
super().has_permission(request, view)
|
|
||||||
]
|
|
||||||
|
|
||||||
# and request.user.email_confirmed,
|
rules = [super().has_permission(request, view),
|
||||||
if hasattr(request, 'user'):
|
|
||||||
rules = [
|
|
||||||
request.user.is_authenticated,
|
request.user.is_authenticated,
|
||||||
super().has_permission(request, view)
|
hasattr(request, 'user')
|
||||||
]
|
]
|
||||||
|
|
||||||
return any(rules)
|
return any(rules)
|
||||||
|
|
||||||
def has_object_permission(self, request, view, obj):
|
def has_object_permission(self, request, view, obj):
|
||||||
# Read permissions are allowed to any request
|
# Read permissions are allowed to any request
|
||||||
rules = [
|
|
||||||
super().has_object_permission(request, view, obj)
|
|
||||||
]
|
|
||||||
|
|
||||||
if hasattr(obj, 'user'):
|
rules = [super().has_object_permission(request, view, obj),
|
||||||
rules = [
|
request.user.is_authenticated,
|
||||||
obj.user == request.user
|
hasattr(request, 'user')
|
||||||
and obj.user.email_confirmed
|
|
||||||
and request.user.is_authenticated,
|
|
||||||
|
|
||||||
super().has_object_permission(request, view, obj)
|
|
||||||
]
|
]
|
||||||
|
|
||||||
return any(rules)
|
return any(rules)
|
||||||
|
|
@ -408,7 +398,7 @@ class IsWineryReviewer(IsStandardUser):
|
||||||
|
|
||||||
est = EstablishmentSubType.objects.filter(establishment_type_id=request.data['type_id'])
|
est = EstablishmentSubType.objects.filter(establishment_type_id=request.data['type_id'])
|
||||||
if est.exists():
|
if est.exists():
|
||||||
role = Role.objects.filter(establishment_subtype_id__in=[type.id for type in est],
|
role = Role.objects.filter(establishment_subtype_id__in=[est_type.id for est_type in est],
|
||||||
role=Role.WINERY_REVIEWER,
|
role=Role.WINERY_REVIEWER,
|
||||||
country_id__in=[country.id for country in countries]) \
|
country_id__in=[country.id for country in countries]) \
|
||||||
.first()
|
.first()
|
||||||
|
|
@ -433,7 +423,7 @@ class IsWineryReviewer(IsStandardUser):
|
||||||
|
|
||||||
est = EstablishmentSubType.objects.filter(establishment_type_id=type_id)
|
est = EstablishmentSubType.objects.filter(establishment_type_id=type_id)
|
||||||
role = Role.objects.filter(role=Role.WINERY_REVIEWER,
|
role = Role.objects.filter(role=Role.WINERY_REVIEWER,
|
||||||
establishment_subtype_id__in=[id for type.id in est],
|
establishment_subtype_id__in=[est_type.id for est_type in est],
|
||||||
country_id=obj.country_id).first()
|
country_id=obj.country_id).first()
|
||||||
|
|
||||||
object_id: int
|
object_id: int
|
||||||
|
|
@ -449,3 +439,159 @@ class IsWineryReviewer(IsStandardUser):
|
||||||
super().has_object_permission(request, view, obj)
|
super().has_object_permission(request, view, obj)
|
||||||
]
|
]
|
||||||
return any(rules)
|
return any(rules)
|
||||||
|
|
||||||
|
|
||||||
|
class IsWineryReviewer(IsStandardUser):
|
||||||
|
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
rules = [
|
||||||
|
super().has_permission(request, view)
|
||||||
|
]
|
||||||
|
|
||||||
|
if 'type_id' in request.data and 'address_id' in request.data and request.user:
|
||||||
|
countries = Address.objects.filter(id=request.data['address_id'])
|
||||||
|
|
||||||
|
est = EstablishmentSubType.objects.filter(establishment_type_id=request.data['type_id'])
|
||||||
|
if est.exists():
|
||||||
|
role = Role.objects.filter(establishment_subtype_id__in=[est_type.id for est_type in est],
|
||||||
|
role=Role.WINERY_REVIEWER,
|
||||||
|
country_id__in=[country.id for country in countries]) \
|
||||||
|
.first()
|
||||||
|
|
||||||
|
rules.append(
|
||||||
|
UserRole.objects.filter(user=request.user, role=role).exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
return any(rules)
|
||||||
|
|
||||||
|
def has_object_permission(self, request, view, obj):
|
||||||
|
rules = [
|
||||||
|
super().has_object_permission(request, view, obj)
|
||||||
|
]
|
||||||
|
|
||||||
|
if hasattr(obj, 'type_id') or hasattr(obj, 'establishment_type_id'):
|
||||||
|
type_id: int
|
||||||
|
if hasattr(obj, 'type_id'):
|
||||||
|
type_id = obj.type_id
|
||||||
|
else:
|
||||||
|
type_id = obj.establishment_type_id
|
||||||
|
|
||||||
|
est = EstablishmentSubType.objects.filter(establishment_type_id=type_id)
|
||||||
|
role = Role.objects.filter(role=Role.WINERY_REVIEWER,
|
||||||
|
establishment_subtype_id__in=[est_type.id for est_type in est],
|
||||||
|
country_id=obj.country_id).first()
|
||||||
|
|
||||||
|
object_id: int
|
||||||
|
if hasattr(obj, 'object_id'):
|
||||||
|
object_id = obj.object_id
|
||||||
|
else:
|
||||||
|
object_id = obj.establishment_id
|
||||||
|
|
||||||
|
rules = [
|
||||||
|
UserRole.objects.filter(user=request.user, role=role,
|
||||||
|
establishment_id=object_id
|
||||||
|
).exists(),
|
||||||
|
super().has_object_permission(request, view, obj)
|
||||||
|
]
|
||||||
|
return any(rules)
|
||||||
|
|
||||||
|
|
||||||
|
class IsProductReviewer(IsStandardUser):
|
||||||
|
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
rules = [
|
||||||
|
super().has_permission(request, view)
|
||||||
|
]
|
||||||
|
|
||||||
|
pk_object = None
|
||||||
|
roles = None
|
||||||
|
permission = False
|
||||||
|
|
||||||
|
if 'site_id' in request.data:
|
||||||
|
if request.data['site_id'] is not None:
|
||||||
|
roles = Role.objects.filter(role=Role.PRODUCT_REVIEWER,
|
||||||
|
site_id=request.data['site_id'])
|
||||||
|
|
||||||
|
if 'pk' in view.kwargs:
|
||||||
|
pk_object = view.kwargs['pk']
|
||||||
|
|
||||||
|
if pk_object is not None:
|
||||||
|
product = Product.objects.get(pk=pk_object)
|
||||||
|
if product.site_id is not None:
|
||||||
|
roles = Role.objects.filter(role=Role.PRODUCT_REVIEWER,
|
||||||
|
site_id=product.site_id)
|
||||||
|
|
||||||
|
if roles is not None:
|
||||||
|
permission = UserRole.objects.filter(user=request.user, role__in=[role for role in roles])\
|
||||||
|
.exists()
|
||||||
|
|
||||||
|
rules.append(permission)
|
||||||
|
return any(rules)
|
||||||
|
|
||||||
|
|
||||||
|
class IsLiquorReviewer(IsStandardUser):
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
rules = [
|
||||||
|
super().has_permission(request, view)
|
||||||
|
]
|
||||||
|
|
||||||
|
pk_object = None
|
||||||
|
roles = None
|
||||||
|
permission = False
|
||||||
|
|
||||||
|
if 'site_id' in request.data and 'product_type_id' in request.data:
|
||||||
|
if request.data['site_id'] is not None \
|
||||||
|
and request.data['product_type_id'] is not None:
|
||||||
|
|
||||||
|
product_types = ProductType.objects. \
|
||||||
|
filter(index_name=ProductType.LIQUOR,
|
||||||
|
id=request.data['product_type_id'])
|
||||||
|
|
||||||
|
if product_types.exists():
|
||||||
|
roles = Role.objects.filter(role=Role.LIQUOR_REVIEWER,
|
||||||
|
site_id=request.data['site_id'])
|
||||||
|
|
||||||
|
if 'pk' in view.kwargs:
|
||||||
|
pk_object = view.kwargs['pk']
|
||||||
|
|
||||||
|
if pk_object is not None:
|
||||||
|
product = Product.objects.get(pk=pk_object)
|
||||||
|
if product.site_id is not None \
|
||||||
|
and product.product_type_id is not None:
|
||||||
|
|
||||||
|
product_types = ProductType.objects. \
|
||||||
|
filter(index_name=ProductType.LIQUOR,
|
||||||
|
id=product.product_type_id)
|
||||||
|
|
||||||
|
if product_types.exists():
|
||||||
|
roles = Role.objects.filter(role=Role.LIQUOR_REVIEWER,
|
||||||
|
site_id=product.site_id)
|
||||||
|
|
||||||
|
if roles is not None:
|
||||||
|
permission = UserRole.objects.filter(user=request.user, role__in=[role for role in roles])\
|
||||||
|
.exists()
|
||||||
|
|
||||||
|
rules.append(permission)
|
||||||
|
return any(rules)
|
||||||
|
|
||||||
|
#
|
||||||
|
# def has_object_permission(self, request, view, obj):
|
||||||
|
# rules = [
|
||||||
|
# super().has_object_permission(request, view, obj)
|
||||||
|
# ]
|
||||||
|
# # pk_object = None
|
||||||
|
# # product = None
|
||||||
|
# # permission = False
|
||||||
|
# #
|
||||||
|
# # if 'pk' in view.kwargs:
|
||||||
|
# # pk_object = view.kwargs['pk']
|
||||||
|
# #
|
||||||
|
# # if pk_object is not None:
|
||||||
|
# # product = Product.objects.get(pk=pk_object)
|
||||||
|
# #
|
||||||
|
# # if product.sites.exists():
|
||||||
|
# # role = Role.objects.filter(role=Role.LIQUOR_REVIEWER, site__in=[site for site in product.sites])
|
||||||
|
# # permission = UserRole.objects.filter(user=request.user, role=role).exists()
|
||||||
|
# #
|
||||||
|
# # rules.append(permission)
|
||||||
|
# return any(rules)
|
||||||
|
|
@ -53,7 +53,6 @@ class TranslateFieldTests(BaseTestCase):
|
||||||
"ru-RU": "Тестовая новость"
|
"ru-RU": "Тестовая новость"
|
||||||
},
|
},
|
||||||
description={"en-GB": "Test description"},
|
description={"en-GB": "Test description"},
|
||||||
start=datetime.now(pytz.utc) + timedelta(hours=-13),
|
|
||||||
end=datetime.now(pytz.utc) + timedelta(hours=13),
|
end=datetime.now(pytz.utc) + timedelta(hours=13),
|
||||||
news_type=self.news_type,
|
news_type=self.news_type,
|
||||||
slugs={'en-GB': 'test'},
|
slugs={'en-GB': 'test'},
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ services:
|
||||||
MYSQL_ROOT_PASSWORD: rootPassword
|
MYSQL_ROOT_PASSWORD: rootPassword
|
||||||
volumes:
|
volumes:
|
||||||
- gm-mysql_db:/var/lib/mysql
|
- gm-mysql_db:/var/lib/mysql
|
||||||
- .:/code
|
|
||||||
|
|
||||||
|
|
||||||
# PostgreSQL database
|
# PostgreSQL database
|
||||||
|
|
@ -30,7 +29,6 @@ services:
|
||||||
- "5436:5432"
|
- "5436:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- gm-db:/var/lib/postgresql/data/
|
- gm-db:/var/lib/postgresql/data/
|
||||||
- .:/code
|
|
||||||
|
|
||||||
|
|
||||||
elasticsearch:
|
elasticsearch:
|
||||||
|
|
|
||||||
|
|
@ -516,8 +516,12 @@ PHONENUMBER_DEFAULT_REGION = "FR"
|
||||||
|
|
||||||
FALLBACK_LOCALE = 'en-GB'
|
FALLBACK_LOCALE = 'en-GB'
|
||||||
|
|
||||||
ESTABLISHMENT_CHOSEN_TAGS = ['gastronomic', 'en_vogue', 'terrace', 'streetfood', 'business', 'bar_cocktail', 'brunch', 'pop']
|
ESTABLISHMENT_CHOSEN_TAGS = ['gastronomic', 'terrace', 'streetfood', 'business', 'bar_cocktail', 'brunch', 'pop']
|
||||||
NEWS_CHOSEN_TAGS = ['eat', 'drink', 'cook', 'style', 'international', 'event', 'partnership']
|
NEWS_CHOSEN_TAGS = ['eat', 'drink', 'cook', 'style', 'international', 'event', 'partnership']
|
||||||
|
ARTISANS_CHOSEN_TAGS = ['butchery', 'bakery', 'patisserie', 'cheese_shop', 'fish_shop', 'ice-cream_maker',
|
||||||
|
'wine_merchant', 'coffe_shop']
|
||||||
|
RECIPES_CHOSEN_TAGS = ['cook', 'eat', 'drink']
|
||||||
|
|
||||||
INTERNATIONAL_COUNTRY_CODES = ['www', 'main', 'next']
|
INTERNATIONAL_COUNTRY_CODES = ['www', 'main', 'next']
|
||||||
|
|
||||||
THUMBNAIL_ENGINE = 'utils.thumbnail_engine.GMEngine'
|
THUMBNAIL_ENGINE = 'utils.thumbnail_engine.GMEngine'
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,7 @@ MEDIA_ROOT = os.path.join(PUBLIC_ROOT, MEDIA_LOCATION)
|
||||||
# SORL thumbnails
|
# SORL thumbnails
|
||||||
THUMBNAIL_DEBUG = True
|
THUMBNAIL_DEBUG = True
|
||||||
|
|
||||||
# ADDED TRANSFER APP
|
|
||||||
INSTALLED_APPS.append('transfer.apps.TransferConfig')
|
|
||||||
|
|
||||||
# DATABASES
|
# DATABASES
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
|
|
@ -86,11 +85,11 @@ LOGGING = {
|
||||||
'py.warnings': {
|
'py.warnings': {
|
||||||
'handlers': ['console'],
|
'handlers': ['console'],
|
||||||
},
|
},
|
||||||
# 'django.db.backends': {
|
'django.db.backends': {
|
||||||
# 'handlers': ['console', ],
|
'handlers': ['console', ],
|
||||||
# 'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
# 'propagate': False,
|
'propagate': False,
|
||||||
# },
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,3 +63,9 @@ pycountry==19.8.18
|
||||||
|
|
||||||
# sql-tree
|
# sql-tree
|
||||||
django-mptt==0.9.1
|
django-mptt==0.9.1
|
||||||
|
|
||||||
|
# slugify
|
||||||
|
python-slugify==4.0.0
|
||||||
|
|
||||||
|
# Export to Excel
|
||||||
|
XlsxWriter==1.2.6
|
||||||
Loading…
Reference in New Issue
Block a user