Projet

Général

Profil

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

Nicolas Roche, 31 mai 2020 14:18

Télécharger (17 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/sms/forms.py                       | 38 +++++++++
 passerelle/sms/models.py                      | 79 ++++++++++++++++++-
 tests/test_sms.py                             | 47 +++++++++++
 8 files changed, 293 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/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

  
29 69
class SMSResource(BaseResource):
70
    manager_form_base_class = SMSConnectorForm
30 71
    category = _('SMS Providers')
31 72
    documentation_url = 'https://doc-publik.entrouvert.com/admin-fonctionnel/les-tutos/configuration-envoi-sms/'
32 73

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

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

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

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

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

  
115
    def __init__(self, *args, **kwargs):
116
        super(SMSResource, self).__init__(*args, **kwargs)
117
        self.get_authorized_display = self._get_authorized_display
118

  
42 119
    def clean_numbers(self, destinations):
43 120
        numbers = []
44 121
        for dest in destinations:
45 122
            # most gateways needs the number prefixed by the country code, this is
46 123
            # really unfortunate.
47 124
            dest = dest.strip()
48 125
            number = ''.join(re.findall('[0-9]', dest))
49 126
            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
-