Projet

Général

Profil

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

Nicolas Roche, 16 novembre 2020 10:11

Télécharger (19,4 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                       | 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_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
from django import forms
27
from django.utils.translation import ugettext_lazy as _
28

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

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

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

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

  
33

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

  
39

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

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

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

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

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

  
53 93

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

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

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

  
67 107
    manager_view_template_name = 'passerelle/manage/messages_service_view.html'
68 108

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

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

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

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

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

  
73 150
    def clean_numbers(self, destinations):
74 151
        numbers = []
75 152
        for dest in destinations:
76 153
            # most gateways needs the number prefixed by the country code, this is
77 154
            # really unfortunate.
78 155
            dest = dest.strip()
79 156
            number = ''.join(re.findall('[0-9]', dest))
80 157
            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.core.urlresolvers 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, admin_user
17 34

  
18 35
import utils
......
368 385
    body = json.loads(request.body.decode())
369 386
    assert 'accessRules' in body
370 387
    redirect_url = body['redirection'][len('http://testserver'):]
371 388

  
372 389
    resp = app.get(redirect_url).follow()
373 390
    assert 'Successfuly completed connector configuration' in resp.text
374 391
    connector.refresh_from_db()
375 392
    assert connector.consumer_key == 'xyz'
393

  
394

  
395
@pytest.mark.parametrize('connector', [ChoositSMSGateway], indirect=True)
396
def test_manager(admin_user, app, connector):
397
    app = login(app)
398
    path = '/%s/%s/' % (connector.get_connector_slug(), connector.slug)
399
    resp = app.get(path)
400
    assert '33' in [
401
        x.text for x in resp.html.find('div', {'id': 'description'}).find_all('p')
402
        if x.text.startswith(_('Default country code'))][0]
403
    assert _('All') in [
404
        x.text for x in resp.html.find_all('p')
405
        if x.text.startswith(_('Authorized Countries'))][0]
406
    assert _('Do no allow') in [
407
        x.text for x in resp.html.find_all('p')
408
        if x.text.startswith(_('Premium rate numbers'))][0]
409

  
410
    path = '/manage/%s/%s/edit' % (connector.get_connector_slug(), connector.slug)
411
    resp = app.get(path)
412
    resp.form['authorized'] = []
413
    resp = resp.form.submit()
414
    assert resp.html.find('div', {'class': 'errornotice'}).p.text == \
415
        'There were errors processing your form.'
416
    resp.html.find('div', {'class': 'error'}).text.strip() == 'This field is required.'
417
    resp.form['authorized'] = [SMSResource.FR_METRO, SMSResource.FR_DOMTOM]
418
    resp = resp.form.submit()
419
    resp = resp.follow()
420
    assert _('France mainland (+33 [67])') in [
421
        x.text for x in resp.html.find_all('p')
422
        if x.text.startswith(_('Authorized Countries'))][0]
376
-