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 main.models import Award, MetaDataContent
from review import models as review_models
@admin.register(models.EstablishmentType)
@ -26,10 +27,29 @@ class MetaDataContentInline(GenericTabularInline):
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)
class EstablishmentAdmin(admin.ModelAdmin):
"""Establishment admin."""
inlines = [AwardInline, MetaDataContentInline]
inlines = [
AwardInline, MetaDataContentInline,
ContactPhoneInline, ContactEmailInline,
ReviewInline]
@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 establishment import models
class EstablishmentFilter(FilterSet):
class EstablishmentFilter(filters.FilterSet):
"""Establishment filterset."""
tag_id = filters.NumberFilter(field_name='tags__metadata__id',)
award_id = filters.NumberFilter(field_name='awards__id',)
search = filters.CharFilter(method='search_text')
class Meta:
"""Meta class."""
model = models.Establishment
fields = (
'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.db import models
from django.utils.translation import gettext_lazy as _
from phonenumber_field.modelfields import PhoneNumberField
from location.models import Address
from utils.models import (ProjectBaseMixin, ImageMixin, TJSONField,
TraslatedFieldsMixin, BaseAttributes)
from utils.models import (
ProjectBaseMixin, ImageMixin, TJSONField,
TraslatedFieldsMixin, BaseAttributes
)
# todo: establishment type&subtypes check
@ -14,7 +17,7 @@ class EstablishmentType(ProjectBaseMixin, TraslatedFieldsMixin):
"""Establishment type model."""
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)
class Meta:
@ -38,7 +41,7 @@ class EstablishmentSubType(ProjectBaseMixin, TraslatedFieldsMixin):
"""Establishment type model."""
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,
on_delete=models.CASCADE,
verbose_name=_('Type'))
@ -51,23 +54,34 @@ class EstablishmentSubType(ProjectBaseMixin, TraslatedFieldsMixin):
verbose_name = _('Establishment subtype')
verbose_name_plural = _('Establishment subtypes')
# def __str__(self):
# """__str__ method."""
# return self.name
def clean_fields(self, exclude=None):
if not self.establishment_type.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):
"""Establishment model."""
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,
verbose_name=_('Description'),
help_text='{"en":"some text"}')
help_text='{"en-GB":"some text"}')
public_mark = models.PositiveIntegerField(blank=True, null=True,
default=None,
verbose_name=_('Public mark'),)
@ -89,6 +103,9 @@ class Establishment(ProjectBaseMixin, ImageMixin, TraslatedFieldsMixin):
verbose_name=_('Price level'))
awards = generic.GenericRelation(to='main.Award')
tags = generic.GenericRelation(to='main.MetaDataContent')
reviews = generic.GenericRelation(to='review.Review')
objects = EstablishmentQuerySet.as_manager()
class Meta:
"""Meta class."""
@ -149,6 +166,97 @@ class EstablishmentSchedule(BaseAttributes):
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):
"""QuerySets for Comment model."""

View File

@ -4,9 +4,28 @@ from rest_framework import serializers
from establishment import models
from location.serializers import AddressSerializer
from main.serializers import MetaDataContentSerializer, AwardSerializer
from review import models as review_models
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):
"""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):
"""Comment serializer"""
nickname = serializers.CharField(source='author.username')
@ -76,6 +105,10 @@ class EstablishmentSerializer(serializers.ModelSerializer):
schedule = EstablishmentScheduleSerializer(source='schedule.schedule',
many=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,
allow_null=True)
@ -98,5 +131,8 @@ class EstablishmentSerializer(serializers.ModelSerializer):
'tags',
'awards',
'schedule',
'phones',
'emails',
'reviews',
'comments',
)

View File

@ -30,6 +30,6 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='country',
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."""
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'))
low_price = models.IntegerField(default=25, verbose_name=_('Low 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)
title = TJSONField(
_('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='')
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
@ -228,7 +228,7 @@ class MetaData(TraslatedFieldsMixin, models.Model):
"""MetaData model."""
label = TJSONField(
_('label'), null=True, blank=True,
default=None, help_text='{"en":"some text"}')
default=None, help_text='{"en-GB":"some text"}')
category = models.ForeignKey(
MetaDataCategory, verbose_name=_('category'), on_delete=models.CASCADE)
@ -250,3 +250,15 @@ class MetaDataContent(models.Model):
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey('content_type', 'object_id')
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'), null=True, blank=True,
default=None, help_text='{"en":"some text"}')
default=None, help_text='{"en-GB":"some text"}')
subtitle = TJSONField(
_('subtitle'), null=True, blank=True,
default=None, help_text='{"en":"some text"}'
default=None, help_text='{"en-GB":"some text"}'
)
description = TJSONField(
_('description'), null=True, blank=True,
default=None, help_text='{"en":"some text"}'
default=None, help_text='{"en-GB":"some text"}'
)
start = models.DateTimeField(_('start'))
end = models.DateTimeField(_('end'))

View File

@ -1,5 +1,5 @@
"""News app common serializers."""
from rest_framework import serializers
from gallery import models as gallery_models
from location.models import Address
from location.serializers import AddressSerializer
@ -23,7 +23,7 @@ class NewsSerializer(serializers.ModelSerializer):
title_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)
image_url = serializers.ImageField(source='image.image')
image_url = serializers.ImageField(source='image.image', allow_null=True)
class Meta:
model = models.News

View File

@ -1,50 +1,37 @@
"""News app common app."""
from rest_framework import generics, permissions
from news import filters
from news.models import News, NewsType
from news import filters, models
from news.serializers import common as serializers
from utils.views import (JWTGenericViewMixin,
JWTListAPIView)
from utils.views import JWTGenericViewMixin, JWTListAPIView
# Mixins
class NewsViewMixin(JWTGenericViewMixin):
"""View mixin for News model"""
class NewsMixin:
"""News mixin."""
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, )
serializer_class = serializers.NewsSerializer
filter_class = filters.NewsListFilterSet
def get_queryset(self, *args, **kwargs):
"""Override get_queryset method"""
return News.objects.published()\
.by_country_code(code=self.request.country_code)\
return models.News.objects.published() \
.by_country_code(code=self.request.country_code) \
.order_by('-is_highlighted', '-created')
# class NewsCreateView(generics.CreateAPIView):
# """News list view."""
# queryset = News.objects.all()
# permission_classes = (permissions.IsAuthenticated, )
# serializer_class = serializers.NewsCreateUpdateSerializer
class NewsListView(NewsMixin, JWTListAPIView):
"""News list view."""
filter_class = filters.NewsListFilterSet
class NewsDetailView(NewsViewMixin, generics.RetrieveAPIView):
class NewsDetailView(NewsMixin, JWTGenericViewMixin, generics.RetrieveAPIView):
"""News detail view."""
permission_classes = (permissions.IsAuthenticatedOrReadOnly, )
serializer_class = serializers.NewsSerializer
class NewsTypeListView(generics.ListAPIView):
"""NewsType list view."""
serializer_class = serializers.NewsTypeSerializer
permission_classes = (permissions.AllowAny, )
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
from review import models
@admin.register(models.Review)
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'))
keywords = models.CharField(max_length=255, verbose_name='Keywords')
text = JSONField(_('Text'), null=True, blank=True,
default=None, help_text='{"en":"some text"}')
default=None, help_text='{"en-GB":"some text"}')
objects = SiteInterfaceDictionaryManager()

View File

@ -1,6 +1,7 @@
"""Utils app method."""
import random
import re
import string
from django.conf import settings
from django.http.request import HttpRequest
@ -60,3 +61,20 @@ def svg_image_path(instance, filename):
instance._meta.model_name,
datetime.now().strftime(settings.REST_DATE_FORMAT),
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',
'main.apps.MainConfig',
'news.apps.NewsConfig',
'notification.apps.NotificationConfig',
'partner.apps.PartnerConfig',
'translation.apps.TranslationConfig',
'configuration.apps.ConfigurationConfig',
@ -82,6 +83,7 @@ EXTERNAL_APPS = [
'django_extensions',
'rest_framework_simplejwt.token_blacklist',
'solo',
'phonenumber_field',
]
@ -302,7 +304,6 @@ CELERY_ACCEPT_CONTENT = ['application/json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = TIME_ZONE
USE_CELERY = False
# Django FCM (Firebase push notificatoins)
FCM_DJANGO_SETTINGS = {
@ -388,5 +389,7 @@ FILE_UPLOAD_PERMISSIONS = 0o644
SOLO_CACHE_TIMEOUT = 300
# REDIRECT URL
SITE_REDIRECT_URL_UNSUBSCRIBE = '/unsubscribe/'
SITE_NAME = 'Gault & Millau'

View File

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

View File

@ -12,11 +12,6 @@ DEFAULT_SUBDOMAIN = 'www'
SITE_DOMAIN_URI = 'testserver.com: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
BROKER_URL = 'amqp://rabbitmq:5672'
CELERY_RESULT_BACKEND = BROKER_URL

View File

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

View File

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