Projet

Général

Profil

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

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                   |  1 +
 passerelle/sms/forms.py                       | 39 +++++++++
 passerelle/sms/models.py                      | 80 ++++++++++++++++++-
 tests/test_sms.py                             | 47 +++++++++++
 9 files changed, 296 insertions(+), 1 deletion(-)
 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

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

  
25 26
    def get_urls(self):
26 27
        try:
27 28
            return import_string('%s.urls.urlpatterns' % self.name)
28 29
        except ImportError:
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
            initial=[SMSResource.ALL],
38
            label=_('Authorized Countries'),
39
        )
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
    # FIXME: add regexp field, to check destination and from format
40 76
    max_message_length = models.IntegerField(_('Maximum message length'), default=160)
41 77

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

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

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

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

  
115
    @classmethod
116
    def get_manager_form_base_class(cls):
117
        from .forms import SMSConnectorForm
118
        return SMSConnectorForm
119

  
42 120
    def clean_numbers(self, destinations):
43 121
        numbers = []
44 122
        for dest in destinations:
45 123
            # most gateways needs the number prefixed by the country code, this is
46 124
            # really unfortunate.
47 125
            dest = dest.strip()
48 126
            number = ''.join(re.findall('[0-9]', dest))
49 127
            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
......
133 150
        'from': '+33699999999',
134 151
        'to': ['+33688888888'],
135 152
    }
136 153
    with mock.patch.object(OVHSMSGateway, 'send_msg') as send_function:
137 154
        send_function.return_value = {}
138 155
        result = app.post_json(path, params=payload)
139 156
        connector.jobs()
140 157
        assert SMSLog.objects.filter(appname=connector.get_connector_slug(), slug=connector.slug).exists()
158

  
159

  
160
@pytest.mark.parametrize('connector', [ChoositSMSGateway], indirect=True)
161
def test_manager(admin_user, app, connector):
162
    app = login(app)
163
    path = '/%s/%s/' % (connector.get_connector_slug(), connector.slug)
164
    resp = app.get(path)
165
    assert '33' in [
166
        x.text for x in resp.html.find('div', {'id': 'description'}).find_all('p')
167
        if x.text.startswith(_('Default country code'))][0]
168
    assert _('All') in [
169
        x.text for x in resp.html.find_all('p')
170
        if x.text.startswith(_('Authorized Countries'))][0]
171
    assert _('Do no allow') in [
172
        x.text for x in resp.html.find_all('p')
173
        if x.text.startswith(_('Premium rate numbers'))][0]
174

  
175
    path = '/manage/%s/%s/edit' % (connector.get_connector_slug(), connector.slug)
176
    resp = app.get(path)
177
    resp.form['authorized'] = []
178
    resp = resp.form.submit()
179
    assert resp.html.find('div', {'class': 'errornotice'}).p.text == \
180
        'There were errors processing your form.'
181
    resp.html.find('div', {'class': 'error'}).text.strip() == 'This field is required.'
182
    resp.form['authorized'] = [SMSResource.FR_METRO, SMSResource.FR_DOMTOM]
183
    resp = resp.form.submit()
184
    resp = resp.follow()
185
    assert _('France mainland (+33 [67])') in [
186
        x.text for x in resp.html.find_all('p')
187
        if x.text.startswith(_('Authorized Countries'))][0]
141
-