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