Projet

Général

Profil

0001-sms-add-number-authorization-masks-to-SMSResource-39.patch

Nicolas Roche, 19 mai 2020 19:52

Télécharger (21,2 ko)

Voir les différences:

Subject: [PATCH 1/2] sms: add number authorization masks to SMSResource
 (#39650)

 .../migrations/0010_auto_20200505_1216.py     | 26 +++++++
 .../migrations/0009_auto_20200505_1216.py     | 26 +++++++
 .../migrations/0009_auto_20200505_1216.py     | 26 +++++++
 .../ovh/migrations/0009_auto_20200505_1216.py | 26 +++++++
 .../migrations/0009_auto_20200505_1216.py     | 26 +++++++
 passerelle/base/__init__.py                   | 13 ++++
 passerelle/sms/forms.py                       | 38 ++++++++++
 passerelle/sms/models.py                      | 74 +++++++++++++++++++
 passerelle/views.py                           |  4 +-
 tests/test_sms.py                             | 47 ++++++++++++
 10 files changed, 304 insertions(+), 2 deletions(-)
 create mode 100644 passerelle/apps/choosit/migrations/0010_auto_20200505_1216.py
 create mode 100644 passerelle/apps/mobyt/migrations/0009_auto_20200505_1216.py
 create mode 100644 passerelle/apps/orange/migrations/0009_auto_20200505_1216.py
 create mode 100644 passerelle/apps/ovh/migrations/0009_auto_20200505_1216.py
 create mode 100644 passerelle/apps/oxyd/migrations/0009_auto_20200505_1216.py
 create mode 100644 passerelle/sms/forms.py
passerelle/apps/choosit/migrations/0010_auto_20200505_1216.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-05-05 10:16
3
from __future__ import unicode_literals
4

  
5
from django.db import migrations, models
6
import passerelle.sms.models
7

  
8

  
9
class Migration(migrations.Migration):
10

  
11
    dependencies = [
12
        ('choosit', '0009_choositsmsgateway_max_message_length'),
13
    ]
14

  
15
    operations = [
16
        migrations.AddField(
17
            model_name='choositsmsgateway',
18
            name='authorized',
19
            field=passerelle.sms.models.SMSMultipleChoiceField(choices=[('fr-metro', 'France mainland (+33)'), ('fr-domtom', 'France DOM/TOM (+262, etc.)'), ('be', 'Belgium (+32) ')], default=['fr-metro', 'fr-domtom', 'be'], max_length=128, null=True, verbose_name='Authorized Countries'),
20
        ),
21
        migrations.AddField(
22
            model_name='choositsmsgateway',
23
            name='premium_rate',
24
            field=models.CharField(choices=[('no', 'Do no allow'), ('yes', 'Allow')], default='no', help_text='This option is only applyed to France mainland', max_length=32, verbose_name='Premium rate numbers'),
25
        ),
26
    ]
passerelle/apps/mobyt/migrations/0009_auto_20200505_1216.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-05-05 10:16
3
from __future__ import unicode_literals
4

  
5
from django.db import migrations, models
6
import passerelle.sms.models
7

  
8

  
9
class Migration(migrations.Migration):
10

  
11
    dependencies = [
12
        ('mobyt', '0008_auto_20200310_1539'),
13
    ]
14

  
15
    operations = [
16
        migrations.AddField(
17
            model_name='mobytsmsgateway',
18
            name='authorized',
19
            field=passerelle.sms.models.SMSMultipleChoiceField(choices=[('fr-metro', 'France mainland (+33)'), ('fr-domtom', 'France DOM/TOM (+262, etc.)'), ('be', 'Belgium (+32) ')], default=['fr-metro', 'fr-domtom', 'be'], max_length=128, null=True, verbose_name='Authorized Countries'),
20
        ),
21
        migrations.AddField(
22
            model_name='mobytsmsgateway',
23
            name='premium_rate',
24
            field=models.CharField(choices=[('no', 'Do no allow'), ('yes', 'Allow')], default='no', help_text='This option is only applyed to France mainland', max_length=32, verbose_name='Premium rate numbers'),
25
        ),
26
    ]
passerelle/apps/orange/migrations/0009_auto_20200505_1216.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-05-05 10:16
3
from __future__ import unicode_literals
4

  
5
from django.db import migrations, models
6
import passerelle.sms.models
7

  
8

  
9
class Migration(migrations.Migration):
10

  
11
    dependencies = [
12
        ('orange', '0008_auto_20200412_1240'),
13
    ]
14

  
15
    operations = [
16
        migrations.AddField(
17
            model_name='orangesmsgateway',
18
            name='authorized',
19
            field=passerelle.sms.models.SMSMultipleChoiceField(choices=[('fr-metro', 'France mainland (+33)'), ('fr-domtom', 'France DOM/TOM (+262, etc.)'), ('be', 'Belgium (+32) ')], default=['fr-metro', 'fr-domtom', 'be'], max_length=128, null=True, verbose_name='Authorized Countries'),
20
        ),
21
        migrations.AddField(
22
            model_name='orangesmsgateway',
23
            name='premium_rate',
24
            field=models.CharField(choices=[('no', 'Do no allow'), ('yes', 'Allow')], default='no', help_text='This option is only applyed to France mainland', max_length=32, verbose_name='Premium rate numbers'),
25
        ),
26
    ]
passerelle/apps/ovh/migrations/0009_auto_20200505_1216.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-05-05 10:16
3
from __future__ import unicode_literals
4

  
5
from django.db import migrations, models
6
import passerelle.sms.models
7

  
8

  
9
class Migration(migrations.Migration):
10

  
11
    dependencies = [
12
        ('ovh', '0008_ovhsmsgateway_max_message_length'),
13
    ]
14

  
15
    operations = [
16
        migrations.AddField(
17
            model_name='ovhsmsgateway',
18
            name='authorized',
19
            field=passerelle.sms.models.SMSMultipleChoiceField(choices=[('fr-metro', 'France mainland (+33)'), ('fr-domtom', 'France DOM/TOM (+262, etc.)'), ('be', 'Belgium (+32) ')], default=['fr-metro', 'fr-domtom', 'be'], max_length=128, null=True, verbose_name='Authorized Countries'),
20
        ),
21
        migrations.AddField(
22
            model_name='ovhsmsgateway',
23
            name='premium_rate',
24
            field=models.CharField(choices=[('no', 'Do no allow'), ('yes', 'Allow')], default='no', help_text='This option is only applyed to France mainland', max_length=32, verbose_name='Premium rate numbers'),
25
        ),
26
    ]
passerelle/apps/oxyd/migrations/0009_auto_20200505_1216.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-05-05 10:16
3
from __future__ import unicode_literals
4

  
5
from django.db import migrations, models
6
import passerelle.sms.models
7

  
8

  
9
class Migration(migrations.Migration):
10

  
11
    dependencies = [
12
        ('oxyd', '0008_oxydsmsgateway_max_message_length'),
13
    ]
14

  
15
    operations = [
16
        migrations.AddField(
17
            model_name='oxydsmsgateway',
18
            name='authorized',
19
            field=passerelle.sms.models.SMSMultipleChoiceField(choices=[('fr-metro', 'France mainland (+33)'), ('fr-domtom', 'France DOM/TOM (+262, etc.)'), ('be', 'Belgium (+32) ')], default=['fr-metro', 'fr-domtom', 'be'], max_length=128, null=True, verbose_name='Authorized Countries'),
20
        ),
21
        migrations.AddField(
22
            model_name='oxydsmsgateway',
23
            name='premium_rate',
24
            field=models.CharField(choices=[('no', 'Do no allow'), ('yes', 'Allow')], default='no', help_text='This option is only applyed to France mainland', max_length=32, verbose_name='Premium rate numbers'),
25
        ),
26
    ]
passerelle/base/__init__.py
13 13
#
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 17
import django.apps
18 18
from django.apps import apps
19 19
from django.utils.module_loading import import_string
20 20

  
21
from passerelle.forms import GenericConnectorForm
22

  
23

  
21 24
class ConnectorAppMixin(object):
22 25
    def get_connector_model(self):
23 26
        return self._connector_model
24 27

  
28
    def get_connector_form(self):
29
        return self._connector_form
30

  
25 31
    def get_urls(self):
26 32
        try:
27 33
            return import_string('%s.urls.urlpatterns' % self.name)
28 34
        except ImportError:
29 35
            return None
30 36

  
31 37
    def get_management_urls(self):
32 38
        try:
......
38 44
class ConnectorAppConfig(ConnectorAppMixin, django.apps.AppConfig):
39 45
    pass
40 46

  
41 47

  
42 48
class AppConfig(django.apps.AppConfig):
43 49
    name = 'passerelle.base'
44 50

  
45 51
    def ready(self):
52
        from passerelle.sms.models import SMSResource
53
        from passerelle.sms.forms import SMSConnectorForm
54

  
46 55
        # once all applications are ready, go through them and mark them as
47 56
        # connectors if they have a get_connector_model() method or a model
48 57
        # that inherits from BaseResource.
49 58
        from .models import BaseResource
50 59
        for app in apps.get_app_configs():
51 60
            connector_model = None
61
            connector_form = GenericConnectorForm
52 62
            if hasattr(app, 'get_connector_model'):
53 63
                connector_model = app.get_connector_model()
54 64
            else:
55 65
                for model in app.get_models():
66
                    if issubclass(model, SMSResource):
67
                        connector_form = SMSConnectorForm
56 68
                    if issubclass(model, BaseResource):
57 69
                        connector_model = model
58 70
                        app._connector_model = model
59 71
                        break
60 72
            if not connector_model:
61 73
                continue
74
            app._connector_form = connector_form
62 75
            if app.__class__ is django.apps.AppConfig:
63 76
                # switch class if it's an application without a custom
64 77
                # appconfig.
65 78
                app.__class__ = ConnectorAppConfig
66 79
            else:
67 80
                # add mixin to base classes if it's an application with a
68 81
                # custom appconfig.
69 82
                app.__class__.__bases__ = (ConnectorAppMixin,) + app.__class__.__bases__
passerelle/sms/forms.py
1
# passerelle - uniform access to multiple data sources and services
2
# Copyright (C) 2020 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
from django import forms
17
from django.utils.translation import ugettext_lazy as _
18

  
19
from passerelle.forms import GenericConnectorForm
20
from passerelle.sms.models import SMSResource
21

  
22

  
23
class SMSConnectorForm(GenericConnectorForm):
24
    class Meta:
25
        model = SMSResource
26
        fields = '__all__'
27
        widgets = {
28
            'premium_rate': forms.RadioSelect,
29
        }
30

  
31
    def __init__(self, *args, **kwargs):
32
        super(SMSConnectorForm, self).__init__(*args, **kwargs)
33

  
34
        self.fields['authorized'] = forms.MultipleChoiceField(
35
            choices=SMSResource.AUTHORIZED,
36
            widget=forms.CheckboxSelectMultiple,
37
            label=_('Authorized Countries'),
38
        )
passerelle/sms/models.py
11 11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 12
# GNU Affero General Public License for more details.
13 13
#
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
import logging
17 17
import re
18 18

  
19
from django.core.exceptions import ValidationError
19 20
from django.db import models
20 21
from django.utils import six
21 22
from django.utils.translation import ugettext_lazy as _
22 23

  
23 24
from passerelle.base.models import BaseResource
24 25
from passerelle.compat import json_loads
25 26
from passerelle.utils.api import endpoint
26 27
from passerelle.utils.jsonresponse import APIError
27 28

  
28 29

  
30
def parse_choice(choice_string):
31
    if choice_string is None:
32
        return []
33
    return choice_string[1:-1].split(',')
34

  
35

  
36
class SMSMultipleChoiceField(models.TextField):
37
    def from_db_value(self, value, *args, **kwargs):
38
        if value is None:
39
            return value
40
        return parse_choice(value)
41

  
42
    def to_python(self, value):
43
        if isinstance(value, list):
44
            return value
45
        if value is None:
46
            return value
47
        return parse_choice(value)
48

  
49
    def validate(self, value, model_instance):
50
        if not self.editable:
51
            # Skip validation for non-editable fields.
52
            return
53

  
54
        for key in value:
55
            for option_key, option_value in self.choices:
56
                if key == option_key:
57
                    break
58
            else:
59
                raise ValidationError(
60
                    self.error_messages['invalid_choice'],
61
                    code='invalid_choice',
62
                    params={'value': value},
63
                )
64

  
65

  
29 66
class SMSResource(BaseResource):
30 67
    category = _('SMS Providers')
31 68
    documentation_url = 'https://doc-publik.entrouvert.com/admin-fonctionnel/les-tutos/configuration-envoi-sms/'
32 69

  
33 70
    _can_send_messages_description = _('Sending messages is limited to the following API users:')
34 71

  
35 72
    default_country_code = models.CharField(verbose_name=_('Default country code'), max_length=3,
36 73
                                            default=u'33')
37 74
    default_trunk_prefix = models.CharField(verbose_name=_('Default trunk prefix'), max_length=2,
38 75
                                            default=u'0')  # Yeah France first !
39 76
    # FIXME: add regexp field, to check destination and from format
40 77
    max_message_length = models.IntegerField(_('Maximum message length'), default=160)
41 78

  
79
    FR_METRO = 'fr-metro'
80
    FR_DOMTOM = 'fr-domtom'
81
    BE = 'be'
82
    ALL = 'all'
83
    AUTHORIZED = [
84
        (FR_METRO, _('France mainland (+33 [67])')),
85
        (FR_DOMTOM, _('France DOM/TOM (+262, etc.)')),
86
        (BE, _('Belgian (+32 4[5-9]) ')),
87
        (ALL, _('All')),
88
    ]
89
    authorized = SMSMultipleChoiceField(
90
        _('Authorized Countries'),
91
        max_length=128, null=True, choices=AUTHORIZED, default=[ALL])
92

  
93
    NO = 'no'
94
    YES = 'yes'
95
    PREMIUM_RATE = [
96
        (NO, _('Do no allow')),
97
        (YES, _('Allow')),
98
    ]
99
    premium_rate = models.CharField(
100
        _('Premium rate numbers'),
101
        max_length=32, choices=PREMIUM_RATE, default=NO,
102
        help_text=_('This option is only applyed to France mainland')
103
    )
104

  
105
    def _get_authorized_display(self):
106
        result = []
107
        for key, value in self.AUTHORIZED:
108
            if key in self.authorized:
109
                result.append(str(value))
110
        return ', '.join(result)
111

  
112
    def __init__(self, *args, **kwargs):
113
        super(SMSResource, self).__init__(*args, **kwargs)
114
        self.get_authorized_display = self._get_authorized_display
115

  
42 116
    def clean_numbers(self, destinations):
43 117
        numbers = []
44 118
        for dest in destinations:
45 119
            # most gateways needs the number prefixed by the country code, this is
46 120
            # really unfortunate.
47 121
            dest = dest.strip()
48 122
            number = ''.join(re.findall('[0-9]', dest))
49 123
            if dest.startswith('+'):
passerelle/views.py
49 49
from jsonschema import validate, ValidationError
50 50

  
51 51
from passerelle.base.models import BaseResource, ResourceLog
52 52
from passerelle.compat import json_loads
53 53
from passerelle.utils.jsonresponse import APIError
54 54
from passerelle.utils.json import unflatten
55 55

  
56 56
from .utils import to_json, is_authorized
57
from .forms import GenericConnectorForm
58 57
from .forms import ResourceLogSearchForm
59 58

  
60 59
if 'mellon' in settings.INSTALLED_APPS:
61 60
    from mellon.utils import get_idps
62 61
else:
63 62
    def get_idps():
64 63
        return []
65 64

  
......
151 150
            if not hasattr(app, 'get_connector_model'):
152 151
                continue
153 152
            if app.get_connector_model().get_connector_slug() == connector:
154 153
                break
155 154
        else:
156 155
            raise Http404()
157 156

  
158 157
        self.model = app.get_connector_model()
158
        self.form = app.get_connector_form()
159 159
        if hasattr(app, 'get_form_class'):
160 160
            self.form_class = app.get_form_class()
161 161
        else:
162 162
            self.form_class = modelform_factory(
163 163
                self.model,
164
                form=GenericConnectorForm,
164
                form=self.form,
165 165
                exclude=self.exclude_fields)
166 166
            for field in self.form_class.base_fields.values():
167 167
                if isinstance(field.widget, ClearableFileInput):
168 168
                    field.widget.template_with_initial = ''\
169 169
                        '%(initial_text)s: %(initial)s '\
170 170
                        '%(clear_template)s<br />%(input_text)s: %(input)s'
171 171

  
172 172
    def dispatch(self, request, *args, **kwargs):
tests/test_sms.py
1
# passerelle - uniform access to multiple data sources and services
2
# Copyright (C) 2020 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/>.
1 16
import mock
2 17
import pytest
3 18

  
4 19
from django.contrib.contenttypes.models import ContentType
20
from django.utils.translation import ugettext as _
5 21

  
22
from passerelle.apps.choosit.models import ChoositSMSGateway
6 23
from passerelle.apps.ovh.models import OVHSMSGateway
7 24
from passerelle.base.models import ApiUser, AccessRight
8 25
from passerelle.sms.models import SMSResource, SMSLog
9 26
from passerelle.utils.jsonresponse import APIError
10 27

  
11 28
from test_manager import login, admin_user
12 29

  
13 30
import utils
......
107 124
        'message': 'plop',
108 125
        'from': '+33699999999',
109 126
        'to': ['+33688888888'],
110 127
    }
111 128
    with mock.patch.object(OVHSMSGateway, 'send_msg') as send_function:
112 129
        send_function.return_value = {}
113 130
        result = app.post_json(path, params=payload)
114 131
        assert SMSLog.objects.filter(appname=connector.get_connector_slug(), slug=connector.slug).exists()
132

  
133

  
134
@pytest.mark.parametrize('connector', [ChoositSMSGateway], indirect=True)
135
def test_manager(admin_user, app, connector):
136
    app = login(app)
137
    path = '/%s/%s/' % (connector.get_connector_slug(), connector.slug)
138
    resp = app.get(path)
139
    assert '33' in [
140
        x.text for x in resp.html.find('div', {'id': 'description'}).find_all('p')
141
        if x.text.startswith(_('Default country code'))][0]
142
    assert _('All') in [
143
        x.text for x in resp.html.find_all('p')
144
        if x.text.startswith(_('Authorized Countries'))][0]
145
    assert _('Do no allow') in [
146
        x.text for x in resp.html.find_all('p')
147
        if x.text.startswith(_('Premium rate numbers'))][0]
148

  
149
    path = '/manage/%s/%s/edit' % (connector.get_connector_slug(), connector.slug)
150
    resp = app.get(path)
151
    resp.form['authorized'] = []
152
    resp = resp.form.submit()
153
    assert resp.html.find('div', {'class': 'errornotice'}).p.text == \
154
        'There were errors processing your form.'
155
    resp.html.find('div', {'class': 'error'}).text.strip() == 'This field is required.'
156
    resp.form['authorized'] = [SMSResource.FR_METRO, SMSResource.FR_DOMTOM]
157
    resp = resp.form.submit()
158
    resp = resp.follow()
159
    assert _('France mainland (+33 [67])') in [
160
        x.text for x in resp.html.find_all('p')
161
        if x.text.startswith(_('Authorized Countries'))][0]
115
-