Merge remote-tracking branch 'origin/feature/develop_ci' into feature/migrate-news
# Conflicts: # apps/gallery/models.py # requirements/base.txt
This commit is contained in:
commit
0adac058c5
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -22,3 +22,5 @@ logs/
|
||||||
|
|
||||||
# dev
|
# dev
|
||||||
./docker-compose.override.yml
|
./docker-compose.override.yml
|
||||||
|
|
||||||
|
celerybeat-schedule
|
||||||
|
|
|
||||||
40
.gitlab-ci.yml
Normal file
40
.gitlab-ci.yml
Normal 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
|
||||||
18
apps/account/migrations/0009_auto_20191002_0648.py
Normal file
18
apps/account/migrations/0009_auto_20191002_0648.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
14
apps/account/migrations/0011_merge_20191011_1336.py
Normal file
14
apps/account/migrations/0011_merge_20191011_1336.py
Normal 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 = [
|
||||||
|
]
|
||||||
14
apps/account/migrations/0012_merge_20191015_0912.py
Normal file
14
apps/account/migrations/0012_merge_20191015_0912.py
Normal 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 = [
|
||||||
|
]
|
||||||
14
apps/account/migrations/0014_merge_20191023_0959.py
Normal file
14
apps/account/migrations/0014_merge_20191023_0959.py
Normal 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 = [
|
||||||
|
]
|
||||||
14
apps/account/migrations/0015_merge_20191023_1317.py
Normal file
14
apps/account/migrations/0015_merge_20191023_1317.py
Normal 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 = [
|
||||||
|
]
|
||||||
24
apps/account/migrations/0016_auto_20191024_0830.py
Normal file
24
apps/account/migrations/0016_auto_20191024_0830.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -89,7 +89,7 @@ class User(AbstractUser):
|
||||||
blank=True, null=True, default=None)
|
blank=True, null=True, default=None)
|
||||||
cropped_image_url = models.URLField(verbose_name=_('Cropped image URL path'),
|
cropped_image_url = models.URLField(verbose_name=_('Cropped image URL path'),
|
||||||
blank=True, null=True, default=None)
|
blank=True, null=True, default=None)
|
||||||
email = models.EmailField(_('email address'), blank=True,
|
email = models.EmailField(_('email address'), unique=True,
|
||||||
null=True, default=None)
|
null=True, default=None)
|
||||||
unconfirmed_email = models.EmailField(_('unconfirmed email'), blank=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)
|
email_confirmed = models.BooleanField(_('email status'), default=False)
|
||||||
|
|
@ -214,6 +214,15 @@ class User(AbstractUser):
|
||||||
template_name=settings.RESETTING_TOKEN_TEMPLATE,
|
template_name=settings.RESETTING_TOKEN_TEMPLATE,
|
||||||
context=context)
|
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):
|
def confirm_email_template(self, country_code):
|
||||||
"""Get confirm email template"""
|
"""Get confirm email template"""
|
||||||
context = {'token': self.confirm_email_token,
|
context = {'token': self.confirm_email_token,
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,14 @@ class ChangePasswordSerializer(serializers.ModelSerializer):
|
||||||
except serializers.ValidationError as e:
|
except serializers.ValidationError as e:
|
||||||
raise serializers.ValidationError({'detail': e.detail})
|
raise serializers.ValidationError({'detail': e.detail})
|
||||||
else:
|
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
|
return attrs
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
"""Serializers for account web"""
|
"""Serializers for account web"""
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.auth import password_validation as password_validators
|
from django.contrib.auth import password_validation as password_validators
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from account import models
|
from account import models, tasks
|
||||||
from utils import exceptions as utils_exceptions
|
from utils import exceptions as utils_exceptions
|
||||||
from utils.methods import username_validator
|
from utils.methods import username_validator
|
||||||
|
|
||||||
|
|
@ -68,4 +69,12 @@ class PasswordResetConfirmSerializer(serializers.ModelSerializer):
|
||||||
# Update user password from instance
|
# Update user password from instance
|
||||||
instance.set_password(validated_data.get('password'))
|
instance.set_password(validated_data.get('password'))
|
||||||
instance.save()
|
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
|
return instance
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,46 @@
|
||||||
"""Account app celery tasks."""
|
"""Account app celery tasks."""
|
||||||
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
from django.utils.translation import gettext_lazy as _
|
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)
|
logging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.INFO)
|
||||||
logger = logging.getLogger(__name__)
|
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
|
@shared_task
|
||||||
def send_reset_password_email(user_id, country_code):
|
def send_reset_password_email(user_id, country_code):
|
||||||
"""Send email to user for reset password."""
|
"""Send email to user for reset password."""
|
||||||
try:
|
send_email(user_id, 'Password_resetting', 'reset_password_template', country_code)
|
||||||
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}')
|
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def confirm_new_email_address(user_id, country_code):
|
def confirm_new_email_address(user_id, country_code):
|
||||||
"""Send email to user new email."""
|
"""Send email to user new email."""
|
||||||
try:
|
send_email(user_id, 'Confirm new email address', 'confirm_email_template', country_code)
|
||||||
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}')
|
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def change_email_address(user_id, country_code):
|
def change_email_address(user_id, country_code):
|
||||||
"""Send email to user new email."""
|
"""Send email to user new email."""
|
||||||
try:
|
send_email(user_id, 'Validate new email address', 'change_email_template', country_code)
|
||||||
user = models.User.objects.get(id=user_id)
|
|
||||||
user.send_email(subject=_('Validate new email address'),
|
|
||||||
message=user.change_email_template(country_code))
|
@shared_task
|
||||||
except:
|
def send_password_changed_email(user_id, country_code):
|
||||||
logger.error(f'METHOD_NAME: {change_email_address.__name__}\n'
|
"""Send email which notifies user that his password had changed"""
|
||||||
f'DETAIL: Exception occurred for user: {user_id}')
|
send_email(user_id, 'Notify password changed', 'notify_password_changed_template', country_code)
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,6 @@ from utils.tokens import GMRefreshToken
|
||||||
# Serializers
|
# Serializers
|
||||||
class SignupSerializer(serializers.ModelSerializer):
|
class SignupSerializer(serializers.ModelSerializer):
|
||||||
"""Signup serializer serializer mixin"""
|
"""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:
|
class Meta:
|
||||||
model = account_models.User
|
model = account_models.User
|
||||||
fields = (
|
fields = (
|
||||||
|
|
@ -32,6 +26,12 @@ class SignupSerializer(serializers.ModelSerializer):
|
||||||
'email',
|
'email',
|
||||||
'newsletter'
|
'newsletter'
|
||||||
)
|
)
|
||||||
|
extra_kwargs = {
|
||||||
|
'username': {'write_only': True},
|
||||||
|
'password': {'write_only': True},
|
||||||
|
'email': {'write_only': True},
|
||||||
|
'newsletter': {'write_only': True}
|
||||||
|
}
|
||||||
|
|
||||||
def validate_username(self, value):
|
def validate_username(self, value):
|
||||||
"""Custom username validation"""
|
"""Custom username validation"""
|
||||||
|
|
|
||||||
0
apps/establishment/management/__init__.py
Normal file
0
apps/establishment/management/__init__.py
Normal file
0
apps/establishment/management/commands/__init__.py
Normal file
0
apps/establishment/management/commands/__init__.py
Normal 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...'))
|
||||||
|
|
@ -8,7 +8,7 @@ def fill_establishment_subtype(apps, schema_editor):
|
||||||
# version than this migration expects. We use the historical version.
|
# version than this migration expects. We use the historical version.
|
||||||
EstablishmentSubType = apps.get_model('establishment', 'EstablishmentSubType')
|
EstablishmentSubType = apps.get_model('establishment', 'EstablishmentSubType')
|
||||||
for n, et in enumerate(EstablishmentSubType.objects.all()):
|
for n, et in enumerate(EstablishmentSubType.objects.all()):
|
||||||
et.index_name = f'Type {n}'
|
et.index_name = 'Type %s' % n
|
||||||
et.save()
|
et.save()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
21
apps/establishment/migrations/0042_establishment_tz.py
Normal file
21
apps/establishment/migrations/0042_establishment_tz.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
"""Establishment models."""
|
"""Establishment models."""
|
||||||
|
from datetime import datetime
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
|
||||||
|
import elasticsearch_dsl
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes import fields as generic
|
from django.contrib.contenttypes import fields as generic
|
||||||
from django.contrib.gis.db.models.functions import Distance
|
from django.contrib.gis.db.models.functions import Distance
|
||||||
|
|
@ -19,6 +21,7 @@ from main.models import Award
|
||||||
from review.models import Review
|
from review.models import Review
|
||||||
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
|
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
|
||||||
TranslatedFieldsMixin, BaseAttributes)
|
TranslatedFieldsMixin, BaseAttributes)
|
||||||
|
from timezone_field import TimeZoneField
|
||||||
|
|
||||||
|
|
||||||
# todo: establishment type&subtypes check
|
# todo: establishment type&subtypes check
|
||||||
|
|
@ -128,15 +131,15 @@ class EstablishmentQuerySet(models.QuerySet):
|
||||||
else:
|
else:
|
||||||
return self.none()
|
return self.none()
|
||||||
|
|
||||||
# def es_search(self, value, locale=None):
|
def es_search(self, value, locale=None):
|
||||||
# """Search text via ElasticSearch."""
|
"""Search text via ElasticSearch."""
|
||||||
# from search_indexes.documents import EstablishmentDocument
|
from search_indexes.documents import EstablishmentDocument
|
||||||
# search = EstablishmentDocument.search().filter(
|
search = EstablishmentDocument.search().filter(
|
||||||
# Elastic_Q('match', name=value) |
|
elasticsearch_dsl.Q('match', name=value) |
|
||||||
# Elastic_Q('match', **{f'description.{locale}': value})
|
elasticsearch_dsl.Q('match', **{f'description.{locale}': value})
|
||||||
# ).execute()
|
).execute()
|
||||||
# ids = [result.meta.id for result in search]
|
ids = [result.meta.id for result in search]
|
||||||
# return self.filter(id__in=ids)
|
return self.filter(id__in=ids)
|
||||||
|
|
||||||
def by_country_code(self, code):
|
def by_country_code(self, code):
|
||||||
"""Return establishments by country code"""
|
"""Return establishments by country code"""
|
||||||
|
|
@ -204,7 +207,7 @@ class EstablishmentQuerySet(models.QuerySet):
|
||||||
.filter(image_url__isnull=False, public_mark__gte=10)
|
.filter(image_url__isnull=False, public_mark__gte=10)
|
||||||
.has_published_reviews()
|
.has_published_reviews()
|
||||||
.annotate_distance(point=establishment.location)
|
.annotate_distance(point=establishment.location)
|
||||||
.order_by('distance')[:settings.LIMITING_QUERY_NUMBER]
|
.order_by('distance')[:settings.LIMITING_QUERY_OBJECTS]
|
||||||
.values('id')
|
.values('id')
|
||||||
)
|
)
|
||||||
return self.filter(id__in=subquery_filter_by_distance) \
|
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)
|
self.filter(image_url__isnull=False, public_mark__gte=10)
|
||||||
.has_published_reviews()
|
.has_published_reviews()
|
||||||
.annotate_distance(point=point)
|
.annotate_distance(point=point)
|
||||||
.order_by('distance')[:settings.LIMITING_QUERY_NUMBER]
|
.order_by('distance')[:settings.LIMITING_QUERY_OBJECTS]
|
||||||
.values('id')
|
.values('id')
|
||||||
)
|
)
|
||||||
return self.filter(id__in=subquery_filter_by_distance) \
|
return self.filter(id__in=subquery_filter_by_distance) \
|
||||||
|
|
@ -351,6 +354,7 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
|
||||||
blank=True, null=True, default=None)
|
blank=True, null=True, default=None)
|
||||||
slug = models.SlugField(unique=True, max_length=255, null=True,
|
slug = models.SlugField(unique=True, max_length=255, null=True,
|
||||||
verbose_name=_('Establishment slug'))
|
verbose_name=_('Establishment slug'))
|
||||||
|
tz = TimeZoneField(default=settings.TIME_ZONE)
|
||||||
|
|
||||||
awards = generic.GenericRelation(to='main.Award', related_query_name='establishment')
|
awards = generic.GenericRelation(to='main.Award', related_query_name='establishment')
|
||||||
tags = models.ManyToManyField('tag.Tag', related_name='establishments',
|
tags = models.ManyToManyField('tag.Tag', related_name='establishments',
|
||||||
|
|
@ -416,6 +420,32 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
|
||||||
def best_price_carte(self):
|
def best_price_carte(self):
|
||||||
return 200
|
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
|
@property
|
||||||
def last_published_review(self):
|
def last_published_review(self):
|
||||||
"""Return last published review"""
|
"""Return last published review"""
|
||||||
|
|
@ -431,8 +461,8 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def the_most_recent_award(self):
|
def the_most_recent_award(self):
|
||||||
return Award.objects.filter(Q(establishment=self) | Q(employees__establishments=self)).latest(
|
return Award.objects.filter(Q(establishment=self) | Q(employees__establishments=self)) \
|
||||||
field_name='vintage_year')
|
.latest(field_name='vintage_year')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def country_id(self):
|
def country_id(self):
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ class EstablishmentListCreateSerializer(EstablishmentBaseSerializer):
|
||||||
'is_publish',
|
'is_publish',
|
||||||
'guestonline_id',
|
'guestonline_id',
|
||||||
'lastable_id',
|
'lastable_id',
|
||||||
|
'tz',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ class BaseTestCase(APITestCase):
|
||||||
self.test_news = News.objects.create(created_by=self.user, modified_by=self.user, title={"en-GB": "Test news"},
|
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,
|
news_type=self.test_news_type,
|
||||||
description={"en-GB": "Description test news"},
|
description={"en-GB": "Description test news"},
|
||||||
playlist=1, start="2020-12-03 12:00:00", end="2020-12-13 12:00:00",
|
start="2020-12-03 12:00:00", end="2020-12-13 12:00:00",
|
||||||
state=News.PUBLISHED, slug='test-news')
|
state=News.PUBLISHED, slug='test-news')
|
||||||
|
|
||||||
self.test_content_type = ContentType.objects.get(app_label="news", model="news")
|
self.test_content_type = ContentType.objects.get(app_label="news", model="news")
|
||||||
|
|
|
||||||
|
|
@ -5,4 +5,9 @@ from gallery.models import Image
|
||||||
|
|
||||||
@admin.register(Image)
|
@admin.register(Image)
|
||||||
class ImageModelAdmin(admin.ModelAdmin):
|
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
|
||||||
|
|
|
||||||
41
apps/gallery/migrations/0002_auto_20190930_0714.py
Normal file
41
apps/gallery/migrations/0002_auto_20190930_0714.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
24
apps/gallery/migrations/0003_auto_20191003_1228.py
Normal file
24
apps/gallery/migrations/0003_auto_20191003_1228.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -1,15 +1,34 @@
|
||||||
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
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.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."""
|
"""Image model."""
|
||||||
|
HORIZONTAL = 0
|
||||||
|
VERTICAL = 1
|
||||||
|
|
||||||
image = ThumbnailerImageField(upload_to=image_path,
|
ORIENTATIONS = (
|
||||||
verbose_name=_('Image file'), max_length=255)
|
(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='')
|
||||||
|
|
||||||
|
objects = ImageQuerySet.as_manager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
@ -18,4 +37,22 @@ class Image(ProjectBaseMixin, ImageMixin):
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""String representation"""
|
"""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()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,20 @@ class ImageSerializer(serializers.ModelSerializer):
|
||||||
# RESPONSE
|
# RESPONSE
|
||||||
url = serializers.ImageField(source='image',
|
url = serializers.ImageField(source='image',
|
||||||
read_only=True)
|
read_only=True)
|
||||||
|
orientation_display = serializers.CharField(source='get_orientation_display',
|
||||||
|
read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class"""
|
"""Meta class"""
|
||||||
model = models.Image
|
model = models.Image
|
||||||
fields = (
|
fields = [
|
||||||
'id',
|
'id',
|
||||||
'file',
|
'file',
|
||||||
'url'
|
'url',
|
||||||
)
|
'orientation',
|
||||||
|
'orientation_display',
|
||||||
|
'title',
|
||||||
|
]
|
||||||
|
extra_kwargs = {
|
||||||
|
'orientation': {'write_only': True}
|
||||||
|
}
|
||||||
|
|
|
||||||
23
apps/gallery/tasks.py
Normal file
23
apps/gallery/tasks.py
Normal 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}')
|
||||||
|
|
@ -6,5 +6,6 @@ from . import views
|
||||||
app_name = 'gallery'
|
app_name = 'gallery'
|
||||||
|
|
||||||
urlpatterns = [
|
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'),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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):
|
class ImageBaseView(generics.GenericAPIView):
|
||||||
"""Upload image to gallery"""
|
"""Base Image view."""
|
||||||
model = models.Image
|
model = models.Image
|
||||||
queryset = models.Image.objects.all()
|
queryset = models.Image.objects.all()
|
||||||
serializer_class = serializers.ImageSerializer
|
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)
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,9 @@ from django.db.models.signals import post_save
|
||||||
from django.db.transaction import on_commit
|
from django.db.transaction import on_commit
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from utils.models import ProjectBaseMixin, SVGImageMixin, TranslatedFieldsMixin, TJSONField
|
|
||||||
from translation.models import Language
|
from translation.models import Language
|
||||||
|
from utils.models import ProjectBaseMixin, SVGImageMixin, TranslatedFieldsMixin, TJSONField
|
||||||
|
|
||||||
|
|
||||||
class Country(TranslatedFieldsMixin, SVGImageMixin, ProjectBaseMixin):
|
class Country(TranslatedFieldsMixin, SVGImageMixin, ProjectBaseMixin):
|
||||||
|
|
|
||||||
57
apps/main/migrations/0022_auto_20191023_1113.py
Normal file
57
apps/main/migrations/0022_auto_20191023_1113.py
Normal 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',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -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):
|
class SiteSettingsQuerySet(models.QuerySet):
|
||||||
"""Extended queryset for SiteSettings model."""
|
"""Extended queryset for SiteSettings model."""
|
||||||
|
|
||||||
|
|
@ -114,6 +130,7 @@ class SiteSettingsQuerySet(models.QuerySet):
|
||||||
|
|
||||||
|
|
||||||
class SiteSettings(ProjectBaseMixin):
|
class SiteSettings(ProjectBaseMixin):
|
||||||
|
|
||||||
subdomain = models.CharField(max_length=255, db_index=True, unique=True,
|
subdomain = models.CharField(max_length=255, db_index=True, unique=True,
|
||||||
verbose_name=_('Subdomain'))
|
verbose_name=_('Subdomain'))
|
||||||
country = models.OneToOneField(Country, on_delete=models.PROTECT,
|
country = models.OneToOneField(Country, on_delete=models.PROTECT,
|
||||||
|
|
@ -135,6 +152,7 @@ class SiteSettings(ProjectBaseMixin):
|
||||||
verbose_name=_('Config'))
|
verbose_name=_('Config'))
|
||||||
ad_config = models.TextField(blank=True, null=True, default=None,
|
ad_config = models.TextField(blank=True, null=True, default=None,
|
||||||
verbose_name=_('AD config'))
|
verbose_name=_('AD config'))
|
||||||
|
currency = models.ForeignKey(Currency, on_delete=models.PROTECT, null=True, default=None)
|
||||||
|
|
||||||
objects = SiteSettingsQuerySet.as_manager()
|
objects = SiteSettingsQuerySet.as_manager()
|
||||||
|
|
||||||
|
|
@ -257,18 +275,6 @@ class AwardType(models.Model):
|
||||||
return self.name
|
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):
|
class CarouselQuerySet(models.QuerySet):
|
||||||
"""Carousel QuerySet."""
|
"""Carousel QuerySet."""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ from rest_framework import serializers
|
||||||
from advertisement.serializers.web import AdvertisementSerializer
|
from advertisement.serializers.web import AdvertisementSerializer
|
||||||
from location.serializers import CountrySerializer
|
from location.serializers import CountrySerializer
|
||||||
from main import models
|
from main import models
|
||||||
from utils.serializers import TranslatedField
|
from utils.serializers import ProjectModelSerializer
|
||||||
|
|
||||||
|
|
||||||
class FeatureSerializer(serializers.ModelSerializer):
|
class FeatureSerializer(serializers.ModelSerializer):
|
||||||
|
|
@ -40,11 +40,24 @@ class SiteFeatureSerializer(serializers.ModelSerializer):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CurrencySerializer(ProjectModelSerializer):
|
||||||
|
"""Currency serializer"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Currency
|
||||||
|
fields = [
|
||||||
|
'id',
|
||||||
|
'name_translated',
|
||||||
|
'sign'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class SiteSettingsSerializer(serializers.ModelSerializer):
|
class SiteSettingsSerializer(serializers.ModelSerializer):
|
||||||
"""Site settings serializer."""
|
"""Site settings serializer."""
|
||||||
|
|
||||||
published_features = SiteFeatureSerializer(source='published_sitefeatures',
|
published_features = SiteFeatureSerializer(source='published_sitefeatures',
|
||||||
many=True, allow_null=True)
|
many=True, allow_null=True)
|
||||||
|
currency = CurrencySerializer()
|
||||||
# todo: remove this
|
# todo: remove this
|
||||||
country_code = serializers.CharField(source='subdomain', read_only=True)
|
country_code = serializers.CharField(source='subdomain', read_only=True)
|
||||||
|
|
||||||
|
|
@ -63,6 +76,7 @@ class SiteSettingsSerializer(serializers.ModelSerializer):
|
||||||
'config',
|
'config',
|
||||||
'ad_config',
|
'ad_config',
|
||||||
'published_features',
|
'published_features',
|
||||||
|
'currency'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -114,17 +128,6 @@ class AwardSerializer(AwardBaseSerializer):
|
||||||
fields = AwardBaseSerializer.Meta.fields + ['award_type', ]
|
fields = AwardBaseSerializer.Meta.fields + ['award_type', ]
|
||||||
|
|
||||||
|
|
||||||
class CurrencySerializer(serializers.ModelSerializer):
|
|
||||||
"""Currency serializer"""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = models.Currency
|
|
||||||
fields = [
|
|
||||||
'id',
|
|
||||||
'name'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class CarouselListSerializer(serializers.ModelSerializer):
|
class CarouselListSerializer(serializers.ModelSerializer):
|
||||||
"""Serializer for retrieving list of carousel items."""
|
"""Serializer for retrieving list of carousel items."""
|
||||||
model_name = serializers.CharField()
|
model_name = serializers.CharField()
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
from news import models
|
from news import models
|
||||||
from .tasks import send_email_with_news
|
from .tasks import send_email_with_news
|
||||||
|
|
||||||
|
|
@ -12,9 +14,10 @@ class NewsTypeAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
def send_email_action(modeladmin, request, queryset):
|
def send_email_action(modeladmin, request, queryset):
|
||||||
news_ids = list(queryset.values_list("id", flat=True))
|
news_ids = list(queryset.values_list("id", flat=True))
|
||||||
|
if settings.USE_CELERY:
|
||||||
send_email_with_news.delay(news_ids)
|
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"
|
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):
|
class NewsAdmin(admin.ModelAdmin):
|
||||||
"""News admin."""
|
"""News admin."""
|
||||||
actions = [send_email_action]
|
actions = [send_email_action]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(models.NewsGallery)
|
||||||
|
class NewsGalleryAdmin(admin.ModelAdmin):
|
||||||
|
"""News gallery admin."""
|
||||||
|
|
|
||||||
26
apps/news/migrations/0015_newsgallery.py
Normal file
26
apps/news/migrations/0015_newsgallery.py
Normal 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',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
19
apps/news/migrations/0016_news_gallery.py
Normal file
19
apps/news/migrations/0016_news_gallery.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
14
apps/news/migrations/0020_merge_20190930_1251.py
Normal file
14
apps/news/migrations/0020_merge_20190930_1251.py
Normal 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 = [
|
||||||
|
]
|
||||||
14
apps/news/migrations/0021_merge_20191002_1300.py
Normal file
14
apps/news/migrations/0021_merge_20191002_1300.py
Normal 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 = [
|
||||||
|
]
|
||||||
14
apps/news/migrations/0022_merge_20191015_0912.py
Normal file
14
apps/news/migrations/0022_merge_20191015_0912.py
Normal 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 = [
|
||||||
|
]
|
||||||
14
apps/news/migrations/0023_merge_20191023_1000.py
Normal file
14
apps/news/migrations/0023_merge_20191023_1000.py
Normal 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 = [
|
||||||
|
]
|
||||||
18
apps/news/migrations/0024_newsgallery_is_main.py
Normal file
18
apps/news/migrations/0024_newsgallery_is_main.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
15
apps/news/migrations/0025_merge_20191023_1317.py
Normal file
15
apps/news/migrations/0025_merge_20191023_1317.py
Normal 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 = [
|
||||||
|
]
|
||||||
21
apps/news/migrations/0026_auto_20191024_0913.py
Normal file
21
apps/news/migrations/0026_auto_20191024_0913.py
Normal 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',
|
||||||
|
),
|
||||||
|
]
|
||||||
17
apps/news/migrations/0027_remove_news_playlist.py
Normal file
17
apps/news/migrations/0027_remove_news_playlist.py
Normal 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',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -141,15 +141,10 @@ class News(BaseAttributes, TranslatedFieldsMixin):
|
||||||
verbose_name=_('End'))
|
verbose_name=_('End'))
|
||||||
slug = models.SlugField(unique=True, max_length=255,
|
slug = models.SlugField(unique=True, max_length=255,
|
||||||
verbose_name=_('News slug'))
|
verbose_name=_('News slug'))
|
||||||
playlist = models.IntegerField(_('playlist'))
|
|
||||||
state = models.PositiveSmallIntegerField(default=WAITING, choices=STATE_CHOICES,
|
state = models.PositiveSmallIntegerField(default=WAITING, choices=STATE_CHOICES,
|
||||||
verbose_name=_('State'))
|
verbose_name=_('State'))
|
||||||
is_highlighted = models.BooleanField(default=False,
|
is_highlighted = models.BooleanField(default=False,
|
||||||
verbose_name=_('Is highlighted'))
|
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)
|
template = models.PositiveIntegerField(choices=TEMPLATE_CHOICES, default=NEWSPAPER)
|
||||||
address = models.ForeignKey('location.Address', blank=True, null=True,
|
address = models.ForeignKey('location.Address', blank=True, null=True,
|
||||||
default=None, verbose_name=_('address'),
|
default=None, verbose_name=_('address'),
|
||||||
|
|
@ -159,6 +154,7 @@ class News(BaseAttributes, TranslatedFieldsMixin):
|
||||||
verbose_name=_('country'))
|
verbose_name=_('country'))
|
||||||
tags = models.ManyToManyField('tag.Tag', related_name='news',
|
tags = models.ManyToManyField('tag.Tag', related_name='news',
|
||||||
verbose_name=_('Tags'))
|
verbose_name=_('Tags'))
|
||||||
|
gallery = models.ManyToManyField('gallery.Image', through='news.NewsGallery')
|
||||||
ratings = generic.GenericRelation(Rating)
|
ratings = generic.GenericRelation(Rating)
|
||||||
|
|
||||||
agenda = models.ForeignKey('news.Agenda', blank=True, null=True,
|
agenda = models.ForeignKey('news.Agenda', blank=True, null=True,
|
||||||
|
|
@ -195,3 +191,36 @@ class News(BaseAttributes, TranslatedFieldsMixin):
|
||||||
@property
|
@property
|
||||||
def same_theme(self):
|
def same_theme(self):
|
||||||
return self.__class__.objects.same_theme(self)[:3]
|
return self.__class__.objects.same_theme(self)[:3]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def main_image(self):
|
||||||
|
return self.news_gallery.main_images().first().image
|
||||||
|
|
||||||
|
|
||||||
|
class NewsGalleryQuerySet(models.QuerySet):
|
||||||
|
"""QuerySet for model News"""
|
||||||
|
|
||||||
|
def main_images(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.SET_NULL,
|
||||||
|
verbose_name=_('news'))
|
||||||
|
image = models.ForeignKey('gallery.Image', null=True,
|
||||||
|
related_name='news_gallery',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
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')
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
"""News app common serializers."""
|
"""News app common serializers."""
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from account.serializers.common import UserBaseSerializer
|
from account.serializers.common import UserBaseSerializer
|
||||||
|
from gallery.models import Image
|
||||||
from location import models as location_models
|
from location import models as location_models
|
||||||
from location.serializers import CountrySimpleSerializer, AddressBaseSerializer
|
from location.serializers import CountrySimpleSerializer, AddressBaseSerializer
|
||||||
from news import models
|
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):
|
class NewsTypeSerializer(serializers.ModelSerializer):
|
||||||
"""News type serializer."""
|
"""News type serializer."""
|
||||||
|
|
||||||
|
|
@ -55,11 +129,8 @@ class NewsTypeSerializer(serializers.ModelSerializer):
|
||||||
class NewsBaseSerializer(ProjectModelSerializer):
|
class NewsBaseSerializer(ProjectModelSerializer):
|
||||||
"""Base serializer for News model."""
|
"""Base serializer for News model."""
|
||||||
|
|
||||||
# read only fields
|
title_translated = TranslatedField()
|
||||||
title_translated = TranslatedField(source='title')
|
|
||||||
subtitle_translated = TranslatedField()
|
subtitle_translated = TranslatedField()
|
||||||
|
|
||||||
# related fields
|
|
||||||
news_type = NewsTypeSerializer(read_only=True)
|
news_type = NewsTypeSerializer(read_only=True)
|
||||||
tags = TagBaseSerializer(read_only=True, many=True)
|
tags = TagBaseSerializer(read_only=True, many=True)
|
||||||
|
|
||||||
|
|
@ -72,14 +143,25 @@ class NewsBaseSerializer(ProjectModelSerializer):
|
||||||
'title_translated',
|
'title_translated',
|
||||||
'subtitle_translated',
|
'subtitle_translated',
|
||||||
'is_highlighted',
|
'is_highlighted',
|
||||||
'image_url',
|
|
||||||
'preview_image_url',
|
|
||||||
'news_type',
|
'news_type',
|
||||||
'tags',
|
'tags',
|
||||||
'slug',
|
'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):
|
class NewsDetailSerializer(NewsBaseSerializer):
|
||||||
"""News detail serializer."""
|
"""News detail serializer."""
|
||||||
|
|
||||||
|
|
@ -88,6 +170,7 @@ class NewsDetailSerializer(NewsBaseSerializer):
|
||||||
author = UserBaseSerializer(source='created_by', read_only=True)
|
author = UserBaseSerializer(source='created_by', read_only=True)
|
||||||
state_display = serializers.CharField(source='get_state_display',
|
state_display = serializers.CharField(source='get_state_display',
|
||||||
read_only=True)
|
read_only=True)
|
||||||
|
gallery = NewsImageSerializer(read_only=True, many=True)
|
||||||
|
|
||||||
class Meta(NewsBaseSerializer.Meta):
|
class Meta(NewsBaseSerializer.Meta):
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
@ -96,12 +179,12 @@ class NewsDetailSerializer(NewsBaseSerializer):
|
||||||
'description_translated',
|
'description_translated',
|
||||||
'start',
|
'start',
|
||||||
'end',
|
'end',
|
||||||
'playlist',
|
|
||||||
'is_publish',
|
'is_publish',
|
||||||
'state',
|
'state',
|
||||||
'state_display',
|
'state_display',
|
||||||
'author',
|
'author',
|
||||||
'country',
|
'country',
|
||||||
|
'gallery',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -160,3 +243,48 @@ class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer,
|
||||||
'template',
|
'template',
|
||||||
'template_display',
|
'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_images().exists():
|
||||||
|
raise serializers.ValidationError({'detail': _('Main image is already added')})
|
||||||
|
|
||||||
|
attrs['news'] = news
|
||||||
|
attrs['image'] = image
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ class BaseTestCase(APITestCase):
|
||||||
title={"en-GB": "Test news"},
|
title={"en-GB": "Test news"},
|
||||||
news_type=self.test_news_type,
|
news_type=self.test_news_type,
|
||||||
description={"en-GB": "Description test news"},
|
description={"en-GB": "Description test news"},
|
||||||
playlist=1, start=datetime.now() + timedelta(hours=-2),
|
start=datetime.now() + timedelta(hours=-2),
|
||||||
end=datetime.now() + timedelta(hours=2),
|
end=datetime.now() + timedelta(hours=2),
|
||||||
state=News.PUBLISHED, slug='test-news-slug',
|
state=News.PUBLISHED, slug='test-news-slug',
|
||||||
country=self.country_ru)
|
country=self.country_ru)
|
||||||
|
|
@ -85,7 +85,6 @@ class NewsTestCase(BaseTestCase):
|
||||||
'description': {"en-GB": "Description test news!"},
|
'description': {"en-GB": "Description test news!"},
|
||||||
'slug': self.test_news.slug,
|
'slug': self.test_news.slug,
|
||||||
'start': self.test_news.start,
|
'start': self.test_news.start,
|
||||||
'playlist': self.test_news.playlist,
|
|
||||||
'news_type_id':self.test_news.news_type_id,
|
'news_type_id':self.test_news.news_type_id,
|
||||||
'country_id': self.country_ru.id
|
'country_id': self.country_ru.id
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
"""News app urlpatterns for backoffice"""
|
"""News app urlpatterns for backoffice"""
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from news import views
|
from news import views
|
||||||
|
|
||||||
app_name = 'news'
|
app_name = 'news'
|
||||||
|
|
@ -7,5 +8,9 @@ app_name = 'news'
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', views.NewsBackOfficeLCView.as_view(), name='list-create'),
|
path('', views.NewsBackOfficeLCView.as_view(), name='list-create'),
|
||||||
path('<int:pk>/', views.NewsBackOfficeRUDView.as_view(),
|
path('<int:pk>/', views.NewsBackOfficeRUDView.as_view(),
|
||||||
name='retrieve-update-destroy'),
|
name='gallery-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'),
|
||||||
]
|
]
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
"""News app views."""
|
"""News app views."""
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db.transaction import on_commit
|
||||||
from django.shortcuts import get_object_or_404
|
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 news import filters, models, serializers
|
||||||
from rating.tasks import add_rating
|
from rating.tasks import add_rating
|
||||||
from utils.permissions import IsCountryAdmin, IsContentPageManager
|
from utils.permissions import IsCountryAdmin, IsContentPageManager
|
||||||
|
|
@ -10,7 +15,6 @@ class NewsMixinView:
|
||||||
"""News mixin."""
|
"""News mixin."""
|
||||||
|
|
||||||
permission_classes = (permissions.AllowAny, )
|
permission_classes = (permissions.AllowAny, )
|
||||||
serializer_class = serializers.NewsBaseSerializer
|
|
||||||
|
|
||||||
def get_queryset(self, *args, **kwargs):
|
def get_queryset(self, *args, **kwargs):
|
||||||
"""Override get_queryset method."""
|
"""Override get_queryset method."""
|
||||||
|
|
@ -24,6 +28,7 @@ class NewsMixinView:
|
||||||
class NewsListView(NewsMixinView, generics.ListAPIView):
|
class NewsListView(NewsMixinView, generics.ListAPIView):
|
||||||
"""News list view."""
|
"""News list view."""
|
||||||
|
|
||||||
|
serializer_class = serializers.NewsListSerializer
|
||||||
filter_class = filters.NewsListFilterSet
|
filter_class = filters.NewsListFilterSet
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -74,6 +79,64 @@ class NewsBackOfficeLCView(NewsBackOfficeMixinView,
|
||||||
return super().get_queryset().with_extended_related()
|
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,
|
class NewsBackOfficeRUDView(NewsBackOfficeMixinView,
|
||||||
generics.RetrieveUpdateDestroyAPIView):
|
generics.RetrieveUpdateDestroyAPIView):
|
||||||
"""Resource for detailed information about news for back-office users."""
|
"""Resource for detailed information about news for back-office users."""
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,13 @@ class EstablishmentDocument(Document):
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
multi=True)
|
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(
|
tags = fields.ObjectField(
|
||||||
properties={
|
properties={
|
||||||
'id': fields.IntegerField(attr='id'),
|
'id': fields.IntegerField(attr='id'),
|
||||||
|
|
@ -39,6 +46,14 @@ class EstablishmentDocument(Document):
|
||||||
properties=OBJECT_FIELD_PROPERTIES),
|
properties=OBJECT_FIELD_PROPERTIES),
|
||||||
},
|
},
|
||||||
multi=True)
|
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(
|
address = fields.ObjectField(
|
||||||
properties={
|
properties={
|
||||||
'id': fields.IntegerField(),
|
'id': fields.IntegerField(),
|
||||||
|
|
|
||||||
|
|
@ -37,14 +37,11 @@ class NewsDocument(Document):
|
||||||
model = models.News
|
model = models.News
|
||||||
fields = (
|
fields = (
|
||||||
'id',
|
'id',
|
||||||
'playlist',
|
|
||||||
'start',
|
'start',
|
||||||
'end',
|
'end',
|
||||||
'slug',
|
'slug',
|
||||||
'state',
|
'state',
|
||||||
'is_highlighted',
|
'is_highlighted',
|
||||||
'image_url',
|
|
||||||
'preview_image_url',
|
|
||||||
'template',
|
'template',
|
||||||
)
|
)
|
||||||
related_models = [models.NewsType]
|
related_models = [models.NewsType]
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,15 @@ class AddressDocumentSerializer(serializers.Serializer):
|
||||||
geo_lat = serializers.FloatField(allow_null=True, source='coordinates.lat')
|
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):
|
class NewsDocumentSerializer(DocumentSerializer):
|
||||||
"""News document serializer."""
|
"""News document serializer."""
|
||||||
|
|
||||||
|
|
@ -68,6 +77,7 @@ class EstablishmentDocumentSerializer(DocumentSerializer):
|
||||||
|
|
||||||
address = AddressDocumentSerializer()
|
address = AddressDocumentSerializer()
|
||||||
tags = TagsDocumentSerializer(many=True)
|
tags = TagsDocumentSerializer(many=True)
|
||||||
|
schedule = ScheduleDocumentSerializer(many=True, allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
@ -84,6 +94,10 @@ class EstablishmentDocumentSerializer(DocumentSerializer):
|
||||||
'preview_image',
|
'preview_image',
|
||||||
'address',
|
'address',
|
||||||
'tags',
|
'tags',
|
||||||
|
'schedule',
|
||||||
|
'works_noon',
|
||||||
|
'works_evening',
|
||||||
|
'works_now',
|
||||||
# 'collections',
|
# 'collections',
|
||||||
# 'establishment_type',
|
# 'establishment_type',
|
||||||
# 'establishment_subtypes',
|
# 'establishment_subtypes',
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ def update_document(sender, **kwargs):
|
||||||
for establishment in establishments:
|
for establishment in establishments:
|
||||||
registry.update(establishment)
|
registry.update(establishment)
|
||||||
if model_name == 'establishmentsubtype':
|
if model_name == 'establishmentsubtype':
|
||||||
if instance(instance, establishment_models.EstablishmentSubType):
|
if isinstance(instance, establishment_models.EstablishmentSubType):
|
||||||
establishments = Establishment.objects.filter(
|
establishments = Establishment.objects.filter(
|
||||||
establishment_subtypes=instance)
|
establishment_subtypes=instance)
|
||||||
for establishment in establishments:
|
for establishment in establishments:
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,9 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
|
||||||
},
|
},
|
||||||
'tags_category_id': {
|
'tags_category_id': {
|
||||||
'field': 'tags.category.id',
|
'field': 'tags.category.id',
|
||||||
|
'lookups': [
|
||||||
|
constants.LOOKUP_QUERY_IN,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
'collection_type': {
|
'collection_type': {
|
||||||
'field': 'collections.collection_type'
|
'field': 'collections.collection_type'
|
||||||
|
|
@ -121,6 +124,24 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
|
||||||
'establishment_subtypes': {
|
'establishment_subtypes': {
|
||||||
'field': 'establishment_subtypes.id'
|
'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 = {
|
geo_spatial_filter_fields = {
|
||||||
|
|
|
||||||
18
apps/tag/migrations/0005_tagcategory_name_indexing.py
Normal file
18
apps/tag/migrations/0005_tagcategory_name_indexing.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -5,6 +5,14 @@ from configuration.models import TranslationSettings
|
||||||
from utils.models import TJSONField, TranslatedFieldsMixin
|
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):
|
class Tag(TranslatedFieldsMixin, models.Model):
|
||||||
"""Tag model."""
|
"""Tag model."""
|
||||||
|
|
||||||
|
|
@ -16,6 +24,8 @@ class Tag(TranslatedFieldsMixin, models.Model):
|
||||||
verbose_name=_('Category'))
|
verbose_name=_('Category'))
|
||||||
priority = models.IntegerField(unique=True, null=True, default=None)
|
priority = models.IntegerField(unique=True, null=True, default=None)
|
||||||
|
|
||||||
|
objects = TagQuerySet.as_manager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
||||||
|
|
@ -56,7 +66,7 @@ class TagCategoryQuerySet(models.QuerySet):
|
||||||
|
|
||||||
def with_tags(self, switcher=True):
|
def with_tags(self, switcher=True):
|
||||||
"""Filter by existing tags."""
|
"""Filter by existing tags."""
|
||||||
return self.filter(tags__isnull=not switcher)
|
return self.exclude(tags__isnull=switcher)
|
||||||
|
|
||||||
|
|
||||||
class TagCategory(TranslatedFieldsMixin, models.Model):
|
class TagCategory(TranslatedFieldsMixin, models.Model):
|
||||||
|
|
@ -69,6 +79,8 @@ class TagCategory(TranslatedFieldsMixin, models.Model):
|
||||||
on_delete=models.SET_NULL, null=True,
|
on_delete=models.SET_NULL, null=True,
|
||||||
default=None)
|
default=None)
|
||||||
public = models.BooleanField(default=False)
|
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()
|
objects = TagCategoryQuerySet.as_manager()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,8 @@ class TagCategoryBaseSerializer(serializers.ModelSerializer):
|
||||||
fields = (
|
fields = (
|
||||||
'id',
|
'id',
|
||||||
'label_translated',
|
'label_translated',
|
||||||
'tags'
|
'index_name',
|
||||||
|
'tags',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
7
apps/tag/urls/mobile.py
Normal file
7
apps/tag/urls/mobile.py
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
from tag.urls.web import urlpatterns as common_urlpatterns
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
urlpatterns.extend(common_urlpatterns)
|
||||||
|
|
@ -7,6 +7,7 @@ app_name = 'tag'
|
||||||
|
|
||||||
router = SimpleRouter()
|
router = SimpleRouter()
|
||||||
router.register(r'categories', views.TagCategoryViewSet)
|
router.register(r'categories', views.TagCategoryViewSet)
|
||||||
|
router.register(r'chosen_tags', views.ChosenTagsView, basename='Tag')
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,22 @@
|
||||||
"""Tag views."""
|
"""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.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from tag import filters, models, serializers
|
from tag import filters, models, serializers
|
||||||
from rest_framework import permissions
|
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
|
# User`s views & viewsets
|
||||||
class TagCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
|
class TagCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
|
||||||
"""ViewSet for TagCategory model."""
|
"""ViewSet for TagCategory model."""
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from datetime import time
|
||||||
|
|
||||||
from utils.models import ProjectBaseMixin
|
from utils.models import ProjectBaseMixin
|
||||||
|
|
||||||
|
|
@ -14,6 +15,8 @@ class Timetable(ProjectBaseMixin):
|
||||||
SATURDAY = 5
|
SATURDAY = 5
|
||||||
SUNDAY = 6
|
SUNDAY = 6
|
||||||
|
|
||||||
|
NOON = time(17, 0)
|
||||||
|
|
||||||
WEEKDAYS_CHOICES = (
|
WEEKDAYS_CHOICES = (
|
||||||
(MONDAY, _('Monday')),
|
(MONDAY, _('Monday')),
|
||||||
(TUESDAY, _('Tuesday')),
|
(TUESDAY, _('Tuesday')),
|
||||||
|
|
@ -32,6 +35,18 @@ class Timetable(ProjectBaseMixin):
|
||||||
opening_at = models.TimeField(verbose_name=_('Opening time'), null=True)
|
opening_at = models.TimeField(verbose_name=_('Opening time'), null=True)
|
||||||
closed_at = models.TimeField(verbose_name=_('Closed time'), null=True)
|
closed_at = models.TimeField(verbose_name=_('Closed time'), null=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def closed_at_str(self):
|
||||||
|
return str(self.closed_at) if self.closed_at else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def works_at_noon(self):
|
||||||
|
return bool(self.closed_at and self.closed_at <= self.NOON)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def works_at_afternoon(self):
|
||||||
|
return bool(self.closed_at and self.closed_at > self.NOON)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
verbose_name = _('Timetable')
|
verbose_name = _('Timetable')
|
||||||
|
|
|
||||||
|
|
@ -77,3 +77,17 @@ class ScheduleCreateSerializer(ScheduleRUDSerializer):
|
||||||
schedule_qs.delete()
|
schedule_qs.delete()
|
||||||
establishment.schedule.add(instance)
|
establishment.schedule.add(instance)
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class TimetableSerializer(serializers.ModelSerializer):
|
||||||
|
"""Serailzier for Timetable model."""
|
||||||
|
weekday_display = serializers.CharField(source='get_weekday_display',
|
||||||
|
read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Timetable
|
||||||
|
fields = (
|
||||||
|
'id',
|
||||||
|
'weekday_display',
|
||||||
|
'works_at_noon',
|
||||||
|
)
|
||||||
|
|
|
||||||
0
apps/timetable/urls/__init__.py
Normal file
0
apps/timetable/urls/__init__.py
Normal file
|
|
@ -6,5 +6,5 @@ from timetable import views
|
||||||
app_name = 'timetable'
|
app_name = 'timetable'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', views.TimetableListView.as_view(), name='list')
|
# path('', views.TimetableListView.as_view(), name='list')
|
||||||
]
|
]
|
||||||
6
apps/timetable/urls/mobile.py
Normal file
6
apps/timetable/urls/mobile.py
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
from timetable.urls.common import urlpatterns as common_urlpatterns
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = []
|
||||||
|
|
||||||
|
urlpatterns.extend(common_urlpatterns)
|
||||||
6
apps/timetable/urls/web.py
Normal file
6
apps/timetable/urls/web.py
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
from timetable.urls.common import urlpatterns as common_urlpatterns
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = []
|
||||||
|
|
||||||
|
urlpatterns.extend(common_urlpatterns)
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from rest_framework import generics
|
from rest_framework import generics, permissions
|
||||||
from timetable import serialziers, models
|
from timetable import serialziers, models
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -6,3 +6,5 @@ class TimetableListView(generics.ListAPIView):
|
||||||
"""Method to get timetables"""
|
"""Method to get timetables"""
|
||||||
serializer_class = serialziers.TimetableSerializer
|
serializer_class = serialziers.TimetableSerializer
|
||||||
queryset = models.Timetable.objects.all()
|
queryset = models.Timetable.objects.all()
|
||||||
|
pagination_class = None
|
||||||
|
permission_classes = (permissions.AllowAny, )
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,19 @@
|
||||||
"""Utils app method."""
|
"""Utils app method."""
|
||||||
|
import logging
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
import string
|
import string
|
||||||
|
|
||||||
|
import requests
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.http.request import HttpRequest
|
from django.http.request import HttpRequest
|
||||||
from django.utils.timezone import datetime
|
from django.utils.timezone import datetime
|
||||||
|
from rest_framework import status
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
|
from os.path import exists
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def generate_code(digits=6, string_output=True):
|
def generate_code(digits=6, string_output=True):
|
||||||
|
|
@ -86,3 +92,30 @@ def get_contenttype(app_label: str, model: str):
|
||||||
qs = ContentType.objects.filter(app_label=app_label, model=model)
|
qs = ContentType.objects.filter(app_label=app_label, model=model)
|
||||||
if qs.exists():
|
if qs.exists():
|
||||||
return qs.first()
|
return qs.first()
|
||||||
|
|
||||||
|
|
||||||
|
def image_url_valid(url: str):
|
||||||
|
"""
|
||||||
|
Check if requested URL is valid.
|
||||||
|
:param url: string
|
||||||
|
:return: boolean
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
assert url.startswith('http')
|
||||||
|
response = requests.request('head', url)
|
||||||
|
except Exception as e:
|
||||||
|
logger.info(f'ConnectionError: {e}')
|
||||||
|
else:
|
||||||
|
return response.status_code == status.HTTP_200_OK
|
||||||
|
|
||||||
|
|
||||||
|
def absolute_url_decorator(func):
|
||||||
|
def get_absolute_image_url(self, obj):
|
||||||
|
"""Get absolute image url"""
|
||||||
|
url_path = func(self, obj)
|
||||||
|
if url_path:
|
||||||
|
if url_path.startswith('/media/'):
|
||||||
|
return f'{settings.MEDIA_URL}{url_path}/'
|
||||||
|
else:
|
||||||
|
return url_path
|
||||||
|
return get_absolute_image_url
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
"""Utils app models."""
|
"""Utils app models."""
|
||||||
|
import logging
|
||||||
from os.path import exists
|
from os.path import exists
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.auth.tokens import PasswordResetTokenGenerator
|
from django.contrib.auth.tokens import PasswordResetTokenGenerator
|
||||||
from django.contrib.gis.db import models
|
from django.contrib.gis.db import models
|
||||||
from django.contrib.postgres.fields import JSONField
|
from django.contrib.postgres.fields import JSONField
|
||||||
|
|
@ -8,10 +11,13 @@ from django.utils import timezone
|
||||||
from django.utils.html import mark_safe
|
from django.utils.html import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _, get_language
|
from django.utils.translation import ugettext_lazy as _, get_language
|
||||||
from easy_thumbnails.fields import ThumbnailerImageField
|
from easy_thumbnails.fields import ThumbnailerImageField
|
||||||
|
from sorl.thumbnail import get_thumbnail
|
||||||
|
from sorl.thumbnail.fields import ImageField as SORLImageField
|
||||||
|
|
||||||
from utils.methods import image_path, svg_image_path
|
from utils.methods import image_path, svg_image_path
|
||||||
from utils.validators import svg_image_validator
|
from utils.validators import svg_image_validator
|
||||||
from django.db.models.fields import Field
|
|
||||||
from django.core import exceptions
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ProjectBaseMixin(models.Model):
|
class ProjectBaseMixin(models.Model):
|
||||||
|
|
@ -118,7 +124,7 @@ class OAuthProjectMixin:
|
||||||
|
|
||||||
def get_source(self):
|
def get_source(self):
|
||||||
"""Method to get of platform"""
|
"""Method to get of platform"""
|
||||||
return NotImplemented
|
return NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class BaseAttributes(ProjectBaseMixin):
|
class BaseAttributes(ProjectBaseMixin):
|
||||||
|
|
@ -177,6 +183,41 @@ class ImageMixin(models.Model):
|
||||||
image_tag.allow_tags = True
|
image_tag.allow_tags = True
|
||||||
|
|
||||||
|
|
||||||
|
class SORLImageMixin(models.Model):
|
||||||
|
"""Abstract model for SORL ImageField"""
|
||||||
|
|
||||||
|
image = SORLImageField(upload_to=image_path,
|
||||||
|
blank=True, null=True, default=None,
|
||||||
|
verbose_name=_('Image'))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta class."""
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
def get_image(self, thumbnail_key: str):
|
||||||
|
"""Get thumbnail image file."""
|
||||||
|
if thumbnail_key in settings.SORL_THUMBNAIL_ALIASES:
|
||||||
|
return get_thumbnail(
|
||||||
|
file_=self.image,
|
||||||
|
**settings.SORL_THUMBNAIL_ALIASES[thumbnail_key])
|
||||||
|
|
||||||
|
def get_image_url(self, thumbnail_key: str):
|
||||||
|
"""Get image thumbnail url."""
|
||||||
|
crop_image = self.get_image(thumbnail_key)
|
||||||
|
if hasattr(crop_image, 'url'):
|
||||||
|
return self.get_image(thumbnail_key).url
|
||||||
|
|
||||||
|
def image_tag(self):
|
||||||
|
"""Admin preview tag."""
|
||||||
|
if self.image:
|
||||||
|
return mark_safe(f'<img src="{self.image.url}" style="max-height: 25%; max-width: 25%" />')
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
image_tag.short_description = _('Image')
|
||||||
|
image_tag.allow_tags = True
|
||||||
|
|
||||||
|
|
||||||
class SVGImageMixin(models.Model):
|
class SVGImageMixin(models.Model):
|
||||||
"""SVG image model."""
|
"""SVG image model."""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,4 +52,4 @@ class EstablishmentPortionPagination(ProjectMobilePagination):
|
||||||
"""
|
"""
|
||||||
Pagination for app establishments with limit page size equal to 12
|
Pagination for app establishments with limit page size equal to 12
|
||||||
"""
|
"""
|
||||||
page_size = settings.LIMITING_OUTPUT_OBJECTS
|
page_size = settings.QUERY_OUTPUT_OBJECTS
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
"""Utils QuerySet Mixins"""
|
"""Utils QuerySet Mixins"""
|
||||||
from django.db import models
|
|
||||||
from django.db.models import Q, Sum, F
|
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from operator import add
|
from operator import add
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.db.models import Q, F
|
||||||
|
|
||||||
from utils.methods import get_contenttype
|
from utils.methods import get_contenttype
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -50,7 +52,7 @@ class RelatedObjectsCountMixin(models.QuerySet):
|
||||||
|
|
||||||
def filter_all_related_gt(self, count):
|
def filter_all_related_gt(self, count):
|
||||||
"""Queryset filter by all related objects count"""
|
"""Queryset filter by all related objects count"""
|
||||||
exp =reduce(add, [F(f"{related_object}_count") for related_object in self._get_related_objects_names()])
|
exp = reduce(add, [F(f"{related_object}_count") for related_object in self._get_related_objects_names()])
|
||||||
return self._annotate_related_objects_count()\
|
return self._annotate_related_objects_count()\
|
||||||
.annotate(all_related_count=exp)\
|
.annotate(all_related_count=exp)\
|
||||||
.filter(all_related_count__gt=count)
|
.filter(all_related_count__gt=count)
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,6 @@ class TranslateFieldTests(BaseTestCase):
|
||||||
"ru-RU": "Тестовая новость"
|
"ru-RU": "Тестовая новость"
|
||||||
},
|
},
|
||||||
description={"en-GB": "Test description"},
|
description={"en-GB": "Test description"},
|
||||||
playlist=1,
|
|
||||||
start=datetime.now(pytz.utc) + timedelta(hours=-13),
|
start=datetime.now(pytz.utc) + timedelta(hours=-13),
|
||||||
end=datetime.now(pytz.utc) + timedelta(hours=13),
|
end=datetime.now(pytz.utc) + timedelta(hours=13),
|
||||||
news_type=self.news_type,
|
news_type=self.news_type,
|
||||||
|
|
|
||||||
Binary file not shown.
79
compose-ci.yml
Normal file
79
compose-ci.yml
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
version: '2'
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
build:
|
||||||
|
context: ./_dockerfiles/db
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
hostname: db
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=postgres
|
||||||
|
- POSTGRES_PASSWORD=postgres
|
||||||
|
- POSTGRES_DB=postgres
|
||||||
|
ports:
|
||||||
|
- "5436:5432"
|
||||||
|
|
||||||
|
elasticsearch:
|
||||||
|
image: elasticsearch:7.3.1
|
||||||
|
hostname: elasticsearch
|
||||||
|
ports:
|
||||||
|
- 9200:9200
|
||||||
|
- 9300:9300
|
||||||
|
environment:
|
||||||
|
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
|
||||||
|
- discovery.type=single-node
|
||||||
|
- xpack.security.enabled=false
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
redis:
|
||||||
|
image: redis:2.8.23
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
|
||||||
|
# Celery
|
||||||
|
worker:
|
||||||
|
build: .
|
||||||
|
command: ./run_celery.sh
|
||||||
|
environment:
|
||||||
|
- SETTINGS_CONFIGURATION=local
|
||||||
|
- DB_NAME=postgres
|
||||||
|
- DB_USERNAME=postgres
|
||||||
|
- DB_HOSTNAME=db
|
||||||
|
- DB_PORT=5432
|
||||||
|
- DB_PASSWORD=postgres
|
||||||
|
links:
|
||||||
|
- db
|
||||||
|
- redis
|
||||||
|
|
||||||
|
worker_beat:
|
||||||
|
build: .
|
||||||
|
command: ./run_celery_beat.sh
|
||||||
|
environment:
|
||||||
|
- SETTINGS_CONFIGURATION=local
|
||||||
|
- DB_NAME=postgres
|
||||||
|
- DB_USERNAME=postgres
|
||||||
|
- DB_HOSTNAME=db
|
||||||
|
- DB_PORT=5432
|
||||||
|
- DB_PASSWORD=postgres
|
||||||
|
links:
|
||||||
|
- db
|
||||||
|
- redis
|
||||||
|
|
||||||
|
# App: G&M
|
||||||
|
gm_app:
|
||||||
|
build: .
|
||||||
|
command: python manage.py runserver 0.0.0.0:8000
|
||||||
|
environment:
|
||||||
|
- SETTINGS_CONFIGURATION=local
|
||||||
|
- DB_HOSTNAME=db
|
||||||
|
- DB_PORT=5432
|
||||||
|
- DB_NAME=postgres
|
||||||
|
- DB_USERNAME=postgres
|
||||||
|
- DB_PASSWORD=postgres
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
- redis
|
||||||
|
- worker
|
||||||
|
- worker_beat
|
||||||
|
- elasticsearch
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
69
fabfile.py
vendored
Normal file
69
fabfile.py
vendored
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
import os # NOQA
|
||||||
|
from fabric.api import * # NOQA
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
user = 'gm'
|
||||||
|
|
||||||
|
env.roledefs = {
|
||||||
|
'develop': {
|
||||||
|
'branch': 'develop',
|
||||||
|
'hosts': ['%s@95.213.204.126' % user, ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
env.root = '~/'
|
||||||
|
env.src = '~/project'
|
||||||
|
|
||||||
|
env.default_branch = 'develop'
|
||||||
|
env.tmpdir = '~/tmp'
|
||||||
|
|
||||||
|
|
||||||
|
def fetch(branch=None):
|
||||||
|
with cd(env.src):
|
||||||
|
role = env.roles[0]
|
||||||
|
run('git pull origin {}'.format(env.roledefs[role]['branch']))
|
||||||
|
|
||||||
|
|
||||||
|
def migrate():
|
||||||
|
with cd(env.src):
|
||||||
|
run('./manage.py migrate')
|
||||||
|
|
||||||
|
|
||||||
|
def install_requirements():
|
||||||
|
with cd(env.src):
|
||||||
|
run('pip install -r requirements/base.txt')
|
||||||
|
|
||||||
|
|
||||||
|
def touch():
|
||||||
|
with cd(env.src):
|
||||||
|
run('touch ~/%s.touch' % user)
|
||||||
|
|
||||||
|
|
||||||
|
def kill_celery():
|
||||||
|
"""Kill celery workers for $user."""
|
||||||
|
with cd(env.src):
|
||||||
|
run('ps -u %s -o pid,fname | grep celery | (while read a b; do kill -9 $a; done;)' % user)
|
||||||
|
|
||||||
|
|
||||||
|
def collectstatic():
|
||||||
|
with cd(env.src):
|
||||||
|
run('./manage.py collectstatic --noinput')
|
||||||
|
|
||||||
|
|
||||||
|
def deploy(branch=None):
|
||||||
|
fetch()
|
||||||
|
install_requirements()
|
||||||
|
migrate()
|
||||||
|
collectstatic()
|
||||||
|
touch()
|
||||||
|
kill_celery()
|
||||||
|
|
||||||
|
|
||||||
|
def rev():
|
||||||
|
"""Show head commit."""
|
||||||
|
with hide('running', 'stdout'):
|
||||||
|
with cd(env.src):
|
||||||
|
commit = run('git rev-parse HEAD')
|
||||||
|
return local('git show -q %s' % commit)
|
||||||
22
project/settings/amazon_s3.py
Normal file
22
project/settings/amazon_s3.py
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
"""Settings for Amazon S3"""
|
||||||
|
import os
|
||||||
|
|
||||||
|
from .base import MEDIA_LOCATION
|
||||||
|
|
||||||
|
# AMAZON S3
|
||||||
|
AWS_S3_REGION_NAME = 'eu-central-1'
|
||||||
|
AWS_ACCESS_KEY_ID = os.getenv('AWS_ACCESS_KEY_ID')
|
||||||
|
AWS_SECRET_ACCESS_KEY = os.getenv('AWS_SECRET_ACCESS_KEY')
|
||||||
|
AWS_STORAGE_BUCKET_NAME = os.getenv('AWS_STORAGE_BUCKET_NAME')
|
||||||
|
AWS_S3_CUSTOM_DOMAIN = f's3.{AWS_S3_REGION_NAME}.amazonaws.com/{AWS_STORAGE_BUCKET_NAME}'
|
||||||
|
AWS_S3_OBJECT_PARAMETERS = {'CacheControl': 'max-age=86400'}
|
||||||
|
AWS_S3_ADDRESSING_STYLE = 'path'
|
||||||
|
|
||||||
|
# Static settings
|
||||||
|
# PUBLIC_STATIC_LOCATION = 'static'
|
||||||
|
# STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{PUBLIC_STATIC_LOCATION}/'
|
||||||
|
# STATICFILES_STORAGE = 'project.storage_backends.PublicStaticStorage'
|
||||||
|
|
||||||
|
# Public media settings
|
||||||
|
MEDIA_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{MEDIA_LOCATION}/'
|
||||||
|
DEFAULT_FILE_STORAGE = 'project.storage_backends.PublicMediaStorage'
|
||||||
|
|
@ -97,6 +97,10 @@ EXTERNAL_APPS = [
|
||||||
'rest_framework_simplejwt.token_blacklist',
|
'rest_framework_simplejwt.token_blacklist',
|
||||||
'solo',
|
'solo',
|
||||||
'phonenumber_field',
|
'phonenumber_field',
|
||||||
|
'timezone_field',
|
||||||
|
'storages',
|
||||||
|
'sorl.thumbnail',
|
||||||
|
'timezonefinder'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -205,19 +209,6 @@ LOCALE_PATHS = (
|
||||||
os.path.abspath(os.path.join(BASE_DIR, 'locale')),
|
os.path.abspath(os.path.join(BASE_DIR, 'locale')),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
|
||||||
# https://docs.djangoproject.com/en/2.2/howto/static-files/
|
|
||||||
|
|
||||||
STATIC_ROOT = os.path.join(PUBLIC_ROOT, 'static')
|
|
||||||
STATIC_URL = '/static/'
|
|
||||||
|
|
||||||
MEDIA_ROOT = os.path.join(PUBLIC_ROOT, 'media')
|
|
||||||
MEDIA_URL = '/media/'
|
|
||||||
|
|
||||||
STATICFILES_DIRS = (
|
|
||||||
os.path.join(PROJECT_ROOT, 'static'),
|
|
||||||
)
|
|
||||||
|
|
||||||
AVAILABLE_VERSIONS = {
|
AVAILABLE_VERSIONS = {
|
||||||
# 'future': '1.0.1',
|
# 'future': '1.0.1',
|
||||||
'current': '1.0.0',
|
'current': '1.0.0',
|
||||||
|
|
@ -294,12 +285,14 @@ SMS_CODE_LENGTH = 6
|
||||||
SEND_SMS = True
|
SEND_SMS = True
|
||||||
SMS_CODE_SHOW = False
|
SMS_CODE_SHOW = False
|
||||||
|
|
||||||
|
|
||||||
# SMSC Settings
|
# SMSC Settings
|
||||||
SMS_SERVICE = 'http://smsc.ru/sys/send.php'
|
SMS_SERVICE = 'http://smsc.ru/sys/send.php'
|
||||||
SMS_LOGIN = os.environ.get('SMS_LOGIN')
|
SMS_LOGIN = os.environ.get('SMS_LOGIN')
|
||||||
SMS_PASSWORD = os.environ.get('SMS_PASSWORD')
|
SMS_PASSWORD = os.environ.get('SMS_PASSWORD')
|
||||||
SMS_SENDER = 'GM'
|
SMS_SENDER = 'GM'
|
||||||
|
|
||||||
|
|
||||||
# EMAIL
|
# EMAIL
|
||||||
EMAIL_USE_TLS = True
|
EMAIL_USE_TLS = True
|
||||||
EMAIL_HOST = 'smtp.mandrillapp.com'
|
EMAIL_HOST = 'smtp.mandrillapp.com'
|
||||||
|
|
@ -307,6 +300,7 @@ EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER')
|
||||||
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD')
|
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD')
|
||||||
EMAIL_PORT = 587
|
EMAIL_PORT = 587
|
||||||
|
|
||||||
|
|
||||||
# Django Rest Swagger
|
# Django Rest Swagger
|
||||||
SWAGGER_SETTINGS = {
|
SWAGGER_SETTINGS = {
|
||||||
# "DEFAULT_GENERATOR_CLASS": "rest_framework.schemas.generators.BaseSchemaGenerator",
|
# "DEFAULT_GENERATOR_CLASS": "rest_framework.schemas.generators.BaseSchemaGenerator",
|
||||||
|
|
@ -329,6 +323,7 @@ REDOC_SETTINGS = {
|
||||||
'LAZY_RENDERING': False,
|
'LAZY_RENDERING': False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# CELERY
|
# CELERY
|
||||||
# RabbitMQ
|
# RabbitMQ
|
||||||
# BROKER_URL = 'amqp://rabbitmq:5672'
|
# BROKER_URL = 'amqp://rabbitmq:5672'
|
||||||
|
|
@ -341,7 +336,7 @@ CELERY_TASK_SERIALIZER = 'json'
|
||||||
CELERY_RESULT_SERIALIZER = 'json'
|
CELERY_RESULT_SERIALIZER = 'json'
|
||||||
CELERY_TIMEZONE = TIME_ZONE
|
CELERY_TIMEZONE = TIME_ZONE
|
||||||
|
|
||||||
# Django FCM (Firebase push notificatoins)
|
# Django FCM (Firebase push notifications)
|
||||||
FCM_DJANGO_SETTINGS = {
|
FCM_DJANGO_SETTINGS = {
|
||||||
'FCM_SERVER_KEY': (
|
'FCM_SERVER_KEY': (
|
||||||
"AAAAJcC4Vbc:APA91bGovq7233-RHu2MbZTsuMU4jNf3obOue8s"
|
"AAAAJcC4Vbc:APA91bGovq7233-RHu2MbZTsuMU4jNf3obOue8s"
|
||||||
|
|
@ -350,39 +345,44 @@ FCM_DJANGO_SETTINGS = {
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Thumbnail settings
|
# Thumbnail settings
|
||||||
THUMBNAIL_ALIASES = {
|
THUMBNAIL_ALIASES = {
|
||||||
'news_preview': {
|
'': {
|
||||||
'web': {'size': (300, 260), }
|
'news_preview': {'size': (300, 260), },
|
||||||
},
|
'news_promo_horizontal_web': {'size': (1900, 600), },
|
||||||
'news_promo_horizontal': {
|
'news_promo_horizontal_mobile': {'size': (375, 260), },
|
||||||
'web': {'size': (1900, 600), },
|
'news_tile_horizontal_web': {'size': (300, 275), },
|
||||||
'mobile': {'size': (375, 260), },
|
'news_tile_horizontal_mobile': {'size': (343, 180), },
|
||||||
},
|
'news_tile_vertical_web': {'size': (300, 380), },
|
||||||
'news_tile_horizontal': {
|
'news_highlight_vertical_web': {'size': (460, 630), },
|
||||||
'web': {'size': (300, 275), },
|
'news_editor_web': {'size': (940, 430), }, # при загрузке через контент эдитор
|
||||||
'mobile': {'size': (343, 180), },
|
'news_editor_mobile': {'size': (343, 260), }, # через контент эдитор в мобильном браузерe
|
||||||
},
|
'avatar_comments_web': {'size': (116, 116), },
|
||||||
'news_tile_vertical': {
|
}
|
||||||
'web': {'size': (300, 380), },
|
|
||||||
},
|
|
||||||
'news_highlight_vertical': {
|
|
||||||
'web': {'size': (460, 630), },
|
|
||||||
},
|
|
||||||
'news_editor': {
|
|
||||||
'web': {'size': (940, 430), }, # при загрузке через контент эдитор
|
|
||||||
'mobile': {'size': (343, 260), }, # через контент эдитор в мобильном браузерe
|
|
||||||
},
|
|
||||||
'avatar_comments': {
|
|
||||||
'web': {'size': (116, 116), },
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Password reset
|
THUMBNAIL_DEFAULT_OPTIONS = {
|
||||||
RESETTING_TOKEN_EXPIRATION = 24 # hours
|
'crop': 'smart',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
GEOIP_PATH = os.path.join(PROJECT_ROOT, 'geoip_db')
|
# SORL
|
||||||
|
THUMBNAIL_QUALITY = 85
|
||||||
|
THUMBNAIL_DEBUG = False
|
||||||
|
SORL_THUMBNAIL_ALIASES = {
|
||||||
|
'news_preview': {'geometry_string': '100x100', 'crop': 'center'},
|
||||||
|
'news_promo_horizontal_web': {'geometry_string': '1900x600', 'crop': 'center'},
|
||||||
|
'news_promo_horizontal_mobile': {'geometry_string': '375x260', 'crop': 'center'},
|
||||||
|
'news_tile_horizontal_web': {'geometry_string': '300x275', 'crop': 'center'},
|
||||||
|
'news_tile_horizontal_mobile': {'geometry_string': '343x180', 'crop': 'center'},
|
||||||
|
'news_tile_vertical_web': {'geometry_string': '300x380', 'crop': 'center'},
|
||||||
|
'news_highlight_vertical_web': {'geometry_string': '460x630', 'crop': 'center'},
|
||||||
|
'news_editor_web': {'geometry_string': '940x430', 'crop': 'center'},
|
||||||
|
'news_editor_mobile': {'geometry_string': '343x260', 'crop': 'center'}, # при загрузке через контент эдитор
|
||||||
|
'avatar_comments_web': {'geometry_string': '116x116', 'crop': 'center'}, # через контент эдитор в мобильном браузерe
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# JWT
|
# JWT
|
||||||
SIMPLE_JWT = {
|
SIMPLE_JWT = {
|
||||||
|
|
@ -422,7 +422,8 @@ PASSWORD_RESET_TIMEOUT_DAYS = 1
|
||||||
RESETTING_TOKEN_TEMPLATE = 'account/password_reset_email.html'
|
RESETTING_TOKEN_TEMPLATE = 'account/password_reset_email.html'
|
||||||
CHANGE_EMAIL_TEMPLATE = 'account/change_email.html'
|
CHANGE_EMAIL_TEMPLATE = 'account/change_email.html'
|
||||||
CONFIRM_EMAIL_TEMPLATE = 'authorization/confirm_email.html'
|
CONFIRM_EMAIL_TEMPLATE = 'authorization/confirm_email.html'
|
||||||
NEWS_EMAIL_TEMPLATE = "news/news_email.html"
|
NEWS_EMAIL_TEMPLATE = 'news/news_email.html'
|
||||||
|
NOTIFICATION_PASSWORD_TEMPLATE = 'account/password_change_email.html'
|
||||||
|
|
||||||
|
|
||||||
# COOKIES
|
# COOKIES
|
||||||
|
|
@ -443,24 +444,42 @@ DATA_UPLOAD_MAX_MEMORY_SIZE = 5242880 # 5MB
|
||||||
FILE_UPLOAD_MAX_MEMORY_SIZE = 5242880 # 5MB
|
FILE_UPLOAD_MAX_MEMORY_SIZE = 5242880 # 5MB
|
||||||
FILE_UPLOAD_PERMISSIONS = 0o644
|
FILE_UPLOAD_PERMISSIONS = 0o644
|
||||||
|
|
||||||
|
|
||||||
# SOLO SETTINGS
|
# SOLO SETTINGS
|
||||||
# todo: make a separate service (redis?) and update solo_cache
|
# todo: make a separate service (redis?) and update solo_cache
|
||||||
SOLO_CACHE = 'default'
|
SOLO_CACHE = 'default'
|
||||||
SOLO_CACHE_PREFIX = 'solo'
|
SOLO_CACHE_PREFIX = 'solo'
|
||||||
SOLO_CACHE_TIMEOUT = 300
|
SOLO_CACHE_TIMEOUT = 300
|
||||||
|
|
||||||
|
|
||||||
# REDIRECT URL
|
# REDIRECT URL
|
||||||
SITE_REDIRECT_URL_UNSUBSCRIBE = '/unsubscribe/'
|
SITE_REDIRECT_URL_UNSUBSCRIBE = '/unsubscribe/'
|
||||||
|
|
||||||
SITE_NAME = 'Gault & Millau'
|
SITE_NAME = 'Gault & Millau'
|
||||||
|
|
||||||
|
|
||||||
# Used in annotations for establishments.
|
# Used in annotations for establishments.
|
||||||
DEFAULT_ESTABLISHMENT_PUBLIC_MARK = 10
|
DEFAULT_ESTABLISHMENT_PUBLIC_MARK = 10
|
||||||
# Limit output objects (see in pagination classes).
|
# Limit output objects (see in pagination classes).
|
||||||
LIMITING_OUTPUT_OBJECTS = 12
|
QUERY_OUTPUT_OBJECTS = 12
|
||||||
# Need to restrict objects to sort (3 times more then expected).
|
# Need to restrict objects to sort (3 times more then expected).
|
||||||
LIMITING_QUERY_NUMBER = LIMITING_OUTPUT_OBJECTS * 3
|
LIMITING_QUERY_OBJECTS = QUERY_OUTPUT_OBJECTS * 3
|
||||||
|
|
||||||
|
|
||||||
# GEO
|
# GEO
|
||||||
# A Spatial Reference System Identifier
|
# A Spatial Reference System Identifier
|
||||||
GEO_DEFAULT_SRID = 4326
|
GEO_DEFAULT_SRID = 4326
|
||||||
|
GEOIP_PATH = os.path.join(PROJECT_ROOT, 'geoip_db')
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/2.2/howto/static-files/
|
||||||
|
STATIC_ROOT = os.path.join(PUBLIC_ROOT, 'static')
|
||||||
|
STATIC_URL = '/static/'
|
||||||
|
|
||||||
|
STATICFILES_DIRS = (
|
||||||
|
os.path.join(PROJECT_ROOT, 'static'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# MEDIA
|
||||||
|
MEDIA_LOCATION = 'media'
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
"""Development settings."""
|
"""Development settings."""
|
||||||
from .base import *
|
from .base import *
|
||||||
|
from .amazon_s3 import *
|
||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
from sentry_sdk.integrations.django import DjangoIntegration
|
from sentry_sdk.integrations.django import DjangoIntegration
|
||||||
|
|
||||||
ALLOWED_HOSTS = ['gm.id-east.ru', '95.213.204.126']
|
ALLOWED_HOSTS = ['gm.id-east.ru', '95.213.204.126', '0.0.0.0']
|
||||||
|
|
||||||
SEND_SMS = False
|
SEND_SMS = False
|
||||||
SMS_CODE_SHOW = True
|
SMS_CODE_SHOW = True
|
||||||
USE_CELERY = False
|
USE_CELERY = True
|
||||||
|
|
||||||
SCHEMA_URI = 'http'
|
SCHEMA_URI = 'http'
|
||||||
DEFAULT_SUBDOMAIN = 'www'
|
DEFAULT_SUBDOMAIN = 'www'
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,22 @@
|
||||||
"""Local settings."""
|
"""Local settings."""
|
||||||
from .base import *
|
from .base import *
|
||||||
import sys
|
import sys
|
||||||
|
# from .amazon_s3 import *
|
||||||
|
|
||||||
ALLOWED_HOSTS = ['*', ]
|
ALLOWED_HOSTS = ['*', ]
|
||||||
|
|
||||||
|
|
||||||
SEND_SMS = False
|
SEND_SMS = False
|
||||||
SMS_CODE_SHOW = True
|
SMS_CODE_SHOW = True
|
||||||
USE_CELERY = True
|
USE_CELERY = True
|
||||||
|
|
||||||
|
|
||||||
SCHEMA_URI = 'http'
|
SCHEMA_URI = 'http'
|
||||||
DEFAULT_SUBDOMAIN = 'www'
|
DEFAULT_SUBDOMAIN = 'www'
|
||||||
SITE_DOMAIN_URI = 'testserver.com:8000'
|
SITE_DOMAIN_URI = 'testserver.com:8000'
|
||||||
DOMAIN_URI = '0.0.0.0:8000'
|
DOMAIN_URI = '0.0.0.0:8000'
|
||||||
|
|
||||||
|
|
||||||
# CELERY
|
# CELERY
|
||||||
# RabbitMQ
|
# RabbitMQ
|
||||||
# BROKER_URL = 'amqp://rabbitmq:5672'
|
# BROKER_URL = 'amqp://rabbitmq:5672'
|
||||||
|
|
@ -22,6 +26,15 @@ CELERY_RESULT_BACKEND = BROKER_URL
|
||||||
CELERY_BROKER_URL = BROKER_URL
|
CELERY_BROKER_URL = BROKER_URL
|
||||||
|
|
||||||
|
|
||||||
|
# MEDIA
|
||||||
|
MEDIA_URL = f'{SCHEMA_URI}://{DOMAIN_URI}/{MEDIA_LOCATION}/'
|
||||||
|
MEDIA_ROOT = os.path.join(PUBLIC_ROOT, MEDIA_LOCATION)
|
||||||
|
|
||||||
|
|
||||||
|
# SORL thumbnails
|
||||||
|
THUMBNAIL_DEBUG = True
|
||||||
|
|
||||||
|
|
||||||
# LOGGING
|
# LOGGING
|
||||||
LOGGING = {
|
LOGGING = {
|
||||||
'version': 1,
|
'version': 1,
|
||||||
|
|
@ -59,6 +72,7 @@ LOGGING = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# ELASTICSEARCH SETTINGS
|
# ELASTICSEARCH SETTINGS
|
||||||
ELASTICSEARCH_DSL = {
|
ELASTICSEARCH_DSL = {
|
||||||
'default': {
|
'default': {
|
||||||
|
|
@ -66,7 +80,6 @@ ELASTICSEARCH_DSL = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ELASTICSEARCH_INDEX_NAMES = {
|
ELASTICSEARCH_INDEX_NAMES = {
|
||||||
# 'search_indexes.documents.news': 'local_news',
|
# 'search_indexes.documents.news': 'local_news',
|
||||||
'search_indexes.documents.establishment': 'local_establishment',
|
'search_indexes.documents.establishment': 'local_establishment',
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
"""Production settings."""
|
"""Production settings."""
|
||||||
from .base import *
|
from .base import *
|
||||||
|
from .amazon_s3 import *
|
||||||
|
|
||||||
# Booking API configuration
|
# Booking API configuration
|
||||||
GUESTONLINE_SERVICE = 'https://api.guestonline.fr/'
|
GUESTONLINE_SERVICE = 'https://api.guestonline.fr/'
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
"""Stage settings."""
|
"""Stage settings."""
|
||||||
from .base import *
|
from .base import *
|
||||||
|
from .amazon_s3 import *
|
||||||
|
|
||||||
ALLOWED_HOSTS = ['gm-stage.id-east.ru', '95.213.204.126']
|
ALLOWED_HOSTS = ['gm-stage.id-east.ru', '95.213.204.126']
|
||||||
|
|
||||||
SEND_SMS = False
|
SEND_SMS = False
|
||||||
SMS_CODE_SHOW = True
|
SMS_CODE_SHOW = True
|
||||||
USE_CELERY = False
|
USE_CELERY = True
|
||||||
|
|
||||||
SCHEMA_URI = 'https'
|
SCHEMA_URI = 'https'
|
||||||
DEFAULT_SUBDOMAIN = 'www'
|
DEFAULT_SUBDOMAIN = 'www'
|
||||||
|
|
|
||||||
12
project/storage_backends.py
Normal file
12
project/storage_backends.py
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
"""Extend storage backend for adding custom parameters"""
|
||||||
|
from storages.backends.s3boto3 import S3Boto3Storage
|
||||||
|
|
||||||
|
|
||||||
|
class PublicMediaStorage(S3Boto3Storage):
|
||||||
|
location = 'media'
|
||||||
|
file_overwrite = False
|
||||||
|
|
||||||
|
|
||||||
|
class PublicStaticStorage(S3Boto3Storage):
|
||||||
|
location = 'static'
|
||||||
|
file_overwrite = False
|
||||||
7
project/templates/account/password_change_email.html
Normal file
7
project/templates/account/password_change_email.html
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{% load i18n %}{% autoescape off %}
|
||||||
|
{% blocktrans %}You're receiving this email because your account's password address at {{ site_name }}.{% endblocktrans %}
|
||||||
|
|
||||||
|
{% trans "Thanks for using our site!" %}
|
||||||
|
|
||||||
|
{% blocktrans %}The {{ site_name }} team{% endblocktrans %}
|
||||||
|
{% endautoescape %}
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -64,7 +64,7 @@ urlpatterns = [
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns = urlpatterns + \
|
urlpatterns = urlpatterns + \
|
||||||
static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
static(settings.MEDIA_LOCATION, document_root=settings.MEDIA_ROOT)
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
urlpatterns.extend(urlpatterns_doc)
|
urlpatterns.extend(urlpatterns_doc)
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,10 @@ app_name = 'mobile'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('establishments/', include('establishment.urls.mobile')),
|
path('establishments/', include('establishment.urls.mobile')),
|
||||||
|
path('location/', include('location.urls.mobile')),
|
||||||
path('main/', include('main.urls.mobile')),
|
path('main/', include('main.urls.mobile')),
|
||||||
path('location/', include('location.urls.mobile'))
|
path('tags/', include('tag.urls.mobile')),
|
||||||
|
path('timetables/', include('timetable.urls.mobile')),
|
||||||
# path('account/', include('account.urls.web')),
|
# path('account/', include('account.urls.web')),
|
||||||
# path('advertisement/', include('advertisement.urls.web')),
|
# path('advertisement/', include('advertisement.urls.web')),
|
||||||
# path('collection/', include('collection.urls.web')),
|
# path('collection/', include('collection.urls.web')),
|
||||||
|
|
|
||||||
|
|
@ -34,4 +34,5 @@ urlpatterns = [
|
||||||
path('translation/', include('translation.urls')),
|
path('translation/', include('translation.urls')),
|
||||||
path('comments/', include('comment.urls.web')),
|
path('comments/', include('comment.urls.web')),
|
||||||
path('favorites/', include('favorites.urls')),
|
path('favorites/', include('favorites.urls')),
|
||||||
|
path('timetables/', include('timetable.urls.web')),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ fcm-django
|
||||||
django-easy-select2
|
django-easy-select2
|
||||||
bootstrap-admin
|
bootstrap-admin
|
||||||
drf-yasg==1.16.0
|
drf-yasg==1.16.0
|
||||||
|
timezonefinder
|
||||||
PySocks!=1.5.7,>=1.5.6;
|
PySocks!=1.5.7,>=1.5.6;
|
||||||
|
|
||||||
djangorestframework==3.9.4
|
djangorestframework==3.9.4
|
||||||
|
|
@ -17,6 +18,7 @@ django-filter==2.1.0
|
||||||
djangorestframework-xml
|
djangorestframework-xml
|
||||||
geoip2==2.9.0
|
geoip2==2.9.0
|
||||||
django-phonenumber-field[phonenumbers]==2.1.0
|
django-phonenumber-field[phonenumbers]==2.1.0
|
||||||
|
django-timezone-field==3.1
|
||||||
|
|
||||||
# auth socials
|
# auth socials
|
||||||
django-rest-framework-social-oauth2==1.1.0
|
django-rest-framework-social-oauth2==1.1.0
|
||||||
|
|
@ -33,6 +35,12 @@ django-elasticsearch-dsl>=7.0.0,<8.0.0
|
||||||
django-elasticsearch-dsl-drf==0.20.2
|
django-elasticsearch-dsl-drf==0.20.2
|
||||||
sentry-sdk==0.11.2
|
sentry-sdk==0.11.2
|
||||||
|
|
||||||
|
# AMAZON S3
|
||||||
|
boto3==1.9.238
|
||||||
|
django-storages==1.7.2
|
||||||
|
|
||||||
|
sorl-thumbnail==12.5.0
|
||||||
|
|
||||||
|
|
||||||
mysqlclient==1.4.4
|
mysqlclient==1.4.4
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user