Merge branch 'develop' into migrate-location-app

# Conflicts:
#	apps/gallery/models.py
#	requirements/base.txt
This commit is contained in:
littlewolf 2019-10-25 09:54:03 +03:00
commit 95b5d4ad99
130 changed files with 2037 additions and 366 deletions

4
.gitignore vendored
View File

@ -21,4 +21,6 @@ logs/
/geoip_db/
# dev
./docker-compose.override.yml
./docker-compose.override.yml
celerybeat-schedule

40
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,40 @@
image: docker:latest
stages:
- build
- test
- deploy
- clean
clean:
stage: clean
script:
- docker-compose -f compose-ci.yml stop
- docker-compose -f compose-ci.yml rm --force gm_app
when: always
buid:
stage: build
script:
- docker-compose -f compose-ci.yml build gm_app
when: always
test:
stage: test
script:
- docker-compose -f compose-ci.yml run gm_app python manage.py test -v 3 --noinput
when: always
deploy-develop:
stage: deploy
only:
- develop
script:
- fab --roles=develop deploy
environment:
name: Develop

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-10-02 06:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0008_auto_20190912_1325'),
]
operations = [
migrations.AlterField(
model_name='user',
name='email',
field=models.EmailField(default=None, max_length=254, null=True, unique=True, verbose_name='email address'),
),
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-10-11 13:36
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('account', '0009_auto_20191002_0648'),
('account', '0010_user_password_confirmed'),
]
operations = [
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-10-15 09:12
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('account', '0011_merge_20191014_1258'),
('account', '0011_merge_20191011_1336'),
]
operations = [
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-10-23 09:59
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('account', '0009_auto_20191002_0648'),
('account', '0013_auto_20191016_0810'),
]
operations = [
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-10-23 13:17
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('account', '0014_merge_20191023_0959'),
('account', '0012_merge_20191015_0912'),
]
operations = [
]

View File

@ -0,0 +1,24 @@
# Generated by Django 2.2.4 on 2019-10-24 08:30
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('account', '0015_merge_20191023_1317'),
]
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')], verbose_name='Role'),
),
migrations.AlterField(
model_name='userrole',
name='establishment',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='establishment.Establishment', verbose_name='Establishment'),
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 2.2.4 on 2019-10-24 08:33
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('account', '0015_merge_20191023_1317'),
]
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')], verbose_name='Role'),
),
migrations.AlterField(
model_name='userrole',
name='establishment',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='establishment.Establishment', verbose_name='Establishment'),
),
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-10-24 12:33
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('account', '0016_auto_20191024_0830'),
('account', '0016_auto_20191024_0833'),
]
operations = [
]

View File

@ -89,7 +89,7 @@ class User(AbstractUser):
blank=True, null=True, default=None)
cropped_image_url = models.URLField(verbose_name=_('Cropped image URL path'),
blank=True, null=True, default=None)
email = models.EmailField(_('email address'), blank=True,
email = models.EmailField(_('email address'), unique=True,
null=True, default=None)
unconfirmed_email = models.EmailField(_('unconfirmed email'), blank=True, null=True, default=None)
email_confirmed = models.BooleanField(_('email status'), default=False)
@ -214,6 +214,15 @@ class User(AbstractUser):
template_name=settings.RESETTING_TOKEN_TEMPLATE,
context=context)
def notify_password_changed_template(self, country_code):
"""Get notification email template"""
context = {'contry_code': country_code}
context.update(self.base_template)
return render_to_string(
template_name=settings.NOTIFICATION_PASSWORD_TEMPLATE,
context=context,
)
def confirm_email_template(self, country_code):
"""Get confirm email template"""
context = {'token': self.confirm_email_token,

View File

@ -127,6 +127,14 @@ class ChangePasswordSerializer(serializers.ModelSerializer):
except serializers.ValidationError as e:
raise serializers.ValidationError({'detail': e.detail})
else:
if settings.USE_CELERY:
tasks.send_password_changed_email(
user_id=self.instance.id,
country_code=self.context.get('request').country_code)
else:
tasks.send_password_changed_email(
user_id=self.instance.id,
country_code=self.context.get('request').country_code)
return attrs
def update(self, instance, validated_data):

View File

@ -1,8 +1,9 @@
"""Serializers for account web"""
from django.conf import settings
from django.contrib.auth import password_validation as password_validators
from rest_framework import serializers
from account import models
from account import models, tasks
from utils import exceptions as utils_exceptions
from utils.methods import username_validator
@ -68,4 +69,12 @@ class PasswordResetConfirmSerializer(serializers.ModelSerializer):
# Update user password from instance
instance.set_password(validated_data.get('password'))
instance.save()
if settings.USE_CELERY:
tasks.send_password_changed_email(
user_id=instance.id,
country_code=self.context.get('request').country_code)
else:
tasks.send_password_changed_email(
user_id=instance.id,
country_code=self.context.get('request').country_code)
return instance

View File

@ -1,47 +1,46 @@
"""Account app celery tasks."""
import inspect
import logging
from celery import shared_task
from django.utils.translation import gettext_lazy as _
from . import models
from account.models import User
logging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.INFO)
logger = logging.getLogger(__name__)
def send_email(user_id: int, subject: str, message_prop: str, country_code: str):
try:
user = User.objects.get(id=user_id)
user.send_email(subject=_(subject),
message=getattr(user, message_prop, lambda _: '')(country_code))
except:
cur_frame = inspect.currentframe()
cal_frame = inspect.getouterframes(cur_frame, 2)
logger.error(f'METHOD_NAME: {cal_frame[1][3]}\n'
f'DETAIL: Exception occurred for user: {user_id}')
@shared_task
def send_reset_password_email(user_id, country_code):
"""Send email to user for reset password."""
try:
user = models.User.objects.get(id=user_id)
user.send_email(subject=_('Password resetting'),
message=user.reset_password_template(country_code))
except:
logger.error(f'METHOD_NAME: {send_reset_password_email.__name__}\n'
f'DETAIL: Exception occurred for reset password: '
f'{user_id}')
send_email(user_id, 'Password_resetting', 'reset_password_template', country_code)
@shared_task
def confirm_new_email_address(user_id, country_code):
"""Send email to user new email."""
try:
user = models.User.objects.get(id=user_id)
user.send_email(subject=_('Validate new email address'),
message=user.confirm_email_template(country_code))
except:
logger.error(f'METHOD_NAME: {confirm_new_email_address.__name__}\n'
f'DETAIL: Exception occurred for user: {user_id}')
send_email(user_id, 'Confirm new email address', 'confirm_email_template', country_code)
@shared_task
def change_email_address(user_id, country_code):
"""Send email to user new email."""
try:
user = models.User.objects.get(id=user_id)
user.send_email(subject=_('Validate new email address'),
message=user.change_email_template(country_code))
except:
logger.error(f'METHOD_NAME: {change_email_address.__name__}\n'
f'DETAIL: Exception occurred for user: {user_id}')
send_email(user_id, 'Validate new email address', 'change_email_template', country_code)
@shared_task
def send_password_changed_email(user_id, country_code):
"""Send email which notifies user that his password had changed"""
send_email(user_id, 'Notify password changed', 'notify_password_changed_template', country_code)

View File

@ -18,12 +18,6 @@ from utils.tokens import GMRefreshToken
# Serializers
class SignupSerializer(serializers.ModelSerializer):
"""Signup serializer serializer mixin"""
# REQUEST
username = serializers.CharField(write_only=True)
password = serializers.CharField(write_only=True)
email = serializers.EmailField(write_only=True)
newsletter = serializers.BooleanField(write_only=True)
class Meta:
model = account_models.User
fields = (
@ -32,6 +26,12 @@ class SignupSerializer(serializers.ModelSerializer):
'email',
'newsletter'
)
extra_kwargs = {
'username': {'write_only': True},
'password': {'write_only': True},
'email': {'write_only': True},
'newsletter': {'write_only': True}
}
def validate_username(self, value):
"""Custom username validation"""

View File

@ -0,0 +1,44 @@
# Generated by Django 2.2.4 on 2019-10-23 07:15
from django.db import migrations
import utils.models
def fill_title_json_from_title(apps, schema_editor):
# We can't import the Person model directly as it may be a newer
# version than this migration expects. We use the historical version.
Collection = apps.get_model('collection', 'Collection')
for collection in Collection.objects.all():
collection.name_json = {'en-GB': collection.name}
collection.save()
class Migration(migrations.Migration):
dependencies = [
('collection', '0014_auto_20191022_1242'),
]
operations = [
migrations.AddField(
model_name='collection',
name='name_json',
field=utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='name'),
),
migrations.RunPython(fill_title_json_from_title, migrations.RunPython.noop),
migrations.RemoveField(
model_name='collection',
name='name',
),
migrations.RenameField(
model_name='collection',
old_name='name_json',
new_name='name',
),
migrations.AlterField(
model_name='collection',
name='name',
field=utils.models.TJSONField(help_text='{"en-GB":"some text"}', verbose_name='name'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.4 on 2019-10-24 13:34
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('collection', '0015_auto_20191023_0715'),
]
operations = [
migrations.AlterField(
model_name='guide',
name='collection',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='collection.Collection', verbose_name='collection'),
),
]

View File

@ -43,9 +43,11 @@ class CollectionQuerySet(RelatedObjectsCountMixin):
return self.filter(is_publish=True)
class Collection(ProjectBaseMixin, CollectionNameMixin, CollectionDateMixin,
class Collection(ProjectBaseMixin, CollectionDateMixin,
TranslatedFieldsMixin, URLImageMixin):
"""Collection model."""
STR_FIELD_NAME = 'name'
ORDINARY = 0 # Ordinary collection
POP = 1 # POP collection
@ -54,6 +56,8 @@ class Collection(ProjectBaseMixin, CollectionNameMixin, CollectionDateMixin,
(POP, _('Pop')),
)
name = TJSONField(verbose_name=_('name'),
help_text='{"en-GB":"some text"}')
collection_type = models.PositiveSmallIntegerField(choices=COLLECTION_TYPES,
default=ORDINARY,
verbose_name=_('Collection type'))
@ -79,10 +83,6 @@ class Collection(ProjectBaseMixin, CollectionNameMixin, CollectionDateMixin,
verbose_name = _('collection')
verbose_name_plural = _('collections')
def __str__(self):
"""String method."""
return f'{self.name}'
class GuideQuerySet(models.QuerySet):
"""QuerySet for Guide."""
@ -101,8 +101,9 @@ class Guide(ProjectBaseMixin, CollectionNameMixin, CollectionDateMixin):
advertorials = JSONField(
_('advertorials'), null=True, blank=True,
default=None, help_text='{"key":"value"}')
collection = models.ForeignKey(
Collection, verbose_name=_('collection'), on_delete=models.CASCADE)
collection = models.ForeignKey(Collection, on_delete=models.CASCADE,
null=True, blank=True, default=None,
verbose_name=_('collection'))
objects = GuideQuerySet.as_manager()

View File

@ -2,18 +2,19 @@ from rest_framework import serializers
from collection import models
from location import models as location_models
from utils.serializers import TranslatedField
class CollectionBaseSerializer(serializers.ModelSerializer):
"""Collection base serializer"""
# RESPONSE
description_translated = serializers.CharField(read_only=True, allow_null=True)
name_translated = TranslatedField()
description_translated = TranslatedField()
class Meta:
model = models.Collection
fields = [
'id',
'name',
'name_translated',
'description_translated',
'image_url',
'slug',
@ -35,8 +36,7 @@ class CollectionSerializer(CollectionBaseSerializer):
queryset=location_models.Country.objects.all(),
write_only=True)
class Meta:
model = models.Collection
class Meta(CollectionBaseSerializer.Meta):
fields = CollectionBaseSerializer.Meta.fields + [
'start',
'end',

View File

@ -0,0 +1,23 @@
from django.core.management.base import BaseCommand
from pytz import timezone as py_tz
from timezonefinder import TimezoneFinder
from establishment.models import Establishment
class Command(BaseCommand):
help = 'Attach correct timestamps according to coordinates to existing establishments'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.tf = TimezoneFinder(in_memory=True)
def handle(self, *args, **options):
for establishment in Establishment.objects.prefetch_related('address').all():
if establishment.address and establishment.address.latitude and establishment.address.longitude:
establishment.tz = py_tz(self.tf.certain_timezone_at(lng=establishment.address.longitude,
lat=establishment.address.latitude))
establishment.save()
self.stdout.write(self.style.SUCCESS(f'Attached timezone {establishment.tz} to {establishment}'))
else:
self.stdout.write(self.style.WARNING(f'Establishment {establishment} has no coordinates'
f'passing...'))

View File

@ -8,7 +8,7 @@ def fill_establishment_subtype(apps, schema_editor):
# version than this migration expects. We use the historical version.
EstablishmentSubType = apps.get_model('establishment', 'EstablishmentSubType')
for n, et in enumerate(EstablishmentSubType.objects.all()):
et.index_name = f'Type {n}'
et.index_name = 'Type %s' % n
et.save()

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-10-23 09:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('establishment', '0040_employee_tags'),
]
operations = [
migrations.AlterField(
model_name='establishment',
name='slug',
field=models.SlugField(max_length=255, null=True, unique=True, verbose_name='Establishment slug'),
),
]

View File

@ -0,0 +1,21 @@
# Generated by Django 2.2.4 on 2019-10-21 17:33
import timezone_field.fields
from django.db import migrations
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
('establishment', '0041_auto_20191023_0920'),
]
operations = [
migrations.AddField(
model_name='establishment',
name='tz',
field=timezone_field.fields.TimeZoneField(default=settings.TIME_ZONE),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 2.2.4 on 2019-10-24 13:13
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('main', '0022_auto_20191023_1113'),
('establishment', '0042_establishment_tz'),
]
operations = [
migrations.AddField(
model_name='establishment',
name='currency',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='main.Currency', verbose_name='currency'),
),
]

View File

@ -1,6 +1,8 @@
"""Establishment models."""
from datetime import datetime
from functools import reduce
import elasticsearch_dsl
from django.conf import settings
from django.contrib.contenttypes import fields as generic
from django.contrib.gis.db.models.functions import Distance
@ -15,10 +17,11 @@ from phonenumber_field.modelfields import PhoneNumberField
from collection.models import Collection
from location.models import Address
from main.models import Award
from main.models import Award, Currency
from review.models import Review
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
TranslatedFieldsMixin, BaseAttributes)
from timezone_field import TimeZoneField
# todo: establishment type&subtypes check
@ -128,15 +131,15 @@ class EstablishmentQuerySet(models.QuerySet):
else:
return self.none()
# def es_search(self, value, locale=None):
# """Search text via ElasticSearch."""
# from search_indexes.documents import EstablishmentDocument
# search = EstablishmentDocument.search().filter(
# Elastic_Q('match', name=value) |
# Elastic_Q('match', **{f'description.{locale}': value})
# ).execute()
# ids = [result.meta.id for result in search]
# return self.filter(id__in=ids)
def es_search(self, value, locale=None):
"""Search text via ElasticSearch."""
from search_indexes.documents import EstablishmentDocument
search = EstablishmentDocument.search().filter(
elasticsearch_dsl.Q('match', name=value) |
elasticsearch_dsl.Q('match', **{f'description.{locale}': value})
).execute()
ids = [result.meta.id for result in search]
return self.filter(id__in=ids)
def by_country_code(self, code):
"""Return establishments by country code"""
@ -204,7 +207,7 @@ class EstablishmentQuerySet(models.QuerySet):
.filter(image_url__isnull=False, public_mark__gte=10)
.has_published_reviews()
.annotate_distance(point=establishment.location)
.order_by('distance')[:settings.LIMITING_QUERY_NUMBER]
.order_by('distance')[:settings.LIMITING_QUERY_OBJECTS]
.values('id')
)
return self.filter(id__in=subquery_filter_by_distance) \
@ -224,7 +227,7 @@ class EstablishmentQuerySet(models.QuerySet):
self.filter(image_url__isnull=False, public_mark__gte=10)
.has_published_reviews()
.annotate_distance(point=point)
.order_by('distance')[:settings.LIMITING_QUERY_NUMBER]
.order_by('distance')[:settings.LIMITING_QUERY_OBJECTS]
.values('id')
)
return self.filter(id__in=subquery_filter_by_distance) \
@ -349,8 +352,9 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
verbose_name=_('Collections'))
preview_image_url = models.URLField(verbose_name=_('Preview image URL path'),
blank=True, null=True, default=None)
slug = models.SlugField(unique=True, max_length=50, null=True,
verbose_name=_('Establishment slug'), editable=True)
slug = models.SlugField(unique=True, max_length=255, null=True,
verbose_name=_('Establishment slug'))
tz = TimeZoneField(default=settings.TIME_ZONE)
awards = generic.GenericRelation(to='main.Award', related_query_name='establishment')
tags = models.ManyToManyField('tag.Tag', related_name='establishments',
@ -358,6 +362,9 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
reviews = generic.GenericRelation(to='review.Review')
comments = generic.GenericRelation(to='comment.Comment')
favorites = generic.GenericRelation(to='favorites.Favorites')
currency = models.ForeignKey(Currency, blank=True, null=True, default=None,
on_delete=models.PROTECT,
verbose_name=_('currency'))
objects = EstablishmentQuerySet.as_manager()
@ -416,6 +423,32 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
def best_price_carte(self):
return 200
@property
def works_noon(self):
""" Used for indexing working by day """
return [ret.weekday for ret in self.schedule.all() if ret.works_at_noon]
@property
def works_evening(self):
""" Used for indexing working by day """
return [ret.weekday for ret in self.schedule.all() if ret.works_at_afternoon]
@property
def works_now(self):
""" Is establishment working now """
now_at_est_tz = datetime.now(tz=self.tz)
current_week = now_at_est_tz.weekday()
schedule_for_today = self.schedule.filter(weekday=current_week).first()
if schedule_for_today is None or schedule_for_today.closed_at is None or schedule_for_today.opening_at is None:
return False
time_at_est_tz = now_at_est_tz.time()
return schedule_for_today.closed_at > time_at_est_tz > schedule_for_today.opening_at
@property
def tags_indexing(self):
return [{'id': tag.metadata.id,
'label': tag.metadata.label} for tag in self.tags.all()]
@property
def last_published_review(self):
"""Return last published review"""
@ -431,8 +464,8 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
@property
def the_most_recent_award(self):
return Award.objects.filter(Q(establishment=self) | Q(employees__establishments=self)).latest(
field_name='vintage_year')
return Award.objects.filter(Q(establishment=self) | Q(employees__establishments=self)) \
.latest(field_name='vintage_year')
@property
def country_id(self):

View File

@ -7,6 +7,7 @@ from establishment.serializers import (
EstablishmentTypeBaseSerializer)
from main.models import Currency
from utils.decorators import with_base_attributes
from utils.serializers import TimeZoneChoiceField
class EstablishmentListCreateSerializer(EstablishmentBaseSerializer):
@ -19,8 +20,8 @@ class EstablishmentListCreateSerializer(EstablishmentBaseSerializer):
phones = ContactPhonesSerializer(read_only=True, many=True, )
emails = ContactEmailsSerializer(read_only=True, many=True, )
socials = SocialNetworkRelatedSerializers(read_only=True, many=True, )
slug = serializers.SlugField(required=True, allow_blank=False, max_length=50)
type = EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True)
tz = TimeZoneChoiceField()
class Meta:
model = models.Establishment
@ -41,6 +42,7 @@ class EstablishmentListCreateSerializer(EstablishmentBaseSerializer):
'is_publish',
'guestonline_id',
'lastable_id',
'tz',
]

View File

@ -159,10 +159,10 @@ class EstablishmentBaseSerializer(ProjectModelSerializer):
"""Base serializer for Establishment model."""
preview_image = serializers.URLField(source='preview_image_url')
slug = serializers.SlugField(allow_blank=False, required=True, max_length=50)
address = AddressBaseSerializer()
in_favorites = serializers.BooleanField(allow_null=True)
tags = TagBaseSerializer(read_only=True, many=True)
currency = CurrencySerializer()
class Meta:
"""Meta class."""
@ -180,6 +180,7 @@ class EstablishmentBaseSerializer(ProjectModelSerializer):
'in_favorites',
'address',
'tags',
'currency'
]

View File

@ -9,6 +9,7 @@ from establishment.models import Establishment, EstablishmentType, Menu
from translation.models import Language
from account.models import Role, UserRole
from location.models import Country, Address, City, Region
from pytz import timezone as py_tz
class BaseTestCase(APITestCase):
@ -77,7 +78,8 @@ class EstablishmentBTests(BaseTestCase):
'name': 'Test establishment',
'type_id': self.establishment_type.id,
'is_publish': True,
'slug': 'test-establishment-slug'
'slug': 'test-establishment-slug',
'tz': py_tz('Europe/Moscow').zone
}
response = self.client.post('/api/back/establishments/', data=data, format='json')

View File

@ -19,9 +19,12 @@ class EstablishmentMixinView:
def get_queryset(self):
"""Overridden method 'get_queryset'."""
return models.Establishment.objects.published() \
.with_base_related() \
.annotate_in_favorites(self.request.user)
qs = models.Establishment.objects.published() \
.with_base_related() \
.annotate_in_favorites(self.request.user)
if self.request.country_code:
qs = qs.by_country_code(self.request.country_code)
return qs
class EstablishmentListView(EstablishmentMixinView, generics.ListAPIView):
@ -30,13 +33,6 @@ class EstablishmentListView(EstablishmentMixinView, generics.ListAPIView):
filter_class = filters.EstablishmentFilter
serializer_class = serializers.EstablishmentBaseSerializer
def get_queryset(self):
"""Overridden method 'get_queryset'."""
qs = super(EstablishmentListView, self).get_queryset()
if self.request.country_code:
qs = qs.by_country_code(self.request.country_code)
return qs
class EstablishmentRetrieveView(EstablishmentMixinView, generics.RetrieveAPIView):
"""Resource for getting a establishment."""

View File

@ -1,14 +1,16 @@
# Create your tests here.
from http.cookies import SimpleCookie
from django.contrib.contenttypes.models import ContentType
from rest_framework.test import APITestCase
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from account.models import User
from establishment.models import Establishment, EstablishmentType
from favorites.models import Favorites
from establishment.models import Establishment, EstablishmentType, EstablishmentSubType
from news.models import NewsType, News
from datetime import datetime
class BaseTestCase(APITestCase):
@ -17,38 +19,57 @@ class BaseTestCase(APITestCase):
self.username = 'sedragurda'
self.password = 'sedragurdaredips19'
self.email = 'sedragurda@desoz.com'
self.user = User.objects.create_user(username=self.username, email=self.email, password=self.password)
tokkens = User.create_jwt_tokens(self.user)
self.client.cookies = SimpleCookie({'access_token': tokkens.get('access_token'),
'refresh_token': tokkens.get('refresh_token')})
self.user = User.objects.create_user(
username=self.username,
email=self.email,
password=self.password
)
self.test_news_type = NewsType.objects.create(name="Test news type")
self.test_news = News.objects.create(created_by=self.user, modified_by=self.user, title={"en-GB": "Test news"},
news_type=self.test_news_type,
description={"en-GB": "Description test news"},
playlist=1, start="2020-12-03 12:00:00", end="2020-12-13 12:00:00",
state=News.PUBLISHED, slug='test-news')
tokens = User.create_jwt_tokens(self.user)
self.client.cookies = SimpleCookie(
{'access_token': tokens.get('access_token'),
'refresh_token': tokens.get('refresh_token')})
self.test_content_type = ContentType.objects.get(app_label="news", model="news")
self.test_news_type = NewsType.objects.create(
name="Test news type",
)
self.test_news = News.objects.create(
created_by=self.user,
modified_by=self.user,
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,
slug='test-news'
)
self.test_favorites = Favorites.objects.create(user=self.user, content_type=self.test_content_type,
object_id=self.test_news.id)
self.test_content_type = ContentType.objects.get(
app_label="news", model="news")
self.test_establishment_type = EstablishmentType.objects.create(name={"en-GB": "test establishment type"},
use_subtypes=False)
self.test_favorites = Favorites.objects.create(
user=self.user, content_type=self.test_content_type,
object_id=self.test_news.id)
self.test_establishment = Establishment.objects.create(name="test establishment",
description={"en-GB": "description of test establishment"},
establishment_type=self.test_establishment_type,
is_publish=True)
self.test_establishment_type = EstablishmentType.objects.create(
name={"en-GB": "test establishment type"},
use_subtypes=False)
self.test_establishment = Establishment.objects.create(
name="test establishment",
description={"en-GB": "description of test establishment"},
establishment_type=self.test_establishment_type,
is_publish=True)
# value for GenericRelation(reverse side) field must be iterable
# value for GenericRelation(reverse side) field must be assigned through "set" method of field
# value for GenericRelation(reverse side) field must be assigned through
# "set" method of field
self.test_establishment.favorites.set([self.test_favorites])
class FavoritesTestCase(BaseTestCase):
def test_func(self):
response = self.client.get("/api/web/favorites/establishments/")
print(response.json())
url = reverse('web:favorites:establishment-list')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)

View File

@ -5,4 +5,9 @@ from gallery.models import Image
@admin.register(Image)
class ImageModelAdmin(admin.ModelAdmin):
"""Image model admin"""
"""Image model admin."""
list_display = ['id', 'title', 'orientation_display', 'image_tag', ]
def orientation_display(self, obj):
"""Get image orientation name."""
return obj.get_orientation_display() if obj.orientation else None

View File

@ -0,0 +1,41 @@
# Generated by Django 2.2.4 on 2019-09-30 07:14
from django.db import migrations, models
import django.db.models.deletion
import easy_thumbnails.fields
import utils.methods
class Migration(migrations.Migration):
dependencies = [
('gallery', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='image',
name='orientation',
field=models.PositiveSmallIntegerField(blank=True, choices=[(0, 'Horizontal'), (1, 'Vertical')], default=None, null=True, verbose_name='image orientation'),
),
migrations.AddField(
model_name='image',
name='parent',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='parent_image', to='gallery.Image', verbose_name='parent image'),
),
migrations.AddField(
model_name='image',
name='source',
field=models.PositiveSmallIntegerField(choices=[(0, 'Mobile'), (1, 'Web'), (2, 'All')], default=0, verbose_name='Source'),
),
migrations.AddField(
model_name='image',
name='title',
field=models.CharField(default='', max_length=255, verbose_name='title'),
),
migrations.AlterField(
model_name='image',
name='image',
field=easy_thumbnails.fields.ThumbnailerImageField(upload_to=utils.methods.image_path, verbose_name='image file'),
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 2.2.4 on 2019-10-03 12:28
from django.db import migrations
import sorl.thumbnail.fields
import utils.methods
class Migration(migrations.Migration):
dependencies = [
('gallery', '0002_auto_20190930_0714'),
]
operations = [
migrations.RemoveField(
model_name='image',
name='parent',
),
migrations.AlterField(
model_name='image',
name='image',
field=sorl.thumbnail.fields.ImageField(upload_to=utils.methods.image_path, verbose_name='image file'),
),
]

View File

@ -1,15 +1,39 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from easy_thumbnails.fields import ThumbnailerImageField
from sorl.thumbnail import delete
from sorl.thumbnail.fields import ImageField as SORLImageField
from utils.methods import image_path
from utils.models import ProjectBaseMixin, ImageMixin
from utils.models import ProjectBaseMixin, SORLImageMixin, PlatformMixin
class Image(ProjectBaseMixin, ImageMixin):
class ImageQuerySet(models.QuerySet):
"""QuerySet for model Image."""
class Image(ProjectBaseMixin, SORLImageMixin, PlatformMixin):
"""Image model."""
HORIZONTAL = 0
VERTICAL = 1
ORIENTATIONS = (
(HORIZONTAL, _('Horizontal')),
(VERTICAL, _('Vertical')),
)
image = SORLImageField(upload_to=image_path,
verbose_name=_('image file'))
orientation = models.PositiveSmallIntegerField(choices=ORIENTATIONS,
blank=True, null=True, default=None,
verbose_name=_('image orientation'))
title = models.CharField(_('title'), max_length=255, default='')
<<<<<<< HEAD
image = ThumbnailerImageField(upload_to=image_path,
verbose_name=_('Image file'), max_length=255)
=======
objects = ImageQuerySet.as_manager()
>>>>>>> develop
class Meta:
"""Meta class."""
@ -18,4 +42,22 @@ class Image(ProjectBaseMixin, ImageMixin):
def __str__(self):
"""String representation"""
return str(self.id)
return f'{self.id}'
def delete_image(self, completely: bool = True):
"""
Deletes an instance and crops of instance from media storage.
:param completely: if set to False then removed only crop neither original image.
"""
try:
# Delete from remote storage
delete(file_=self.image.file, delete_file=completely)
except FileNotFoundError:
pass
finally:
if completely:
# Delete an instance of image
super().delete()

View File

@ -12,13 +12,20 @@ class ImageSerializer(serializers.ModelSerializer):
# RESPONSE
url = serializers.ImageField(source='image',
read_only=True)
orientation_display = serializers.CharField(source='get_orientation_display',
read_only=True)
class Meta:
"""Meta class"""
model = models.Image
fields = (
fields = [
'id',
'file',
'url'
)
'url',
'orientation',
'orientation_display',
'title',
]
extra_kwargs = {
'orientation': {'write_only': True}
}

23
apps/gallery/tasks.py Normal file
View File

@ -0,0 +1,23 @@
"""Gallery app celery tasks."""
import logging
from celery import shared_task
from . import models
logging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.INFO)
logger = logging.getLogger(__name__)
@shared_task
def delete_image(image_id: int, completely: bool = True):
"""Delete an image from remote storage."""
image_qs = models.Image.objects.filter(id=image_id)
if image_qs.exists():
try:
image = image_qs.first()
image.delete_image(completely=completely)
except:
logger.error(f'TASK_NAME: delete_image\n'
f'DETAIL: Exception occurred while deleting an image '
f'and related crops from remote storage: image_id - {image_id}')

View File

@ -6,5 +6,6 @@ from . import views
app_name = 'gallery'
urlpatterns = [
path('upload/', views.ImageUploadView.as_view(), name='upload-image')
path('', views.ImageListCreateView.as_view(), name='list-create-image'),
path('<int:pk>/', views.ImageRetrieveDestroyView.as_view(), name='retrieve-destroy-image'),
]

View File

@ -1,10 +1,30 @@
from rest_framework import generics
from django.conf import settings
from django.db.transaction import on_commit
from rest_framework import generics, status
from rest_framework.response import Response
from . import models, serializers
from . import tasks, models, serializers
class ImageUploadView(generics.CreateAPIView):
"""Upload image to gallery"""
class ImageBaseView(generics.GenericAPIView):
"""Base Image view."""
model = models.Image
queryset = models.Image.objects.all()
serializer_class = serializers.ImageSerializer
class ImageListCreateView(ImageBaseView, generics.ListCreateAPIView):
"""List/Create Image view."""
class ImageRetrieveDestroyView(ImageBaseView, generics.RetrieveDestroyAPIView):
"""Destroy view for model Image"""
def delete(self, request, *args, **kwargs):
"""Override destroy view"""
instance = self.get_object()
if settings.USE_CELERY:
on_commit(lambda: tasks.delete_image.delay(image_id=instance.id))
else:
on_commit(lambda: tasks.delete_image(image_id=instance.id))
return Response(status=status.HTTP_204_NO_CONTENT)

View File

@ -5,8 +5,9 @@ from django.db.models.signals import post_save
from django.db.transaction import on_commit
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from utils.models import ProjectBaseMixin, SVGImageMixin, TranslatedFieldsMixin, TJSONField
from translation.models import Language
from utils.models import ProjectBaseMixin, SVGImageMixin, TranslatedFieldsMixin, TJSONField
class Country(TranslatedFieldsMixin, SVGImageMixin, ProjectBaseMixin):

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-10-23 07:50
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('main', '0019_award_image_url'),
('main', '0019_auto_20191022_1359'),
]
operations = [
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-10-25 04:23
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('main', '0019_auto_20191022_1359'),
('main', '0019_award_image_url'),
]
operations = [
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-10-23 09:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0020_merge_20191023_0750'),
]
operations = [
migrations.AlterField(
model_name='feature',
name='slug',
field=models.SlugField(max_length=255, unique=True),
),
]

View File

@ -0,0 +1,57 @@
# Generated by Django 2.2.4 on 2019-10-23 11:13
from django.db import migrations, models
import django.db.models.deletion
import utils.models
def fill_currency_name(apps, schema_editor):
# We can't import the Person model directly as it may be a newer
# version than this migration expects. We use the historical version.
Currency = apps.get_model('main', 'Currency')
for currency in Currency.objects.all():
currency.name_json = {'en-GB': currency.name}
currency.save()
class Migration(migrations.Migration):
dependencies = [
('main', '0021_auto_20191023_0924'),
]
operations = [
migrations.AddField(
model_name='currency',
name='sign',
field=models.CharField(default='?', max_length=1, verbose_name='sign'),
preserve_default=False,
),
migrations.AddField(
model_name='currency',
name='slug',
field=models.SlugField(default='?', max_length=255, unique=True),
preserve_default=False,
),
migrations.AddField(
model_name='sitesettings',
name='currency',
field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='main.Currency'),
),
migrations.AddField(
model_name='currency',
name='name_json',
field=utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='name'),
),
migrations.RunPython(fill_currency_name, migrations.RunPython.noop),
migrations.RemoveField(
model_name='currency',
name='name',
),
migrations.RenameField(
model_name='currency',
old_name='name_json',
new_name='name',
),
]

View File

@ -106,6 +106,22 @@ from utils.querysets import ContentTypeQuerySetMixin
#
class Currency(TranslatedFieldsMixin, models.Model):
"""Currency model."""
name = TJSONField(
_('name'), null=True, blank=True,
default=None, help_text='{"en-GB":"some text"}')
sign = models.CharField(_('sign'), max_length=1)
slug = models.SlugField(max_length=255, unique=True)
class Meta:
verbose_name = _('currency')
verbose_name_plural = _('currencies')
def __str__(self):
return f'{self.name}'
class SiteSettingsQuerySet(models.QuerySet):
"""Extended queryset for SiteSettings model."""
@ -114,6 +130,7 @@ class SiteSettingsQuerySet(models.QuerySet):
class SiteSettings(ProjectBaseMixin):
subdomain = models.CharField(max_length=255, db_index=True, unique=True,
verbose_name=_('Subdomain'))
country = models.OneToOneField(Country, on_delete=models.PROTECT,
@ -135,6 +152,7 @@ class SiteSettings(ProjectBaseMixin):
verbose_name=_('Config'))
ad_config = models.TextField(blank=True, null=True, default=None,
verbose_name=_('AD config'))
currency = models.ForeignKey(Currency, on_delete=models.PROTECT, null=True, default=None)
objects = SiteSettingsQuerySet.as_manager()
@ -182,7 +200,7 @@ class Page(models.Model):
class Feature(ProjectBaseMixin, PlatformMixin):
"""Feature model."""
slug = models.CharField(max_length=255, unique=True)
slug = models.SlugField(max_length=255, unique=True)
priority = models.IntegerField(unique=True, null=True, default=None)
route = models.ForeignKey(Page, on_delete=models.PROTECT, null=True, default=None)
site_settings = models.ManyToManyField(SiteSettings, through='SiteFeature')
@ -257,18 +275,6 @@ class AwardType(models.Model):
return self.name
class Currency(models.Model):
"""Currency model."""
name = models.CharField(_('name'), max_length=50)
class Meta:
verbose_name = _('currency')
verbose_name_plural = _('currencies')
def __str__(self):
return f'{self.name}'
class CarouselQuerySet(models.QuerySet):
"""Carousel QuerySet."""

View File

@ -4,7 +4,7 @@ from rest_framework import serializers
from advertisement.serializers.web import AdvertisementSerializer
from location.serializers import CountrySerializer
from main import models
from utils.serializers import TranslatedField
from utils.serializers import ProjectModelSerializer, TranslatedField
class FeatureSerializer(serializers.ModelSerializer):
@ -40,11 +40,26 @@ class SiteFeatureSerializer(serializers.ModelSerializer):
)
class CurrencySerializer(ProjectModelSerializer):
"""Currency serializer."""
name_translated = TranslatedField()
class Meta:
model = models.Currency
fields = [
'id',
'name_translated',
'sign'
]
class SiteSettingsSerializer(serializers.ModelSerializer):
"""Site settings serializer."""
published_features = SiteFeatureSerializer(source='published_sitefeatures',
many=True, allow_null=True)
currency = CurrencySerializer()
# todo: remove this
country_code = serializers.CharField(source='subdomain', read_only=True)
@ -63,6 +78,7 @@ class SiteSettingsSerializer(serializers.ModelSerializer):
'config',
'ad_config',
'published_features',
'currency'
)
@ -114,17 +130,6 @@ class AwardSerializer(AwardBaseSerializer):
fields = AwardBaseSerializer.Meta.fields + ['award_type', ]
class CurrencySerializer(serializers.ModelSerializer):
"""Currency serializer"""
class Meta:
model = models.Currency
fields = [
'id',
'name'
]
class CarouselListSerializer(serializers.ModelSerializer):
"""Serializer for retrieving list of carousel items."""
model_name = serializers.CharField()

View File

@ -1,4 +1,6 @@
from django.contrib import admin
from django.conf import settings
from news import models
from .tasks import send_email_with_news
@ -12,9 +14,10 @@ class NewsTypeAdmin(admin.ModelAdmin):
def send_email_action(modeladmin, request, queryset):
news_ids = list(queryset.values_list("id", flat=True))
send_email_with_news.delay(news_ids)
if settings.USE_CELERY:
send_email_with_news.delay(news_ids)
else:
send_email_with_news(news_ids)
send_email_action.short_description = "Send the selected news by email"
@ -24,3 +27,8 @@ send_email_action.short_description = "Send the selected news by email"
class NewsAdmin(admin.ModelAdmin):
"""News admin."""
actions = [send_email_action]
@admin.register(models.NewsGallery)
class NewsGalleryAdmin(admin.ModelAdmin):
"""News gallery admin."""

View File

@ -0,0 +1,26 @@
# Generated by Django 2.2.4 on 2019-09-30 08:57
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('gallery', '0002_auto_20190930_0714'),
]
operations = [
migrations.CreateModel(
name='NewsGallery',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='news_gallery', to='gallery.Image', verbose_name='gallery')),
('news', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='news_gallery', to='news.News', verbose_name='news')),
],
options={
'verbose_name': 'news gallery',
'verbose_name_plural': 'news galleries',
},
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.4 on 2019-09-30 12:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('gallery', '0002_auto_20190930_0714'),
('news', '0015_newsgallery'),
]
operations = [
migrations.AddField(
model_name='news',
name='gallery',
field=models.ManyToManyField(through='news.NewsGallery', to='gallery.Image'),
),
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-09-30 12:51
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('news', '0019_news_author'),
('news', '0016_news_gallery'),
]
operations = [
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-10-02 13:00
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('news', '0020_remove_news_author'),
('news', '0020_merge_20190930_1251'),
]
operations = [
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-10-15 09:12
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('news', '0021_auto_20191009_1408'),
('news', '0021_merge_20191002_1300'),
]
operations = [
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-10-23 09:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('news', '0022_auto_20191021_1306'),
]
operations = [
migrations.AlterField(
model_name='news',
name='slug',
field=models.SlugField(max_length=255, unique=True, verbose_name='News slug'),
),
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-10-23 10:00
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('news', '0021_merge_20191002_1300'),
('news', '0022_auto_20191021_1306'),
]
operations = [
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-10-25 04:23
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('news', '0021_auto_20191021_1120'),
('news', '0022_auto_20191021_1306'),
]
operations = [
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-10-23 10:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('news', '0023_merge_20191023_1000'),
]
operations = [
migrations.AddField(
model_name='newsgallery',
name='is_main',
field=models.BooleanField(default=False, verbose_name='Is the main image'),
),
]

View File

@ -0,0 +1,15 @@
# Generated by Django 2.2.4 on 2019-10-23 13:17
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('news', '0024_newsgallery_is_main'),
('news', '0023_auto_20191023_0903'),
('news', '0022_merge_20191015_0912'),
]
operations = [
]

View File

@ -0,0 +1,21 @@
# Generated by Django 2.2.4 on 2019-10-24 09:13
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('news', '0025_merge_20191023_1317'),
]
operations = [
migrations.RemoveField(
model_name='news',
name='image_url',
),
migrations.RemoveField(
model_name='news',
name='preview_image_url',
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 2.2.4 on 2019-10-24 09:37
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('news', '0026_auto_20191024_0913'),
]
operations = [
migrations.RemoveField(
model_name='news',
name='playlist',
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 2.2.4 on 2019-10-24 16:49
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('news', '0027_remove_news_playlist'),
]
operations = [
migrations.AlterField(
model_name='newsgallery',
name='image',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='news_gallery', to='gallery.Image', verbose_name='gallery'),
),
migrations.AlterField(
model_name='newsgallery',
name='news',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='news_gallery', to='news.News', verbose_name='news'),
),
migrations.AlterUniqueTogether(
name='newsgallery',
unique_together={('news', 'is_main')},
),
]

View File

@ -141,15 +141,10 @@ class News(BaseAttributes, TranslatedFieldsMixin):
verbose_name=_('End'))
slug = models.SlugField(unique=True, max_length=255,
verbose_name=_('News slug'))
playlist = models.IntegerField(_('playlist'))
state = models.PositiveSmallIntegerField(default=WAITING, choices=STATE_CHOICES,
verbose_name=_('State'))
is_highlighted = models.BooleanField(default=False,
verbose_name=_('Is highlighted'))
image_url = models.URLField(blank=True, null=True, default=None,
verbose_name=_('Image URL path'))
preview_image_url = models.URLField(blank=True, null=True, default=None,
verbose_name=_('Preview image URL path'))
template = models.PositiveIntegerField(choices=TEMPLATE_CHOICES, default=NEWSPAPER)
address = models.ForeignKey('location.Address', blank=True, null=True,
default=None, verbose_name=_('address'),
@ -159,6 +154,7 @@ class News(BaseAttributes, TranslatedFieldsMixin):
verbose_name=_('country'))
tags = models.ManyToManyField('tag.Tag', related_name='news',
verbose_name=_('Tags'))
gallery = models.ManyToManyField('gallery.Image', through='news.NewsGallery')
ratings = generic.GenericRelation(Rating)
agenda = models.ForeignKey('news.Agenda', blank=True, null=True,
@ -195,3 +191,48 @@ class News(BaseAttributes, TranslatedFieldsMixin):
@property
def same_theme(self):
return self.__class__.objects.same_theme(self)[:3]
@property
def main_image(self):
qs = self.news_gallery.main_image()
if qs.exists():
return qs.first().image
@property
def image_url(self):
return self.main_image.image.url if self.main_image else None
@property
def preview_image_url(self):
if self.main_image:
return self.main_image.get_image_url(thumbnail_key='news_preview')
class NewsGalleryQuerySet(models.QuerySet):
"""QuerySet for model News"""
def main_image(self):
"""Return objects with flag is_main is True"""
return self.filter(is_main=True)
class NewsGallery(models.Model):
news = models.ForeignKey(News, null=True,
related_name='news_gallery',
on_delete=models.CASCADE,
verbose_name=_('news'))
image = models.ForeignKey('gallery.Image', null=True,
related_name='news_gallery',
on_delete=models.CASCADE,
verbose_name=_('gallery'))
is_main = models.BooleanField(default=False,
verbose_name=_('Is the main image'))
objects = NewsGalleryQuerySet.as_manager()
class Meta:
"""NewsGallery meta class."""
verbose_name = _('news gallery')
verbose_name_plural = _('news galleries')
unique_together = ('news', 'is_main')

View File

@ -1,6 +1,9 @@
"""News app common serializers."""
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from account.serializers.common import UserBaseSerializer
from gallery.models import Image
from location import models as location_models
from location.serializers import CountrySimpleSerializer, AddressBaseSerializer
from news import models
@ -42,6 +45,77 @@ class NewsBannerSerializer(ProjectModelSerializer):
)
class CropImageSerializer(serializers.Serializer):
"""Serializer for crop images for News object."""
preview_url = serializers.SerializerMethodField()
promo_horizontal_web_url = serializers.SerializerMethodField()
promo_horizontal_mobile_url = serializers.SerializerMethodField()
tile_horizontal_web_url = serializers.SerializerMethodField()
tile_horizontal_mobile_url = serializers.SerializerMethodField()
tile_vertical_web_url = serializers.SerializerMethodField()
highlight_vertical_web_url = serializers.SerializerMethodField()
editor_web_url = serializers.SerializerMethodField()
editor_mobile_url = serializers.SerializerMethodField()
def get_preview_url(self, obj):
"""Get crop preview."""
return obj.instance.get_image_url('news_preview')
def get_promo_horizontal_web_url(self, obj):
"""Get crop promo_horizontal_web."""
return obj.instance.get_image_url('news_promo_horizontal_web')
def get_promo_horizontal_mobile_url(self, obj):
"""Get crop promo_horizontal_mobile."""
return obj.instance.get_image_url('news_promo_horizontal_mobile')
def get_tile_horizontal_web_url(self, obj):
"""Get crop tile_horizontal_web."""
return obj.instance.get_image_url('news_tile_horizontal_web')
def get_tile_horizontal_mobile_url(self, obj):
"""Get crop tile_horizontal_mobile."""
return obj.instance.get_image_url('news_tile_horizontal_mobile')
def get_tile_vertical_web_url(self, obj):
"""Get crop tile_vertical_web."""
return obj.instance.get_image_url('news_tile_vertical_web')
def get_highlight_vertical_web_url(self, obj):
"""Get crop highlight_vertical_web."""
return obj.instance.get_image_url('news_highlight_vertical_web')
def get_editor_web_url(self, obj):
"""Get crop editor_web."""
return obj.instance.get_image_url('news_editor_web')
def get_editor_mobile_url(self, obj):
"""Get crop editor_mobile."""
return obj.instance.get_image_url('news_editor_mobile')
class NewsImageSerializer(serializers.ModelSerializer):
"""Serializer for returning crop images of news image."""
orientation_display = serializers.CharField(source='get_orientation_display',
read_only=True)
original_url = serializers.URLField(source='image.url')
auto_crop_images = CropImageSerializer(source='image', allow_null=True)
class Meta:
model = Image
fields = [
'id',
'title',
'orientation_display',
'original_url',
'auto_crop_images',
]
extra_kwargs = {
'orientation': {'write_only': True}
}
class NewsTypeSerializer(serializers.ModelSerializer):
"""News type serializer."""
@ -55,11 +129,8 @@ class NewsTypeSerializer(serializers.ModelSerializer):
class NewsBaseSerializer(ProjectModelSerializer):
"""Base serializer for News model."""
# read only fields
title_translated = TranslatedField(source='title')
title_translated = TranslatedField()
subtitle_translated = TranslatedField()
# related fields
news_type = NewsTypeSerializer(read_only=True)
tags = TagBaseSerializer(read_only=True, many=True)
@ -72,14 +143,25 @@ class NewsBaseSerializer(ProjectModelSerializer):
'title_translated',
'subtitle_translated',
'is_highlighted',
'image_url',
'preview_image_url',
'news_type',
'tags',
'slug',
)
class NewsListSerializer(NewsBaseSerializer):
"""List serializer for News model."""
image = NewsImageSerializer(source='main_image', allow_null=True)
class Meta(NewsBaseSerializer.Meta):
"""Meta class."""
fields = NewsBaseSerializer.Meta.fields + (
'image',
)
class NewsDetailSerializer(NewsBaseSerializer):
"""News detail serializer."""
@ -88,6 +170,7 @@ class NewsDetailSerializer(NewsBaseSerializer):
author = UserBaseSerializer(source='created_by', read_only=True)
state_display = serializers.CharField(source='get_state_display',
read_only=True)
gallery = NewsImageSerializer(read_only=True, many=True)
class Meta(NewsBaseSerializer.Meta):
"""Meta class."""
@ -96,12 +179,12 @@ class NewsDetailSerializer(NewsBaseSerializer):
'description_translated',
'start',
'end',
'playlist',
'is_publish',
'state',
'state_display',
'author',
'country',
'gallery',
)
@ -160,3 +243,48 @@ class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer,
'template',
'template_display',
)
class NewsBackOfficeGallerySerializer(serializers.ModelSerializer):
"""Serializer class for model NewsGallery."""
class Meta:
"""Meta class"""
model = models.NewsGallery
fields = [
'id',
'is_main',
]
def get_request_kwargs(self):
"""Get url kwargs from request."""
return self.context.get('request').parser_context.get('kwargs')
def validate(self, attrs):
"""Override validate method."""
news_pk = self.get_request_kwargs().get('pk')
image_id = self.get_request_kwargs().get('image_id')
is_main = attrs.get('is_main')
news_qs = models.News.objects.filter(pk=news_pk)
image_qs = Image.objects.filter(id=image_id)
if not news_qs.exists():
raise serializers.ValidationError({'detail': _('News not found')})
if not image_qs.exists():
raise serializers.ValidationError({'detail': _('Image not found')})
news = news_qs.first()
image = image_qs.first()
if news.news_gallery.filter(image=image).exists():
raise serializers.ValidationError({'detail': _('Image is already added')})
if is_main and news.news_gallery.main_image().exists():
raise serializers.ValidationError({'detail': _('Main image is already added')})
attrs['news'] = news
attrs['image'] = image
return attrs

View File

@ -18,11 +18,14 @@ class BaseTestCase(APITestCase):
self.username = 'sedragurda'
self.password = 'sedragurdaredips19'
self.email = 'sedragurda@desoz.com'
self.user = User.objects.create_user(username=self.username, email=self.email, password=self.password)
#get tokkens
tokkens = User.create_jwt_tokens(self.user)
self.client.cookies = SimpleCookie({'access_token': tokkens.get('access_token'),
'refresh_token': tokkens.get('refresh_token')})
self.user = User.objects.create_user(
username=self.username, email=self.email, password=self.password)
# 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.test_news_type = NewsType.objects.create(name="Test news type")
self.lang = Language.objects.get(
@ -46,21 +49,25 @@ class BaseTestCase(APITestCase):
)
user_role.save()
self.test_news = News.objects.create(created_by=self.user, modified_by=self.user,
title={"en-GB": "Test news"},
news_type=self.test_news_type,
description={"en-GB": "Description test news"},
playlist=1, start=datetime.now() + timedelta(hours=-2),
end=datetime.now() + timedelta(hours=2),
state=News.PUBLISHED, slug='test-news-slug',
country=self.country_ru)
self.test_news = News.objects.create(
created_by=self.user, modified_by=self.user,
title={"en-GB": "Test news"},
news_type=self.test_news_type,
description={"en-GB": "Description test news"},
start=datetime.now() + timedelta(hours=-2),
end=datetime.now() + timedelta(hours=2),
state=News.PUBLISHED,
slug='test-news-slug',
country=self.country_ru,
)
class NewsTestCase(BaseTestCase):
def setUp(self):
super().setUp()
def test_web_news(self):
response = self.client.get("/api/web/news/")
response = self.client.get(reverse('web:news:list'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
response = self.client.get(f"/api/web/news/slug/{self.test_news.slug}/")
@ -85,7 +92,6 @@ class NewsTestCase(BaseTestCase):
'description': {"en-GB": "Description test news!"},
'slug': self.test_news.slug,
'start': self.test_news.start,
'playlist': self.test_news.playlist,
'news_type_id':self.test_news.news_type_id,
'country_id': self.country_ru.id
}

View File

@ -1,5 +1,6 @@
"""News app urlpatterns for backoffice"""
from django.urls import path
from news import views
app_name = 'news'
@ -8,4 +9,8 @@ urlpatterns = [
path('', views.NewsBackOfficeLCView.as_view(), name='list-create'),
path('<int:pk>/', views.NewsBackOfficeRUDView.as_view(),
name='retrieve-update-destroy'),
path('<int:pk>/gallery/', views.NewsBackOfficeGalleryListView.as_view(),
name='gallery-list'),
path('<int:pk>/gallery/<int:image_id>/', views.NewsBackOfficeGalleryCreateDestroyView.as_view(),
name='gallery-create-destroy'),
]

View File

@ -1,6 +1,11 @@
"""News app views."""
from django.conf import settings
from django.db.transaction import on_commit
from django.shortcuts import get_object_or_404
from rest_framework import generics, permissions
from rest_framework import generics, permissions, status
from rest_framework.response import Response
from gallery.tasks import delete_image
from news import filters, models, serializers
from rating.tasks import add_rating
from utils.permissions import IsCountryAdmin, IsContentPageManager
@ -9,21 +14,40 @@ from utils.permissions import IsCountryAdmin, IsContentPageManager
class NewsMixinView:
"""News mixin."""
permission_classes = (permissions.AllowAny, )
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.NewsBaseSerializer
def get_queryset(self, *args, **kwargs):
from django.conf import settings
"""Override get_queryset method."""
qs = models.News.objects.published().with_base_related()\
qs = models.News.objects.published().with_base_related() \
.order_by('-is_highlighted', '-created')
if self.request.country_code:
qs = qs.by_country_code(self.request.country_code)
country_code = self.request.country_code
if country_code:
# temp code
# Temporary stub for international news logic
# (по договорённости с заказчиком на демонстрации 4 ноября
# здесь будет 6 фиксированных новостей)
# TODO replace this stub with actual logic
if hasattr(settings, 'HARDCODED_INTERNATIONAL_NEWS_IDS'):
if country_code and country_code != 'www' and country_code != 'main':
qs = qs.by_country_code(country_code)
else:
qs = models.News.objects.filter(
id__in=settings.HARDCODED_INTERNATIONAL_NEWS_IDS)
return qs
# temp code
qs = qs.by_country_code(country_code)
return qs
class NewsListView(NewsMixinView, generics.ListAPIView):
"""News list view."""
serializer_class = serializers.NewsListSerializer
filter_class = filters.NewsListFilterSet
@ -74,6 +98,64 @@ class NewsBackOfficeLCView(NewsBackOfficeMixinView,
return super().get_queryset().with_extended_related()
class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView,
generics.CreateAPIView,
generics.DestroyAPIView):
"""Resource for a create gallery for news for back-office users."""
serializer_class = serializers.NewsBackOfficeGallerySerializer
def get_object(self):
"""
Returns the object the view is displaying.
"""
news_qs = self.filter_queryset(self.get_queryset())
news = get_object_or_404(news_qs, pk=self.kwargs['pk'])
gallery = get_object_or_404(news.news_gallery, image_id=self.kwargs['image_id'])
# May raise a permission denied
self.check_object_permissions(self.request, gallery)
return gallery
def create(self, request, *args, **kwargs):
"""Override create method"""
super().create(request, *args, **kwargs)
return Response(status=status.HTTP_201_CREATED)
def destroy(self, request, *args, **kwargs):
"""Override destroy method."""
gallery_obj = self.get_object()
if settings.USE_CELERY:
on_commit(lambda: delete_image.delay(image_id=gallery_obj.image.id,
completely=False))
else:
on_commit(lambda: delete_image(image_id=gallery_obj.image.id,
completely=False))
# Delete an instances of NewsGallery model
gallery_obj.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class NewsBackOfficeGalleryListView(NewsBackOfficeMixinView, generics.ListAPIView):
"""Resource for returning gallery for news for back-office users."""
serializer_class = serializers.NewsImageSerializer
def get_object(self):
"""Override get_object method."""
qs = super(NewsBackOfficeGalleryListView, self).get_queryset()
news = get_object_or_404(qs, pk=self.kwargs['pk'])
# May raise a permission denied
self.check_object_permissions(self.request, news)
return news
def get_queryset(self):
"""Override get_queryset method."""
return self.get_object().gallery.all()
class NewsBackOfficeRUDView(NewsBackOfficeMixinView,
generics.RetrieveUpdateDestroyAPIView):
"""Resource for detailed information about news for back-office users."""

7
apps/product/apps.py Normal file
View File

@ -0,0 +1,7 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class ProductConfig(AppConfig):
name = 'product'
verbose_name = _('Product')

110
apps/product/models.py Normal file
View File

@ -0,0 +1,110 @@
"""Product app models."""
from django.db import models
from django.contrib.postgres.fields import JSONField
from django.utils.translation import gettext_lazy as _
from utils.models import (BaseAttributes, ProjectBaseMixin,
TranslatedFieldsMixin, TJSONField)
class ProductType(TranslatedFieldsMixin, ProjectBaseMixin):
"""ProductType model."""
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'))
use_subtypes = models.BooleanField(_('Use subtypes'), default=True)
class Meta:
"""Meta class."""
verbose_name = _('Product type')
verbose_name_plural = _('Product types')
class ProductSubType(TranslatedFieldsMixin, ProjectBaseMixin):
"""ProductSubtype model."""
product_type = models.ForeignKey(ProductType, on_delete=models.CASCADE,
related_name='subtypes',
verbose_name=_('Product type'))
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'))
class Meta:
"""Meta class."""
verbose_name = _('Product type')
verbose_name_plural = _('Product types')
class ProductManager(models.Manager):
"""Extended manager for Product model."""
class ProductQuerySet(models.QuerySet):
"""Product queryset."""
def common(self):
return self.filter(category=self.model.COMMON)
def online(self):
return self.filter(category=self.model.ONLINE)
class Product(TranslatedFieldsMixin, BaseAttributes):
"""Product models."""
COMMON = 0
ONLINE = 1
CATEGORY_CHOICES = (
(COMMON, _('Common')),
(ONLINE, _('Online')),
)
category = models.PositiveIntegerField(choices=CATEGORY_CHOICES,
default=COMMON)
name = TJSONField(_('Name'), null=True, blank=True, default=None,
help_text='{"en-GB":"some text"}')
description = TJSONField(_('Description'), null=True, blank=True,
default=None, help_text='{"en-GB":"some text"}')
characteristics = JSONField(_('Characteristics'))
country = models.ForeignKey('location.Country', on_delete=models.PROTECT,
verbose_name=_('Country'))
available = models.BooleanField(_('Available'), default=True)
type = models.ForeignKey(ProductType, on_delete=models.PROTECT,
related_name='products', verbose_name=_('Type'))
subtypes = models.ManyToManyField(ProductSubType, related_name='products',
verbose_name=_('Subtypes'))
objects = ProductManager.from_queryset(ProductQuerySet)()
class Meta:
"""Meta class."""
verbose_name = _('Product')
verbose_name_plural = _('Products')
class OnlineProductManager(ProductManager):
"""Extended manger for OnlineProduct model."""
def get_queryset(self):
"""Overrided get_queryset method."""
return super().get_queryset().online()
class OnlineProduct(Product):
"""Online product."""
objects = OnlineProductManager.from_queryset(ProductQuerySet)()
class Meta:
"""Meta class."""
proxy = True
verbose_name = _('Online product')
verbose_name_plural = _('Online products')

View File

View File

View File

@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View File

@ -1,8 +0,0 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class ProductsConfig(AppConfig):
"""Products model."""
name = 'products'
verbose_name = _('products')

View File

@ -1,63 +0,0 @@
# from django.contrib.postgres.fields import JSONField
# from django.db import models
# from django.utils.translation import gettext_lazy as _
#
# from utils.models import BaseAttributes
#
#
# class ProductManager(models.Manager):
# """Product manager."""
#
#
# class ProductQuerySet(models.QuerySet):
# """Product queryset."""
#
#
# class Product(BaseAttributes):
# """Product models."""
# name = models.CharField(_('name'), max_length=255)
# country = models.ForeignKey('location.Country', on_delete=models.CASCADE)
# region = models.ForeignKey('location.Region', on_delete=models.CASCADE)
# # ASK: What is the "subregion"
#
# description = JSONField(_('description'))
# characteristics = JSONField(_('characteristics'))
# metadata_values = JSONField(_('metadata_values'))
# # common_relations_id
# # product_region_id
# code = models.CharField(_('code'), max_length=255)
# available = models.BooleanField(_('available'))
#
# # dealer_type
# # target_scope
# # target_type
# # rank
# # excluding_tax_unit_price
# # column_21
# # currencies_id
# # vintage
# # producer_price
# # producer_description
# # annual_produced_quantity
# # production_method_description
# # unit_name
# # unit
# # unit_values
# # organic_source
# # certificates
# # establishments_id
# # restrictions
# #
# objects = ProductManager.from_queryset(ProductQuerySet)()
#
# class Meta:
# verbose_name = _('product')
# verbose_name_plural = _('products')
#
#
# class ProductType(models.Model):
# """ProductType model."""
#
# class Meta:
# verbose_name_plural = _('product types')
# verbose_name = _('product type')

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@ -1 +0,0 @@
# Create your views here.

View File

@ -32,6 +32,13 @@ class EstablishmentDocument(Document):
}),
},
multi=True)
works_evening = fields.ListField(fields.IntegerField(
attr='works_evening'
))
works_noon = fields.ListField(fields.IntegerField(
attr='works_noon'
))
works_now = fields.BooleanField(attr='works_now')
tags = fields.ObjectField(
properties={
'id': fields.IntegerField(attr='id'),
@ -39,6 +46,14 @@ class EstablishmentDocument(Document):
properties=OBJECT_FIELD_PROPERTIES),
},
multi=True)
schedule = fields.ListField(fields.ObjectField(
properties={
'id': fields.IntegerField(attr='id'),
'weekday': fields.IntegerField(attr='weekday'),
'weekday_display': fields.KeywordField(attr='get_weekday_display'),
'closed_at': fields.KeywordField(attr='closed_at_str'),
}
))
address = fields.ObjectField(
properties={
'id': fields.IntegerField(),

View File

@ -24,6 +24,8 @@ class NewsDocument(Document):
country = fields.ObjectField(properties={'id': fields.IntegerField(),
'code': fields.KeywordField()})
web_url = fields.KeywordField(attr='web_url')
image_url = fields.KeywordField(attr='image_url')
preview_image_url = fields.KeywordField(attr='preview_image_url')
tags = fields.ObjectField(
properties={
'id': fields.IntegerField(attr='id'),
@ -37,14 +39,11 @@ class NewsDocument(Document):
model = models.News
fields = (
'id',
'playlist',
'start',
'end',
'slug',
'state',
'is_highlighted',
'image_url',
'preview_image_url',
'template',
)
related_models = [models.NewsType]

View File

@ -30,6 +30,15 @@ class AddressDocumentSerializer(serializers.Serializer):
geo_lat = serializers.FloatField(allow_null=True, source='coordinates.lat')
class ScheduleDocumentSerializer(serializers.Serializer):
"""Schedule serializer for ES Document"""
id = serializers.IntegerField()
weekday = serializers.IntegerField()
weekday_display = serializers.CharField()
closed_at = serializers.CharField()
class NewsDocumentSerializer(DocumentSerializer):
"""News document serializer."""
@ -68,6 +77,7 @@ class EstablishmentDocumentSerializer(DocumentSerializer):
address = AddressDocumentSerializer()
tags = TagsDocumentSerializer(many=True)
schedule = ScheduleDocumentSerializer(many=True, allow_null=True)
class Meta:
"""Meta class."""
@ -84,32 +94,11 @@ class EstablishmentDocumentSerializer(DocumentSerializer):
'preview_image',
'address',
'tags',
'schedule',
'works_noon',
'works_evening',
'works_now',
# 'collections',
# 'establishment_type',
# 'establishment_subtypes',
)
# def to_representation(self, instance):
# ret = super().to_representation(instance)
# dict_merge = lambda a, b: a.update(b) or a
#
# ret['tags'] = map(lambda tag: dict_merge(tag, {'label_translated': get_translated_value(tag.pop('label'))}),
# ret['tags'])
# ret['establishment_subtypes'] = map(
# lambda subtype: dict_merge(subtype, {'name_translated': get_translated_value(subtype.pop('name'))}),
# ret['establishment_subtypes'])
# if ret.get('establishment_type'):
# ret['establishment_type']['name_translated'] = get_translated_value(ret['establishment_type'].pop('name'))
# if ret.get('address'):
# ret['address']['city']['country']['name_translated'] = get_translated_value(
# ret['address']['city']['country'].pop('name'))
# location = ret['address'].pop('location')
# if location:
# ret['address']['geo_lon'] = location['lon']
# ret['address']['geo_lat'] = location['lat']
#
# ret['type'] = ret.pop('establishment_type')
# ret['subtypes'] = ret.pop('establishment_subtypes')
#
# return ret

View File

@ -38,7 +38,7 @@ def update_document(sender, **kwargs):
for establishment in establishments:
registry.update(establishment)
if model_name == 'establishmentsubtype':
if instance(instance, establishment_models.EstablishmentSubType):
if isinstance(instance, establishment_models.EstablishmentSubType):
establishments = Establishment.objects.filter(
establishment_subtypes=instance)
for establishment in establishments:

View File

@ -111,6 +111,9 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
},
'tags_category_id': {
'field': 'tags.category.id',
'lookups': [
constants.LOOKUP_QUERY_IN,
],
},
'collection_type': {
'field': 'collections.collection_type'
@ -121,6 +124,24 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
'establishment_subtypes': {
'field': 'establishment_subtypes.id'
},
'works_noon': {
'field': 'works_noon',
'lookups': [
constants.LOOKUP_QUERY_IN,
],
},
'works_evening': {
'field': 'works_evening',
'lookups': [
constants.LOOKUP_QUERY_IN,
],
},
'works_now': {
'field': 'works_now',
'lookups': [
constants.LOOKUP_FILTER_TERM,
]
},
}
geo_spatial_filter_fields = {

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-10-22 15:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tag', '0004_tag_priority'),
]
operations = [
migrations.AddField(
model_name='tagcategory',
name='index_name',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='indexing name', unique=True),
),
]

View File

@ -5,6 +5,14 @@ from configuration.models import TranslationSettings
from utils.models import TJSONField, TranslatedFieldsMixin
class TagQuerySet(models.QuerySet):
def filter_chosen(self):
return self.exclude(priority__isnull=True)
def order_by_priority(self):
return self.order_by('priority')
class Tag(TranslatedFieldsMixin, models.Model):
"""Tag model."""
@ -16,6 +24,8 @@ class Tag(TranslatedFieldsMixin, models.Model):
verbose_name=_('Category'))
priority = models.IntegerField(unique=True, null=True, default=None)
objects = TagQuerySet.as_manager()
class Meta:
"""Meta class."""
@ -56,7 +66,7 @@ class TagCategoryQuerySet(models.QuerySet):
def with_tags(self, switcher=True):
"""Filter by existing tags."""
return self.filter(tags__isnull=not switcher)
return self.exclude(tags__isnull=switcher)
class TagCategory(TranslatedFieldsMixin, models.Model):
@ -69,6 +79,8 @@ class TagCategory(TranslatedFieldsMixin, models.Model):
on_delete=models.SET_NULL, null=True,
default=None)
public = models.BooleanField(default=False)
index_name = models.CharField(max_length=255, blank=True, null=True,
verbose_name=_('indexing name'), unique=True)
objects = TagCategoryQuerySet.as_manager()

View File

@ -49,7 +49,8 @@ class TagCategoryBaseSerializer(serializers.ModelSerializer):
fields = (
'id',
'label_translated',
'tags'
'index_name',
'tags',
)

7
apps/tag/urls/mobile.py Normal file
View File

@ -0,0 +1,7 @@
from tag.urls.web import urlpatterns as common_urlpatterns
urlpatterns = [
]
urlpatterns.extend(common_urlpatterns)

View File

@ -7,6 +7,7 @@ app_name = 'tag'
router = SimpleRouter()
router.register(r'categories', views.TagCategoryViewSet)
router.register(r'chosen_tags', views.ChosenTagsView, basename='Tag')
urlpatterns = [

View File

@ -1,11 +1,22 @@
"""Tag views."""
from rest_framework import viewsets, mixins, status
from rest_framework import viewsets, mixins, status, generics
from rest_framework.decorators import action
from rest_framework.response import Response
from tag import filters, models, serializers
from rest_framework import permissions
class ChosenTagsView(generics.ListAPIView, viewsets.GenericViewSet):
pagination_class = None
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.TagBaseSerializer
def get_queryset(self):
return models.Tag.objects\
.filter_chosen() \
.order_by_priority()
# User`s views & viewsets
class TagCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
"""ViewSet for TagCategory model."""

Some files were not shown because too many files have changed in this diff Show More