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."""
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',
@ -300,7 +301,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 = {
@ -385,3 +385,6 @@ FILE_UPLOAD_PERMISSIONS = 0o644
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
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')),