commit 8759c981d456f9340c58f4f1bf036044cb754897
Author: Serghei Mihai <smihai@entrouvert.com>
Date:   Mon May 2 15:32:52 2016 +0200

    api: subscriptions management endpoint (#10794)

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 <http://www.gnu.org/licenses/>.
 
+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'] == []
