From 673b250e73a9121debfd81fba749e7dea1a2a154 Mon Sep 17 00:00:00 2001 From: Serghei Mihai Date: Mon, 2 May 2016 15:32:52 +0200 Subject: [PATCH 2/2] api: subscriptions management endpoint (#10794) --- corbo/api_urls.py | 3 +- corbo/api_views.py | 66 +++++++++++++++++++++++++++++ corbo/channels.py | 5 +++ corbo/migrations/0004_auto_20160502_1706.py | 42 ++++++++++++++++++ corbo/models.py | 10 ++++- tests/test_api.py | 49 ++++++++++++++++++++- 6 files changed, 172 insertions(+), 3 deletions(-) create mode 100644 corbo/migrations/0004_auto_20160502_1706.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 11a6cc4..a6e89d2 100644 --- a/corbo/api_views.py +++ b/corbo/api_views.py @@ -14,10 +14,18 @@ # 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.utils.translation import ugettext_lazy as _ from django.views.generic.base import View +from django.core.exceptions import PermissionDenied +from django.views.decorators.csrf import csrf_exempt from .models import Category, Subscription from .utils import to_json, get_channels +from .channels import get_channel_display_name, get_channel_choices + class NewslettersView(View): @@ -29,3 +37,61 @@ class NewslettersView(View): 'transports': get_channels()} newsletters.append(newsletter) return newsletters + + +class SubscriptionsView(View): + + @csrf_exempt + def dispatch(self, request, *args, **kwargs): + return super(SubscriptionsView, self).dispatch(request, *args, **kwargs) + + def get_subscriptions(self, email): + subscriptions = defaultdict(dict) + for s in Subscription.objects.filter(identifier=email): + cat_id = s.category.pk + subscriptions[cat_id]['id'] = str(cat_id) + subscriptions[cat_id]['text'] = s.category.name + transport = {'id': s.channel, 'text': get_channel_display_name(s.channel)} + 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): + cat = Category.objects.get(pk=category_id) + available_transports = [channel_id for channel_id, channel_name in get_channel_choices()] + for transport in transports: + s, created = cat.subscription_set.get_or_create(channel=transport, + identifier=email) + to_remove = set(available_transports) - set(transports) + # unsubscribe categories with empty transports + cat.subscription_set.filter(identifier=email, channel__in=to_remove).delete() + + @to_json('api') + def get(self, request): + email = request.GET.get('email') + if not email: + raise PermissionDenied('Email parameter required') + return self.get_subscriptions(email) + + @to_json('api') + def post(self, request): + email = request.GET.get('email') + 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) + return True + + @to_json('api') + def delete(self, request): + email = request.GET.get('email') + if not email: + raise PermissionDenied('Email parameter required') + + for subscription in self.get_subscriptions(email): + self.update_subscriptions(subscription['id'], [], email) + return True diff --git a/corbo/channels.py b/corbo/channels.py index 19a30f3..eb45e65 100644 --- a/corbo/channels.py +++ b/corbo/channels.py @@ -9,6 +9,11 @@ def get_channel_choices(include=[], exclude=[]): for identifier, display_name in channel.get_choices(): yield (identifier, display_name) +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_20160502_1706.py b/corbo/migrations/0004_auto_20160502_1706.py new file mode 100644 index 0000000..50546d3 --- /dev/null +++ b/corbo/migrations/0004_auto_20160502_1706.py @@ -0,0 +1,42 @@ +# -*- 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.AddField( + model_name='subscription', + name='channel', + field=models.CharField(default='', max_length=32, verbose_name='channel', choices=[(b'homepage', 'Homepage'), (b'sms', 'SMS'), (b'email', 'Email')]), + preserve_default=False, + ), + migrations.AddField( + model_name='subscription', + name='identifier', + field=models.CharField(help_text='ex.: email, mobile phone number, jabber id', max_length=128, verbose_name='identifier', blank=True), + preserve_default=True, + ), + migrations.AddField( + model_name='subscriptiontype', + name='category', + field=models.ForeignKey(default='', to='corbo.Category'), + preserve_default=False, + ), + migrations.AddField( + model_name='subscriptiontype', + name='channel', + field=models.CharField(default='', max_length=32, verbose_name='channel', choices=[(b'homepage', 'Homepage'), (b'sms', 'SMS'), (b'email', 'Email')]), + preserve_default=False, + ), + migrations.AlterUniqueTogether( + name='subscription', + unique_together=set([('channel', 'category', 'identifier')]), + ), + ] diff --git a/corbo/models.py b/corbo/models.py index d4b942c..799618a 100644 --- a/corbo/models.py +++ b/corbo/models.py @@ -66,7 +66,10 @@ class Broadcast(models.Model): class SubscriptionType(models.Model): + category = models.ForeignKey('Category') subscription = models.ForeignKey('Subscription') + channel = models.CharField(_('channel'), max_length=32, + choices=channels.get_channel_choices(), blank=False) identifier = models.CharField(_('identifier'), max_length=128, blank=True, help_text=_('ex.: email, mobile phone number, jabber id')) @@ -75,5 +78,10 @@ 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')) + channel = models.CharField(_('channel'), max_length=32, + choices=channels.get_channel_choices(), blank=False) + identifier = models.CharField(_('identifier'), max_length=128, blank=True, + help_text=_('ex.: email, mobile phone number, jabber id')) + class Meta: - unique_together = ('user', 'category') + unique_together = ('channel', 'category', 'identifier') diff --git a/tests/test_api.py b/tests/test_api.py index 4995c88..bd73a31 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -3,8 +3,9 @@ import json from django.core.urlresolvers import reverse +from django.utils.http import urlencode -from corbo.models import Category, Announce, Broadcast +from corbo.models import Category, Announce, Broadcast, Subscription from corbo.utils import get_channels pytestmark = pytest.mark.django_db @@ -43,3 +44,49 @@ 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_(app, categories, announces): + resp = app.get(reverse('subscriptions'), status=403) + assert resp.json['err_desc'] == 'Email parameter required' + foo = 'foo@example.com' + for category in categories: + subscription = Subscription.objects.create(channel='email', + category=category, identifier=foo) + 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'] == 'email' + + +def test_update_subscriptions(app, categories, announces): + params = urlencode({'email': 'foo@example.com'}) + subscriptions_url = reverse('subscriptions') + '?' + params + for category in categories: + transports = [] + for transport in get_channels(): + transports.append(transport['id']) + 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'}) + 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