Notification app & subscription

This commit is contained in:
evgeniy-st 2019-08-30 15:40:53 +03:00
parent 62da2ee114
commit 2a8ffb16de
18 changed files with 339 additions and 6 deletions

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, throttling
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,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',
@ -300,7 +301,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 = {
@ -385,3 +385,6 @@ FILE_UPLOAD_PERMISSIONS = 0o644
SOLO_CACHE_TIMEOUT = 300 SOLO_CACHE_TIMEOUT = 300
# REDIRECT URL
SITE_REDIRECT_URL_UNSUBSCRIBE = '/unsubscribe/'

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