0001-misc-remove-newsletter-app-53541.patch
combo/apps/newsletters/README | ||
---|---|---|
1 |
Combo newsletters cell |
|
2 |
====================== |
|
3 | ||
4 |
This cell is enabled by default. |
|
5 | ||
6 |
It expects a webservice returning newsletters and user subscriptions in the |
|
7 |
following format: |
|
8 | ||
9 |
[{'id': '1', 'text': 'Democratie locale', |
|
10 |
'transports': [{'id': 'mail', 'text': 'mail'}]}, |
|
11 |
{'id': '2', 'text': 'Rencontres de quartiers', |
|
12 |
'transports': [{'id': 'mail', 'text': 'mail'}]}, |
|
13 |
{'id': '3', 'text': 'Environnement', |
|
14 |
'transports': [{'id': 'mail', 'text': 'mail'}, |
|
15 |
{'id': 'sms', 'text': 'sms'}, |
|
16 |
{'id': 'rss', 'text': 'rss'}]}, |
|
17 |
{'id': '4', 'text': u'Marchés publics', |
|
18 |
'transports': [{'id': 'mail', 'text': 'mail'}, |
|
19 |
{'id': 'rss', 'text': 'rss'}]}, |
|
20 |
{'id': '5', 'text': "Offres d'emploi", |
|
21 |
'transports': [{'id': 'mail', 'text': 'mail'}, |
|
22 |
{'id': 'rss', 'text': 'rss'}]}, |
|
23 |
{'id': '6', 'text': 'Infos créche', |
|
24 |
'transports': [{'id': 'sms', 'text': 'sms'}, |
|
25 |
{'id': 'rss', 'text': 'rss'}]}, |
|
26 |
{'id': '7', 'text': 'Familles', |
|
27 |
'transports': [{'id': 'mail', 'text': 'mail'}, |
|
28 |
{'id': 'sms', 'text': 'sms'}]}, |
|
29 |
{'id': '8', 'text': 'Travaux', |
|
30 |
'transports': [{'id': 'mail', 'text': 'mail'}, |
|
31 |
{'id': 'sms', 'text': 'sms'}, |
|
32 |
{'id': 'rss', 'text': 'rss'}]}] |
|
33 | ||
34 | ||
35 |
The url to the webservice should be provided in the instatiation form. The |
|
36 |
fields **resources_restrictions** and **transports_restrictions** allow to |
|
37 |
filter the newsletters by their name and transport means. |
|
38 | ||
39 |
**resources_restrictions** field is a comma separated list of newsletters |
|
40 |
slugs. For example: __rencontres-de-quartiers,infos-creche__. |
|
41 | ||
42 |
In this case only the following newsletters will be exposed in the |
|
43 |
subscriptions form: |
|
44 |
[{'id': '2', 'text': 'Rencontres de quartiers', |
|
45 |
'transports': [{'id': 'mail', 'text': 'mail'}]}, |
|
46 |
{'id': '6', 'text': 'Infos créche', |
|
47 |
'transports': [{'id': 'sms', 'text': 'sms'}, |
|
48 |
{'id': 'rss', 'text': 'rss'}]}] |
|
49 | ||
50 |
**transport_restrictions** field is a comma separated list of transport types. |
|
51 |
Example: __sms,rss__ |
|
52 | ||
53 |
In this case only the newsletters containing one of these transports will be |
|
54 |
shown: |
|
55 | ||
56 |
[{'id': '3', 'text': 'Environnement', |
|
57 |
'transports': [{'id': 'mail', 'text': 'mail'}, |
|
58 |
{'id': 'sms', 'text': 'sms'}, |
|
59 |
{'id': 'rss', 'text': 'rss'}]}, |
|
60 |
{'id': '4', 'text': u'Marchés publics', |
|
61 |
'transports': [{'id': 'mail', 'text': 'mail'}, |
|
62 |
{'id': 'rss', 'text': 'rss'}]}, |
|
63 |
{'id': '5', 'text': "Offres d'emploi", |
|
64 |
'transports': [{'id': 'mail', 'text': 'mail'}, |
|
65 |
{'id': 'rss', 'text': 'rss'}]}, |
|
66 |
{'id': '6', 'text': 'Infos créche', |
|
67 |
'transports': [{'id': 'sms', 'text': 'sms'}, |
|
68 |
{'id': 'rss', 'text': 'rss'}]}, |
|
69 |
{'id': '7', 'text': 'Familles', |
|
70 |
'transports': [{'id': 'mail', 'text': 'mail'}, |
|
71 |
{'id': 'sms', 'text': 'sms'}]}, |
|
72 |
{'id': '8', 'text': 'Travaux', |
|
73 |
'transports': [{'id': 'mail', 'text': 'mail'}, |
|
74 |
{'id': 'sms', 'text': 'sms'}, |
|
75 |
{'id': 'rss', 'text': 'rss'}]}] |
combo/apps/newsletters/__init__.py | ||
---|---|---|
1 |
# combo - content management system |
|
2 |
# Copyright (C) 2015 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
import django.apps |
|
18 |
from django.utils.translation import ugettext_lazy as _ |
|
19 | ||
20 | ||
21 |
class AppConfig(django.apps.AppConfig): |
|
22 |
name = 'combo.apps.newsletters' |
|
23 |
verbose_name = _('Newsletters') |
|
24 | ||
25 |
def get_before_urls(self): |
|
26 |
from . import urls |
|
27 | ||
28 |
return urls.urlpatterns |
|
29 | ||
30 | ||
31 |
default_app_config = 'combo.apps.newsletters.AppConfig' |
combo/apps/newsletters/forms.py | ||
---|---|---|
1 |
# combo - content management system |
|
2 |
# Copyright (C) 2015 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
import logging |
|
18 | ||
19 |
from django import forms |
|
20 |
from django.utils.translation import ugettext_lazy as _ |
|
21 | ||
22 | ||
23 |
class NewslettersManageForm(forms.Form): |
|
24 |
def __init__(self, *args, **kwargs): |
|
25 |
logger = logging.getLogger(__name__) |
|
26 |
self.request = kwargs.pop('request') |
|
27 |
self.user = self.request.user |
|
28 |
self.instance = kwargs.pop('instance') |
|
29 |
self.themes = set() |
|
30 |
super(NewslettersManageForm, self).__init__(*args, **kwargs) |
|
31 | ||
32 |
# initialize cleane data in order to be able to add errors |
|
33 |
self.cleaned_data = {} |
|
34 |
try: |
|
35 |
newsletters = self.instance.get_newsletters() |
|
36 |
except Exception as e: |
|
37 |
self.add_error(None, _('An error occured while getting newsletters. Please try later.')) |
|
38 |
logger.error('Error occured while getting newsletters: %r', e) |
|
39 |
return |
|
40 |
self.params = {} |
|
41 |
user_name_id = self.user.get_name_id() |
|
42 |
if user_name_id: |
|
43 |
self.params['uuid'] = user_name_id |
|
44 | ||
45 |
# get mobile number from mellon session as it is not user attribute |
|
46 |
if self.request.session.get('mellon_session'): |
|
47 |
self.params['mobile'] = self.request.session['mellon_session'].get('mobile', '') |
|
48 |
try: |
|
49 |
subscriptions = self.instance.get_subscriptions(self.user, **self.params) |
|
50 |
except Exception as e: |
|
51 |
self.add_error(None, _('An error occured while getting subscriptions. Please try later.')) |
|
52 |
logger.error('Error occured while getting subscriptions: %r', e) |
|
53 |
return |
|
54 | ||
55 |
for newsletter in newsletters: |
|
56 |
if not self.instance.check_resource(newsletter['text']): |
|
57 |
continue |
|
58 |
choices = [] |
|
59 |
initial = [] |
|
60 |
for transport in newsletter['transports']: |
|
61 |
if not self.instance.check_transport(transport['id']): |
|
62 |
continue |
|
63 |
self.themes.add((transport['id'], transport['text'])) |
|
64 |
choices.append((transport['id'], '')) |
|
65 |
if transport in newsletter['transports']: |
|
66 |
for subscription in subscriptions: |
|
67 |
if subscription['id'] == newsletter['id']: |
|
68 |
initial = [t['id'] for t in subscription['transports']] |
|
69 |
self.fields[newsletter['id']] = forms.MultipleChoiceField( |
|
70 |
label=newsletter['text'], |
|
71 |
help_text=transport['id'], |
|
72 |
choices=choices, |
|
73 |
initial=initial, |
|
74 |
widget=forms.CheckboxSelectMultiple(), |
|
75 |
required=False, |
|
76 |
) |
|
77 | ||
78 |
def save(self): |
|
79 |
self.full_clean() |
|
80 |
subscriptions = [] |
|
81 |
for key, value in self.cleaned_data.items(): |
|
82 |
subscriptions.append({'id': key, 'transports': value}) |
|
83 |
self.instance.set_subscriptions(subscriptions, self.user, **self.params) |
combo/apps/newsletters/migrations/0001_initial.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
from __future__ import unicode_literals |
|
3 | ||
4 |
from django.db import migrations, models |
|
5 | ||
6 | ||
7 |
class Migration(migrations.Migration): |
|
8 | ||
9 |
dependencies = [ |
|
10 |
('auth', '0001_initial'), |
|
11 |
('data', '0013_parameterscell'), |
|
12 |
] |
|
13 | ||
14 |
operations = [ |
|
15 |
migrations.CreateModel( |
|
16 |
name='NewslettersCell', |
|
17 |
fields=[ |
|
18 |
( |
|
19 |
'id', |
|
20 |
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True), |
|
21 |
), |
|
22 |
('placeholder', models.CharField(max_length=20)), |
|
23 |
('order', models.PositiveIntegerField()), |
|
24 |
('slug', models.SlugField(verbose_name='Slug', blank=True)), |
|
25 |
('public', models.BooleanField(default=True, verbose_name='Public')), |
|
26 |
( |
|
27 |
'restricted_to_unlogged', |
|
28 |
models.BooleanField(default=False, verbose_name='Restrict to unlogged users'), |
|
29 |
), |
|
30 |
('title', models.CharField(max_length=128, verbose_name='Title')), |
|
31 |
('url', models.URLField(max_length=128, verbose_name='Newsletters service url')), |
|
32 |
( |
|
33 |
'resources_restrictions', |
|
34 |
models.CharField( |
|
35 |
help_text='list of resources(themes) separated by commas', |
|
36 |
max_length=1024, |
|
37 |
verbose_name='resources restrictions', |
|
38 |
blank=True, |
|
39 |
), |
|
40 |
), |
|
41 |
( |
|
42 |
'transports_restrictions', |
|
43 |
models.CharField( |
|
44 |
help_text='list of transports separated by commas', |
|
45 |
max_length=1024, |
|
46 |
verbose_name='transports restrictions', |
|
47 |
blank=True, |
|
48 |
), |
|
49 |
), |
|
50 |
('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)), |
|
51 |
('page', models.ForeignKey(to='data.Page', on_delete=models.CASCADE)), |
|
52 |
], |
|
53 |
options={ |
|
54 |
'verbose_name': 'Newsletters', |
|
55 |
}, |
|
56 |
bases=(models.Model,), |
|
57 |
), |
|
58 |
] |
combo/apps/newsletters/migrations/0002_newsletterscell_extra_css_class.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
from __future__ import unicode_literals |
|
3 | ||
4 |
from django.db import migrations, models |
|
5 | ||
6 | ||
7 |
class Migration(migrations.Migration): |
|
8 | ||
9 |
dependencies = [ |
|
10 |
('newsletters', '0001_initial'), |
|
11 |
] |
|
12 | ||
13 |
operations = [ |
|
14 |
migrations.AddField( |
|
15 |
model_name='newsletterscell', |
|
16 |
name='extra_css_class', |
|
17 |
field=models.CharField(max_length=100, verbose_name='Extra classes for CSS styling', blank=True), |
|
18 |
), |
|
19 |
] |
combo/apps/newsletters/migrations/0003_newsletterscell_last_update_timestamp.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
from __future__ import unicode_literals |
|
3 | ||
4 |
import datetime |
|
5 | ||
6 |
from django.db import migrations, models |
|
7 |
from django.utils.timezone import utc |
|
8 | ||
9 | ||
10 |
class Migration(migrations.Migration): |
|
11 | ||
12 |
dependencies = [ |
|
13 |
('newsletters', '0002_newsletterscell_extra_css_class'), |
|
14 |
] |
|
15 | ||
16 |
operations = [ |
|
17 |
migrations.AddField( |
|
18 |
model_name='newsletterscell', |
|
19 |
name='last_update_timestamp', |
|
20 |
field=models.DateTimeField(default=datetime.datetime.now(utc), auto_now=True), |
|
21 |
preserve_default=False, |
|
22 |
), |
|
23 |
] |
combo/apps/newsletters/models.py | ||
---|---|---|
1 |
# combo - content management system |
|
2 |
# Copyright (C) 2015 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
import json |
|
18 |
import logging |
|
19 | ||
20 |
from django.conf import settings |
|
21 |
from django.db import models |
|
22 |
from django.forms import models as model_forms |
|
23 |
from django.template.defaultfilters import slugify |
|
24 |
from django.utils.http import urlencode |
|
25 |
from django.utils.translation import ugettext_lazy as _ |
|
26 |
from requests.exceptions import HTTPError, RequestException |
|
27 | ||
28 |
from combo.data.library import register_cell_class |
|
29 |
from combo.data.models import CellBase |
|
30 |
from combo.utils import requests |
|
31 | ||
32 |
from .forms import NewslettersManageForm |
|
33 | ||
34 | ||
35 |
class SubscriptionsSaveError(Exception): |
|
36 |
pass |
|
37 | ||
38 | ||
39 |
@register_cell_class |
|
40 |
class NewslettersCell(CellBase): |
|
41 |
title = models.CharField(verbose_name=_('Title'), max_length=128) |
|
42 |
url = models.URLField(verbose_name=_('Newsletters service url'), max_length=128) |
|
43 |
resources_restrictions = models.CharField( |
|
44 |
_('resources restrictions'), |
|
45 |
blank=True, |
|
46 |
max_length=1024, |
|
47 |
help_text=_('list of resources(themes) separated by commas'), |
|
48 |
) |
|
49 |
transports_restrictions = models.CharField( |
|
50 |
_('transports restrictions'), |
|
51 |
blank=True, |
|
52 |
max_length=1024, |
|
53 |
help_text=_('list of transports separated by commas'), |
|
54 |
) |
|
55 | ||
56 |
template_name = 'newsletters/newsletters.html' |
|
57 |
user_dependant = True |
|
58 | ||
59 |
@classmethod |
|
60 |
def is_enabled(cls): |
|
61 |
return settings.NEWSLETTERS_CELL_ENABLED |
|
62 | ||
63 |
class Meta: |
|
64 |
verbose_name = _('Newsletters') |
|
65 | ||
66 |
def get_default_form_class(self): |
|
67 |
model_fields = ('title', 'url', 'resources_restrictions', 'transports_restrictions') |
|
68 |
return model_forms.modelform_factory(self.__class__, fields=model_fields) |
|
69 | ||
70 |
def simplify(self, name): |
|
71 |
return slugify(name.strip()) |
|
72 | ||
73 |
def get_resources_restrictions(self): |
|
74 |
return list(filter(None, map(self.simplify, self.resources_restrictions.strip().split(',')))) |
|
75 | ||
76 |
def get_transports_restrictions(self): |
|
77 |
return list(filter(None, map(self.simplify, self.transports_restrictions.strip().split(',')))) |
|
78 | ||
79 |
def check_resource(self, resource): |
|
80 |
restrictions = self.get_resources_restrictions() |
|
81 |
if restrictions and self.simplify(resource) not in restrictions: |
|
82 |
return False |
|
83 |
return True |
|
84 | ||
85 |
def check_transport(self, transport): |
|
86 |
restrictions = self.get_transports_restrictions() |
|
87 |
if restrictions and transport not in restrictions: |
|
88 |
return False |
|
89 |
return True |
|
90 | ||
91 |
def filter_data(self, data): |
|
92 |
filtered = [] |
|
93 |
for item in data: |
|
94 |
if not self.check_resource(item['text']): |
|
95 |
continue |
|
96 |
for t in item['transports']: |
|
97 |
if self.check_transport(t['id']): |
|
98 |
filtered.append(item) |
|
99 |
return filtered |
|
100 | ||
101 |
def get_newsletters(self): |
|
102 |
endpoint = self.url + 'newsletters/' |
|
103 |
try: |
|
104 |
response = requests.get(endpoint, remote_service='auto', cache_duration=60, without_user=True) |
|
105 |
response.raise_for_status() |
|
106 |
except RequestException: |
|
107 |
return [] |
|
108 |
json_response = response.json() |
|
109 |
return self.filter_data(json_response['data']) |
|
110 | ||
111 |
def get_subscriptions(self, user, **kwargs): |
|
112 |
endpoint = self.url + 'subscriptions/' |
|
113 |
try: |
|
114 |
response = requests.get( |
|
115 |
endpoint, remote_service='auto', user=user, cache_duration=0, params=kwargs |
|
116 |
) |
|
117 |
response.raise_for_status() |
|
118 |
except RequestException: |
|
119 |
return [] |
|
120 |
json_response = response.json() |
|
121 |
return self.filter_data(json_response['data']) |
|
122 | ||
123 |
def set_subscriptions(self, subscriptions, user, **kwargs): |
|
124 |
logger = logging.getLogger(__name__) |
|
125 |
# uuid is mandatory to store subscriptions |
|
126 |
if 'uuid' not in kwargs: |
|
127 |
raise SubscriptionsSaveError |
|
128 |
headers = {'Content-type': 'application/json', 'Accept': 'application/json'} |
|
129 |
endpoint = self.url + 'subscriptions/' |
|
130 |
try: |
|
131 |
response = requests.post( |
|
132 |
endpoint, |
|
133 |
remote_service='auto', |
|
134 |
data=json.dumps(subscriptions), |
|
135 |
user=user, |
|
136 |
federation_key='email', |
|
137 |
params=kwargs, |
|
138 |
headers=headers, |
|
139 |
) |
|
140 |
response.raise_for_status() |
|
141 |
if not response.json()['data']: |
|
142 |
raise SubscriptionsSaveError |
|
143 |
except HTTPError as e: |
|
144 |
logger.error( |
|
145 |
u'set subscriptions on %s returned an HTTP error code: %s', |
|
146 |
e.response.request.url, |
|
147 |
e.response.status_code, |
|
148 |
) |
|
149 |
raise SubscriptionsSaveError |
|
150 |
except RequestException as e: |
|
151 |
logger.error(u'set subscriptions on %s failed with exception: %s', endpoint, e) |
|
152 |
raise SubscriptionsSaveError |
|
153 | ||
154 |
def render(self, context): |
|
155 |
user = context.get('user') |
|
156 |
if user and user.is_authenticated: |
|
157 |
form = NewslettersManageForm(instance=self, request=context['request']) |
|
158 |
context['form'] = form |
|
159 |
return super(NewslettersCell, self).render(context) |
|
160 | ||
161 |
def is_visible(self, **kwargs): |
|
162 |
user = kwargs.get('user') |
|
163 |
if user is None or not user.is_authenticated: |
|
164 |
return False |
|
165 |
return super(NewslettersCell, self).is_visible(**kwargs) |
combo/apps/newsletters/templates/newsletters/newsletters.html | ||
---|---|---|
1 |
{% load i18n %} |
|
2 |
<h2>{{ cell.title }}</h2> |
|
3 |
{% if form %} |
|
4 |
{% if form.non_field_errors %} |
|
5 |
{{ form.non_field_errors }} |
|
6 |
{% else %} |
|
7 |
<form method="post" action={% url 'newsletters-update' pk=cell.pk %}> |
|
8 |
{% csrf_token %} |
|
9 | ||
10 |
{% for field in form %} |
|
11 |
{{ field.error }} |
|
12 |
{% endfor %} |
|
13 |
<table class="newsletters-form"> |
|
14 |
<thead> |
|
15 |
<tr> |
|
16 |
<td>{% trans "Theme" %}</td> |
|
17 |
{% for id, theme in form.themes %} |
|
18 |
<td data-id="{{ id }}">{{ theme }}</td> |
|
19 |
{% endfor %} |
|
20 |
</tr> |
|
21 |
</thead> |
|
22 |
<tbody> |
|
23 |
{% for field in form %} |
|
24 |
<tr> |
|
25 |
<td>{{ field.label }}</td> |
|
26 |
{% for id, theme in form.themes %}<td data-id="{{ id }}"> |
|
27 |
{% for w in field %} |
|
28 |
{% with choice_value=w.data.value choice_value18=w.choice_value %} |
|
29 |
{% if choice_value == id or choice_value18 == id %}{{ w }}{% endif %} |
|
30 |
{% endwith %} |
|
31 |
{% endfor %} |
|
32 |
</td> |
|
33 |
{% endfor %} |
|
34 |
</tr> |
|
35 |
{% endfor %} |
|
36 |
</tbody> |
|
37 |
</table> |
|
38 |
<button>{% trans "Modify" %}</button> |
|
39 |
</form> |
|
40 |
{% endif %} |
|
41 |
{% else %} |
|
42 |
<div>{% trans "No subscriptions" %}</div> |
|
43 |
{% endif %} |
combo/apps/newsletters/urls.py | ||
---|---|---|
1 |
from django.conf.urls import url |
|
2 |
from django.contrib.auth.decorators import login_required |
|
3 | ||
4 |
from .views import NewslettersView |
|
5 | ||
6 |
urlpatterns = [ |
|
7 |
url( |
|
8 |
r'^newsletters/(?P<pk>\w+)/update$', |
|
9 |
login_required(NewslettersView.as_view()), |
|
10 |
name='newsletters-update', |
|
11 |
), |
|
12 |
] |
combo/apps/newsletters/views.py | ||
---|---|---|
1 |
# combo - content management system |
|
2 |
# Copyright (C) 2015 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
from django.contrib import messages |
|
18 |
from django.http import HttpResponseRedirect |
|
19 |
from django.utils.translation import ugettext_lazy as _ |
|
20 |
from django.views.generic import FormView |
|
21 | ||
22 |
from .forms import NewslettersManageForm |
|
23 |
from .models import NewslettersCell, SubscriptionsSaveError |
|
24 | ||
25 | ||
26 |
class NewslettersView(FormView): |
|
27 |
http_method_names = ['post'] |
|
28 |
form_class = NewslettersManageForm |
|
29 | ||
30 |
def form_valid(self, form): |
|
31 |
try: |
|
32 |
form.save() |
|
33 |
messages.info(self.request, _('Your subscriptions are successfully saved')) |
|
34 |
except SubscriptionsSaveError: |
|
35 |
messages.error( |
|
36 |
self.request, _('An error occured while saving your subscriptions. Please try later.') |
|
37 |
) |
|
38 |
return super(NewslettersView, self).form_valid(form) |
|
39 | ||
40 |
def get_form_kwargs(self): |
|
41 |
kwargs = super(NewslettersView, self).get_form_kwargs() |
|
42 |
self.instance = NewslettersCell.objects.get(pk=self.kwargs['pk']) |
|
43 |
kwargs.update({'request': self.request, 'instance': self.instance}) |
|
44 |
return kwargs |
|
45 | ||
46 |
def form_invalid(self, form): |
|
47 |
messages.error(self.request, _('An error occured while saving your subscriptions. Please try later.')) |
|
48 |
return HttpResponseRedirect(self.get_success_url()) |
|
49 | ||
50 |
def get_success_url(self): |
|
51 |
return self.instance.page.get_online_url() |
combo/settings.py | ||
---|---|---|
70 | 70 |
'combo.apps.family', |
71 | 71 |
'combo.apps.dataviz', |
72 | 72 |
'combo.apps.lingo', |
73 |
'combo.apps.newsletters', |
|
74 | 73 |
'combo.apps.fargo', |
75 | 74 |
'combo.apps.notifications', |
76 | 75 |
'combo.apps.search', |
... | ... | |
363 | 362 |
# hide work-in-progress/experimental/broken/legacy/whatever cells for now |
364 | 363 |
BOOKING_CALENDAR_CELL_ENABLED = False |
365 | 364 |
LEGACY_CHART_CELL_ENABLED = False |
366 |
NEWSLETTERS_CELL_ENABLED = False |
|
367 | 365 | |
368 | 366 | |
369 | 367 |
def debug_show_toolbar(request): |
tests/settings.py | ||
---|---|---|
82 | 82 | |
83 | 83 |
BOOKING_CALENDAR_CELL_ENABLED = True |
84 | 84 |
LEGACY_CHART_CELL_ENABLED = True |
85 |
NEWSLETTERS_CELL_ENABLED = True |
|
86 | 85 | |
87 | 86 |
USER_PROFILE_CONFIG = { |
88 | 87 |
'fields': [ |
tests/test_manager.py | ||
---|---|---|
862 | 862 |
resp.form['site_file'] = Upload('site-export.json', site_export, 'application/json') |
863 | 863 |
with CaptureQueriesContext(connection) as ctx: |
864 | 864 |
resp = resp.form.submit() |
865 |
assert len(ctx.captured_queries) in [303, 304]
|
|
865 |
assert len(ctx.captured_queries) in [298, 299]
|
|
866 | 866 |
assert Page.objects.count() == 4 |
867 | 867 |
assert PageSnapshot.objects.all().count() == 4 |
868 | 868 | |
... | ... | |
873 | 873 |
resp.form['site_file'] = Upload('site-export.json', site_export, 'application/json') |
874 | 874 |
with CaptureQueriesContext(connection) as ctx: |
875 | 875 |
resp = resp.form.submit() |
876 |
assert len(ctx.captured_queries) == 272
|
|
876 |
assert len(ctx.captured_queries) == 268
|
|
877 | 877 |
assert set(Page.objects.get(slug='one').related_cells['cell_types']) == set( |
878 | 878 |
['data_textcell', 'data_linkcell'] |
879 | 879 |
) |
... | ... | |
2178 | 2178 | |
2179 | 2179 |
with CaptureQueriesContext(connection) as ctx: |
2180 | 2180 |
resp2 = resp.click('view', index=1) |
2181 |
assert len(ctx.captured_queries) == 71
|
|
2181 |
assert len(ctx.captured_queries) == 70
|
|
2182 | 2182 |
assert Page.snapshots.latest('pk').related_cells == {'cell_types': ['data_textcell']} |
2183 | 2183 |
assert resp2.text.index('Hello world') < resp2.text.index('Foobar3') |
2184 | 2184 | |
... | ... | |
2239 | 2239 |
resp = resp.click('restore', index=6) |
2240 | 2240 |
with CaptureQueriesContext(connection) as ctx: |
2241 | 2241 |
resp = resp.form.submit().follow() |
2242 |
assert len(ctx.captured_queries) == 144
|
|
2242 |
assert len(ctx.captured_queries) == 142
|
|
2243 | 2243 | |
2244 | 2244 |
resp2 = resp.click('See online') |
2245 | 2245 |
assert resp2.text.index('Foobar1') < resp2.text.index('Foobar2') < resp2.text.index('Foobar3') |
tests/test_newsletters_cell.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 | ||
3 |
import json |
|
4 | ||
5 |
import mock |
|
6 |
import pytest |
|
7 |
import requests |
|
8 |
from django.contrib.auth.models import User |
|
9 |
from django.template.defaultfilters import slugify |
|
10 | ||
11 |
from combo.apps.newsletters.forms import NewslettersManageForm |
|
12 |
from combo.apps.newsletters.models import NewslettersCell, SubscriptionsSaveError |
|
13 |
from combo.data.models import Page |
|
14 |
from combo.utils import check_query |
|
15 | ||
16 |
pytestmark = pytest.mark.django_db |
|
17 | ||
18 |
NEWSLETTERS = [ |
|
19 |
{'id': '1', 'text': 'Democratie locale', 'transports': [{'id': 'mail', 'text': 'mail'}]}, |
|
20 |
{'id': '2', 'text': 'Rencontres de quartiers', 'transports': [{'id': 'mail', 'text': 'mail'}]}, |
|
21 |
{ |
|
22 |
'id': '3', |
|
23 |
'text': 'Environnement', |
|
24 |
'transports': [ |
|
25 |
{'id': 'mail', 'text': 'mail'}, |
|
26 |
{'id': 'sms', 'text': 'sms'}, |
|
27 |
{'id': 'rss', 'text': 'rss'}, |
|
28 |
], |
|
29 |
}, |
|
30 |
{ |
|
31 |
'id': '4', |
|
32 |
'text': u'Marchés publics', |
|
33 |
'transports': [{'id': 'mail', 'text': 'mail'}, {'id': 'rss', 'text': 'rss'}], |
|
34 |
}, |
|
35 |
{ |
|
36 |
'id': '5', |
|
37 |
'text': "Offres d'emploi", |
|
38 |
'transports': [{'id': 'mail', 'text': 'mail'}, {'id': 'rss', 'text': 'rss'}], |
|
39 |
}, |
|
40 |
{ |
|
41 |
'id': '6', |
|
42 |
'text': 'Infos créche', |
|
43 |
'transports': [{'id': 'sms', 'text': 'sms'}, {'id': 'rss', 'text': 'rss'}], |
|
44 |
}, |
|
45 |
{ |
|
46 |
'id': '7', |
|
47 |
'text': 'Familles', |
|
48 |
'transports': [{'id': 'mail', 'text': 'mail'}, {'id': 'sms', 'text': 'sms'}], |
|
49 |
}, |
|
50 |
{ |
|
51 |
'id': '8', |
|
52 |
'text': 'Travaux', |
|
53 |
'transports': [ |
|
54 |
{'id': 'mail', 'text': 'mail'}, |
|
55 |
{'id': 'sms', 'text': 'sms'}, |
|
56 |
{'id': 'rss', 'text': 'rss'}, |
|
57 |
], |
|
58 |
}, |
|
59 |
] |
|
60 | ||
61 |
SUBSCRIPTIONS = [ |
|
62 |
{'id': '3', 'text': 'Environnement', 'transports': [{'id': 'mail', 'text': 'mail'}]}, |
|
63 |
{'id': '7', 'text': 'Familles', 'transports': [{'id': 'mail', 'text': 'mail'}]}, |
|
64 |
{'id': '5', 'text': "Offres d'emploi", 'transports': [{'id': 'mail', 'text': 'mail'}]}, |
|
65 |
{ |
|
66 |
'id': '6', |
|
67 |
'text': 'Infos créche', |
|
68 |
'transports': [{'id': 'sms', 'text': 'sms'}, {'id': 'rss', 'text': 'rss'}], |
|
69 |
}, |
|
70 |
] |
|
71 | ||
72 |
USER_EMAIL = 'foobar@example.com' |
|
73 | ||
74 | ||
75 |
@pytest.fixture |
|
76 |
def cell(): |
|
77 |
page = Page() |
|
78 |
page.save() |
|
79 |
cell = NewslettersCell(title='Newsletter test', url='http://example.org/', page=page, order=0) |
|
80 |
cell.save() |
|
81 |
return cell |
|
82 | ||
83 | ||
84 |
@pytest.fixture |
|
85 |
def user(): |
|
86 |
try: |
|
87 |
user = User.objects.get(username='foo') |
|
88 |
except User.DoesNotExist: |
|
89 |
user = User.objects.create(username='foo', email=USER_EMAIL) |
|
90 |
return user |
|
91 | ||
92 | ||
93 |
@mock.patch('combo.apps.newsletters.models.requests.get') |
|
94 |
def test_get_newsletters_by_transports(mock_get, cell): |
|
95 |
restrictions = ('mail', 'sms') |
|
96 |
cell.transports_restrictions = ','.join(restrictions) |
|
97 |
expected_newsletters = [] |
|
98 |
for n in NEWSLETTERS: |
|
99 |
for t in n['transports']: |
|
100 |
if t['id'] in restrictions: |
|
101 |
expected_newsletters.append(n) |
|
102 |
continue |
|
103 |
mock_json = mock.Mock() |
|
104 |
mock_json.json.return_value = {'err': 0, 'data': NEWSLETTERS} |
|
105 |
mock_get.return_value = mock_json |
|
106 |
assert cell.get_newsletters() == expected_newsletters |
|
107 |
assert mock_get.call_args[1]['without_user'] |
|
108 |
assert 'user' not in mock_get.call_args[1] |
|
109 | ||
110 |
mock_get.side_effect = requests.RequestException |
|
111 |
assert cell.get_newsletters() == [] |
|
112 | ||
113 | ||
114 |
@mock.patch('combo.apps.newsletters.models.requests.get') |
|
115 |
def test_get_newsletters_by_unrestricted_transports(mock_get, cell): |
|
116 |
cell.transports_restrictions = '' |
|
117 |
expected_newsletters = [] |
|
118 |
for n in NEWSLETTERS: |
|
119 |
for t in n['transports']: |
|
120 |
expected_newsletters.append(n) |
|
121 |
mock_json = mock.Mock() |
|
122 |
mock_json.json.return_value = {'err': 0, 'data': NEWSLETTERS} |
|
123 |
mock_get.return_value = mock_json |
|
124 |
assert cell.get_newsletters() == expected_newsletters |
|
125 | ||
126 | ||
127 |
@mock.patch('combo.apps.newsletters.models.requests.get') |
|
128 |
def test_get_newsletters_by_resources(mock_get, cell): |
|
129 |
restrictions = ('marches-publics', 'familles', 'democratie-locale') |
|
130 |
cell.transports_restrictions = 'mail' |
|
131 |
cell.resources_restrictions = ','.join(restrictions) |
|
132 |
expected_newsletters = [] |
|
133 |
for n in NEWSLETTERS: |
|
134 |
if slugify(n['text']) in restrictions: |
|
135 |
expected_newsletters.append(n) |
|
136 |
mock_json = mock.Mock() |
|
137 |
mock_json.json.return_value = {'err': 0, 'data': NEWSLETTERS} |
|
138 |
mock_get.return_value = mock_json |
|
139 |
assert cell.get_newsletters() == expected_newsletters |
|
140 | ||
141 | ||
142 |
@mock.patch('combo.apps.newsletters.models.requests.get') |
|
143 |
def test_get_subscriptions(mock_get, cell, user): |
|
144 |
restrictions = ('mail', 'sms') |
|
145 |
cell.transports_restrictions = ','.join(restrictions) |
|
146 |
expected_subscriptions = [] |
|
147 |
for n in SUBSCRIPTIONS: |
|
148 |
for t in n['transports']: |
|
149 |
if t['id'] in restrictions: |
|
150 |
expected_subscriptions.append(n) |
|
151 |
continue |
|
152 |
mock_json = mock.Mock() |
|
153 |
mock_json.json.return_value = {'err': 0, 'data': SUBSCRIPTIONS} |
|
154 |
mock_get.return_value = mock_json |
|
155 |
assert cell.get_subscriptions(user) == expected_subscriptions |
|
156 |
assert mock_get.call_args[1]['user'].email == USER_EMAIL |
|
157 | ||
158 |
mock_get.side_effect = requests.RequestException |
|
159 |
assert cell.get_subscriptions(user) == [] |
|
160 | ||
161 | ||
162 |
@mock.patch('combo.utils.requests_wrapper.RequestsSession.send') |
|
163 |
def test_get_subscriptions_signature_check(mock_send, cell, user): |
|
164 |
restrictions = ('mail', 'sms') |
|
165 |
cell.transports_restrictions = ','.join(restrictions) |
|
166 |
expected_subscriptions = [] |
|
167 |
for n in SUBSCRIPTIONS: |
|
168 |
for t in n['transports']: |
|
169 |
if t['id'] in restrictions: |
|
170 |
expected_subscriptions.append(n) |
|
171 |
continue |
|
172 |
mock_json = mock.Mock(status_code=200) |
|
173 |
mock_json.json.return_value = {'err': 0, 'data': SUBSCRIPTIONS} |
|
174 |
mock_send.return_value = mock_json |
|
175 |
cell.get_subscriptions(user) |
|
176 |
url = mock_send.call_args[0][0].url |
|
177 |
assert check_query(url.split('?', 1)[-1], 'combo') |
|
178 | ||
179 | ||
180 |
@mock.patch('combo.apps.newsletters.models.requests.post') |
|
181 |
def test_failed_set_subscriptions(mock_post, cell, user): |
|
182 |
restrictions = ('sms', 'mail') |
|
183 |
cell.transports_restrictions = ','.join(restrictions) |
|
184 |
subscriptions = [ |
|
185 |
{'id': '1', 'transports': [{'id': 'mail', 'text': 'mail'}]}, |
|
186 |
{'id': '7', 'transports': [{'id': 'sms', 'text': 'sms'}]}, |
|
187 |
{'id': '8', 'transports': [{'id': 'sms', 'text': 'sms'}, {'id': 'mail', 'text': 'mail'}]}, |
|
188 |
] |
|
189 |
mock_post.side_effect = requests.ConnectionError |
|
190 |
with pytest.raises(SubscriptionsSaveError): |
|
191 |
cell.set_subscriptions(subscriptions, user, uuid='useruuid') |
|
192 | ||
193 |
mock_post.side_effect = requests.HTTPError(response=mock.MagicMock()) |
|
194 |
with pytest.raises(SubscriptionsSaveError): |
|
195 |
cell.set_subscriptions(subscriptions, user, uuid='useruuid') |
|
196 | ||
197 | ||
198 |
def test_set_subscriptions_with_no_uuid(cell, user): |
|
199 |
restrictions = ('sms', 'mail') |
|
200 |
cell.transports_restrictions = ','.join(restrictions) |
|
201 |
subscriptions = [ |
|
202 |
{'id': '1', 'transports': [{'id': 'mail', 'text': 'mail'}]}, |
|
203 |
{'id': '8', 'transports': [{'id': 'sms', 'text': 'sms'}, {'id': 'mail', 'text': 'mail'}]}, |
|
204 |
] |
|
205 |
with pytest.raises(SubscriptionsSaveError): |
|
206 |
cell.set_subscriptions(subscriptions, user) |
|
207 | ||
208 | ||
209 |
@mock.patch('combo.apps.newsletters.models.requests.post') |
|
210 |
def test_set_subscriptions(mock_post, cell, user): |
|
211 |
restrictions = ('sms', 'mail') |
|
212 |
cell.transports_restrictions = ','.join(restrictions) |
|
213 |
subscriptions = [ |
|
214 |
{'id': '1', 'transports': [{'id': 'mail', 'text': 'mail'}]}, |
|
215 |
{'id': '7', 'transports': [{'id': 'sms', 'text': 'sms'}]}, |
|
216 |
{'id': '8', 'transports': [{'id': 'sms', 'text': 'sms'}, {'id': 'mail', 'text': 'mail'}]}, |
|
217 |
] |
|
218 |
mock_json = mock.Mock() |
|
219 |
mock_json.json.return_value = {'err': 0, 'data': True} |
|
220 |
mock_post.return_value = mock_json |
|
221 |
cell.set_subscriptions(subscriptions, user, uuid='useruuid') |
|
222 |
args, kwargs = mock_post.call_args |
|
223 |
assert kwargs['params']['uuid'] == 'useruuid' |
|
224 |
assert kwargs['federation_key'] == 'email' |
|
225 |
assert kwargs['user'].email == USER_EMAIL |
|
226 | ||
227 | ||
228 |
@mock.patch('combo.apps.newsletters.models.requests.get') |
|
229 |
def test_get_subscriptions_with_name_id_and_mobile(mock_get, cell, user): |
|
230 |
restrictions = ('sms', 'mail') |
|
231 |
cell.transports_restrictions = ','.join(restrictions) |
|
232 |
mock_json = mock.Mock() |
|
233 |
mock_json.json.return_value = {'err': 0, 'data': NEWSLETTERS} |
|
234 |
mock_get.return_value = mock_json |
|
235 | ||
236 |
fake_saml_request = mock.Mock() |
|
237 |
fake_saml_request.user = mock.Mock(email=USER_EMAIL) |
|
238 |
fake_saml_request.user.get_name_id.return_value = 'nameid' |
|
239 |
fake_saml_request.session = {'mellon_session': {'mobile': '0607080900'}} |
|
240 | ||
241 |
form = NewslettersManageForm(instance=cell, request=fake_saml_request) |
|
242 |
args, kwargs = mock_get.call_args |
|
243 |
assert kwargs['params'] == {'uuid': 'nameid', 'mobile': '0607080900'} |
|
244 | ||
245 | ||
246 |
def mocked_requests_get(*args, **kwargs): |
|
247 |
url = args[0] |
|
248 | ||
249 |
class MockResponse(mock.Mock): |
|
250 |
status_code = 200 |
|
251 | ||
252 |
def json(self): |
|
253 |
return json.loads(self.content) |
|
254 | ||
255 |
if 'newsletters' in url: |
|
256 |
return MockResponse(content=json.dumps({'data': NEWSLETTERS})) |
|
257 |
else: |
|
258 |
return MockResponse(content=json.dumps({'data': SUBSCRIPTIONS})) |
|
259 | ||
260 | ||
261 |
@mock.patch('combo.apps.newsletters.models.requests.get', side_effect=mocked_requests_get) |
|
262 |
def test_subscriptions_form(mock_get, cell, user): |
|
263 |
restrictions = ('sms', 'mail') |
|
264 |
cell.transports_restrictions = ','.join(restrictions) |
|
265 |
newsletters = [n['id'] for n in cell.get_newsletters()] |
|
266 |
assert mock_get.call_args[1]['without_user'] |
|
267 |
subscriptions = [s['id'] for s in cell.get_subscriptions(user)] |
|
268 |
fake_request = mock.Mock(user=mock.Mock(username='username', email=USER_EMAIL), session={}) |
|
269 |
form = NewslettersManageForm(instance=cell, request=fake_request) |
|
270 |
# test if all newsletters are present |
|
271 |
for f_id, field in form.fields.items(): |
|
272 |
assert f_id in newsletters |
|
273 |
# test if initial fields are in the restrictions |
|
274 |
for s in subscriptions: |
|
275 |
assert set(form.fields[s].initial).intersection(set(restrictions)) |
tests/test_search.py | ||
---|---|---|
1364 | 1364 |
assert IndexedCell.objects.count() == 50 |
1365 | 1365 |
with CaptureQueriesContext(connection) as ctx: |
1366 | 1366 |
index_site() |
1367 |
assert len(ctx.captured_queries) == 224
|
|
1367 |
assert len(ctx.captured_queries) == 223
|
|
1368 | 1368 | |
1369 | 1369 |
SearchCell.objects.create( |
1370 | 1370 |
page=page, placeholder='content', order=0, _search_services={'data': ['search1']} |
1371 |
- |