From 0d0253e40f0e3bf1a953652ee96999dc97c33927 Mon Sep 17 00:00:00 2001
From: Nicolas ROCHE <nroche@entrouvert.com>
Date: Sun, 3 May 2020 11:42:17 +0200
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                       | 26 +++++++
 passerelle/sms/models.py                      | 74 +++++++++++++++++++
 tests/test_sms.py                             | 21 ++++++
 8 files changed, 251 insertions(+)
 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

diff --git a/passerelle/apps/choosit/migrations/0010_auto_20200505_1216.py b/passerelle/apps/choosit/migrations/0010_auto_20200505_1216.py
new file mode 100644
index 00000000..8f9f8993
--- /dev/null
+++ b/passerelle/apps/choosit/migrations/0010_auto_20200505_1216.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.18 on 2020-05-05 10:16
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import passerelle.sms.models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('choosit', '0009_choositsmsgateway_max_message_length'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='choositsmsgateway',
+            name='authorized',
+            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'),
+        ),
+        migrations.AddField(
+            model_name='choositsmsgateway',
+            name='premium_rate',
+            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'),
+        ),
+    ]
diff --git a/passerelle/apps/mobyt/migrations/0009_auto_20200505_1216.py b/passerelle/apps/mobyt/migrations/0009_auto_20200505_1216.py
new file mode 100644
index 00000000..92821c17
--- /dev/null
+++ b/passerelle/apps/mobyt/migrations/0009_auto_20200505_1216.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.18 on 2020-05-05 10:16
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import passerelle.sms.models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('mobyt', '0008_auto_20200310_1539'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='mobytsmsgateway',
+            name='authorized',
+            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'),
+        ),
+        migrations.AddField(
+            model_name='mobytsmsgateway',
+            name='premium_rate',
+            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'),
+        ),
+    ]
diff --git a/passerelle/apps/orange/migrations/0009_auto_20200505_1216.py b/passerelle/apps/orange/migrations/0009_auto_20200505_1216.py
new file mode 100644
index 00000000..ca239df9
--- /dev/null
+++ b/passerelle/apps/orange/migrations/0009_auto_20200505_1216.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.18 on 2020-05-05 10:16
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import passerelle.sms.models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('orange', '0008_auto_20200412_1240'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='orangesmsgateway',
+            name='authorized',
+            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'),
+        ),
+        migrations.AddField(
+            model_name='orangesmsgateway',
+            name='premium_rate',
+            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'),
+        ),
+    ]
diff --git a/passerelle/apps/ovh/migrations/0009_auto_20200505_1216.py b/passerelle/apps/ovh/migrations/0009_auto_20200505_1216.py
new file mode 100644
index 00000000..f8a7616e
--- /dev/null
+++ b/passerelle/apps/ovh/migrations/0009_auto_20200505_1216.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.18 on 2020-05-05 10:16
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import passerelle.sms.models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('ovh', '0008_ovhsmsgateway_max_message_length'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='ovhsmsgateway',
+            name='authorized',
+            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'),
+        ),
+        migrations.AddField(
+            model_name='ovhsmsgateway',
+            name='premium_rate',
+            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'),
+        ),
+    ]
diff --git a/passerelle/apps/oxyd/migrations/0009_auto_20200505_1216.py b/passerelle/apps/oxyd/migrations/0009_auto_20200505_1216.py
new file mode 100644
index 00000000..d72d1a82
--- /dev/null
+++ b/passerelle/apps/oxyd/migrations/0009_auto_20200505_1216.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.18 on 2020-05-05 10:16
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import passerelle.sms.models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('oxyd', '0008_oxydsmsgateway_max_message_length'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='oxydsmsgateway',
+            name='authorized',
+            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'),
+        ),
+        migrations.AddField(
+            model_name='oxydsmsgateway',
+            name='premium_rate',
+            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'),
+        ),
+    ]
diff --git a/passerelle/sms/forms.py b/passerelle/sms/forms.py
index 6b24eb1d..909de142 100644
--- a/passerelle/sms/forms.py
+++ b/passerelle/sms/forms.py
@@ -8,16 +8,42 @@
 #
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU Affero General Public License for more details.
 #
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
+from django import forms
+from django.utils.translation import ugettext_lazy as _
+
 from passerelle.forms import GenericConnectorForm
 from passerelle.sms.models import SMSResource
 
 
 class SMSConnectorForm(GenericConnectorForm):
     class Meta:
         model = SMSResource
         fields = '__all__'
+        widgets = {
+            'premium_rate': forms.RadioSelect,
+        }
+
+    def __init__(self, *args, **kwargs):
+        super(SMSConnectorForm, self).__init__(*args, **kwargs)
+
+        FR_METRO = 'fr-metro'
+        FR_DOMTOM = 'fr-domtom'
+        BE = 'be'
+        ALL = 'all'
+        AUTHORIZED = [
+            (FR_METRO, _('France mainland (+33)')),
+            (FR_DOMTOM, _('France DOM/TOM (+262, etc.)')),
+            (BE, _('Belgium (+32) ')),
+            (ALL, _('All')),
+        ]
+
+        self.fields['authorized'] = forms.MultipleChoiceField(
+            choices=AUTHORIZED,
+            widget=forms.CheckboxSelectMultiple,
+            label=_('Authorized Countries'),
+        )
diff --git a/passerelle/sms/models.py b/passerelle/sms/models.py
index 9bf29def..cbf73665 100644
--- a/passerelle/sms/models.py
+++ b/passerelle/sms/models.py
@@ -11,39 +11,113 @@
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU Affero General Public License for more details.
 #
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 import logging
 import re
 
+from django.core.exceptions import ValidationError
 from django.db import models
 from django.utils import six
 from django.utils.translation import ugettext_lazy as _
 
 from passerelle.base.models import BaseResource
 from passerelle.compat import json_loads
 from passerelle.utils.api import endpoint
 from passerelle.utils.jsonresponse import APIError
 
 
+def parse_choice(choice_string):
+    if choice_string is None:
+        return []
+    return choice_string[1:-1].split(',')
+
+
+class SMSMultipleChoiceField(models.TextField):
+    def from_db_value(self, value, *args, **kwargs):
+        if value is None:
+            return value
+        return parse_choice(value)
+
+    def to_python(self, value):
+        if isinstance(value, list):
+            return value
+        if value is None:
+            return value
+        return parse_choice(value)
+
+    def validate(self, value, model_instance):
+        if not self.editable:
+            # Skip validation for non-editable fields.
+            return
+
+        for key in value:
+            for option_key, option_value in self.choices:
+                if key == option_key:
+                    break
+            else:
+                raise ValidationError(
+                    self.error_messages['invalid_choice'],
+                    code='invalid_choice',
+                    params={'value': value},
+                )
+
+
 class SMSResource(BaseResource):
     category = _('SMS Providers')
     documentation_url = 'https://doc-publik.entrouvert.com/admin-fonctionnel/les-tutos/configuration-envoi-sms/'
 
     _can_send_messages_description = _('Sending messages is limited to the following API users:')
 
     default_country_code = models.CharField(verbose_name=_('Default country code'), max_length=3,
                                             default=u'33')
     default_trunk_prefix = models.CharField(verbose_name=_('Default trunk prefix'), max_length=2,
                                             default=u'0')  # Yeah France first !
     # FIXME: add regexp field, to check destination and from format
     max_message_length = models.IntegerField(_('Maximum message length'), default=160)
 
+    FR_METRO = 'fr-metro'
+    FR_DOMTOM = 'fr-domtom'
+    BE = 'be'
+    ALL = 'all'
+    AUTHORIZED = [
+        (FR_METRO, _('France mainland (+33 [67])')),
+        (FR_DOMTOM, _('France DOM/TOM (+262, etc.)')),
+        (BE, _('Belgian (+32 4[5-9]) ')),
+        (ALL, _('All')),
+    ]
+    authorized = SMSMultipleChoiceField(
+        _('Authorized Countries'),
+        max_length=128, null=True, choices=AUTHORIZED, default=[ALL])
+
+    NO = 'no'
+    YES = 'yes'
+    PREMIUM_RATE = [
+        (NO, _('Do no allow')),
+        (YES, _('Allow')),
+    ]
+    premium_rate = models.CharField(
+        _('Premium rate numbers'),
+        max_length=32, choices=PREMIUM_RATE, default=NO,
+        help_text=_('This option is only applyed to France mainland')
+    )
+
+    def _get_authorized_display(self):
+        result = []
+        for key, value in self.AUTHORIZED:
+            if key in self.authorized:
+                result.append(str(value))
+        return ', '.join(result)
+
+    def __init__(self, *args, **kwargs):
+        super(SMSResource, self).__init__(*args, **kwargs)
+        self.get_authorized_display = self._get_authorized_display
+
     def clean_numbers(self, destinations):
         numbers = []
         for dest in destinations:
             # most gateways needs the number prefixed by the country code, this is
             # really unfortunate.
             dest = dest.strip()
             number = ''.join(re.findall('[0-9]', dest))
             if dest.startswith('+'):
diff --git a/tests/test_sms.py b/tests/test_sms.py
index 7390d1fb..a9882422 100644
--- a/tests/test_sms.py
+++ b/tests/test_sms.py
@@ -1,8 +1,9 @@
+import logging
 import mock
 import pytest
 
 from django.contrib.contenttypes.models import ContentType
 from django.utils.translation import ugettext as _
 
 from passerelle.apps.choosit.models import ChoositSMSGateway
 from passerelle.apps.ovh.models import OVHSMSGateway
@@ -119,8 +120,28 @@ def test_sms_log(app, connector):
 @pytest.mark.parametrize('connector', [ChoositSMSGateway], indirect=True)
 def test_manager(admin_user, app, connector):
     app = login(app)
     path = '/%s/%s/' % (connector.get_connector_slug(), connector.slug)
     resp = app.get(path)
     assert '33' in [
         x.text for x in resp.html.find('div', {'id': 'description'}).find_all('p')
         if x.text.startswith(_('Default country code'))][0]
+    assert _('All') in [
+        x.text for x in resp.html.find_all('p')
+        if x.text.startswith(_('Authorized Countries'))][0]
+    assert _('Do no allow') in [
+        x.text for x in resp.html.find_all('p')
+        if x.text.startswith(_('Premium rate numbers'))][0]
+
+    path = '/manage/%s/%s/edit' % (connector.get_connector_slug(), connector.slug)
+    resp = app.get(path)
+    resp.form['authorized'] = []
+    resp = resp.form.submit()
+    assert resp.html.find('div', {'class': 'errornotice'}).p.text == \
+        'There were errors processing your form.'
+    resp.html.find('div', {'class': 'error'}).text.strip() == 'This field is required.'
+    resp.form['authorized'] = [SMSResource.FR_METRO, SMSResource.FR_DOMTOM]
+    resp = resp.form.submit()
+    resp = resp.follow()
+    assert _('France mainland (+33 [67])') in [
+        x.text for x in resp.html.find_all('p')
+        if x.text.startswith(_('Authorized Countries'))][0]
-- 
2.26.2

