From d0250b73c4ac2687e6e7dc27baeb261ce2f76559 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 | 82 +++++++++++++++++++++++++++++ corbo/forms.py | 26 +-------- corbo/migrations/0004_auto_20160504_1744.py | 55 +++++++++++++++++++ corbo/models.py | 18 ++----- tests/test_api.py | 58 ++++++++++++++++++-- 6 files changed, 201 insertions(+), 41 deletions(-) create mode 100644 corbo/migrations/0004_auto_20160504_1744.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 a1f15be..b168e83 100644 --- a/corbo/api_views.py +++ b/corbo/api_views.py @@ -14,6 +14,13 @@ # 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 django.db import transaction + from rest_framework.views import APIView from rest_framework.response import Response @@ -30,3 +37,78 @@ class NewslettersView(APIView): 'transports': transports} newsletters.append(newsletter) return Response({'data': newsletters}) + + +class SubscriptionsView(APIView): + + def get_subscriptions(self, email, uuid=None): + subscriptions = defaultdict(dict) + identifier = 'mailto:'+email + for s in Subscription.objects.filter(Q(identifier=identifier)|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': transport_id} + if 'transports' in subscriptions[cat_id]: + subscriptions[cat_id]['transports'].append(transport) + else: + subscriptions[cat_id]['transports'] = [transport] + + return subscriptions.values() + + @transaction.atomic + def update_subscriptions(self, category_id, transports, email, uuid=None): + uuid = uuid or u'' + identifier = u'mailto:' + if email: + identifier += email + try: + cat = Category.objects.get(pk=category_id) + except Category.DoesNotExist: + return + + subcriptions = cat.subscription_set + + if email: + if 'mailto' in transports: + subcriptions.get_or_create(identifier=identifier, uuid=uuid) + if uuid: + subcriptions.exclude(uuid=uuid).filter(identifier=identifier).delete() + else: + subcriptions.filter(identifier=identifier).delete() + + if uuid: + if 'homepage' in transports: + subcriptions.get_or_create(uuid=uuid) + else: + subcriptions.filter(identifier='', uuid=uuid).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/forms.py b/corbo/forms.py index e7a3ac8..39775e8 100644 --- a/corbo/forms.py +++ b/corbo/forms.py @@ -5,9 +5,6 @@ from .models import Announce, Category, Broadcast, channel_choices class AnnounceForm(forms.ModelForm): - transport_channel = forms.MultipleChoiceField(required=False, - choices=channel_choices, - widget=forms.CheckboxSelectMultiple()) class Meta: model = Announce @@ -20,32 +17,13 @@ class AnnounceForm(forms.ModelForm): } fields = '__all__' - def __init__(self, *args, **kwargs): - super(AnnounceForm, self).__init__(*args, **kwargs) - self.fields['transport_channel'].initial = [b.channel for \ - b in self.instance.broadcast_set.all()] - - def clean_transport_channel(self): - channels = self.cleaned_data['transport_channel'] - limit = 160 - if 'sms' in channels and \ - len(self.cleaned_data['text']) > limit: - raise forms.ValidationError(_('Announce content exceeds %s chars' - ' and cannot be sent by SMS' % limit)) - return channels - def save(self, *args, **kwargs): instance = super(AnnounceForm, self).save(*args, **kwargs) if instance: - print "CD: %s" % self.cleaned_data - channels = self.cleaned_data['transport_channel'] - for channel in channels: - Broadcast.objects.get_or_create(announce=instance, - channel=channel) - self.instance.broadcast_set.exclude(announce=instance, - channel__in=channels).delete() + Broadcast.objects.get_or_create(announce=instance) return instance + class CategoryForm(forms.ModelForm): class Meta: fields = ('name', ) diff --git a/corbo/migrations/0004_auto_20160504_1744.py b/corbo/migrations/0004_auto_20160504_1744.py new file mode 100644 index 0000000..6d1d846 --- /dev/null +++ b/corbo/migrations/0004_auto_20160504_1744.py @@ -0,0 +1,55 @@ +# -*- 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.: mailto, homepage, ...', 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='subscription', + name='category', + field=models.ForeignKey(verbose_name='Category', to='corbo.Category'), + preserve_default=True, + ), + migrations.AlterUniqueTogether( + name='broadcast', + unique_together=set([]), + ), + migrations.RemoveField( + model_name='broadcast', + name='channel', + ), + migrations.AlterUniqueTogether( + name='subscription', + unique_together=set([('category', 'identifier', 'uuid')]), + ), + migrations.RemoveField( + model_name='subscription', + name='user', + ), + ] diff --git a/corbo/models.py b/corbo/models.py index 1f48e60..d6d5bb6 100644 --- a/corbo/models.py +++ b/corbo/models.py @@ -53,8 +53,6 @@ class Announce(models.Model): class Broadcast(models.Model): announce = models.ForeignKey(Announce, verbose_name=_('announce')) - channel = models.CharField(_('channel'), max_length=32, - choices=channel_choices, blank=False) time = models.DateTimeField(_('sent time'), auto_now_add=True) result = models.TextField(_('result'), blank=True) @@ -65,18 +63,12 @@ class Broadcast(models.Model): class Meta: verbose_name = _('sent') ordering = ('-time',) - 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')) + 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.: mailto, homepage, ...')) class Meta: - unique_together = ('user', 'category') + unique_together = ('category', 'identifier', 'uuid') diff --git a/tests/test_api.py b/tests/test_api.py index d72c51f..bebfd9d 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,10 +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.models import Category, Announce, Broadcast, Subscription +from corbo.models import channel_choices pytestmark = pytest.mark.django_db @@ -24,10 +27,10 @@ def announces(): announces = [] for category in Category.objects.all(): a = Announce.objects.create(category=category, title='By email') - Broadcast.objects.create(announce=a, channel='mailto') + Broadcast.objects.create(announce=a) announces.append(a) a = Announce.objects.create(category=category, title='On homepage') - Broadcast.objects.create(announce=a, channel='homepage') + Broadcast.objects.create(announce=a) announces.append(a) return announces @@ -44,3 +47,52 @@ def test_get_newsletters(app, categories, announces): assert category['transports'] == [{'id': 'mailto', 'text': 'Email'}, {'id': 'homepage', 'text': 'Homepage'} ] + + +def test_get_subscriptions_by_email(app, categories, announces): + resp = app.get(reverse('subscriptions'), status=403) + foo = 'foo@example.com' + for identifier, name in channel_choices[:1]: + 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 channel_choices[:1]: + 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) + print resp.json['data'] + 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