From 8943fc234932a41212ac9cea30bdb4b6b3d8de27 Mon Sep 17 00:00:00 2001 From: Serghei Mihai Date: Mon, 30 Jan 2017 14:38:00 +0100 Subject: [PATCH] import subscribers from csv (#14010) --- corbo/forms.py | 23 +++++++ corbo/manage_urls.py | 4 +- corbo/templates/corbo/category_detail.html | 5 ++ .../templates/corbo/subscriptions_import_form.html | 23 +++++++ corbo/views.py | 32 ++++++++- tests/test_subscribers.py | 75 ++++++++++++++++++++++ 6 files changed, 159 insertions(+), 3 deletions(-) create mode 100644 corbo/templates/corbo/subscriptions_import_form.html create mode 100644 tests/test_subscribers.py diff --git a/corbo/forms.py b/corbo/forms.py index 2fe8010..4ebda46 100644 --- a/corbo/forms.py +++ b/corbo/forms.py @@ -1,7 +1,11 @@ +import csv + from django import forms from django.utils.translation import ugettext_lazy as _ from django.utils.text import slugify from django.core.exceptions import ObjectDoesNotExist +from django.core import validators +from django.core.exceptions import ValidationError from .models import Announce, Category, Broadcast, channel_choices @@ -45,3 +49,22 @@ class CategoryForm(forms.ModelForm): slug = '%s-%s' % (base_slug, i) self.instance.slug = slug return super(CategoryForm, self).save(commit=commit) + + +class SubscriptionsImportForm(forms.Form): + subscribers = forms.FileField(_('Subscribers'), + help_text=_('utf-8 encoded, comma separated file with email addresses on first column')) + + def clean_subscribers(self, *args, **kwargs): + subscribers = [] + reader = csv.reader(self.cleaned_data['subscribers']) + for idx, row in enumerate(reader, 1): + print row + if not row or not row[0]: + continue + try: + validators.validate_email(row[0]) + except ValidationError: + raise ValidationError(_('Invalid email address at line %d' % idx)) + subscribers.append(row[0]) + return subscribers diff --git a/corbo/manage_urls.py b/corbo/manage_urls.py index f4c519b..1bf68ee 100644 --- a/corbo/manage_urls.py +++ b/corbo/manage_urls.py @@ -2,7 +2,7 @@ from django.conf.urls import patterns, include, url from .views import add_announce, edit_announce, delete_announce, \ add_category, edit_category, view_category, delete_category, manage, \ - menu_json + subscriptions_import, menu_json urlpatterns = patterns('', url(r'^$', manage, name='manage'), @@ -20,5 +20,7 @@ urlpatterns = patterns('', name='edit_category'), url(r'^category/delete/(?P[\w-]+)$', delete_category, name='delete_category'), + url(r'^category/(?P[\w-]+)/import-subscriptions/$', subscriptions_import, + name='subscriptions-import'), url(r'^menu.json$', menu_json), ) diff --git a/corbo/templates/corbo/category_detail.html b/corbo/templates/corbo/category_detail.html index df31de9..6c6f836 100644 --- a/corbo/templates/corbo/category_detail.html +++ b/corbo/templates/corbo/category_detail.html @@ -10,6 +10,11 @@ {% block appbar %}

{{ object.name }}

+ + + {% trans 'Delete' %} {% trans 'Edit' %} {% trans 'New announce' %} diff --git a/corbo/templates/corbo/subscriptions_import_form.html b/corbo/templates/corbo/subscriptions_import_form.html new file mode 100644 index 0000000..bda5266 --- /dev/null +++ b/corbo/templates/corbo/subscriptions_import_form.html @@ -0,0 +1,23 @@ +{% extends "corbo/category_detail.html" %} +{% load i18n static %} + +{% block breadcrumb %} +{{ block.super }} +{{ category }} +{% endblock %} + +{% block appbar %} +

{% trans "Import subscriptions" %}

+{% endblock %} + + +{% block content %} +
+ {% csrf_token %} + {{ form.as_p }} +
+ + {% trans "Cancel" %} +
+
+{% endblock %} diff --git a/corbo/views.py b/corbo/views.py index 6acd03d..1a30e1d 100644 --- a/corbo/views.py +++ b/corbo/views.py @@ -6,7 +6,7 @@ from django.core import signing from django.utils import timezone from django.core.urlresolvers import reverse from django.views.generic import CreateView, UpdateView, DeleteView, \ - ListView, TemplateView, RedirectView, DetailView + ListView, TemplateView, RedirectView, DetailView, FormView from django.contrib.syndication.views import Feed from django.shortcuts import resolve_url from django.utils.encoding import force_text @@ -14,10 +14,12 @@ from django.utils.feedgenerator import Atom1Feed as DjangoAtom1Feed from django.http import HttpResponseRedirect, HttpResponse, Http404 from django.contrib.auth import logout as auth_logout from django.contrib.auth import views as auth_views +from django.contrib import messages from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ngettext import models -from .forms import AnnounceForm, CategoryForm +from .forms import AnnounceForm, CategoryForm, SubscriptionsImportForm try: from mellon.utils import get_idps @@ -222,6 +224,32 @@ class AtomView(Feed): atom = AtomView() +class SubscriptionsImportView(FormView): + form_class = SubscriptionsImportForm + template_name = 'corbo/subscriptions_import_form.html' + + def get_context_data(self, **kwargs): + context = super(SubscriptionsImportView, self).get_context_data(**kwargs) + context['category'] = models.Category.objects.get(slug=self.kwargs['slug']) + return context + + def get_success_url(self): + category = models.Category.objects.get(slug=self.kwargs['slug']) + return reverse('view_category', kwargs={'slug': category.slug}) + + def form_valid(self, form): + new = 0 + c = models.Category.objects.get(slug=self.kwargs['slug']) + for email in form.cleaned_data['subscribers']: + obj, created = models.Subscription.objects.get_or_create(category=c, identifier='mailto:%s' % email) + if created: + new += 1 + messages.info(self.request, ngettext('%(new)d subscribers added', '%(new)d subscribers added', new) % {'new': new}) + return super(SubscriptionsImportView, self).form_valid(form) + +subscriptions_import = SubscriptionsImportView.as_view() + + def menu_json(request): label = _('Announces') json_str = json.dumps([{'label': force_text(label), diff --git a/tests/test_subscribers.py b/tests/test_subscribers.py new file mode 100644 index 0000000..4c3d611 --- /dev/null +++ b/tests/test_subscribers.py @@ -0,0 +1,75 @@ +import pytest +from webtest import TestApp, Upload + +from django.core.wsgi import get_wsgi_application +from django.utils.text import slugify +from django.core.urlresolvers import reverse +from django.contrib.auth import get_user_model + +from corbo.models import Category, Subscription + +pytestmark = pytest.mark.django_db + +CATEGORIES = (u'Alerts',) + +CSV_CONTENT = """foo@example.net, Foo, +john.doe@example.net, John Doe, +bar@localhost, Bar, +""" + + +@pytest.fixture +def categories(): + categories = [] + for category in CATEGORIES: + c, created = Category.objects.get_or_create(name=category, slug=slugify(category)) + categories.append(c) + return categories + +@pytest.fixture +def admin(): + User = get_user_model() + admin = User.objects.create_superuser(username='admin', + password='password', email='admin@example.net') + return admin + +def login(app, username='admin', password='password'): + login_page = app.get('/login/') + login_form = login_page.forms[0] + login_form['username'] = username + login_form['password'] = password + resp = login_form.submit() + assert resp.status_int == 302 + return app + +def test_subscribe_from_csv(admin, categories): + app = login(TestApp(get_wsgi_application())) + for c in categories: + page = app.get(reverse('subscriptions-import', kwargs={'slug': c.slug})) + form = page.form + form['subscribers'] = Upload('users.csv', CSV_CONTENT) + res = form.submit() + assert res.status_code == 302 + assert Subscription.objects.filter(category=c).count() == len(CSV_CONTENT.splitlines()) + +def test_subscribe_from_csv_with_empty_lines(admin, categories): + app = login(TestApp(get_wsgi_application())) + content = CSV_CONTENT + '\n\n\n' + for c in categories: + page = app.get(reverse('subscriptions-import', kwargs={'slug': c.slug})) + form = page.form + form['subscribers'] = Upload('users.csv', content) + res = form.submit() + assert res.status_code == 302 + assert Subscription.objects.filter(category=c).count() == len(CSV_CONTENT.splitlines()) + +def test_subscribe_with_invalid_email(admin, categories): + app = login(TestApp(get_wsgi_application())) + content = CSV_CONTENT + '\nwrong, Wrong user,' + for category in categories: + page = app.get(reverse('subscriptions-import', kwargs={'slug': category.slug})) + form = page.form + form['subscribers'] = Upload('users.csv', content) + page = form.submit() + assert page.status_code == 200 + page.mustcontain('Invalid email address at line') -- 2.11.0