Projet

Général

Profil

0001-sms-move-sms-code-into-a-dedicated-directory-42426.patch

Nicolas Roche, 19 mai 2020 12:09

Télécharger (19,2 ko)

Voir les différences:

Subject: [PATCH] sms: move sms code into a dedicated directory (#42426)

 passerelle/apps/choosit/models.py           |  4 +-
 passerelle/apps/mobyt/models.py             |  4 +-
 passerelle/apps/orange/models.py            |  3 +-
 passerelle/apps/ovh/models.py               |  4 +-
 passerelle/apps/oxyd/models.py              |  4 +-
 passerelle/base/migrations/0021_move_sms.py | 28 ++++++
 passerelle/base/models.py                   | 75 +---------------
 passerelle/settings.py                      |  1 +
 passerelle/sms/__init__.py                  |  0
 passerelle/sms/migrations/0001_initial.py   | 32 +++++++
 passerelle/sms/migrations/__init__.py       |  0
 passerelle/sms/models.py                    | 97 +++++++++++++++++++++
 tests/test_sms.py                           |  3 +-
 13 files changed, 171 insertions(+), 84 deletions(-)
 create mode 100644 passerelle/base/migrations/0021_move_sms.py
 create mode 100644 passerelle/sms/__init__.py
 create mode 100644 passerelle/sms/migrations/0001_initial.py
 create mode 100644 passerelle/sms/migrations/__init__.py
 create mode 100644 passerelle/sms/models.py
passerelle/apps/choosit/models.py
1 1
# -*- coding: utf-8 -*-
2 2
import json
3 3
import requests
4 4

  
5
from django.db import models
5 6
from django.utils.six import string_types
6 7
from django.utils.translation import ugettext_lazy as _
7
from django.db import models
8 8

  
9
from passerelle.sms.models import SMSResource
9 10
from passerelle.utils.jsonresponse import APIError
10
from passerelle.base.models import SMSResource
11 11

  
12 12

  
13 13
class ChoositSMSGateway(SMSResource):
14 14
    key = models.CharField(verbose_name=_('Key'), max_length=64)
15 15

  
16 16
    TEST_DEFAULTS = {
17 17
        'create_kwargs': {
18 18
            'key': '1234',
passerelle/apps/mobyt/models.py
1
from django.utils.translation import ugettext_lazy as _
2 1
from django.db import models
2
from django.utils.translation import ugettext_lazy as _
3 3

  
4 4
import requests
5 5

  
6
from passerelle.sms.models import SMSResource
6 7
from passerelle.utils.jsonresponse import APIError
7
from passerelle.base.models import SMSResource
8 8

  
9 9

  
10 10
class MobytSMSGateway(SMSResource):
11 11
    URL = 'http://multilevel.mobyt.fr/sms/batch.php'
12 12
    MESSAGES_QUALITIES = (
13 13
        ('l', _('sms direct')),
14 14
        ('ll', _('sms low-cost')),
15 15
        ('n', _('sms top')),
passerelle/apps/orange/models.py
17 17
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 18
# GNU Affero General Public License for more details.
19 19
#
20 20
# You should have received a copy of the GNU Affero General Public License
21 21
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
22 22
from django.db import models
23 23
from django.utils.translation import ugettext_lazy as _
24 24

  
25
from passerelle.base.models import SMSResource
25
from passerelle.sms.models import SMSResource
26 26
from passerelle.utils.jsonresponse import APIError
27 27

  
28

  
28 29
BASE_API = 'https://contact-everyone.orange-business.com/api/v1.2/'
29 30
URL_TOKEN = BASE_API + 'oauth/token'
30 31
URL_GROUPS = BASE_API + 'groups'
31 32
URL_DIFFUSION = BASE_API + 'groups/%s/diffusion-requests'
32 33

  
33 34

  
34 35
class OrangeError(APIError):
35 36
    pass
passerelle/apps/ovh/models.py
1 1
import requests
2 2

  
3
from django.utils.translation import ugettext_lazy as _
4 3
from django.db import models
5 4
from django.utils.encoding import force_text
5
from django.utils.translation import ugettext_lazy as _
6 6

  
7
from passerelle.sms.models import SMSResource
7 8
from passerelle.utils.jsonresponse import APIError
8
from passerelle.base.models import SMSResource
9 9

  
10 10

  
11 11
class OVHSMSGateway(SMSResource):
12 12
    URL = 'https://www.ovh.com/cgi-bin/sms/http2sms.cgi'
13 13
    MESSAGES_CLASSES = (
14 14
        (0, _('Message are directly shown to users on phone screen '
15 15
              'at reception. The message is never stored, neither in the '
16 16
              'phone memory nor in the SIM card. It is deleted as '
passerelle/apps/oxyd/models.py
1 1
import requests
2 2

  
3 3
from django.db import models
4 4
from django.utils.encoding import force_text
5
from django.utils.translation import ugettext_lazy as _
5 6

  
6 7
from passerelle.utils.jsonresponse import APIError
7
from passerelle.base.models import SMSResource
8
from django.utils.translation import ugettext_lazy as _
8
from passerelle.sms.models import SMSResource
9 9

  
10 10

  
11 11
class OxydSMSGateway(SMSResource):
12 12
    username = models.CharField(verbose_name=_('Username'), max_length=64)
13 13
    password = models.CharField(verbose_name=_('Password'), max_length=64)
14 14

  
15 15
    manager_view_template_name = 'passerelle/manage/messages_service_view.html'
16 16

  
passerelle/base/migrations/0021_move_sms.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-05-19 09:03
3
from __future__ import unicode_literals
4

  
5
from django.db import migrations
6

  
7

  
8
class Migration(migrations.Migration):
9

  
10
    dependencies = [
11
        ('base', '0020_auto_20200519_1129'),
12
    ]
13

  
14
    operations = [
15
        migrations.SeparateDatabaseAndState(
16
            state_operations=[
17
                migrations.DeleteModel(
18
                    name='SMSLog',
19
                ),
20
            ],
21
            database_operations=[
22
                migrations.AlterModelTable(
23
                    name='SMSLog',
24
                    table='sms_smslog',
25
                ),
26
            ],
27
        ),
28
    ]
passerelle/base/models.py
9 9
import traceback
10 10
import base64
11 11
import itertools
12 12
import uuid
13 13

  
14 14
from django.apps import apps
15 15
from django.conf import settings
16 16
from django.contrib.postgres.fields import JSONField
17
from django.core.exceptions import ValidationError, ObjectDoesNotExist, PermissionDenied
17
from django.core.exceptions import ValidationError, PermissionDenied
18 18
from django.core.urlresolvers import reverse
19 19
from django.db import connection, models, transaction
20 20
from django.db.models import Q
21 21
from django.test import override_settings
22 22
from django.utils.text import slugify
23 23
from django.utils import timezone, six
24
from django.utils import six
25 24
from django.utils.encoding import force_text
26 25
from django.utils.six.moves.urllib.parse import urlparse
27 26
from django.utils.translation import ugettext_lazy as _
28 27
from django.utils.timezone import now
29 28
from django.core.files.base import ContentFile
30 29

  
31 30
from django.contrib.contenttypes.models import ContentType
32 31
from django.contrib.contenttypes import fields
33 32

  
34 33
from model_utils.managers import InheritanceManager as ModelUtilsInheritanceManager
35 34

  
36 35
import passerelle
37
import requests
38
from passerelle.compat import json_loads
39 36
from passerelle.utils.api import endpoint
40 37
from passerelle.utils.jsonresponse import APIError
41 38

  
42 39
KEYTYPE_CHOICES = (
43 40
    ('API', _('API Key')),
44 41
    ('SIGN', _('HMAC Signature')),
45 42
)
46 43

  
......
910 907
        max_length=128,
911 908
        verbose_name=_('HTTP and HTTPS proxy'),
912 909
        blank=True)
913 910

  
914 911
    class Meta:
915 912
        abstract = True
916 913

  
917 914

  
918
class SMSResource(BaseResource):
919
    category = _('SMS Providers')
920
    documentation_url = 'https://doc-publik.entrouvert.com/admin-fonctionnel/les-tutos/configuration-envoi-sms/'
921

  
922
    _can_send_messages_description = _('Sending messages is limited to the following API users:')
923

  
924
    default_country_code = models.CharField(verbose_name=_('Default country code'), max_length=3,
925
                                            default=u'33')
926
    default_trunk_prefix = models.CharField(verbose_name=_('Default trunk prefix'), max_length=2,
927
                                            default=u'0')
928
    max_message_length = models.IntegerField(_('Maximum message length'), default=160)
929

  
930
    def clean_numbers(self, destinations):
931

  
932
        numbers = []
933
        for dest in destinations:
934
            # most gateways needs the number prefixed by the country code, this is
935
            # really unfortunate.
936
            dest = dest.strip()
937
            number = ''.join(re.findall('[0-9]', dest))
938
            if dest.startswith('+'):
939
                number = '00' + number
940
            elif number.startswith('00'):
941
                # assumes 00 is international access code, remove it
942
                pass
943
            elif number.startswith(self.default_trunk_prefix):
944
                number = '00' + self.default_country_code + number[len(self.default_trunk_prefix):]
945
            else:
946
                raise APIError('phone number %r is unsupported (no international prefix, '
947
                               'no local trunk prefix)' % number)
948
            numbers.append(number)
949
        return numbers
950

  
951
    @endpoint(perm='can_send_messages', methods=['post'])
952
    def send(self, request, *args, **kwargs):
953
        try:
954
            data = json_loads(request.body)
955
            assert isinstance(data, dict), 'JSON payload is not a dict'
956
            assert 'message' in data, 'missing "message" in JSON payload'
957
            assert 'from' in data, 'missing "from" in JSON payload'
958
            assert 'to' in data, 'missing "to" in JSON payload'
959
            assert isinstance(data['message'], six.text_type), 'message is not a string'
960
            assert isinstance(data['from'], six.text_type), 'from is not a string'
961
            assert all(map(lambda x: isinstance(x, six.text_type), data['to'])), \
962
                'to is not a list of strings'
963
        except (ValueError, AssertionError) as e:
964
            raise APIError('Payload error: %s' % e)
965
        data['message'] = data['message'][:self.max_message_length]
966
        data['to'] = self.clean_numbers(data['to'])
967
        stop = not bool('nostop' in request.GET)
968
        logging.info('sending message %r to %r with sending number %r',
969
                     data['message'], data['to'], data['from'])
970
        result = {'data': self.send_msg(data['message'], data['from'], data['to'], stop=stop)}
971
        SMSLog.objects.create(appname=self.get_connector_slug(), slug=self.slug)
972
        return result
973

  
974
    class Meta:
975
        abstract = True
976

  
977

  
978
@six.python_2_unicode_compatible
979
class SMSLog(models.Model):
980
    timestamp = models.DateTimeField(auto_now_add=True)
981
    appname = models.CharField(max_length=128, verbose_name='appname', null=True)
982
    slug = models.CharField(max_length=128, verbose_name='slug', null=True)
983

  
984
    def __str__(self):
985
        return '%s %s %s' % (self.timestamp, self.appname, self.slug)
986

  
987

  
988 915
@six.python_2_unicode_compatible
989 916
class BaseQuery(models.Model):
990 917
    '''Base for building custom queries.
991 918

  
992 919
    It must define "resource" attribute as a ForeignKey to a BaseResource subclass,
993 920
    and probably extend its "as_endpoint" method to document its parameters.
994 921
    '''
995 922

  
passerelle/settings.py
113 113
    'django.contrib.contenttypes',
114 114
    'django.contrib.sessions',
115 115
    'django.contrib.messages',
116 116
    'django.contrib.staticfiles',
117 117
    'django.contrib.admin',
118 118
    'django.contrib.postgres',
119 119
    # base app
120 120
    'passerelle.base',
121
    'passerelle.sms',
121 122
    # connectors
122 123
    'passerelle.apps.actesweb',
123 124
    'passerelle.apps.airquality',
124 125
    'passerelle.apps.api_entreprise',
125 126
    'passerelle.apps.api_particulier',
126 127
    'passerelle.apps.arcgis',
127 128
    'passerelle.apps.arpege_ecp',
128 129
    'passerelle.apps.astregs',
passerelle/sms/migrations/0001_initial.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-05-19 09:03
3
from __future__ import unicode_literals
4

  
5
from django.db import migrations, models
6

  
7

  
8
class Migration(migrations.Migration):
9

  
10
    initial = True
11

  
12
    dependencies = [
13
        ('base', '0021_move_sms'),
14
    ]
15

  
16
    operations = [
17
        migrations.SeparateDatabaseAndState(
18
            state_operations=[
19
                migrations.CreateModel(
20
                    name='SMSLog',
21
                    fields=[
22
                        ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
23
                        ('timestamp', models.DateTimeField(auto_now_add=True)),
24
                        ('appname', models.CharField(max_length=128, null=True, verbose_name='appname')),
25
                        ('slug', models.CharField(max_length=128, null=True, verbose_name='slug')),
26
                    ],
27
                ),
28
            ],
29
            # Table already exists. See base/migrations/0021_move_sms.py
30
            database_operations=[],
31
        ),
32
    ]
passerelle/sms/models.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
import logging
17
import re
18

  
19
from django.db import models
20
from django.utils import six
21
from django.utils.translation import ugettext_lazy as _
22

  
23
from passerelle.base.models import BaseResource
24
from passerelle.compat import json_loads
25
from passerelle.utils.api import endpoint
26
from passerelle.utils.jsonresponse import APIError
27

  
28

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

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

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

  
42
    def clean_numbers(self, destinations):
43
        numbers = []
44
        for dest in destinations:
45
            # most gateways needs the number prefixed by the country code, this is
46
            # really unfortunate.
47
            dest = dest.strip()
48
            number = ''.join(re.findall('[0-9]', dest))
49
            if dest.startswith('+'):
50
                number = '00' + number
51
            elif number.startswith('00'):
52
                # assumes 00 is international access code, remove it
53
                pass
54
            elif number.startswith(self.default_trunk_prefix):
55
                number = '00' + self.default_country_code + number[len(self.default_trunk_prefix):]
56
            else:
57
                raise APIError('phone number %r is unsupported (no international prefix, '
58
                               'no local trunk prefix)' % number)
59
            numbers.append(number)
60
        return numbers
61

  
62
    @endpoint(perm='can_send_messages', methods=['post'])
63
    def send(self, request, *args, **kwargs):
64
        try:
65
            data = json_loads(request.body)
66
            assert isinstance(data, dict), 'JSON payload is not a dict'
67
            assert 'message' in data, 'missing "message" in JSON payload'
68
            assert 'from' in data, 'missing "from" in JSON payload'
69
            assert 'to' in data, 'missing "to" in JSON payload'
70
            assert isinstance(data['message'], six.text_type), 'message is not a string'
71
            assert isinstance(data['from'], six.text_type), 'from is not a string'
72
            assert all(map(lambda x: isinstance(x, six.text_type), data['to'])), \
73
                'to is not a list of strings'
74
        except (ValueError, AssertionError) as e:
75
            raise APIError('Payload error: %s' % e)
76
        data['message'] = data['message'][:self.max_message_length]
77
        data['to'] = self.clean_numbers(data['to'])
78
        stop = not bool('nostop' in request.GET)
79
        logging.info('sending message %r to %r with sending number %r',
80
                     data['message'], data['to'], data['from'])
81
        # unfortunately it lacks a batch API...
82
        result = {'data': self.send_msg(data['message'], data['from'], data['to'], stop=stop)}
83
        SMSLog.objects.create(appname=self.get_connector_slug(), slug=self.slug)
84
        return result
85

  
86
    class Meta:
87
        abstract = True
88

  
89

  
90
@six.python_2_unicode_compatible
91
class SMSLog(models.Model):
92
    timestamp = models.DateTimeField(auto_now_add=True)
93
    appname = models.CharField(max_length=128, verbose_name='appname', null=True)
94
    slug = models.CharField(max_length=128, verbose_name='slug', null=True)
95

  
96
    def __str__(self):
97
        return '%s %s %s' % (self.timestamp, self.appname, self.slug)
tests/test_sms.py
1 1
import mock
2 2
import pytest
3 3

  
4 4
from django.contrib.contenttypes.models import ContentType
5 5

  
6 6
from passerelle.apps.ovh.models import OVHSMSGateway
7
from passerelle.base.models import ApiUser, AccessRight, SMSResource, SMSLog
7
from passerelle.base.models import ApiUser, AccessRight
8
from passerelle.sms.models import SMSResource, SMSLog
8 9
from passerelle.utils.jsonresponse import APIError
9 10

  
10 11
from test_manager import login, admin_user
11 12

  
12 13
import utils
13 14

  
14 15
pytestmark = pytest.mark.django_db
15 16

  
16
-