Add basic push notifications system

parent 2f8ad4e1
......@@ -13,3 +13,4 @@ django-ical>=1.4,<2
django-libsass>=0.7,<1
django-cors-headers>=2.0.0,<2.1
python-magic>=0.4.13,<0.5
pyfcm>=1.4.2,<1.5
default_app_config = 'pushnotifications.apps.PushNotificationsConfig'
from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
from pushnotifications import models
from pushnotifications.models import Message
@admin.register(models.Device)
class DeviceAdmin(admin.ModelAdmin):
list_display = ('user', 'type', 'active', 'date_created')
list_filter = ('active', 'type')
actions = ('enable', 'disable')
search_fields = ('name', 'device_id', 'user__username')
def enable(self, request, queryset):
queryset.update(active=True)
enable.short_description = _('Enable selected devices')
def disable(self, request, queryset):
queryset.update(active=False)
disable.short_description = _('Disable selected devices')
@admin.register(models.Message)
class MessageAdmin(admin.ModelAdmin):
list_display = ('title', 'body', 'sent', 'success', 'failure')
filter_horizontal = ('users',)
list_filter = ('sent',)
def get_fields(self, request, obj=None):
if obj and obj.sent:
return 'users', 'title', 'body', 'success', 'failure'
return 'users', 'title', 'body'
def get_readonly_fields(self, request, obj=None):
if obj and obj.sent:
return 'users', 'title', 'body', 'success', 'failure'
return super().get_readonly_fields(request, obj)
def change_view(self, request, object_id, form_url='', **kwargs):
obj = Message.objects.filter(id=object_id)[0]
return super(MessageAdmin, self).change_view(
request, object_id, form_url, {'message': obj})
from rest_framework import permissions
class IsOwner(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
# must be the owner to view the object
return obj.user == request.user
from __future__ import absolute_import
from rest_framework.serializers import ModelSerializer
from pushnotifications.models import Device
class DeviceSerializer(ModelSerializer):
class Meta:
model = Device
fields = ('pk', 'registration_id', 'active', 'date_created', 'type')
read_only_fields = ('date_created',)
extra_kwargs = {'active': {'default': True}}
from rest_framework import routers
from pushnotifications.api import viewsets
router = routers.SimpleRouter()
router.register(r'devices', viewsets.DeviceViewSet)
urlpatterns = router.urls
from rest_framework import permissions
from rest_framework.viewsets import ModelViewSet
from pushnotifications.api.permissions import IsOwner
from pushnotifications.api.serializers import DeviceSerializer
from pushnotifications.models import Device
class DeviceViewSet(ModelViewSet):
permission_classes = (permissions.IsAuthenticated, IsOwner)
queryset = Device.objects.all()
serializer_class = DeviceSerializer
def get_queryset(self):
# filter all devices to only those belonging to the current user
return self.queryset.filter(user=self.request.user)
def perform_create(self, serializer):
try:
serializer.instance = Device.objects.get(
user=self.request.user,
registration_id=serializer.validated_data['registration_id']
)
except Device.DoesNotExist:
pass
serializer.save(user=self.request.user)
def perform_update(self, serializer):
serializer.save(user=self.request.user)
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class PushNotificationsConfig(AppConfig):
name = 'pushnotifications'
verbose_name = _('Push Notifications')
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-09-11 12:18
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Device',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('registration_id', models.TextField(verbose_name='Registration token')),
('type', models.CharField(choices=[('ios', 'iOS'), ('android', 'Android')], max_length=10)),
('active', models.BooleanField(default=True, help_text='Inactive devices will not be sent notifications', verbose_name='Is active')),
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Registration date')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Message',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=150, verbose_name='title')),
('body', models.TextField(verbose_name='body')),
('sent', models.BooleanField(default=False, verbose_name='sent')),
('failure', models.IntegerField(blank=True, null=True, verbose_name='failure')),
('success', models.IntegerField(blank=True, null=True, verbose_name='success')),
('users', models.ManyToManyField(to=settings.AUTH_USER_MODEL)),
],
),
migrations.AlterUniqueTogether(
name='device',
unique_together=set([('registration_id', 'user')]),
),
]
from __future__ import unicode_literals
from django.conf import settings as django_settings
from django.db import models
from django.utils.translation import ugettext_lazy as _
from pyfcm import FCMNotification
from thaliawebsite import settings
class Device(models.Model):
DEVICE_TYPES = (
('ios', 'iOS'),
('android', 'Android')
)
registration_id = models.TextField(verbose_name=_("Registration token"))
type = models.CharField(choices=DEVICE_TYPES, max_length=10)
active = models.BooleanField(
verbose_name=_("Is active"), default=True,
help_text=_("Inactive devices will not be sent notifications")
)
user = models.ForeignKey(django_settings.AUTH_USER_MODEL,
blank=False, null=False)
date_created = models.DateTimeField(
verbose_name=_("Registration date"), auto_now_add=True, null=False
)
class Meta:
unique_together = ('registration_id', 'user',)
class Message(models.Model):
users = models.ManyToManyField(django_settings.AUTH_USER_MODEL)
title = models.CharField(
max_length=150,
verbose_name=_('title')
)
body = models.TextField(
verbose_name=_('body')
)
sent = models.BooleanField(
verbose_name=_('sent'),
default=False
)
failure = models.IntegerField(
verbose_name=_('failure'),
blank=True,
null=True,
)
success = models.IntegerField(
verbose_name=_('success'),
blank=True,
null=True,
)
def __str__(self):
return '{}: {}'.format(self.title, self.body)
def send(self, **kwargs):
if self:
reg_ids = list(
Device.objects.filter(user__in=self.users.all(), active=True)
.values_list('registration_id', flat=True))
if len(reg_ids) == 0:
return None
result = FCMNotification(
api_key=settings.PUSH_NOTIFICATIONS_API_KEY
).notify_multiple_devices(
registration_ids=reg_ids,
message_title=self.title,
message_body=self.body,
color='#E62272',
sound='default',
**kwargs
)
results = result['results']
for (index, item) in enumerate(results):
if 'error' in item:
reg_id = reg_ids[index]
if (item['error'] == 'NotRegistered' or
item['error'] == 'InvalidRegistration'):
Device.objects.filter(registration_id=reg_id).delete()
else:
Device.objects.filter(
registration_id=reg_id).update(active=False)
self.sent = True
self.success = result['success']
self.failure = result['failure']
self.save()
return result
return None
.submit-row a {
&.default {
text-transform: uppercase;
}
&.button {
float: right;
padding: 10px 15px;
line-height: 15px;
margin: 0 0 0 8px;
}
}
.pushnotifications-row a.button {
background-color: #e65c95;
font-size: 13px;
&.default {
background-color: #e62272;
&:hover, &:active {
background-color: #cd2167;
}
}
&:hover, &:active {
background-color: #d25389;
}
}
{% extends "admin/change_form.html" %}
{% load i18n admin_urls static compress %}
{% block extrastyle %}
{{ block.super }}
{% compress css %}<link rel="stylesheet" type="text/x-scss" href="{% static 'admin/pushnotifications/css/forms.scss' %}" />{% endcompress %}
{% endblock %}
{% block submit_buttons_bottom %}
{{ block.super }}
{% if message and not message.sent %}
<div class="submit-row pushnotifications-row">
<a href="{% url 'pushnotifications:admin-send' pk=message.pk %}" class="default button">{% trans "Send message" %}</a>
</div>
{% endif %}
{% endblock %}
from django.conf.urls import url
from . import views
app_name = "pushnotifications"
urlpatterns = [
url(r'admin/send/(?P<pk>\d+)/$', views.admin_send, name='admin-send'),
]
from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.auth.decorators import permission_required
from django.shortcuts import get_object_or_404, redirect
from pushnotifications.models import Message
@staff_member_required
@permission_required('pushnotifications.change_message')
def admin_send(request, pk):
message = get_object_or_404(Message, pk=pk)
message.send()
return redirect('admin:pushnotifications_message_changelist')
......@@ -79,6 +79,7 @@ PASSWORD_HASHERS = [
WIKI_API_KEY = os.environ.get('WIKI_API_KEY', 'changeme')
MIGRATION_KEY = os.environ.get('MIGRATION_KEY')
PUSH_NOTIFICATIONS_API_KEY = os.environ.get('PUSH_NOTIFICATIONS_API_KEY', '')
if os.environ.get('DJANGO_SSLONLY'):
SECURE_SSL_REDIRECT = True
......
......@@ -57,6 +57,7 @@ INSTALLED_APPS = [
'corsheaders',
# Our apps
'thaliawebsite', # include for admin settings
'pushnotifications',
'members',
'documents',
'activemembers',
......@@ -241,6 +242,9 @@ BOARD_NOTIFICATION_ADDRESS = 'info@thalia.nu'
# Partners notification email
PARTNER_EMAIL = "samenwerking@thalia.nu"
# Push notifications API key
PUSH_NOTIFICATIONS_API_KEY = ''
# Photos settings
PHOTO_UPLOAD_SIZE = 1920, 1080
......
......@@ -107,10 +107,12 @@ urlpatterns = [
url(r'^', include('partners.api.urls')),
url(r'^', include('mailinglists.api.urls')),
url(r'^', include('pizzas.api.urls')),
url(r'^', include('pushnotifications.api.urls')),
], namespace='v1')),
])),
url(r'^education/', include('education.urls')),
url(r'^announcements/', include('announcements.urls')),
url(r'^pushnotifications/', include('pushnotifications.urls')),
# Default login helpers
url(r'^login/$', login, {'authentication_form': AuthenticationForm},
name='login'),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment