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/sms/forms.py                       | 39 ++++++++
 passerelle/sms/models.py                      | 89 ++++++++++++++++++-
 tests/test_sms.py                             | 47 ++++++++++
 8 files changed, 304 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
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
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
27 30

  
28 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
                )
66

  
67

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

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

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

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

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

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

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

  
117
    @classmethod
118
    def get_manager_form_class(cls, *args, **kwargs):
119
        from .forms import SMSConnectorForm
120

  
121
        form_class = modelform_factory(cls, form=SMSConnectorForm, **kwargs)
122
        for field in form_class.base_fields.values():
123
            if isinstance(field.widget, ClearableFileInput):
124
                field.widget.template_with_initial = ''\
125
                    '%(initial_text)s: %(initial)s '\
126
                    '%(clear_template)s<br />%(input_text)s: %(input)s'
127
        return form_class
128

  
42 129
    def clean_numbers(self, destinations):
43 130
        numbers = []
44 131
        for dest in destinations:
45 132
            # most gateways needs the number prefixed by the country code, this is
46 133
            # really unfortunate.
47 134
            dest = dest.strip()
48 135
            number = ''.join(re.findall('[0-9]', dest))
49 136
            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
-