From a0954a1d25c37a9df67be5bf5ecfd7cc4ccd6722 Mon Sep 17 00:00:00 2001 From: Serghei Mihai Date: Mon, 2 May 2016 15:32:52 +0200 Subject: [PATCH 2/3] api: subscriptions management endpoint (#10794) --- corbo/api_urls.py | 3 +- corbo/api_views.py | 69 +++++++++++++++++++++++++++++ corbo/channels.py | 5 +++ corbo/migrations/0004_auto_20160504_1231.py | 53 ++++++++++++++++++++++ corbo/models.py | 15 ++----- tests/test_api.py | 54 +++++++++++++++++++++- 6 files changed, 185 insertions(+), 14 deletions(-) create mode 100644 corbo/migrations/0004_auto_20160504_1231.py diff --git a/corbo/api_urls.py b/corbo/api_urls.py index cb275e5..6f77fc4 100644 --- a/corbo/api_urls.py +++ b/corbo/api_urls.py @@ -16,8 +16,9 @@ from django.conf.urls import patterns, include, url -from .api_views import NewslettersView +from .api_views import NewslettersView, SubscriptionsView urlpatterns = patterns('', url(r'^newsletters/', NewslettersView.as_view(), name='newsletters'), + url(r'^subscriptions/', SubscriptionsView.as_view(), name='subscriptions'), ) diff --git a/corbo/api_views.py b/corbo/api_views.py index e9db43f..716bf0a 100644 --- a/corbo/api_views.py +++ b/corbo/api_views.py @@ -14,10 +14,17 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import json +from collections import defaultdict + +from django.core.exceptions import PermissionDenied +from django.db.models import Q + from rest_framework.views import APIView from rest_framework.response import Response from .models import Category, Subscription +from .channels import get_channel_display_name, get_channel_choices from .channels import get_channels @@ -30,3 +37,65 @@ class NewslettersView(APIView): 'transports': get_channels()} newsletters.append(newsletter) return Response({'data': newsletters}) + + +class SubscriptionsView(APIView): + + def get_subscriptions(self, email, uuid=None): + subscriptions = defaultdict(dict) + for s in Subscription.objects.filter(Q(identifier='mailto:'+email)|Q(uuid=uuid)): + cat_id = s.category.pk + subscriptions[cat_id]['id'] = str(cat_id) + subscriptions[cat_id]['text'] = s.category.name + transport_id, transport_name = identifier.split(':') + transport = {'id': transport_id, + 'text': get_channel_display_name(transport_id)} + if 'transports' in subscriptions[cat_id]: + subscriptions[cat_id]['transports'].append(transport) + else: + subscriptions[cat_id]['transports'] = [transport] + + return subscriptions.values() + + def update_subscriptions(self, category_id, transports, email, uuid=None): + cat = Category.objects.get(pk=category_id) + available_transports = [channel_id for channel_id, channel_name in get_channel_choices()] + for transport in transports: + uri = '%s:%s' % (transport, email) + s, created = cat.subscription_set.get_or_create(identifier=uri) + if not s.uuid and uuid: + s.uuid = uuid + s.save() + to_remove = set(available_transports) - set(transports) + # unsubscribe categories with empty transports + for to in to_remove: + uri = '%s:%s' % (transport, email) + Subscription.objects.filter(category=cat, identifier=uri).delete() + + def get(self, request): + email = request.GET.get('email') + uuid = request.GET.get('uuid') + if not email: + raise PermissionDenied('Email parameter required') + return Response({'data': self.get_subscriptions(email, uuid)}) + + def post(self, request): + email = request.GET.get('email') + uuid = request.GET.get('uuid') + if not email: + raise PermissionDenied('Email parameter required') + data = json.loads(request.body) + for subscription in data: + self.update_subscriptions(subscription['id'], subscription['transports'], + email, uuid) + return Response({'data': True}) + + def delete(self, request): + email = request.GET.get('email') + uuid = request.GET.get('uuid') + if not email: + raise PermissionDenied('Email parameter required') + + for subscription in self.get_subscriptions(email): + self.update_subscriptions(subscription['id'], [], email, uuid) + return Response({'data': True}) diff --git a/corbo/channels.py b/corbo/channels.py index f63eeb7..788ca37 100644 --- a/corbo/channels.py +++ b/corbo/channels.py @@ -12,6 +12,11 @@ def get_channel_choices(include=[], exclude=[]): def get_channels(): return [{'id': c_id, 'text': unicode(c_name)} for c_id, c_name in get_channel_choices()] +def get_channel_display_name(channel_id): + for identifier, display_name in get_channel_choices(): + if channel_id in identifier: + return unicode(display_name) + class HomepageChannel(object): identifier = 'homepage' diff --git a/corbo/migrations/0004_auto_20160504_1231.py b/corbo/migrations/0004_auto_20160504_1231.py new file mode 100644 index 0000000..79fdcae --- /dev/null +++ b/corbo/migrations/0004_auto_20160504_1231.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('corbo', '0003_auto_20160427_1342'), + ] + + operations = [ + migrations.RemoveField( + model_name='subscriptiontype', + name='subscription', + ), + migrations.DeleteModel( + name='SubscriptionType', + ), + migrations.AddField( + model_name='subscription', + name='identifier', + field=models.CharField(help_text='ex.: mail, mobile, ...', max_length=128, verbose_name='identifier', blank=True), + preserve_default=True, + ), + migrations.AddField( + model_name='subscription', + name='uuid', + field=models.CharField(max_length=128, verbose_name='User identifier', blank=True), + preserve_default=True, + ), + migrations.AlterField( + model_name='broadcast', + name='channel', + field=models.CharField(max_length=32, verbose_name='channel', choices=[(b'homepage', 'Homepage'), (b'mobile', 'SMS'), (b'mail', 'Email')]), + preserve_default=True, + ), + migrations.AlterField( + model_name='subscription', + name='category', + field=models.ForeignKey(verbose_name='Category', to='corbo.Category'), + preserve_default=True, + ), + migrations.AlterUniqueTogether( + name='subscription', + unique_together=set([]), + ), + migrations.RemoveField( + model_name='subscription', + name='user', + ), + ] diff --git a/corbo/models.py b/corbo/models.py index d4b942c..2be2e22 100644 --- a/corbo/models.py +++ b/corbo/models.py @@ -65,15 +65,8 @@ class Broadcast(models.Model): unique_together = ('announce', 'channel') -class SubscriptionType(models.Model): - subscription = models.ForeignKey('Subscription') - identifier = models.CharField(_('identifier'), max_length=128, blank=True, - help_text=_('ex.: email, mobile phone number, jabber id')) - - class Subscription(models.Model): - user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('user'), - blank=True, null=True) - category = models.ForeignKey('Category', verbose_name=_('category')) - class Meta: - unique_together = ('user', 'category') + category = models.ForeignKey('Category', verbose_name=_('Category')) + uuid = models.CharField(_('User identifier'), max_length=128, blank=True) + identifier = models.CharField(_('identifier'), max_length=128, blank=True, + help_text=_('ex.: mail, mobile, ...')) diff --git a/tests/test_api.py b/tests/test_api.py index 56f728d..2dab0c2 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,11 +1,13 @@ import pytest import json +from uuid import uuid4 from django.core.urlresolvers import reverse +from django.utils.http import urlencode -from corbo.models import Category, Announce, Broadcast -from corbo.channels import get_channels +from corbo.models import Category, Announce, Broadcast, Subscription +from corbo.channels import get_channels, get_channel_choices pytestmark = pytest.mark.django_db @@ -43,3 +45,51 @@ def test_get_newsletters(app, categories, announces): assert category['text'] in CATEGORIES assert 'transports' in category assert category['transports'] == get_channels() + + +def test_get_subscriptions_by_email(app, categories, announces): + resp = app.get(reverse('subscriptions'), status=403) + foo = 'foo@example.com' + for identifier, name in get_channel_choices(): + for category in categories: + uri = '%s:%s' % (identifier, foo) + subscription = Subscription.objects.create(identifier=uri, + category=category) + resp = app.get(reverse('subscriptions'), {'email': foo}, status=200) + assert 'data' in resp.json + data = resp.json['data'] + for d in data: + assert d['id'] in [str(category.id) for category in categories] + assert d['text'] in [category.name for category in categories] + for t in d['transports']: + assert t['id'] == identifier + + +def test_update_subscriptions(app, categories, announces): + params = urlencode({'email': 'foo@example.com', + 'uuid': str(uuid4())}) + subscriptions_url = reverse('subscriptions') + '?' + params + for category in categories: + transports = [] + for identifier, name in get_channel_choices(): + transports.append(identifier) + category_id = str(category.id) + subscriptions = [{'id': category_id, + 'text': category.name, + 'transports': transports}] + resp = app.post_json(subscriptions_url , subscriptions) + if resp.json['data']: + resp = app.get(subscriptions_url, status=200) + for cat in resp.json['data']: + if cat['id'] == category_id: + sub_transports = [c['id'] for c in cat['transports']] + assert sub_transports == transports + + +def test_delete_subscriptions(app, categories, announces): + params = urlencode({'email': 'foo@example.com', 'uuid': str(uuid4())}) + subscriptions_url = reverse('subscriptions') + '?' + params + resp = app.delete(subscriptions_url) + if resp.json['data']: + resp = app.get(subscriptions_url, status=200) + assert resp.json['data'] == [] -- 2.8.1