Projet

Général

Profil

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

Nicolas Roche, 01 février 2021 17:05

Télécharger (19,3 ko)

Voir les différences:

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

 .../migrations/0010_auto_20201116_1004.py     | 26 +++++++
 .../migrations/0009_auto_20201116_1004.py     | 26 +++++++
 .../migrations/0009_auto_20201116_1004.py     | 26 +++++++
 .../ovh/migrations/0013_auto_20201116_1004.py | 26 +++++++
 .../migrations/0009_auto_20201116_1004.py     | 26 +++++++
 .../migrations/0002_auto_20201116_1004.py     | 26 +++++++
 passerelle/sms/forms.py                       | 36 +++++++++
 passerelle/sms/models.py                      | 77 ++++++++++++++++++-
 tests/test_sms.py                             | 47 +++++++++++
 9 files changed, 315 insertions(+), 1 deletion(-)
 create mode 100644 passerelle/apps/choosit/migrations/0010_auto_20201116_1004.py
 create mode 100644 passerelle/apps/mobyt/migrations/0009_auto_20201116_1004.py
 create mode 100644 passerelle/apps/orange/migrations/0009_auto_20201116_1004.py
 create mode 100644 passerelle/apps/ovh/migrations/0013_auto_20201116_1004.py
 create mode 100644 passerelle/apps/oxyd/migrations/0009_auto_20201116_1004.py
 create mode 100644 passerelle/apps/twilio/migrations/0002_auto_20201116_1004.py
passerelle/apps/choosit/migrations/0010_auto_20201116_1004.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-11-16 09:04
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_20201116_1004.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-11-16 09:04
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_20201116_1004.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-11-16 09:04
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/0013_auto_20201116_1004.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-11-16 09:04
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', '0012_auto_20201027_1121'),
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_20201116_1004.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-11-16 09:04
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_20201116_1004.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-11-16 09:04
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/>.
1 16
from django import forms
2 17
from django.utils.translation import ugettext_lazy as _
3 18

  
19
from passerelle.forms import GenericConnectorForm
20

  
21

  
4 22
class SmsTestSendForm(forms.Form):
5 23
    number = forms.CharField(label=_('To'), max_length=12)
6 24
    sender = forms.CharField(label=_('From'), max_length=12)
7 25
    message = forms.CharField(label=_('Message'), max_length=128)
26

  
27
class SMSConnectorForm(GenericConnectorForm):
28
    class Meta:
29
        fields = '__all__'
30
        widgets = {
31
            'premium_rate': forms.RadioSelect,
32
        }
33

  
34
    def __init__(self, *args, **kwargs):
35
        from passerelle.sms.models import SMSResource
36

  
37
        super(SMSConnectorForm, self).__init__(*args, **kwargs)
38
        self.fields['authorized'] = forms.MultipleChoiceField(
39
            choices=SMSResource.AUTHORIZED,
40
            widget=forms.CheckboxSelectMultiple,
41
            initial=[SMSResource.ALL],
42
            label=_('Authorized Countries'),
43
        )
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.module_loading import import_string
22 23
from django.utils.translation import ugettext_lazy as _
23 24

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

  
31

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

  
37

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

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

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

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

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

  
53 91

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

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

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

  
67 105
    manager_view_template_name = 'passerelle/manage/messages_service_view.html'
68 106

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

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

  
69 133
    @classmethod
70 134
    def get_management_urls(cls):
71 135
        return import_string('passerelle.sms.urls.management_urlpatterns')
72 136

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

  
144
    def __init__(self, *args, **kwargs):
145
        super(SMSResource, self).__init__(*args, **kwargs)
146
        self.get_authorized_display = self._get_authorized_display
147

  
73 148
    def clean_numbers(self, destinations):
74 149
        numbers = []
75 150
        for dest in destinations:
76 151
            # most gateways needs the number prefixed by the country code, this is
77 152
            # really unfortunate.
78 153
            dest = dest.strip()
79 154
            number = ''.join(re.findall('[0-9]', dest))
80 155
            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 json
3 18
import mock
4 19
import pytest
5 20
from requests import RequestException
6 21

  
7 22
from django.conf import settings
8 23
from django.contrib.contenttypes.models import ContentType
9 24
from django.urls import reverse
25
from django.utils.translation import ugettext as _
10 26

  
27
from passerelle.apps.choosit.models import ChoositSMSGateway
11 28
from passerelle.apps.ovh.models import OVHSMSGateway
12 29
from passerelle.base.models import ApiUser, AccessRight, Job
13 30
from passerelle.sms.models import SMSResource, SMSLog
14 31
from passerelle.utils.jsonresponse import APIError
15 32

  
16 33
from test_manager import login
17 34

  
18 35
import utils
......
393 410
    body = json.loads(request.body.decode())
394 411
    assert 'accessRules' in body
395 412
    redirect_url = body['redirection'][len('http://testserver'):]
396 413

  
397 414
    resp = app.get(redirect_url).follow()
398 415
    assert 'Successfuly completed connector configuration' in resp.text
399 416
    connector.refresh_from_db()
400 417
    assert connector.consumer_key == 'xyz'
418

  
419

  
420
@pytest.mark.parametrize('connector', [ChoositSMSGateway], indirect=True)
421
def test_manager(admin_user, app, connector):
422
    app = login(app)
423
    path = '/%s/%s/' % (connector.get_connector_slug(), connector.slug)
424
    resp = app.get(path)
425
    assert '33' in [
426
        x.text for x in resp.html.find('div', {'id': 'description'}).find_all('p')
427
        if x.text.startswith(_('Default country code'))][0]
428
    assert _('All') in [
429
        x.text for x in resp.html.find_all('p')
430
        if x.text.startswith(_('Authorized Countries'))][0]
431
    assert _('Do no allow') in [
432
        x.text for x in resp.html.find_all('p')
433
        if x.text.startswith(_('Premium rate numbers'))][0]
434

  
435
    path = '/manage/%s/%s/edit' % (connector.get_connector_slug(), connector.slug)
436
    resp = app.get(path)
437
    resp.form['authorized'] = []
438
    resp = resp.form.submit()
439
    assert resp.html.find('div', {'class': 'errornotice'}).p.text == \
440
        'There were errors processing your form.'
441
    assert resp.html.find('div', {'class': 'error'}).text.strip() == 'This field is required.'
442
    resp.form['authorized'] = [SMSResource.FR_METRO, SMSResource.FR_DOMTOM]
443
    resp = resp.form.submit()
444
    resp = resp.follow()
445
    assert _('France mainland (+33 [67])') in [
446
        x.text for x in resp.html.find_all('p')
447
        if x.text.startswith(_('Authorized Countries'))][0]
401
-