Merge branch 'feature/gm-148' into 'develop'

Feature/gm 148

See merge request gm/gm-backend!67
This commit is contained in:
a.feteleu 2019-10-23 13:08:00 +00:00
commit 2058ad09c6
59 changed files with 993 additions and 117 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
@ -12,6 +14,7 @@ from django.db.models import When, Case, F, ExpressionWrapper, Subquery, Q
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from phonenumber_field.modelfields import PhoneNumberField from phonenumber_field.modelfields import PhoneNumberField
from pytz import timezone as py_tz
from collection.models import Collection from collection.models import Collection
from location.models import Address from location.models import Address
@ -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) \
@ -416,6 +419,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=py_tz(self.address.tz_name))
current_week = now_at_est_tz.weekday()
schedule_for_today = self.schedule.filter(weekday=current_week).first()
if schedule_for_today 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 +460,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):

View File

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

View File

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

View File

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

View File

@ -1,15 +1,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')) (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.title}'
def delete_image(self, completely: bool = True):
"""
Deletes an instance and crops of instance from media storage.
:param completely: if set to False then removed only crop neither original image.
"""
try:
# Delete from remote storage
delete(file_=self.image.file, delete_file=completely)
except FileNotFoundError:
pass
finally:
if completely:
# Delete an instance of image
super().delete()

View File

@ -12,13 +12,20 @@ class ImageSerializer(serializers.ModelSerializer):
# RESPONSE # 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
View File

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

View File

@ -6,5 +6,6 @@ from . import views
app_name = 'gallery' 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'),
] ]

View File

@ -1,10 +1,30 @@
from rest_framework import generics from django.conf import settings
from django.db.transaction import on_commit
from rest_framework import generics, status
from rest_framework.response import Response
from . import models, serializers from . import tasks, models, serializers
class ImageUploadView(generics.CreateAPIView): 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)

View File

@ -5,8 +5,10 @@ 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 timezonefinder import TimezoneFinder
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):
@ -112,6 +114,11 @@ class Address(models.Model):
def get_street_name(self): def get_street_name(self):
return self.street_name_1 or self.street_name_2 return self.street_name_1 or self.street_name_2
@property
def tz_name(self):
tf = TimezoneFinder(in_memory=True)
return tf.certain_timezone_at(lng=self.latitude, lat=self.longitude)
@property @property
def latitude(self): def latitude(self):
return self.coordinates.y if self.coordinates else float(0) return self.coordinates.y if self.coordinates else float(0)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -146,10 +146,6 @@ class News(BaseAttributes, TranslatedFieldsMixin):
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 +155,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 +192,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')

View File

@ -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."""
@ -56,7 +130,7 @@ class NewsBaseSerializer(ProjectModelSerializer):
"""Base serializer for News model.""" """Base serializer for News model."""
# read only fields # read only fields
title_translated = TranslatedField(source='title') title_translated = TranslatedField()
subtitle_translated = TranslatedField() subtitle_translated = TranslatedField()
# related fields # related fields
@ -72,14 +146,40 @@ 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."""
# read only fields
title_translated = TranslatedField()
subtitle_translated = TranslatedField()
# related fields
news_type = NewsTypeSerializer(read_only=True)
tags = TagBaseSerializer(read_only=True, many=True)
image = NewsImageSerializer(source='main_image', allow_null=True)
class Meta:
"""Meta class."""
model = models.News
fields = (
'id',
'title_translated',
'subtitle_translated',
'is_highlighted',
'news_type',
'tags',
'slug',
'image',
)
class NewsDetailSerializer(NewsBaseSerializer): class NewsDetailSerializer(NewsBaseSerializer):
"""News detail serializer.""" """News detail serializer."""
@ -88,6 +188,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."""
@ -102,6 +203,7 @@ class NewsDetailSerializer(NewsBaseSerializer):
'state_display', 'state_display',
'author', 'author',
'country', 'country',
'gallery',
) )
@ -160,3 +262,42 @@ 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')
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')})
attrs['news'] = news
attrs['image'] = image
return attrs

View File

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

View File

@ -1,6 +1,13 @@
"""News app views.""" """News app views."""
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
from django.conf import settings
from django.db.transaction import on_commit
from django.shortcuts import get_object_or_404
from rest_framework import generics, permissions, 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
@ -23,7 +30,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 +81,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."""

View File

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

View File

@ -24,6 +24,7 @@ class NewsDocument(Document):
country = fields.ObjectField(properties={'id': fields.IntegerField(), country = fields.ObjectField(properties={'id': fields.IntegerField(),
'code': fields.KeywordField()}) 'code': fields.KeywordField()})
web_url = fields.KeywordField(attr='web_url') web_url = fields.KeywordField(attr='web_url')
preview_image_url = fields.TextField(attr='preview_image_url')
tags = fields.ObjectField( tags = fields.ObjectField(
properties={ properties={
'id': fields.IntegerField(attr='id'), 'id': fields.IntegerField(attr='id'),
@ -43,8 +44,6 @@ class NewsDocument(Document):
'slug', 'slug',
'state', 'state',
'is_highlighted', 'is_highlighted',
'image_url',
'preview_image_url',
'template', 'template',
) )
related_models = [models.NewsType] related_models = [models.NewsType]

View File

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

View File

@ -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,25 @@ 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_FILTER_TERM,
],
},
'works_now': {
'field': 'works_now',
'lookups': [
constants.LOOKUP_FILTER_EXISTS,
constants.LOOKUP_QUERY_IN,
]
},
} }
geo_spatial_filter_fields = { geo_spatial_filter_fields = {

View File

@ -56,7 +56,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):

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

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

View File

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

View File

@ -77,3 +77,16 @@ 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',
)

View File

View 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')
] ]

View File

@ -0,0 +1,6 @@
from timetable.urls.common import urlpatterns as common_urlpatterns
urlpatterns = []
urlpatterns.extend(common_urlpatterns)

View File

@ -0,0 +1,6 @@
from timetable.urls.common import urlpatterns as common_urlpatterns
urlpatterns = []
urlpatterns.extend(common_urlpatterns)

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

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

View File

@ -95,6 +95,9 @@ EXTERNAL_APPS = [
'rest_framework_simplejwt.token_blacklist', 'rest_framework_simplejwt.token_blacklist',
'solo', 'solo',
'phonenumber_field', 'phonenumber_field',
'storages',
'sorl.thumbnail',
'timezonefinder'
] ]
@ -194,19 +197,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',
@ -283,12 +273,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'
@ -296,6 +288,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",
@ -318,6 +311,7 @@ REDOC_SETTINGS = {
'LAZY_RENDERING': False, 'LAZY_RENDERING': False,
} }
# CELERY # CELERY
# RabbitMQ # RabbitMQ
# BROKER_URL = 'amqp://rabbitmq:5672' # BROKER_URL = 'amqp://rabbitmq:5672'
@ -330,7 +324,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"
@ -339,39 +333,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 = {
@ -433,24 +432,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'

View File

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

View File

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

View File

@ -1,9 +1,10 @@
"""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/'
GUESTONLINE_TOKEN = '' GUESTONLINE_TOKEN = ''
LASTABLE_SERVICE = '' LASTABLE_SERVICE = ''
LASTABLE_TOKEN = '' LASTABLE_TOKEN = ''
LASTABLE_PROXY = '' LASTABLE_PROXY = ''

View File

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

View 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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

@ -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
@ -33,6 +34,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
# temp solution # temp solution
redis==3.2.0 redis==3.2.0
amqp>=2.4.0 amqp>=2.4.0