Projet

Général

Profil

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

Nicolas Roche, 21 septembre 2020 12:05

Télécharger (18,9 ko)

Voir les différences:

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

 .../migrations/0010_auto_20200720_1007.py     | 26 ++++++
 .../migrations/0009_auto_20200720_1007.py     | 26 ++++++
 .../migrations/0009_auto_20200720_1007.py     | 26 ++++++
 .../ovh/migrations/0010_auto_20200921_1005.py | 26 ++++++
 .../migrations/0009_auto_20200720_1007.py     | 26 ++++++
 .../migrations/0002_auto_20200720_1007.py     | 26 ++++++
 passerelle/sms/forms.py                       | 38 +++++++++
 passerelle/sms/models.py                      | 79 ++++++++++++++++++-
 tests/test_sms.py                             | 47 +++++++++++
 9 files changed, 319 insertions(+), 1 deletion(-)
 create mode 100644 passerelle/apps/choosit/migrations/0010_auto_20200720_1007.py
 create mode 100644 passerelle/apps/mobyt/migrations/0009_auto_20200720_1007.py
 create mode 100644 passerelle/apps/orange/migrations/0009_auto_20200720_1007.py
 create mode 100644 passerelle/apps/ovh/migrations/0010_auto_20200921_1005.py
 create mode 100644 passerelle/apps/oxyd/migrations/0009_auto_20200720_1007.py
 create mode 100644 passerelle/apps/twilio/migrations/0002_auto_20200720_1007.py
 create mode 100644 passerelle/sms/forms.py
passerelle/apps/choosit/migrations/0010_auto_20200720_1007.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-07-20 08:07
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 [67])'), ('fr-domtom', 'France DOM/TOM (+262, etc.)'), ('be', 'Belgian (+32 4[5-9]) '), ('all', 'All')], default=['all'], 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_20200720_1007.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-07-20 08:07
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 [67])'), ('fr-domtom', 'France DOM/TOM (+262, etc.)'), ('be', 'Belgian (+32 4[5-9]) '), ('all', 'All')], default=['all'], 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_20200720_1007.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-07-20 08:07
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 [67])'), ('fr-domtom', 'France DOM/TOM (+262, etc.)'), ('be', 'Belgian (+32 4[5-9]) '), ('all', 'All')], default=['all'], 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/0010_auto_20200921_1005.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-09-21 08:05
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', '0009_auto_20200730_1047'),
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 [67])'), ('fr-domtom', 'France DOM/TOM (+262, etc.)'), ('be', 'Belgian (+32 4[5-9]) '), ('all', 'All')], default=['all'], 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_20200720_1007.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-07-20 08:07
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 [67])'), ('fr-domtom', 'France DOM/TOM (+262, etc.)'), ('be', 'Belgian (+32 4[5-9]) '), ('all', 'All')], default=['all'], 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/apps/twilio/migrations/0002_auto_20200720_1007.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-07-20 08:07
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
        ('twilio', '0001_initial'),
13
    ]
14

  
15
    operations = [
16
        migrations.AddField(
17
            model_name='twiliosmsgateway',
18
            name='authorized',
19
            field=passerelle.sms.models.SMSMultipleChoiceField(choices=[('fr-metro', 'France mainland (+33 [67])'), ('fr-domtom', 'France DOM/TOM (+262, etc.)'), ('be', 'Belgian (+32 4[5-9]) '), ('all', 'All')], default=['all'], max_length=128, null=True, verbose_name='Authorized Countries'),
20
        ),
21
        migrations.AddField(
22
            model_name='twiliosmsgateway',
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/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

  
21

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

  
29
    def __init__(self, *args, **kwargs):
30
        from passerelle.sms.models import SMSResource
31

  
32
        super(SMSConnectorForm, self).__init__(*args, **kwargs)
33
        self.fields['authorized'] = forms.MultipleChoiceField(
34
            choices=SMSResource.AUTHORIZED,
35
            widget=forms.CheckboxSelectMultiple,
36
            initial=[SMSResource.ALL],
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
21
from django.forms.models import modelform_factory
20 22
from django.utils import six
21 23
from django.utils.translation import ugettext_lazy as _
24
from django.forms.widgets import ClearableFileInput
22 25

  
23 26
from passerelle.base.models import BaseResource
24 27
from passerelle.compat import json_loads
25 28
from passerelle.utils.api import endpoint
26 29
from passerelle.utils.jsonresponse import APIError
30
from passerelle.sms.forms import SMSConnectorForm
31

  
32

  
33
def parse_choice(choice_string):
34
    if choice_string is None:
35
        return []
36
    return choice_string[1:-1].split(',')
37

  
38

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

  
45
    def to_python(self, value):
46
        if isinstance(value, list):
47
            return value
48
        if value is None:
49
            return value
50
        return parse_choice(value)
51

  
52
    def validate(self, value, model_instance):
53
        if not self.editable:
54
            # Skip validation for non-editable fields.
55
            return
56

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

  
28 68
SEND_SCHEMA = {
29 69
    '$schema': 'http://json-schema.org/draft-04/schema#',
30 70
    "type": "object",
31 71
    'required': ['message', 'from', 'to'],
32 72
    'properties': {
33 73
        'message': {
34 74
            'description': 'String message',
......
46 86
                'pattern': r'^\+?\d+$'
47 87
            },
48 88
        },
49 89
    }
50 90
}
51 91

  
52 92

  
53 93
class SMSResource(BaseResource):
94
    manager_form_base_class = SMSConnectorForm
54 95
    category = _('SMS Providers')
55 96
    documentation_url = 'https://doc-publik.entrouvert.com/admin-fonctionnel/les-tutos/configuration-envoi-sms/'
56 97

  
57 98
    _can_send_messages_description = _('Sending messages is limited to the following API users:')
58 99

  
59 100
    default_country_code = models.CharField(verbose_name=_('Default country code'), max_length=3,
60 101
                                            default=u'33')
61 102
    default_trunk_prefix = models.CharField(verbose_name=_('Default trunk prefix'), max_length=2,
62 103
                                            default=u'0')  # Yeah France first !
63
    # FIXME: add regexp field, to check destination and from format
64 104
    max_message_length = models.IntegerField(_('Maximum message length'), default=160)
65 105

  
66 106
    manager_view_template_name = 'passerelle/manage/messages_service_view.html'
67 107

  
108
    FR_METRO = 'fr-metro'
109
    FR_DOMTOM = 'fr-domtom'
110
    BE_ = 'be'
111
    ALL = 'all'
112
    AUTHORIZED = [
113
        (FR_METRO, _('France mainland (+33 [67])')),
114
        (FR_DOMTOM, _('France DOM/TOM (+262, etc.)')),
115
        (BE_, _('Belgian (+32 4[5-9]) ')),
116
        (ALL, _('All')),
117
    ]
118
    authorized = SMSMultipleChoiceField(
119
        _('Authorized Countries'),
120
        max_length=128, null=True, choices=AUTHORIZED, default=[ALL])
121

  
122
    NO_ = 'no'
123
    YES = 'yes'
124
    PREMIUM_RATE = [
125
        (NO_, _('Do no allow')),
126
        (YES, _('Allow')),
127
    ]
128
    premium_rate = models.CharField(
129
        _('Premium rate numbers'),
130
        max_length=32, choices=PREMIUM_RATE, default=NO_,
131
        help_text=_('This option is only applyed to France mainland')
132
    )
133

  
134
    def _get_authorized_display(self):
135
        result = []
136
        for key, value in self.AUTHORIZED:
137
            if key in self.authorized:
138
                result.append(str(value))
139
        return ', '.join(result)
140

  
141
    def __init__(self, *args, **kwargs):
142
        super(SMSResource, self).__init__(*args, **kwargs)
143
        self.get_authorized_display = self._get_authorized_display
144

  
68 145
    def clean_numbers(self, destinations):
69 146
        numbers = []
70 147
        for dest in destinations:
71 148
            # most gateways needs the number prefixed by the country code, this is
72 149
            # really unfortunate.
73 150
            dest = dest.strip()
74 151
            number = ''.join(re.findall('[0-9]', dest))
75 152
            if dest.startswith('+'):
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 isodate
2 17
import mock
3 18
import pytest
4 19
from requests import RequestException
5 20

  
6 21
from django.contrib.contenttypes.models import ContentType
22
from django.utils.translation import ugettext as _
7 23

  
24
from passerelle.apps.choosit.models import ChoositSMSGateway
8 25
from passerelle.apps.ovh.models import OVHSMSGateway
9 26
from passerelle.base.models import ApiUser, AccessRight, Job
10 27
from passerelle.sms.models import SMSResource, SMSLog
11 28
from passerelle.utils.jsonresponse import APIError
12 29

  
13 30
from test_manager import login, admin_user
14 31

  
15 32
import utils
......
204 221
    url = connector.API_URL % {'serviceName': 'sms-test42', 'login': 'john'}
205 222
    with utils.mock_url(url, resp, 200) as mocked:
206 223
        connector.jobs()
207 224
    job = Job.objects.get(id=job_id)
208 225
    assert job.status == 'completed'
209 226

  
210 227
    request = mocked.handlers[0].call['requests'][0]
211 228
    assert 'X-Ovh-Signature' in request.headers
229

  
230

  
231
@pytest.mark.parametrize('connector', [ChoositSMSGateway], indirect=True)
232
def test_manager(admin_user, app, connector):
233
    app = login(app)
234
    path = '/%s/%s/' % (connector.get_connector_slug(), connector.slug)
235
    resp = app.get(path)
236
    assert '33' in [
237
        x.text for x in resp.html.find('div', {'id': 'description'}).find_all('p')
238
        if x.text.startswith(_('Default country code'))][0]
239
    assert _('All') in [
240
        x.text for x in resp.html.find_all('p')
241
        if x.text.startswith(_('Authorized Countries'))][0]
242
    assert _('Do no allow') in [
243
        x.text for x in resp.html.find_all('p')
244
        if x.text.startswith(_('Premium rate numbers'))][0]
245

  
246
    path = '/manage/%s/%s/edit' % (connector.get_connector_slug(), connector.slug)
247
    resp = app.get(path)
248
    resp.form['authorized'] = []
249
    resp = resp.form.submit()
250
    assert resp.html.find('div', {'class': 'errornotice'}).p.text == \
251
        'There were errors processing your form.'
252
    resp.html.find('div', {'class': 'error'}).text.strip() == 'This field is required.'
253
    resp.form['authorized'] = [SMSResource.FR_METRO, SMSResource.FR_DOMTOM]
254
    resp = resp.form.submit()
255
    resp = resp.follow()
256
    assert _('France mainland (+33 [67])') in [
257
        x.text for x in resp.html.find_all('p')
258
        if x.text.startswith(_('Authorized Countries'))][0]
212
-