gm-148: refactored

This commit is contained in:
Anatoly 2019-10-02 10:16:32 +03:00
parent aeebdd02c6
commit d9635a8599
20 changed files with 241 additions and 54 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

@ -58,7 +58,7 @@ class User(AbstractUser):
blank=True, null=True, default=None)
cropped_image_url = models.URLField(verbose_name=_('Cropped image URL path'),
blank=True, null=True, default=None)
email = models.EmailField(_('email address'), blank=True,
email = models.EmailField(_('email address'), unique=True,
null=True, default=None)
email_confirmed = models.BooleanField(_('email status'), default=False)
newsletter = models.NullBooleanField(default=True)

View File

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

View File

@ -0,0 +1,20 @@
# Generated by Django 2.2.4 on 2019-10-01 06:47
from django.db import migrations
import sorl.thumbnail.fields
import utils.methods
class Migration(migrations.Migration):
dependencies = [
('gallery', '0002_auto_20190930_0714'),
]
operations = [
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,16 +1,16 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from easy_thumbnails.fields import ThumbnailerImageField
from sorl.thumbnail.fields import ImageField as SORLImageField
from utils.methods import image_path
from utils.models import ProjectBaseMixin, ImageMixin, PlatformMixin
from utils.models import ProjectBaseMixin, SORLImageMixin, PlatformMixin
class ImageQuerySet(models.QuerySet):
"""QuerySet for model Image."""
class Image(ProjectBaseMixin, ImageMixin, PlatformMixin):
class Image(ProjectBaseMixin, SORLImageMixin, PlatformMixin):
"""Image model."""
HORIZONTAL = 0
VERTICAL = 1
@ -20,8 +20,8 @@ class Image(ProjectBaseMixin, ImageMixin, PlatformMixin):
(VERTICAL, _('Vertical')),
)
image = ThumbnailerImageField(upload_to=image_path,
verbose_name=_('image file'))
image = SORLImageField(upload_to=image_path,
verbose_name=_('image file'))
parent = models.ForeignKey('self',
blank=True, null=True, default=None,
related_name='parent_image',

View File

@ -8,7 +8,6 @@ class ImageSerializer(serializers.ModelSerializer):
# REQUEST
file = serializers.ImageField(source='image',
write_only=True)
title = serializers.CharField()
orientation = serializers.ChoiceField(choices=models.Image.ORIENTATIONS,
write_only=True)
@ -21,7 +20,7 @@ class ImageSerializer(serializers.ModelSerializer):
class Meta:
"""Meta class"""
model = models.Image
fields = (
fields = [
'id',
'file',
'url',
@ -29,5 +28,4 @@ class ImageSerializer(serializers.ModelSerializer):
'orientation',
'orientation_display',
'title',
)
]

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

@ -2,6 +2,7 @@
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from account.serializers.common import UserSerializer
from gallery.models import Image
from gallery.serializers import ImageSerializer
from location import models as location_models
@ -9,7 +10,26 @@ from location.serializers import CountrySimpleSerializer
from main.serializers import MetaDataContentSerializer
from news import models
from utils.serializers import TranslatedField
from account.serializers.common import UserSerializer
class NewsImageSerializer(ImageSerializer):
"""News images"""
promo_web_url = serializers.SerializerMethodField()
promo_mobile_url = serializers.SerializerMethodField()
class Meta:
model = Image
fields = ImageSerializer.Meta.fields + [
'promo_web_url',
'promo_mobile_url'
]
def get_promo_web_url(self, obj):
return obj.get_image_url(thumbnail_key='news_promo_horizontal_web')
def get_promo_mobile_url(self, obj):
return obj.get_image_url(thumbnail_key='news_promo_horizontal_mobile')
class NewsTypeSerializer(serializers.ModelSerializer):
"""News type serializer."""
@ -31,7 +51,7 @@ class NewsBaseSerializer(serializers.ModelSerializer):
# related fields
news_type = NewsTypeSerializer(read_only=True)
tags = MetaDataContentSerializer(read_only=True, many=True)
gallery = ImageSerializer(read_only=True, many=True)
gallery = NewsImageSerializer(read_only=True, many=True)
class Meta:
"""Meta class."""
@ -115,14 +135,11 @@ class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer,
class NewsBackOfficeGallerySerializer(serializers.ModelSerializer):
"""Serializer class for model NewsGallery."""
image = ImageSerializer(read_only=True)
class Meta:
"""Meta class"""
model = models.NewsGallery
fields = [
'id',
'image',
]
def get_request_kwargs(self):

View File

@ -1,8 +1,8 @@
"""News app views."""
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.serializers import ImageSerializer
from news import filters, models, serializers
@ -82,10 +82,15 @@ class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView,
return gallery
def create(self, request, *args, **kwargs):
"""Override create method"""
super().create(request, *args, **kwargs)
return Response(status=status.HTTP_201_CREATED)
class NewsBackOfficeGalleryListView(NewsBackOfficeMixinView, generics.ListAPIView):
"""Resource for returning gallery for news for back-office users."""
serializer_class = ImageSerializer
serializer_class = serializers.NewsImageSerializer
def get_object(self):
"""Override get_object method."""

View File

@ -1,4 +1,5 @@
"""Utils app method."""
import logging
import random
import re
import string
@ -10,6 +11,9 @@ from django.http.request import HttpRequest
from django.utils.timezone import datetime
from rest_framework import status
from rest_framework.request import Request
from os.path import exists
logger = logging.getLogger(__name__)
def generate_code(digits=6, string_output=True):
@ -91,12 +95,18 @@ def get_contenttype(app_label: str, model: str):
def image_url_valid(url: str):
# In case if image storage is not on CDN
if url.startswith('/media/'):
url = f'{settings.SCHEMA_URI}://{settings.DOMAIN_URI}{url}'
response = requests.request('head', url)
if response.status_code == status.HTTP_200_OK:
return True
"""
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):
@ -105,7 +115,7 @@ def absolute_url_decorator(func):
url_path = func(self, obj)
if url_path:
if url_path.startswith('/media/'):
return f'{settings.SCHEMA_URI}://{settings.DOMAIN_URI}{url_path}/'
return f'{settings.MEDIA_URL}{url_path}/'
else:
return url_path
return get_absolute_image_url

View File

@ -1,4 +1,5 @@
"""Utils app models."""
import logging
from os.path import exists
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.contrib.gis.db import models
@ -10,8 +11,12 @@ from django.utils.translation import ugettext_lazy as _, get_language
from easy_thumbnails.fields import ThumbnailerImageField
from utils.methods import image_path, svg_image_path
from utils.validators import svg_image_validator
from django.db.models.fields import Field
from django.core import exceptions
from sorl.thumbnail.fields import ImageField as SORLImageField
from sorl.thumbnail import get_thumbnail
from django.conf import settings
from utils.methods import image_url_valid
logger = logging.getLogger(__name__)
class ProjectBaseMixin(models.Model):
@ -177,6 +182,38 @@ class ImageMixin(models.Model):
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=None):
"""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=None):
"""Get image thumbnail url."""
return self.get_image(thumbnail_key).url
def image_tag(self):
"""Admin preview tag."""
if self.image:
return mark_safe('<img src="%s" />' % self.image.url)
else:
return None
image_tag.short_description = _('Image')
image_tag.allow_tags = True
class SVGImageMixin(models.Model):
"""SVG image model."""

View File

@ -0,0 +1,19 @@
"""Settings for Amazon S3"""
import os
from .base import MEDIA_LOCATION
# AMAZON S3
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'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com'
AWS_S3_OBJECT_PARAMETERS = {'CacheControl': 'max-age=86400'}
# 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

@ -91,6 +91,8 @@ EXTERNAL_APPS = [
'rest_framework_simplejwt.token_blacklist',
'solo',
'phonenumber_field',
'storages',
'sorl.thumbnail',
]
@ -190,19 +192,6 @@ LOCALE_PATHS = (
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 = {
# 'future': '1.0.1',
'current': '1.0.0',
@ -262,6 +251,7 @@ SOCIAL_AUTH_FACEBOOK_PROFILE_EXTRA_PARAMS = {
'fields': 'id, name, email',
}
# SMS Settings
SMS_EXPIRATION = 5
SMS_SEND_DELAY = 30
@ -272,12 +262,14 @@ SMS_CODE_LENGTH = 6
SEND_SMS = True
SMS_CODE_SHOW = False
# SMSC Settings
SMS_SERVICE = 'http://smsc.ru/sys/send.php'
SMS_LOGIN = 'GM2019'
SMS_PASSWORD = '}#6%Qe7CYG7n'
SMS_SENDER = 'GM'
# EMAIL
EMAIL_USE_TLS = True
EMAIL_HOST = 'smtp.gmail.com'
@ -285,6 +277,7 @@ EMAIL_HOST_USER = 'anatolyfeteleu@gmail.com'
EMAIL_HOST_PASSWORD = 'nggrlnbehzksgmbt'
EMAIL_PORT = 587
# Django Rest Swagger
SWAGGER_SETTINGS = {
# "DEFAULT_GENERATOR_CLASS": "rest_framework.schemas.generators.BaseSchemaGenerator",
@ -307,6 +300,7 @@ REDOC_SETTINGS = {
'LAZY_RENDERING': False,
}
# CELERY
BROKER_URL = 'amqp://rabbitmq:5672'
CELERY_RESULT_BACKEND = BROKER_URL
@ -316,6 +310,7 @@ CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = TIME_ZONE
# Django FCM (Firebase push notifications)
FCM_DJANGO_SETTINGS = {
'FCM_SERVER_KEY': (
@ -325,6 +320,7 @@ FCM_DJANGO_SETTINGS = {
),
}
# Thumbnail settings
THUMBNAIL_ALIASES = {
'': {
@ -345,11 +341,23 @@ THUMBNAIL_DEFAULT_OPTIONS = {
'crop': 'smart',
}
# Password reset
RESETTING_TOKEN_EXPIRATION = 24 # hours
# 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
}
GEOIP_PATH = os.path.join(PROJECT_ROOT, 'geoip_db')
# JWT
SIMPLE_JWT = {
@ -409,17 +417,20 @@ DATA_UPLOAD_MAX_MEMORY_SIZE = 5242880 # 5MB
FILE_UPLOAD_MAX_MEMORY_SIZE = 5242880 # 5MB
FILE_UPLOAD_PERMISSIONS = 0o644
# SOLO SETTINGS
# todo: make a separate service (redis?) and update solo_cache
SOLO_CACHE = 'default'
SOLO_CACHE_PREFIX = 'solo'
SOLO_CACHE_TIMEOUT = 300
# REDIRECT URL
SITE_REDIRECT_URL_UNSUBSCRIBE = '/unsubscribe/'
SITE_NAME = 'Gault & Millau'
# Used in annotations for establishments.
DEFAULT_ESTABLISHMENT_PUBLIC_MARK = 10
# Limit output objects (see in pagination classes).
@ -427,6 +438,19 @@ QUERY_OUTPUT_OBJECTS = 12
# Need to restrict objects to sort (3 times more then expected).
LIMITING_QUERY_OBJECTS = QUERY_OUTPUT_OBJECTS * 3
# GEO
# A Spatial Reference System Identifier
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_LOCATION = 'media'

View File

@ -1,9 +1,10 @@
"""Development settings."""
from .base import *
from .amazon_s3 import *
import sentry_sdk
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
SMS_CODE_SHOW = True

View File

@ -17,6 +17,8 @@ BROKER_URL = 'amqp://rabbitmq:5672'
CELERY_RESULT_BACKEND = BROKER_URL
CELERY_BROKER_URL = BROKER_URL
MEDIA_ROOT = os.path.join(PUBLIC_ROOT, MEDIA_LOCATION)
MEDIA_URL = f'{SCHEMA_URI}://{DOMAIN_URI}/{MEDIA_LOCATION}/'
# LOGGING
LOGGING = {
@ -62,8 +64,10 @@ ELASTICSEARCH_DSL = {
}
}
ELASTICSEARCH_INDEX_NAMES = {
# 'search_indexes.documents.news': 'local_news',
'search_indexes.documents.establishment': 'local_establishment',
}
}
# SORL thumbnails
THUMBNAIL_DEBUG = True

View File

@ -1,2 +1,3 @@
"""Production settings."""
from .base import *
from .amazon_s3 import *

View File

@ -1,5 +1,6 @@
"""Stage settings."""
from .base import *
from .amazon_s3 import *
ALLOWED_HOSTS = ['gm-stage.id-east.ru', '95.213.204.126']

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

View File

@ -64,7 +64,7 @@ urlpatterns = [
]
urlpatterns = urlpatterns + \
static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
static(settings.MEDIA_LOCATION, document_root=settings.MEDIA_ROOT)
if settings.DEBUG:
urlpatterns.extend(urlpatterns_doc)

View File

@ -32,4 +32,10 @@ djangorestframework-simplejwt==4.3.0
django-elasticsearch-dsl>=7.0.0,<8.0.0
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