Merge remote-tracking branch 'origin/develop' into features/comments

# Conflicts:
#	apps/establishment/models.py
#	apps/establishment/serializers.py
This commit is contained in:
Anatoly 2019-09-01 14:14:38 +03:00
commit 21dd87e0ec
40 changed files with 876 additions and 66 deletions

View File

@ -4,6 +4,7 @@ from django.contrib.contenttypes.admin import GenericTabularInline
from establishment import models from establishment import models
from main.models import Award, MetaDataContent from main.models import Award, MetaDataContent
from review import models as review_models
@admin.register(models.EstablishmentType) @admin.register(models.EstablishmentType)
@ -26,10 +27,29 @@ class MetaDataContentInline(GenericTabularInline):
extra = 0 extra = 0
class ContactPhoneInline(admin.TabularInline):
"""Contact phone inline admin."""
model = models.ContactPhone
extra = 0
class ContactEmailInline(admin.TabularInline):
"""Contact email inline admin."""
model = models.ContactEmail
extra = 0
class ReviewInline(GenericTabularInline):
model = review_models.Review
extra = 0
@admin.register(models.Establishment) @admin.register(models.Establishment)
class EstablishmentAdmin(admin.ModelAdmin): class EstablishmentAdmin(admin.ModelAdmin):
"""Establishment admin.""" """Establishment admin."""
inlines = [AwardInline, MetaDataContentInline] inlines = [
AwardInline, MetaDataContentInline,
ContactPhoneInline, ContactEmailInline,
ReviewInline]
@admin.register(models.EstablishmentSchedule) @admin.register(models.EstablishmentSchedule)

View File

@ -1,16 +1,28 @@
from django_filters import FilterSet """Establishment app filters."""
from django.core.validators import EMPTY_VALUES
from django_filters import rest_framework as filters from django_filters import rest_framework as filters
from establishment import models from establishment import models
class EstablishmentFilter(FilterSet): class EstablishmentFilter(filters.FilterSet):
"""Establishment filterset."""
tag_id = filters.NumberFilter(field_name='tags__metadata__id',) tag_id = filters.NumberFilter(field_name='tags__metadata__id',)
award_id = filters.NumberFilter(field_name='awards__id',) award_id = filters.NumberFilter(field_name='awards__id',)
search = filters.CharFilter(method='search_text')
class Meta: class Meta:
"""Meta class."""
model = models.Establishment model = models.Establishment
fields = ( fields = (
'tag_id', 'tag_id',
'award_id' 'award_id',
'search',
) )
def search_text(self, queryset, name, value):
"""Search text."""
if value not in EMPTY_VALUES:
return queryset.search(value, locale=self.request.locale)
return queryset

View File

@ -0,0 +1,77 @@
# Generated by Django 2.2.4 on 2019-09-01 08:31
from django.db import migrations, models
import django.db.models.deletion
import phonenumber_field.modelfields
import utils.models
class Migration(migrations.Migration):
dependencies = [
('location', '0009_auto_20190901_0831'),
('establishment', '0004_auto_20190828_1156'),
]
operations = [
migrations.CreateModel(
name='Contact',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('address', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='location.Address')),
],
options={
'verbose_name': 'contact',
'verbose_name_plural': 'contacts',
},
),
migrations.AlterField(
model_name='establishment',
name='description',
field=utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='Description'),
),
migrations.AlterField(
model_name='establishment',
name='name',
field=utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='Name'),
),
migrations.AlterField(
model_name='establishmentsubtype',
name='name',
field=utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='Description'),
),
migrations.AlterField(
model_name='establishmenttype',
name='name',
field=utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='Description'),
),
migrations.CreateModel(
name='ContactPhone',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('phone', phonenumber_field.modelfields.PhoneNumberField(max_length=128)),
('contact', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='phones', to='establishment.Contact')),
],
options={
'verbose_name': 'contact phone',
'verbose_name_plural': 'contact phones',
},
),
migrations.CreateModel(
name='ContactEmail',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email', models.EmailField(max_length=254)),
('contact', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='emails', to='establishment.Contact')),
],
options={
'verbose_name': 'contact email',
'verbose_name_plural': 'contact emails',
},
),
migrations.AddField(
model_name='contact',
name='establishment',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contacts', to='establishment.Establishment'),
),
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-09-01 08:46
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('establishment', '0005_auto_20190901_0831'),
('establishment', '0005_establishmentschedule'),
]
operations = [
]

View File

@ -0,0 +1,30 @@
# Generated by Django 2.2.4 on 2019-09-01 10:32
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('establishment', '0006_merge_20190901_0846'),
]
operations = [
migrations.RemoveField(
model_name='contactemail',
name='contact',
),
migrations.RemoveField(
model_name='contactphone',
name='contact',
),
migrations.DeleteModel(
name='Contact',
),
migrations.DeleteModel(
name='ContactEmail',
),
migrations.DeleteModel(
name='ContactPhone',
),
]

View File

@ -0,0 +1,39 @@
# Generated by Django 2.2.4 on 2019-09-01 10:36
from django.db import migrations, models
import django.db.models.deletion
import phonenumber_field.modelfields
class Migration(migrations.Migration):
dependencies = [
('establishment', '0007_auto_20190901_1032'),
]
operations = [
migrations.CreateModel(
name='ContactPhone',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('phone', phonenumber_field.modelfields.PhoneNumberField(max_length=128)),
('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='phones', to='establishment.Establishment')),
],
options={
'verbose_name': 'contact phone',
'verbose_name_plural': 'contact phones',
},
),
migrations.CreateModel(
name='ContactEmail',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email', models.EmailField(max_length=254)),
('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='emails', to='establishment.Establishment')),
],
options={
'verbose_name': 'contact email',
'verbose_name_plural': 'contact emails',
},
),
]

View File

@ -3,10 +3,13 @@ from django.contrib.contenttypes import fields as generic
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
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 phonenumber_field.modelfields import PhoneNumberField
from location.models import Address from location.models import Address
from utils.models import (ProjectBaseMixin, ImageMixin, TJSONField, from utils.models import (
TraslatedFieldsMixin, BaseAttributes) ProjectBaseMixin, ImageMixin, TJSONField,
TraslatedFieldsMixin, BaseAttributes
)
# todo: establishment type&subtypes check # todo: establishment type&subtypes check
@ -14,7 +17,7 @@ class EstablishmentType(ProjectBaseMixin, TraslatedFieldsMixin):
"""Establishment type model.""" """Establishment type model."""
name = TJSONField(blank=True, null=True, default=None, verbose_name=_('Description'), name = TJSONField(blank=True, null=True, default=None, verbose_name=_('Description'),
help_text='{"en":"some text"}') help_text='{"en-GB":"some text"}')
use_subtypes = models.BooleanField(_('Use subtypes'), default=True) use_subtypes = models.BooleanField(_('Use subtypes'), default=True)
class Meta: class Meta:
@ -38,7 +41,7 @@ class EstablishmentSubType(ProjectBaseMixin, TraslatedFieldsMixin):
"""Establishment type model.""" """Establishment type model."""
name = TJSONField(blank=True, null=True, default=None, verbose_name=_('Description'), name = TJSONField(blank=True, null=True, default=None, verbose_name=_('Description'),
help_text='{"en":"some text"}') help_text='{"en-GB":"some text"}')
establishment_type = models.ForeignKey(EstablishmentType, establishment_type = models.ForeignKey(EstablishmentType,
on_delete=models.CASCADE, on_delete=models.CASCADE,
verbose_name=_('Type')) verbose_name=_('Type'))
@ -51,23 +54,34 @@ class EstablishmentSubType(ProjectBaseMixin, TraslatedFieldsMixin):
verbose_name = _('Establishment subtype') verbose_name = _('Establishment subtype')
verbose_name_plural = _('Establishment subtypes') verbose_name_plural = _('Establishment subtypes')
# def __str__(self):
# """__str__ method."""
# return self.name
def clean_fields(self, exclude=None): def clean_fields(self, exclude=None):
if not self.establishment_type.use_subtypes: if not self.establishment_type.use_subtypes:
raise ValidationError(_('Establishment type is not use subtypes.')) raise ValidationError(_('Establishment type is not use subtypes.'))
class EstablishmentQuerySet(models.QuerySet):
"""Extended queryset for Establishment model."""
def search(self, value, locale=None):
"""Search text in JSON fields."""
if locale is not None:
filters = [
{f'name__{locale}__icontains': value},
{f'description__{locale}__icontains': value}
]
return self.filter(reduce(lambda x, y: x | y, [models.Q(**i) for i in filters]))
else:
return self.none()
class Establishment(ProjectBaseMixin, ImageMixin, TraslatedFieldsMixin): class Establishment(ProjectBaseMixin, ImageMixin, TraslatedFieldsMixin):
"""Establishment model.""" """Establishment model."""
name = TJSONField(blank=True, null=True, default=None, name = TJSONField(blank=True, null=True, default=None,
verbose_name=_('Name'), help_text='{"en":"some text"}') verbose_name=_('Name'), help_text='{"en-GB":"some text"}')
description = TJSONField(blank=True, null=True, default=None, description = TJSONField(blank=True, null=True, default=None,
verbose_name=_('Description'), verbose_name=_('Description'),
help_text='{"en":"some text"}') help_text='{"en-GB":"some text"}')
public_mark = models.PositiveIntegerField(blank=True, null=True, public_mark = models.PositiveIntegerField(blank=True, null=True,
default=None, default=None,
verbose_name=_('Public mark'),) verbose_name=_('Public mark'),)
@ -89,6 +103,9 @@ class Establishment(ProjectBaseMixin, ImageMixin, TraslatedFieldsMixin):
verbose_name=_('Price level')) verbose_name=_('Price level'))
awards = generic.GenericRelation(to='main.Award') awards = generic.GenericRelation(to='main.Award')
tags = generic.GenericRelation(to='main.MetaDataContent') tags = generic.GenericRelation(to='main.MetaDataContent')
reviews = generic.GenericRelation(to='review.Review')
objects = EstablishmentQuerySet.as_manager()
class Meta: class Meta:
"""Meta class.""" """Meta class."""
@ -149,6 +166,97 @@ class EstablishmentSchedule(BaseAttributes):
verbose_name_plural = _('Establishment schedules') verbose_name_plural = _('Establishment schedules')
class ContactPhone(models.Model):
"""Contact phone model."""
establishment = models.ForeignKey(
Establishment, related_name='phones', on_delete=models.CASCADE)
phone = PhoneNumberField()
class Meta:
verbose_name = _('contact phone')
verbose_name_plural = _('contact phones')
def __str__(self):
return f'{self.phone.as_e164}'
class ContactEmail(models.Model):
"""Contact email model."""
establishment = models.ForeignKey(
Establishment, related_name='emails', on_delete=models.CASCADE)
email = models.EmailField()
class Meta:
verbose_name = _('contact email')
verbose_name_plural = _('contact emails')
def __str__(self):
return f'{self.email}'
#
# class Wine(TraslatedFieldsMixin, models.Model):
# """Wine model."""
# establishment = models.ForeignKey(
# 'establishment.Establishment', verbose_name=_('establishment'),
# on_delete=models.CASCADE)
# bottles = models.IntegerField(_('bottles'))
# price_min = models.DecimalField(
# _('price min'), max_digits=14, decimal_places=2)
# price_max = models.DecimalField(
# _('price max'), max_digits=14, decimal_places=2)
# by_glass = models.BooleanField(_('by glass'))
# price_glass_min = models.DecimalField(
# _('price min'), max_digits=14, decimal_places=2)
# price_glass_max = models.DecimalField(
# _('price max'), max_digits=14, decimal_places=2)
#
#
# class Plate(TraslatedFieldsMixin, models.Model):
# """Plate model."""
#
# STARTER = 0
# MAIN = 1
# COURSE = 2
# DESSERT = 3
#
# PLATE_TYPE_CHOICES = (
# (STARTER, _('starter')),
# (MAIN, _('main')),
# (COURSE, _('course')),
# (DESSERT, _('dessert')),
# )
# name = models.CharField(_('name'), max_length=255)
# plate_type = models.PositiveSmallIntegerField(_('plate_type'), choices=PLATE_TYPE_CHOICES)
# description = TJSONField(
# blank=True, null=True, default=None, verbose_name=_('description'),
# help_text='{"en-GB":"some text"}')
# price = models.DecimalField(
# _('price'), max_digits=14, decimal_places=2)
# is_signature_plate = models.BooleanField(_('is signature plate'))
# currency = models.ForeignKey(
# 'main.Currency', verbose_name=_('currency'), on_delete=models.CASCADE)
#
# menu = models.ManyToManyField(to='Plate', verbose_name=_(''), through='establishment.Menu')
#
# class Meta:
# verbose_name = _('plate')
# verbose_name_plural = _('plates')
#
# def __str__(self):
# return f'plate_id:{self.id}'
#
#
# class Menu(TraslatedFieldsMixin, BaseAttributes):
# """Menu model."""
# establishment = models.ForeignKey(
# 'establishment.Establishment', verbose_name=_('establishment'),
# on_delete=models.CASCADE)
# plate = models.ForeignKey(Plate, verbose_name=_('menu'), on_delete=models.CASCADE)
#
# class Meta:
# verbose_name = _('menu')
# verbose_name_plural = _('menu')
class CommentQuerySet(models.QuerySet): class CommentQuerySet(models.QuerySet):
"""QuerySets for Comment model.""" """QuerySets for Comment model."""

View File

@ -4,9 +4,28 @@ from rest_framework import serializers
from establishment import models from establishment import models
from location.serializers import AddressSerializer from location.serializers import AddressSerializer
from main.serializers import MetaDataContentSerializer, AwardSerializer from main.serializers import MetaDataContentSerializer, AwardSerializer
from review import models as review_models
from timetable.models import Timetable from timetable.models import Timetable
class ContactPhonesSerializer(serializers.ModelSerializer):
"""Contact phone serializer"""
class Meta:
model = models.ContactPhone
fields = [
'phone'
]
class ContactEmailsSerializer(serializers.ModelSerializer):
"""Contact email serializer"""
class Meta:
model = models.ContactEmail
fields = [
'email'
]
class EstablishmentTypeSerializer(serializers.ModelSerializer): class EstablishmentTypeSerializer(serializers.ModelSerializer):
"""Serializer for EstablishmentType model.""" """Serializer for EstablishmentType model."""
@ -47,6 +66,16 @@ class EstablishmentScheduleSerializer(serializers.ModelSerializer):
) )
class ReviewSerializer(serializers.ModelSerializer):
"""Serializer for model Review."""
class Meta:
"""Meta class."""
model = review_models.Review
fields = (
'text',
)
class CommentSerializer(serializers.ModelSerializer): class CommentSerializer(serializers.ModelSerializer):
"""Comment serializer""" """Comment serializer"""
nickname = serializers.CharField(source='author.username') nickname = serializers.CharField(source='author.username')
@ -76,6 +105,10 @@ class EstablishmentSerializer(serializers.ModelSerializer):
schedule = EstablishmentScheduleSerializer(source='schedule.schedule', schedule = EstablishmentScheduleSerializer(source='schedule.schedule',
many=True, many=True,
allow_null=True) allow_null=True)
phones = ContactPhonesSerializer(read_only=True, many=True, )
emails = ContactEmailsSerializer(read_only=True, many=True, )
reviews = ReviewSerializer(source='reviews.last',
allow_null=True)
comments = CommentSerializer(many=True, comments = CommentSerializer(many=True,
allow_null=True) allow_null=True)
@ -98,5 +131,8 @@ class EstablishmentSerializer(serializers.ModelSerializer):
'tags', 'tags',
'awards', 'awards',
'schedule', 'schedule',
'phones',
'emails',
'reviews',
'comments', 'comments',
) )

View File

@ -30,6 +30,6 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='country', model_name='country',
name='name', name='name',
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=None, help_text='{"en":"some text"}', null=True, verbose_name='Text'), field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='Text'),
), ),
] ]

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.4 on 2019-09-01 08:31
import django.contrib.postgres.fields.jsonb
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('location', '0008_auto_20190827_1302'),
]
operations = [
migrations.AlterField(
model_name='country',
name='name',
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='Name'),
),
]

View File

@ -17,7 +17,7 @@ class Country(SVGImageMixin, ProjectBaseMixin):
"""Country model.""" """Country model."""
name = JSONField(null=True, blank=True, default=None, name = JSONField(null=True, blank=True, default=None,
verbose_name=_('Name'), help_text='{"en":"some text"}') verbose_name=_('Name'), help_text='{"en-GB":"some text"}')
code = models.CharField(max_length=255, unique=True, verbose_name=_('Code')) code = models.CharField(max_length=255, unique=True, verbose_name=_('Code'))
low_price = models.IntegerField(default=25, verbose_name=_('Low price')) low_price = models.IntegerField(default=25, verbose_name=_('Low price'))
high_price = models.IntegerField(default=50, verbose_name=_('High price')) high_price = models.IntegerField(default=50, verbose_name=_('High price'))

View File

@ -0,0 +1,35 @@
# Generated by Django 2.2.4 on 2019-09-01 10:32
from django.db import migrations, models
import utils.models
class Migration(migrations.Migration):
dependencies = [
('main', '0012_auto_20190829_1155'),
]
operations = [
migrations.CreateModel(
name='Currency',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50, verbose_name='name')),
],
options={
'verbose_name': 'currency',
'verbose_name_plural': 'currencies',
},
),
migrations.AlterField(
model_name='award',
name='title',
field=utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='title'),
),
migrations.AlterField(
model_name='metadata',
name='label',
field=utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='label'),
),
]

View File

@ -190,7 +190,7 @@ class Award(TraslatedFieldsMixin, models.Model):
award_type = models.ForeignKey('main.AwardType', on_delete=models.CASCADE) award_type = models.ForeignKey('main.AwardType', on_delete=models.CASCADE)
title = TJSONField( title = TJSONField(
_('title'), null=True, blank=True, _('title'), null=True, blank=True,
default=None, help_text='{"en":"some text"}') default=None, help_text='{"en-GB":"some text"}')
vintage_year = models.CharField(_('vintage year'), max_length=255, default='') vintage_year = models.CharField(_('vintage year'), max_length=255, default='')
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
@ -228,7 +228,7 @@ class MetaData(TraslatedFieldsMixin, models.Model):
"""MetaData model.""" """MetaData model."""
label = TJSONField( label = TJSONField(
_('label'), null=True, blank=True, _('label'), null=True, blank=True,
default=None, help_text='{"en":"some text"}') default=None, help_text='{"en-GB":"some text"}')
category = models.ForeignKey( category = models.ForeignKey(
MetaDataCategory, verbose_name=_('category'), on_delete=models.CASCADE) MetaDataCategory, verbose_name=_('category'), on_delete=models.CASCADE)
@ -250,3 +250,15 @@ class MetaDataContent(models.Model):
object_id = models.PositiveIntegerField() object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey('content_type', 'object_id') content_object = generic.GenericForeignKey('content_type', 'object_id')
metadata = models.ForeignKey(MetaData, on_delete=models.CASCADE) metadata = models.ForeignKey(MetaData, on_delete=models.CASCADE)
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}'

View File

@ -0,0 +1,29 @@
# Generated by Django 2.2.4 on 2019-09-01 10:32
from django.db import migrations
import utils.models
class Migration(migrations.Migration):
dependencies = [
('news', '0008_auto_20190828_1522'),
]
operations = [
migrations.AlterField(
model_name='news',
name='description',
field=utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='description'),
),
migrations.AlterField(
model_name='news',
name='subtitle',
field=utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='subtitle'),
),
migrations.AlterField(
model_name='news',
name='title',
field=utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='title'),
),
]

View File

@ -43,14 +43,14 @@ class News(BaseAttributes, TraslatedFieldsMixin):
title = TJSONField( title = TJSONField(
_('title'), null=True, blank=True, _('title'), null=True, blank=True,
default=None, help_text='{"en":"some text"}') default=None, help_text='{"en-GB":"some text"}')
subtitle = TJSONField( subtitle = TJSONField(
_('subtitle'), null=True, blank=True, _('subtitle'), null=True, blank=True,
default=None, help_text='{"en":"some text"}' default=None, help_text='{"en-GB":"some text"}'
) )
description = TJSONField( description = TJSONField(
_('description'), null=True, blank=True, _('description'), null=True, blank=True,
default=None, help_text='{"en":"some text"}' default=None, help_text='{"en-GB":"some text"}'
) )
start = models.DateTimeField(_('start')) start = models.DateTimeField(_('start'))
end = models.DateTimeField(_('end')) end = models.DateTimeField(_('end'))

View File

@ -1,5 +1,5 @@
"""News app common serializers."""
from rest_framework import serializers from rest_framework import serializers
from gallery import models as gallery_models from gallery import models as gallery_models
from location.models import Address from location.models import Address
from location.serializers import AddressSerializer from location.serializers import AddressSerializer
@ -23,7 +23,7 @@ class NewsSerializer(serializers.ModelSerializer):
title_translated = serializers.CharField(read_only=True, allow_null=True) title_translated = serializers.CharField(read_only=True, allow_null=True)
subtitle_translated = serializers.CharField(read_only=True, allow_null=True) subtitle_translated = serializers.CharField(read_only=True, allow_null=True)
description_translated = serializers.CharField(read_only=True, allow_null=True) description_translated = serializers.CharField(read_only=True, allow_null=True)
image_url = serializers.ImageField(source='image.image') image_url = serializers.ImageField(source='image.image', allow_null=True)
class Meta: class Meta:
model = models.News model = models.News

View File

@ -1,50 +1,37 @@
"""News app common app."""
from rest_framework import generics, permissions from rest_framework import generics, permissions
from news import filters, models
from news import filters
from news.models import News, NewsType
from news.serializers import common as serializers from news.serializers import common as serializers
from utils.views import (JWTGenericViewMixin, from utils.views import JWTGenericViewMixin, JWTListAPIView
JWTListAPIView)
# Mixins class NewsMixin:
class NewsViewMixin(JWTGenericViewMixin): """News mixin."""
"""View mixin for News model"""
def get_queryset(self, *args, **kwargs):
"""Override get_queryset method"""
return News.objects.annotate_localized_fields(locale=self.request.locale)
class NewsListView(NewsViewMixin, JWTListAPIView):
"""News list view."""
permission_classes = (permissions.AllowAny, ) permission_classes = (permissions.AllowAny, )
serializer_class = serializers.NewsSerializer serializer_class = serializers.NewsSerializer
filter_class = filters.NewsListFilterSet
def get_queryset(self, *args, **kwargs): def get_queryset(self, *args, **kwargs):
"""Override get_queryset method""" """Override get_queryset method"""
return News.objects.published()\ return models.News.objects.published() \
.by_country_code(code=self.request.country_code)\ .by_country_code(code=self.request.country_code) \
.order_by('-is_highlighted', '-created') .order_by('-is_highlighted', '-created')
# class NewsCreateView(generics.CreateAPIView): class NewsListView(NewsMixin, JWTListAPIView):
# """News list view.""" """News list view."""
# queryset = News.objects.all()
# permission_classes = (permissions.IsAuthenticated, ) filter_class = filters.NewsListFilterSet
# serializer_class = serializers.NewsCreateUpdateSerializer
class NewsDetailView(NewsViewMixin, generics.RetrieveAPIView): class NewsDetailView(NewsMixin, JWTGenericViewMixin, generics.RetrieveAPIView):
"""News detail view.""" """News detail view."""
permission_classes = (permissions.IsAuthenticatedOrReadOnly, )
serializer_class = serializers.NewsSerializer
class NewsTypeListView(generics.ListAPIView): class NewsTypeListView(generics.ListAPIView):
"""NewsType list view.""" """NewsType list view."""
serializer_class = serializers.NewsTypeSerializer serializer_class = serializers.NewsTypeSerializer
permission_classes = (permissions.AllowAny, ) permission_classes = (permissions.AllowAny, )
pagination_class = None pagination_class = None
queryset = NewsType.objects.all() queryset = models.NewsType.objects.all()

View File

View File

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

View File

@ -0,0 +1,7 @@
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class NotificationConfig(AppConfig):
name = 'notification'
verbose_name = _('notification')

View File

@ -0,0 +1,37 @@
# Generated by Django 2.2.4 on 2019-08-30 11:22
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Subscriber',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')),
('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('email', models.EmailField(blank=True, default=None, max_length=254, null=True, unique=True, verbose_name='Email')),
('ip_address', models.GenericIPAddressField(blank=True, default=None, null=True, verbose_name='IP address')),
('country_code', models.CharField(blank=True, default=None, max_length=10, null=True, verbose_name='Country code')),
('locale', models.CharField(blank=True, default=None, max_length=10, null=True, verbose_name='Locale identifier')),
('state', models.PositiveIntegerField(choices=[(0, 'Unusable'), (1, 'Usable')], default=1, verbose_name='State')),
('update_code', models.CharField(blank=True, db_index=True, default=None, max_length=254, null=True, verbose_name='Token')),
('user', models.OneToOneField(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='subscriber', to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'verbose_name': 'Subscriber',
'verbose_name_plural': 'Subscribers',
},
),
]

View File

124
apps/notification/models.py Normal file
View File

@ -0,0 +1,124 @@
"""Notification app models."""
from django.conf import settings
from django.db import models
from django.utils.translation import ugettext_lazy as _
from account.models import User
from utils.methods import generate_string_code
from utils.models import ProjectBaseMixin
# todo: associate user & subscriber after users registration
class SubscriberManager(models.Manager):
"""Extended manager for Subscriber model."""
def make_subscriber(self, email=None, user=None, ip_address=None, country_code=None,
locale=None, *args, **kwargs):
"""Make subscriber and update info."""
# search existing object
if not user:
user = User.objects.filter(email=email).first()
if user:
obj = self.model.objects.filter(models.Q(user=user) | models.Q(
email=user.email)).first()
else:
obj = self.model.objects.filter(email=email).first()
# update or create
if obj:
if user:
obj.user = user
obj.email = None
else:
obj.email = email
obj.ip_address = ip_address
obj.country_code = country_code
obj.locale = locale
obj.state = self.model.USABLE
obj.update_code = generate_string_code()
obj.save()
else:
obj = self.model.objects.create(user=user, email=email, ip_address=ip_address,
country_code=country_code, locale=locale)
return obj
def associate_user(self, user):
"""Associate user."""
obj = self.model.objects.filter(user=user).first()
if obj is None:
obj = self.model.objects.filter(email=user.email_confirmed, user__isnull=True).first()
if obj:
obj.user = user
obj.email = None
obj.save()
return obj
class SubscriberQuerySet(models.QuerySet):
"""Extended queryset for Subscriber model."""
def by_usable(self, switcher=True):
if switcher:
return self.filter(state=self.model.USABLE)
else:
return self.filter(state=self.model.UNUSABLE)
class Subscriber(ProjectBaseMixin):
"""Subscriber model."""
UNUSABLE = 0
USABLE = 1
STATE_CHOICES = (
(UNUSABLE, _('Unusable')),
(USABLE, _('Usable')),
)
user = models.OneToOneField(User, blank=True, null=True, default=None,
on_delete=models.SET_NULL, related_name='subscriber',
verbose_name=_('User'))
email = models.EmailField(blank=True, null=True, default=None, unique=True,
verbose_name=_('Email'))
ip_address = models.GenericIPAddressField(blank=True, null=True, default=None,
verbose_name=_('IP address'))
country_code = models.CharField(max_length=10, blank=True, null=True, default=None,
verbose_name=_('Country code'))
locale = models.CharField(blank=True, null=True, default=None,
max_length=10, verbose_name=_('Locale identifier'))
state = models.PositiveIntegerField(choices=STATE_CHOICES, default=USABLE,
verbose_name=_('State'))
update_code = models.CharField(max_length=254, blank=True, null=True, default=None,
db_index=True, verbose_name=_('Token'))
objects = SubscriberManager.from_queryset(SubscriberQuerySet)()
class Meta:
"""Meta class."""
verbose_name = _('Subscriber')
verbose_name_plural = _('Subscribers')
def save(self, *args, **kwargs):
"""Overrided save method."""
if self.update_code is None:
self.update_code = generate_string_code()
return super(Subscriber, self).save(*args, **kwargs)
def unsubscribe(self):
"""Unsubscribe user."""
self.state = self.UNUSABLE
self.save()
@property
def send_to(self):
"""Actual email."""
return self.user.email if self.user else self.email
@property
def link_to_unsubscribe(self):
"""Link to unsubscribe."""
schema = settings.SCHEMA_URI
site_domain = settings.SITE_DOMAIN_URI
url = settings.SITE_REDIRECT_URL_UNSUBSCRIBE
query = f'?code={self.update_code}'
return f'{schema}://{site_domain}{url}{query}'

View File

@ -0,0 +1,47 @@
"""Notification app serializers."""
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from notification import models
from utils.methods import get_user_ip
class SubscribeSerializer(serializers.ModelSerializer):
"""Subscribe serializer."""
email = serializers.EmailField(required=False, source='send_to')
state_display = serializers.CharField(source='get_state_display', read_only=True)
class Meta:
"""Meta class."""
model = models.Subscriber
fields = ('email', 'state', 'state_display')
read_only_fields = ('state', 'state_display')
def validate(self, attrs):
"""Validate attrs."""
request = self.context.get('request')
user = request.user
# validate email
email = attrs.get('send_to')
if user.is_authenticated:
if email is not None and email != user.email:
raise serializers.ValidationError(_('Does not match user email'))
else:
if email is None:
raise serializers.ValidationError({'email': _('This field is required.')})
# append info
attrs['email'] = email
attrs['country_code'] = request.country_code
attrs['locale'] = request.locale
attrs['ip_address'] = get_user_ip(request)
if user.is_authenticated:
attrs['user'] = user
return attrs
def create(self, validated_data):
"""Create obj."""
obj = models.Subscriber.objects.make_subscriber(**validated_data)
return obj

View File

View File

@ -0,0 +1,12 @@
"""Notification app common urlconf."""
from django.urls import path
from notification.views import common
urlpatterns = [
path('subscribe/', common.SubscribeView.as_view(), name='subscribe'),
path('subscribe-info/', common.SubscribeInfoAuthUserView.as_view(), name='check-code-auth'),
path('subscribe-info/<code>/', common.SubscribeInfoView.as_view(), name='check-code'),
path('unsubscribe/', common.UnsubscribeAuthUserView.as_view(), name='unsubscribe-auth'),
path('unsubscribe/<code>/', common.UnsubscribeView.as_view(), name='unsubscribe'),
]

View File

@ -0,0 +1,7 @@
"""Establishment app web urlconf."""
from notification.urls.common import urlpatterns as common_urlpatterns
urlpatterns = []
urlpatterns.extend(common_urlpatterns)

View File

View File

@ -0,0 +1,78 @@
"""Notification app common views."""
from django.shortcuts import get_object_or_404
from rest_framework import generics, permissions
from rest_framework.response import Response
from notification import models
from notification.serializers import common as serializers
class SubscribeView(generics.GenericAPIView):
"""Subscribe View."""
queryset = models.Subscriber.objects.all()
permission_classes = (permissions.AllowAny, )
serializer_class = serializers.SubscribeSerializer
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(data=serializer.data)
class SubscribeInfoView(generics.RetrieveAPIView):
"""Subscribe info view."""
lookup_field = 'update_code'
lookup_url_kwarg = 'code'
permission_classes = (permissions.AllowAny, )
queryset = models.Subscriber.objects.all()
serializer_class = serializers.SubscribeSerializer
class SubscribeInfoAuthUserView(generics.RetrieveAPIView):
"""Subscribe info auth user view."""
permission_classes = (permissions.IsAuthenticated, )
queryset = models.Subscriber.objects.all()
serializer_class = serializers.SubscribeSerializer
def get_object(self):
user = self.request.user
queryset = self.filter_queryset(self.get_queryset())
filter_kwargs = {'user': user}
obj = get_object_or_404(queryset, **filter_kwargs)
self.check_object_permissions(self.request, obj)
return obj
class UnsubscribeView(generics.GenericAPIView):
"""Unsubscribe view."""
lookup_field = 'update_code'
lookup_url_kwarg = 'code'
permission_classes = (permissions.AllowAny, )
queryset = models.Subscriber.objects.all()
serializer_class = serializers.SubscribeSerializer
def patch(self, request, *args, **kw):
obj = self.get_object()
obj.unsubscribe()
serializer = self.get_serializer(instance=obj)
return Response(data=serializer.data)
class UnsubscribeAuthUserView(generics.GenericAPIView):
"""Unsubscribe auth user view."""
permission_classes = (permissions.IsAuthenticated, )
queryset = models.Subscriber.objects.all()
serializer_class = serializers.SubscribeSerializer
def patch(self, request, *args, **kw):
user = request.user
obj = get_object_or_404(models.Subscriber, user=user)
obj.unsubscribe()
serializer = self.get_serializer(instance=obj)
return Response(data=serializer.data)

View File

@ -1,7 +1,3 @@
from django.contrib import admin # @admin.register(models.Review)
from review import models # class ReviewAdminModel(admin.ModelAdmin):
# """Admin model for model Review."""
@admin.register(models.Review)
class ReviewAdminModel(admin.ModelAdmin):
"""Admin model for model Review."""

View File

@ -0,0 +1,43 @@
# Generated by Django 2.2.4 on 2019-09-01 09:32
import django.core.validators
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('translation', '0002_siteinterfacedictionary'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Review',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')),
('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('object_id', models.PositiveIntegerField()),
('text', models.TextField(verbose_name='Text')),
('status', models.PositiveSmallIntegerField(choices=[(0, 'To investigate'), (1, 'To review'), (2, 'Ready')], default=0)),
('published_at', models.DateTimeField(blank=True, default=None, help_text='Review published datetime', null=True, verbose_name='Publish datetime')),
('vintage', models.IntegerField(validators=[django.core.validators.MinValueValidator(1900), django.core.validators.MaxValueValidator(2100)], verbose_name='Year of review')),
('child', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='review.Review', verbose_name='Child review')),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='review_records_created', to=settings.AUTH_USER_MODEL, verbose_name='created by')),
('language', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reviews', to='translation.Language', verbose_name='Review language')),
('modified_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='review_records_modified', to=settings.AUTH_USER_MODEL, verbose_name='modified by')),
('reviewer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reviews', to=settings.AUTH_USER_MODEL, verbose_name='Reviewer')),
],
options={
'verbose_name': 'Review',
'verbose_name_plural': 'Reviews',
},
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.4 on 2019-09-01 10:32
import django.contrib.postgres.fields.jsonb
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('translation', '0002_siteinterfacedictionary'),
]
operations = [
migrations.AlterField(
model_name='siteinterfacedictionary',
name='text',
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='Text'),
),
]

View File

@ -49,7 +49,7 @@ class SiteInterfaceDictionary(ProjectBaseMixin):
verbose_name=_('Page')) verbose_name=_('Page'))
keywords = models.CharField(max_length=255, verbose_name='Keywords') keywords = models.CharField(max_length=255, verbose_name='Keywords')
text = JSONField(_('Text'), null=True, blank=True, text = JSONField(_('Text'), null=True, blank=True,
default=None, help_text='{"en":"some text"}') default=None, help_text='{"en-GB":"some text"}')
objects = SiteInterfaceDictionaryManager() objects = SiteInterfaceDictionaryManager()

View File

@ -1,6 +1,7 @@
"""Utils app method.""" """Utils app method."""
import random import random
import re import re
import string
from django.conf import settings from django.conf import settings
from django.http.request import HttpRequest from django.http.request import HttpRequest
@ -60,3 +61,20 @@ def svg_image_path(instance, filename):
instance._meta.model_name, instance._meta.model_name,
datetime.now().strftime(settings.REST_DATE_FORMAT), datetime.now().strftime(settings.REST_DATE_FORMAT),
filename) filename)
def get_user_ip(request):
"""Get user ip."""
meta_dict = request.META
x_forwarded_for = meta_dict.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = meta_dict.get('REMOTE_ADDR')
return ip
def generate_string_code(size=64,
chars=string.ascii_lowercase + string.ascii_uppercase + string.digits):
"""Generate string code."""
return ''.join([random.SystemRandom().choice(chars) for _ in range(size)])

View File

@ -61,6 +61,7 @@ PROJECT_APPS = [
'location.apps.LocationConfig', 'location.apps.LocationConfig',
'main.apps.MainConfig', 'main.apps.MainConfig',
'news.apps.NewsConfig', 'news.apps.NewsConfig',
'notification.apps.NotificationConfig',
'partner.apps.PartnerConfig', 'partner.apps.PartnerConfig',
'translation.apps.TranslationConfig', 'translation.apps.TranslationConfig',
'configuration.apps.ConfigurationConfig', 'configuration.apps.ConfigurationConfig',
@ -82,6 +83,7 @@ EXTERNAL_APPS = [
'django_extensions', 'django_extensions',
'rest_framework_simplejwt.token_blacklist', 'rest_framework_simplejwt.token_blacklist',
'solo', 'solo',
'phonenumber_field',
] ]
@ -302,7 +304,6 @@ CELERY_ACCEPT_CONTENT = ['application/json']
CELERY_TASK_SERIALIZER = 'json' CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json' CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = TIME_ZONE CELERY_TIMEZONE = TIME_ZONE
USE_CELERY = False
# Django FCM (Firebase push notificatoins) # Django FCM (Firebase push notificatoins)
FCM_DJANGO_SETTINGS = { FCM_DJANGO_SETTINGS = {
@ -388,5 +389,7 @@ FILE_UPLOAD_PERMISSIONS = 0o644
SOLO_CACHE_TIMEOUT = 300 SOLO_CACHE_TIMEOUT = 300
# REDIRECT URL
SITE_REDIRECT_URL_UNSUBSCRIBE = '/unsubscribe/'
SITE_NAME = 'Gault & Millau' SITE_NAME = 'Gault & Millau'

View File

@ -5,6 +5,7 @@ ALLOWED_HOSTS = ['gm.id-east.ru', '95.213.204.126']
SEND_SMS = False SEND_SMS = False
SMS_CODE_SHOW = True SMS_CODE_SHOW = True
USE_CELERY = False
SCHEMA_URI = 'http' SCHEMA_URI = 'http'
DEFAULT_SUBDOMAIN = 'www' DEFAULT_SUBDOMAIN = 'www'

View File

@ -12,11 +12,6 @@ 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'
# OTHER SETTINGS
API_HOST = '0.0.0.0:8000'
API_HOST_URL = 'http://%s' % API_HOST
# CELERY # CELERY
BROKER_URL = 'amqp://rabbitmq:5672' BROKER_URL = 'amqp://rabbitmq:5672'
CELERY_RESULT_BACKEND = BROKER_URL CELERY_RESULT_BACKEND = BROKER_URL

View File

@ -23,6 +23,7 @@ urlpatterns = [
path('collection/', include('collection.urls.web')), path('collection/', include('collection.urls.web')),
path('establishments/', include('establishment.urls.web')), path('establishments/', include('establishment.urls.web')),
path('news/', include('news.urls.web')), path('news/', include('news.urls.web')),
path('notifications/', include('notification.urls.web')),
path('partner/', include('partner.urls.web')), path('partner/', include('partner.urls.web')),
path('location/', include('location.urls')), path('location/', include('location.urls')),
path('main/', include('main.urls')), path('main/', include('main.urls')),

View File

@ -17,6 +17,7 @@ djangorestframework-xml
celery celery
amqp>=2.4.0 amqp>=2.4.0
geoip2==2.9.0 geoip2==2.9.0
django-phonenumber-field[phonenumbers]==2.1.0
# auth socials # auth socials
djangorestframework-oauth djangorestframework-oauth