Revision e01c978b
Added by Frédéric Péters over 4 years ago
corbo/migrations/0001_initial.py | ||
---|---|---|
45 | 45 |
name='Subscription', |
46 | 46 |
fields=[ |
47 | 47 |
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), |
48 |
('category', models.ForeignKey(verbose_name='category', to='corbo.Category')), |
|
49 |
('user', models.ForeignKey(verbose_name='user', blank=True, to=settings.AUTH_USER_MODEL, null=True)), |
|
48 |
('category', models.ForeignKey(verbose_name='category', |
|
49 |
to='corbo.Category', on_delete=models.CASCADE)), |
|
50 |
('user', models.ForeignKey(verbose_name='user', blank=True, |
|
51 |
to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE)), |
|
50 | 52 |
], |
51 | 53 |
options={ |
52 | 54 |
}, |
... | ... | |
57 | 59 |
fields=[ |
58 | 60 |
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), |
59 | 61 |
('identifier', models.CharField(help_text='ex.: email, mobile phone number, jabber id', max_length=128, verbose_name='identifier', blank=True)), |
60 |
('subscription', models.ForeignKey(to='corbo.Subscription')), |
|
62 |
('subscription', models.ForeignKey(to='corbo.Subscription', on_delete=models.CASCADE)),
|
|
61 | 63 |
], |
62 | 64 |
options={ |
63 | 65 |
}, |
... | ... | |
70 | 72 |
migrations.AddField( |
71 | 73 |
model_name='announce', |
72 | 74 |
name='category', |
73 |
field=models.ForeignKey(verbose_name='category', to='corbo.Category'), |
|
75 |
field=models.ForeignKey(verbose_name='category', |
|
76 |
to='corbo.Category', on_delete=models.CASCADE), |
|
74 | 77 |
preserve_default=True, |
75 | 78 |
), |
76 | 79 |
] |
corbo/migrations/0002_auto_20150127_2221.py | ||
---|---|---|
18 | 18 |
('channel', models.CharField(max_length=32, verbose_name='channel', choices=[(b'sms', 'SMS'), (b'email', 'Email')])), |
19 | 19 |
('time', models.DateTimeField(auto_now_add=True, verbose_name='sent time')), |
20 | 20 |
('result', models.TextField(verbose_name='result', blank=True)), |
21 |
('announce', models.ForeignKey(verbose_name='announce', to='corbo.Announce')), |
|
21 |
('announce', models.ForeignKey(verbose_name='announce', |
|
22 |
to='corbo.Announce', on_delete=models.CASCADE)), |
|
22 | 23 |
], |
23 | 24 |
options={ |
24 | 25 |
'ordering': ('-time',), |
corbo/migrations/0004_auto_20160504_1744.py | ||
---|---|---|
33 | 33 |
migrations.AlterField( |
34 | 34 |
model_name='subscription', |
35 | 35 |
name='category', |
36 |
field=models.ForeignKey(verbose_name='Category', to='corbo.Category'), |
|
36 |
field=models.ForeignKey(verbose_name='Category', to='corbo.Category', on_delete=models.CASCADE),
|
|
37 | 37 |
preserve_default=True, |
38 | 38 |
), |
39 | 39 |
migrations.AlterUniqueTogether( |
corbo/models.py | ||
---|---|---|
65 | 65 |
|
66 | 66 |
|
67 | 67 |
class Announce(models.Model): |
68 |
category = models.ForeignKey('Category', verbose_name=_('category')) |
|
68 |
category = models.ForeignKey('Category', verbose_name=_('category'), |
|
69 |
on_delete=models.CASCADE) |
|
69 | 70 |
title = models.CharField(_('title'), max_length=256, |
70 | 71 |
help_text=_('maximum 256 characters')) |
71 | 72 |
identifier = models.CharField(max_length=256, null=True, blank=True) |
... | ... | |
150 | 151 |
|
151 | 152 |
|
152 | 153 |
class Broadcast(models.Model): |
153 |
announce = models.ForeignKey(Announce, verbose_name=_('announce')) |
|
154 |
announce = models.ForeignKey(Announce, verbose_name=_('announce'), on_delete=models.CASCADE)
|
|
154 | 155 |
deliver_time = models.DateTimeField(_('Deliver time'), null=True) |
155 | 156 |
delivery_count = models.IntegerField(_('Delivery count'), default=0) |
156 | 157 |
|
... | ... | |
187 | 188 |
|
188 | 189 |
|
189 | 190 |
class Subscription(models.Model): |
190 |
category = models.ForeignKey('Category', verbose_name=_('Category')) |
|
191 |
category = models.ForeignKey('Category', verbose_name=_('Category'), on_delete=models.CASCADE)
|
|
191 | 192 |
uuid = models.CharField(_('User identifier'), max_length=128, blank=True) |
192 | 193 |
identifier = models.CharField(_('identifier'), max_length=128, blank=True, |
193 | 194 |
help_text=_('ex.: mailto, ...')) |
corbo/monkeypatch.py | ||
---|---|---|
14 | 14 |
# You should have received a copy of the GNU Affero General Public License |
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 |
|
17 |
from django.core.urlresolvers import reverse
|
|
17 |
from django.urls import reverse
|
|
18 | 18 |
from django.forms.utils import flatatt |
19 | 19 |
from django.template.loader import render_to_string |
20 | 20 |
from django.utils.encoding import force_text |
... | ... | |
24 | 24 |
|
25 | 25 |
import ckeditor.widgets |
26 | 26 |
|
27 |
def ckeditor_render(self, name, value, attrs=None): |
|
27 |
def ckeditor_render(self, name, value, attrs=None, renderer=None):
|
|
28 | 28 |
if value is None: |
29 | 29 |
value = '' |
30 | 30 |
final_attrs = {'name': name} |
31 |
if getattr(self, 'attrs', None): |
|
32 |
final_attrs.update(self.attrs) |
|
31 | 33 |
if attrs: |
32 | 34 |
final_attrs.update(attrs) |
33 | 35 |
if 'filebrowserUploadUrl' not in self.config: |
corbo/settings.py | ||
---|---|---|
42 | 42 |
'rest_framework', |
43 | 43 |
) |
44 | 44 |
|
45 |
MIDDLEWARE_CLASSES = (
|
|
45 |
MIDDLEWARE = ( |
|
46 | 46 |
'django.contrib.sessions.middleware.SessionMiddleware', |
47 | 47 |
'django.middleware.common.CommonMiddleware', |
48 | 48 |
'django.middleware.csrf.CsrfViewMiddleware', |
49 | 49 |
'django.contrib.auth.middleware.AuthenticationMiddleware', |
50 |
'django.contrib.auth.middleware.SessionAuthenticationMiddleware', |
|
51 | 50 |
'django.contrib.messages.middleware.MessageMiddleware', |
52 | 51 |
'django.middleware.clickjacking.XFrameOptionsMiddleware', |
53 | 52 |
) |
corbo/urls.py | ||
---|---|---|
8 | 8 |
from django.contrib.admin.views.decorators import staff_member_required |
9 | 9 |
|
10 | 10 |
from .urls_utils import decorated_includes, manager_required |
11 |
from .views import homepage, atom, unsubscribe, unsubscription_done, login, logout
|
|
11 |
from .views import homepage, atom, unsubscribe, unsubscription_done, LoginView, LogoutView
|
|
12 | 12 |
|
13 | 13 |
from .manage_urls import urlpatterns as manage_urls |
14 | 14 |
from .api_urls import urlpatterns as api_urls |
... | ... | |
21 | 21 |
url(r'^atom$', atom, name='atom'), |
22 | 22 |
url(r'^manage/', decorated_includes(manager_required, |
23 | 23 |
include(manage_urls))), |
24 |
url(r'^admin/', include(admin.site.urls)),
|
|
24 |
url(r'^admin/', admin.site.urls),
|
|
25 | 25 |
url(r'^api/', include(api_urls)), |
26 | 26 |
url(r'^unsubscribe/done/$', unsubscription_done, |
27 | 27 |
name='unsubscription_done'), |
28 | 28 |
url(r'^unsubscribe/(?P<unsubscription_token>[\w:-]+)$', unsubscribe, |
29 | 29 |
name='unsubscribe'), |
30 |
url(r'^logout/$', logout, name='auth_logout'),
|
|
31 |
url(r'^login/$', login, name='auth_login'),
|
|
30 |
url(r'^logout/$',LogoutView.as_view(), name='auth_logout'),
|
|
31 |
url(r'^login/$', LoginView.as_view(), name='auth_login'),
|
|
32 | 32 |
url(r'^ckeditor/upload/', staff_member_required(ckeditor_views.upload), |
33 | 33 |
name='ckeditor_upload'), |
34 | 34 |
url(r'^ckeditor/browse/', never_cache(staff_member_required(ckeditor_views.browse)), |
corbo/urls_utils.py | ||
---|---|---|
16 | 16 |
|
17 | 17 |
# Decorating URL includes, <https://djangosnippets.org/snippets/2532/> |
18 | 18 |
|
19 |
import django |
|
20 |
|
|
19 | 21 |
from django.contrib.auth.decorators import user_passes_test |
20 | 22 |
from django.core.exceptions import PermissionDenied |
21 |
from django.core.urlresolvers import RegexURLPattern, RegexURLResolver |
|
22 | 23 |
|
23 |
class DecoratedURLPattern(RegexURLPattern): |
|
24 |
if django.VERSION < (2, 0, 0): |
|
25 |
from django.urls.resolvers import RegexURLPattern as URLPattern |
|
26 |
from django.urls.resolvers import RegexURLResolver as URLResolver |
|
27 |
else: |
|
28 |
from django.urls.resolvers import URLPattern, URLResolver |
|
29 |
|
|
30 |
|
|
31 |
class DecoratedURLPattern(URLPattern): |
|
24 | 32 |
def resolve(self, *args, **kwargs): |
25 | 33 |
result = super(DecoratedURLPattern, self).resolve(*args, **kwargs) |
26 | 34 |
if result: |
27 | 35 |
result.func = self._decorate_with(result.func) |
28 | 36 |
return result |
29 | 37 |
|
30 |
class DecoratedRegexURLResolver(RegexURLResolver): |
|
38 |
|
|
39 |
class DecoratedURLResolver(URLResolver): |
|
31 | 40 |
def resolve(self, *args, **kwargs): |
32 |
result = super(DecoratedRegexURLResolver, self).resolve(*args, **kwargs)
|
|
41 |
result = super(DecoratedURLResolver, self).resolve(*args, **kwargs) |
|
33 | 42 |
if result: |
34 | 43 |
result.func = self._decorate_with(result.func) |
35 | 44 |
return result |
36 | 45 |
|
46 |
|
|
37 | 47 |
def decorated_includes(func, includes, *args, **kwargs): |
38 | 48 |
urlconf_module, app_name, namespace = includes |
39 | 49 |
|
40 | 50 |
for item in urlconf_module: |
41 |
if isinstance(item, RegexURLPattern): |
|
51 |
if isinstance(item, URLResolver): |
|
52 |
item.__class__ = DecoratedURLResolver |
|
53 |
else: |
|
42 | 54 |
item.__class__ = DecoratedURLPattern |
43 |
item._decorate_with = func |
|
44 |
|
|
45 |
elif isinstance(item, RegexURLResolver): |
|
46 |
item.__class__ = DecoratedRegexURLResolver |
|
47 |
item._decorate_with = func |
|
55 |
item._decorate_with = func |
|
48 | 56 |
|
49 | 57 |
return urlconf_module, app_name, namespace |
50 | 58 |
|
59 |
|
|
51 | 60 |
def manager_required(function=None, login_url=None): |
52 | 61 |
def check_manager(user): |
53 | 62 |
if user and user.is_staff: |
54 | 63 |
return True |
55 |
if user and not user.is_anonymous():
|
|
64 |
if user and not user.is_anonymous: |
|
56 | 65 |
raise PermissionDenied() |
57 | 66 |
# As the last resort, show the login form |
58 | 67 |
return False |
corbo/utils.py | ||
---|---|---|
27 | 27 |
from django.template import loader |
28 | 28 |
from django.utils.translation import activate |
29 | 29 |
from django.core.files.storage import DefaultStorage |
30 |
from django.core.urlresolvers import reverse
|
|
30 |
from django.urls import reverse
|
|
31 | 31 |
from django.core import signing |
32 | 32 |
from django.utils.six.moves.urllib import parse as urlparse |
33 | 33 |
|
corbo/views.py | ||
---|---|---|
4 | 4 |
from django.contrib import messages |
5 | 5 |
from django.core import signing |
6 | 6 |
from django.utils import timezone |
7 |
from django.core.urlresolvers import reverse
|
|
7 |
from django.urls import reverse
|
|
8 | 8 |
from django.views.generic import CreateView, UpdateView, DeleteView, \ |
9 | 9 |
ListView, TemplateView, RedirectView, DetailView, FormView |
10 | 10 |
from django.contrib.syndication.views import Feed |
11 | 11 |
from django.shortcuts import resolve_url |
12 |
from django.utils.decorators import method_decorator |
|
12 | 13 |
from django.utils.encoding import force_text |
13 | 14 |
from django.utils.feedgenerator import Atom1Feed as DjangoAtom1Feed |
14 | 15 |
from django.utils.http import quote |
... | ... | |
18 | 19 |
from django.contrib import messages |
19 | 20 |
from django.utils.translation import ugettext_lazy as _ |
20 | 21 |
from django.utils.translation import ngettext |
22 |
from django.views.decorators.cache import never_cache |
|
21 | 23 |
|
22 | 24 |
from . import models |
23 | 25 |
from .forms import AnnounceForm, CategoryForm, SubscriptionsImportForm, \ |
... | ... | |
30 | 32 |
get_idps = lambda: [] |
31 | 33 |
|
32 | 34 |
|
33 |
def login(request, *args, **kwargs): |
|
34 |
if any(get_idps()): |
|
35 |
if 'next' not in request.GET: |
|
36 |
return HttpResponseRedirect(resolve_url('mellon_login')) |
|
37 |
return HttpResponseRedirect(resolve_url('mellon_login') + '?next=' + |
|
38 |
quote(request.GET.get('next'))) |
|
39 |
return auth_views.login(request, *args, **kwargs) |
|
35 |
class LoginView(auth_views.LoginView): |
|
36 |
def get(self, request, *args, **kwargs): |
|
37 |
if any(get_idps()): |
|
38 |
if not 'next' in request.GET: |
|
39 |
return HttpResponseRedirect(resolve_url('mellon_login')) |
|
40 |
return HttpResponseRedirect( |
|
41 |
resolve_url('mellon_login') + '?next=' + quote(request.GET.get('next')) |
|
42 |
) |
|
43 |
return super(LoginView, self).get(request, *args, **kwargs) |
|
40 | 44 |
|
41 | 45 |
|
42 |
def logout(request, next_page=None): |
|
43 |
if any(get_idps()): |
|
44 |
return HttpResponseRedirect(resolve_url('mellon_logout')) |
|
45 |
auth_logout(request) |
|
46 |
if next_page is not None: |
|
47 |
next_page = resolve_url(next_page) |
|
48 |
else: |
|
49 |
next_page = '/' |
|
50 |
return HttpResponseRedirect(next_page) |
|
46 |
class LogoutView(auth_views.LogoutView): |
|
47 |
@method_decorator(never_cache) |
|
48 |
def dispatch(self, request, *args, **kwargs): |
|
49 |
if any(get_idps()): |
|
50 |
return HttpResponseRedirect(resolve_url('mellon_logout')) |
|
51 |
return super(LogoutView, self).dispatch(request, *args, **kwargs) |
|
51 | 52 |
|
52 | 53 |
|
53 | 54 |
class HomepageView(RedirectView): |
corbo/widgets.py | ||
---|---|---|
83 | 83 |
|
84 | 84 |
super(PickerWidgetMixin, self).__init__(attrs, format=self.format) |
85 | 85 |
|
86 |
def render(self, name, value, attrs=None): |
|
86 |
def render(self, name, value, attrs=None, renderer=None):
|
|
87 | 87 |
final_attrs = self.build_attrs(attrs) |
88 | 88 |
rendered_widget = super(PickerWidgetMixin, self).render(name, value, final_attrs) |
89 | 89 |
|
setup.py | ||
---|---|---|
109 | 109 |
'Programming Language :: Python', |
110 | 110 |
'Programming Language :: Python :: 2', |
111 | 111 |
], |
112 |
install_requires=['django>1.7, <1.12',
|
|
112 |
install_requires=['django>1.7, <2.3',
|
|
113 | 113 |
'django-ckeditor<4.5.4', |
114 |
'djangorestframework>=3.3,<3.7',
|
|
114 |
'djangorestframework>=3.3,<3.8',
|
|
115 | 115 |
'html2text', |
116 | 116 |
'gadjo', |
117 | 117 |
'emails', |
tests/settings.py | ||
---|---|---|
1 | 1 |
# Add corbo hobo agent |
2 | 2 |
INSTALLED_APPS = ('corbo.hobo_agent', 'hobo.agent.common') + INSTALLED_APPS |
3 |
|
|
4 |
REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] = ['rest_framework.authentication.BasicAuthentication'] |
tests/test_announces.py | ||
---|---|---|
6 | 6 |
|
7 | 7 |
from django.core.files.storage import DefaultStorage |
8 | 8 |
from django.utils import timezone |
9 |
from django.core.urlresolvers import reverse
|
|
9 |
from django.urls import reverse
|
|
10 | 10 |
from django.conf import settings |
11 | 11 |
from django.test import override_settings |
12 | 12 |
|
tests/test_api.py | ||
---|---|---|
4 | 4 |
from uuid import uuid4 |
5 | 5 |
|
6 | 6 |
|
7 |
from django.core.urlresolvers import reverse
|
|
7 |
from django.urls import reverse
|
|
8 | 8 |
from django.utils.http import urlencode |
9 | 9 |
from django.contrib.auth import get_user_model |
10 | 10 |
from django.utils.text import slugify |
... | ... | |
50 | 50 |
|
51 | 51 |
|
52 | 52 |
def test_get_newsletters(app, categories, announces, user): |
53 |
resp = app.get(reverse('newsletters'), status=403)
|
|
53 |
resp = app.get(reverse('newsletters'), status=(401, 403))
|
|
54 | 54 |
app.authorization = ('Basic', ('john.doe', 'password')) |
55 | 55 |
resp = app.get(reverse('newsletters')) |
56 | 56 |
data = resp.json |
... | ... | |
65 | 65 |
|
66 | 66 |
|
67 | 67 |
def test_get_subscriptions(app, categories, announces, user): |
68 |
resp = app.get(reverse('subscriptions'), status=403)
|
|
68 |
resp = app.get(reverse('subscriptions'), status=(401, 403))
|
|
69 | 69 |
uuid = str(uuid4()) |
70 |
resp = app.get(reverse('subscriptions'), params={'uuid': uuid}, status=403)
|
|
70 |
resp = app.get(reverse('subscriptions'), params={'uuid': uuid}, status=(401, 403))
|
|
71 | 71 |
app.authorization = ('Basic', ('john.doe', 'password')) |
72 | 72 |
|
73 | 73 |
for identifier, name in channel_choices: |
... | ... | |
105 | 105 |
def test_delete_subscriptions(app, categories, announces, user): |
106 | 106 |
params = urlencode({'uuid': str(uuid4())}) |
107 | 107 |
subscriptions_url = reverse('subscriptions') + '?' + params |
108 |
resp = app.delete(subscriptions_url, status=403)
|
|
108 |
resp = app.delete(subscriptions_url, status=(401, 403))
|
|
109 | 109 |
app.authorization = ('Basic', ('john.doe', 'password')) |
110 | 110 |
resp = app.delete(subscriptions_url) |
111 | 111 |
if resp.json['data']: |
... | ... | |
119 | 119 |
url = '/api/subscribe/?uuid=%s&email=john@example.net' % uuid |
120 | 120 |
|
121 | 121 |
# anonymous |
122 |
resp = app.post_json(url, params=payload, status=403)
|
|
122 |
resp = app.post_json(url, params=payload, status=(401, 403))
|
|
123 | 123 |
assert resp.json['detail'] == 'Authentication credentials were not provided.' |
124 | 124 |
|
125 | 125 |
# authenticated |
tests/test_broadcasting.py | ||
---|---|---|
7 | 7 |
import random |
8 | 8 |
import requests |
9 | 9 |
|
10 |
from django.core.urlresolvers import reverse
|
|
10 |
from django.urls import reverse
|
|
11 | 11 |
from django.core import mail, signing |
12 | 12 |
from django.utils import timezone |
13 | 13 |
from django.core.files.storage import DefaultStorage |
tests/test_data_migrations.py | ||
---|---|---|
2 | 2 |
|
3 | 3 |
import pytest |
4 | 4 |
|
5 |
import django |
|
5 | 6 |
from django.db import connection |
6 | 7 |
from django.db.migrations.executor import MigrationExecutor |
7 | 8 |
|
... | ... | |
9 | 10 |
|
10 | 11 |
|
11 | 12 |
def test_subscription_sms_identifier_format_migration(): |
13 |
if django.VERSION >= (2, 0, 0): |
|
14 |
pytest.skip('NotSupportedError') |
|
12 | 15 |
executor = MigrationExecutor(connection) |
13 | 16 |
app = 'corbo' |
14 | 17 |
migrate_from = [(app, '0009_auto_20170120_1533')] |
tests/test_manager.py | ||
---|---|---|
3 | 3 |
import os |
4 | 4 |
import pytest |
5 | 5 |
|
6 |
from django.core.urlresolvers import reverse
|
|
6 |
from django.urls import reverse
|
|
7 | 7 |
from django.contrib.auth.models import User |
8 | 8 |
from django.test import override_settings |
9 | 9 |
|
tests/test_subscribers.py | ||
---|---|---|
4 | 4 |
from webtest import Upload |
5 | 5 |
|
6 | 6 |
from django.utils.text import slugify |
7 |
from django.core.urlresolvers import reverse
|
|
7 |
from django.urls import reverse
|
|
8 | 8 |
from django.contrib.auth import get_user_model |
9 | 9 |
|
10 | 10 |
from corbo.models import Category, Subscription |
tox.ini | ||
---|---|---|
1 | 1 |
[tox] |
2 | 2 |
toxworkdir = {env:TMPDIR:/tmp}/tox-{env:USER}/corbo/{env:BRANCH_NAME:} |
3 |
envlist = py2-coverage-django111,py3-django111 |
|
3 |
envlist = py2-coverage-django111,py3-django111,py3-django22
|
|
4 | 4 |
|
5 | 5 |
[testenv] |
6 | 6 |
usedevelop = |
... | ... | |
10 | 10 |
CORBO_SETTINGS_FILE=tests/settings.py |
11 | 11 |
coverage: COVERAGE=--junitxml=test_results.xml --cov-report xml --cov-report html --cov=corbo/ --cov-config .coveragerc |
12 | 12 |
deps = |
13 |
django>=1.11,<1.12 |
|
13 |
django111: django>=1.11,<1.12 |
|
14 |
django22: django>=2.2,<2.3 |
|
14 | 15 |
http://git.entrouvert.org/hobo.git/snapshot/hobo-master.tar.gz |
15 | 16 |
pytest-cov |
16 |
pytest-django>=3.1.1,<3.4.6
|
|
17 |
pytest>=3.0.4
|
|
18 |
django-webtest<1.9.3
|
|
19 |
django-ckeditor<4.5.3
|
|
20 |
djangorestframework>=3.3,<3.7
|
|
21 |
pylint==1.4.0
|
|
22 |
astroid==1.3.2
|
|
17 |
pytest-django |
|
18 |
pytest |
|
19 |
django-webtest |
|
20 |
git+http://git.entrouvert.org/debian/django-ckeditor.git
|
|
21 |
djangorestframework>=3.3,<3.8
|
|
22 |
pylint |
|
23 |
astroid |
|
23 | 24 |
mock |
24 | 25 |
commands = |
25 | 26 |
py.test {env:COVERAGE:} {posargs:tests/} |
Also available in: Unified diff
general: update for compatibility with django 2.2 (#41626)