Projet

Général

Profil

0001-add-phone-calls-connector-29829.patch

Thomas Noël, 17 janvier 2019 11:38

Télécharger (20,7 ko)

Voir les différences:

Subject: [PATCH] add phone calls connector (#29829)

 passerelle/apps/phonecalls/__init__.py        |   0
 .../phonecalls/migrations/0001_initial.py     |  55 +++++
 .../apps/phonecalls/migrations/__init__.py    |   0
 passerelle/apps/phonecalls/models.py          | 149 ++++++++++++
 passerelle/settings.py                        |   1 +
 tests/test_phonecalls.py                      | 213 ++++++++++++++++++
 6 files changed, 418 insertions(+)
 create mode 100644 passerelle/apps/phonecalls/__init__.py
 create mode 100644 passerelle/apps/phonecalls/migrations/0001_initial.py
 create mode 100644 passerelle/apps/phonecalls/migrations/__init__.py
 create mode 100644 passerelle/apps/phonecalls/models.py
 create mode 100644 tests/test_phonecalls.py
passerelle/apps/phonecalls/migrations/0001_initial.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.17 on 2019-01-16 14:36
3
from __future__ import unicode_literals
4

  
5
from django.db import migrations, models
6
import django.db.models.deletion
7
import jsonfield.fields
8

  
9

  
10
class Migration(migrations.Migration):
11

  
12
    initial = True
13

  
14
    dependencies = [
15
        ('base', '0010_loggingparameters_trace_emails'),
16
    ]
17

  
18
    operations = [
19
        migrations.CreateModel(
20
            name='Call',
21
            fields=[
22
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
23
                ('callee', models.CharField(max_length=64)),
24
                ('caller', models.CharField(max_length=64)),
25
                ('start_timestamp', models.DateTimeField(auto_now_add=True)),
26
                ('end_timestamp', models.DateTimeField(default=None, null=True)),
27
                ('details', jsonfield.fields.JSONField(default={})),
28
            ],
29
            options={
30
                'ordering': ['-end_timestamp', '-start_timestamp'],
31
                'get_latest_by': 'start_timestamp',
32
                'verbose_name': 'Phone Call',
33
            },
34
        ),
35
        migrations.CreateModel(
36
            name='PhoneCalls',
37
            fields=[
38
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
39
                ('title', models.CharField(max_length=50, verbose_name='Title')),
40
                ('description', models.TextField(verbose_name='Description')),
41
                ('slug', models.SlugField(unique=True)),
42
                ('max_call_duration', models.PositiveIntegerField(default=120, verbose_name='Maximum duration of a call, in minutes.')),
43
                ('data_retention_period', models.PositiveIntegerField(default=60, verbose_name='Data retention period, in days.')),
44
                ('users', models.ManyToManyField(blank=True, to='base.ApiUser')),
45
            ],
46
            options={
47
                'verbose_name': 'Phone Calls',
48
            },
49
        ),
50
        migrations.AddField(
51
            model_name='call',
52
            name='resource',
53
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='phonecalls.PhoneCalls'),
54
        ),
55
    ]
passerelle/apps/phonecalls/models.py
1
# passerelle - uniform access to multiple data sources and services
2
# Copyright (C) 2019  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

  
17
from django.db import models
18
from django.utils.timezone import now, timedelta, make_naive
19
from django.utils.translation import ugettext_lazy as _
20
from jsonfield import JSONField
21

  
22
from passerelle.base.models import BaseResource
23
from passerelle.utils.api import endpoint
24

  
25

  
26
class PhoneCalls(BaseResource):
27
    category = _('Telephony')
28

  
29
    max_call_duration = models.PositiveIntegerField(
30
            _('Maximum duration of a call, in minutes.'),
31
            help_text=_('Each hour, too long calls are closed.'),
32
            default=120)
33
    data_retention_period = models.PositiveIntegerField(
34
            _('Data retention period, in days.'),
35
            help_text=_('Each day, old calls are removed.'),
36
            default=60)
37

  
38
    class Meta:
39
        verbose_name = _('Phone Calls')
40

  
41
    @endpoint(name='call-start',
42
              perm='can_access',
43
              parameters={
44
                  'callee': {'description': _('Callee number'),
45
                             'example_value': '142'},
46
                  'caller': {'description': _('Caller number'),
47
                             'example_value': '0143350135'},
48
              })
49
    def call_start(self, request, callee, caller, **kwargs):
50
        existing_call = Call.objects.filter(resource=self,
51
                                            callee=callee, caller=caller,
52
                                            end_timestamp=None).last()
53
        if existing_call:
54
            existing_call.details = kwargs
55
            existing_call.save()
56
            return {'data': existing_call.json()}
57
        new_call = Call(resource=self, callee=callee, caller=caller, details=kwargs)
58
        new_call.save()
59
        return {'data': new_call.json()}
60

  
61
    @endpoint(name='call-stop',
62
              perm='can_access',
63
              parameters={
64
                  'callee': {'description': _('Callee number'),
65
                             'example_value': '142'},
66
                  'caller': {'description': _('Caller number'),
67
                             'example_value': '0143350135'},
68
              })
69
    def call_stop(self, request, callee, caller, **kwargs):
70
        # close all current callee/caller calls
71
        data = []
72
        for current_call in Call.objects.filter(resource=self,
73
                                                callee=callee, caller=caller,
74
                                                end_timestamp=None):
75
            current_call.end_timestamp = now()
76
            current_call.save()
77
            data.append(current_call.json())
78
        return {'data': data}
79

  
80
    @endpoint(name='calls',
81
              perm='can_access',
82
              parameters={
83
                  'callee': {'description': _('Callee number'),
84
                             'example_value': '142'},
85
                  'limit': {'description': _('Maximal number of results')},
86
              })
87
    def calls(self, request, callee=None, caller=None, limit=30):
88
        calls = Call.objects.filter(resource=self)
89
        if callee:
90
            calls = calls.filter(callee=callee)
91
        if caller:
92
            calls = calls.filter(caller=caller)
93

  
94
        def json_list(calls):
95
            return [call.json() for call in calls[:limit]]
96
        return {
97
            'data': {
98
                'current': json_list(calls.filter(end_timestamp__isnull=True)),
99
                'past': json_list(calls.filter(end_timestamp__isnull=False)),
100
            }
101
        }
102

  
103
    def hourly(self):
104
        super(PhoneCalls, self).hourly()
105
        # close unfinished long calls
106
        maximal_time = now() - timedelta(minutes=self.max_call_duration)
107
        Call.objects.filter(resource=self, end_timestamp=None,
108
                            start_timestamp__lt=maximal_time).update(end_timestamp=now())
109

  
110
    def daily(self):
111
        super(PhoneCalls, self).daily()
112
        # remove finished old calls
113
        maximal_time = now() - timedelta(days=self.data_retention_period)
114
        Call.objects.filter(resource=self, end_timestamp__isnull=False,
115
                            end_timestamp__lt=maximal_time).delete()
116

  
117

  
118
class Call(models.Model):
119
    resource = models.ForeignKey(PhoneCalls)
120
    callee = models.CharField(blank=False, max_length=64)
121
    caller = models.CharField(blank=False, max_length=64)
122
    start_timestamp = models.DateTimeField(auto_now_add=True)
123
    end_timestamp = models.DateTimeField(null=True, default=None)
124
    details = JSONField(default={})
125

  
126
    class Meta:
127
        verbose_name = _('Phone Call')
128
        ordering = ['-start_timestamp']
129

  
130
    def json(self):
131
        # We use make_naive to send localtime, because this API will be used
132
        # by javascript, which it will not be comfortable with UTC datetimes
133
        if self.end_timestamp:
134
            is_current = False
135
            end_timestamp = make_naive(self.end_timestamp)
136
            duration = (self.end_timestamp - self.start_timestamp).seconds
137
        else:
138
            is_current = True
139
            end_timestamp = None
140
            duration = (now() - self.start_timestamp).seconds
141
        return {
142
            'caller': self.caller,
143
            'callee': self.callee,
144
            'start': make_naive(self.start_timestamp),
145
            'end': end_timestamp,
146
            'is_current': is_current,
147
            'duration': duration,
148
            'details': self.details,
149
        }
passerelle/settings.py
138 138
    'passerelle.apps.ovh',
139 139
    'passerelle.apps.oxyd',
140 140
    'passerelle.apps.pastell',
141
    'passerelle.apps.phonecalls',
141 142
    'passerelle.apps.solis',
142 143
    'passerelle.apps.arpege_ecp',
143 144
    # backoffice templates and static
tests/test_phonecalls.py
1
import pytest
2
import utils
3

  
4
from django.contrib.contenttypes.models import ContentType
5
from django.utils.timezone import now, timedelta
6

  
7
from passerelle.apps.phonecalls.models import PhoneCalls, Call
8
from passerelle.base.models import ApiUser, AccessRight
9

  
10

  
11
@pytest.fixture
12
def phonecalls(db):
13
    phonecalls = PhoneCalls.objects.create(slug='test')
14
    apikey = ApiUser.objects.create(username='all', keytype='API', key='123')
15
    obj_type = ContentType.objects.get_for_model(phonecalls)
16
    AccessRight.objects.create(codename='can_access', apiuser=apikey,
17
                               resource_type=obj_type,
18
                               resource_pk=phonecalls.pk)
19
    return phonecalls
20

  
21

  
22
def test_phonecalls_start_stop(app, phonecalls):
23
    start_endpoint = utils.generic_endpoint_url('phonecalls', 'call-start',
24
                                                slug=phonecalls.slug)
25
    assert start_endpoint == '/phonecalls/test/call-start'
26
    stop_endpoint = utils.generic_endpoint_url('phonecalls', 'call-stop',
27
                                               slug=phonecalls.slug)
28
    assert stop_endpoint == '/phonecalls/test/call-stop'
29
    calls_endpoint = utils.generic_endpoint_url('phonecalls', 'calls',
30
                                                slug=phonecalls.slug)
31
    assert calls_endpoint == '/phonecalls/test/calls'
32

  
33
    resp = app.get(start_endpoint, status=403)
34
    assert resp.json['err'] == 1
35
    assert resp.json['err_class'] == 'django.core.exceptions.PermissionDenied'
36
    resp = app.get(stop_endpoint, status=403)
37
    assert resp.json['err'] == 1
38
    assert resp.json['err_class'] == 'django.core.exceptions.PermissionDenied'
39
    resp = app.get(calls_endpoint, status=403)
40
    assert resp.json['err'] == 1
41
    assert resp.json['err_class'] == 'django.core.exceptions.PermissionDenied'
42

  
43
    resp = app.get(start_endpoint, params={'apikey': '123'}, status=400)
44
    assert resp.json['err'] == 1
45
    assert resp.json['err_class'] == 'passerelle.views.WrongParameter'
46
    assert 'missing parameters' in resp.json['err_desc']
47
    resp = app.get(stop_endpoint, params={'apikey': '123'}, status=400)
48
    assert resp.json['err'] == 1
49
    assert resp.json['err_class'] == 'passerelle.views.WrongParameter'
50
    assert 'missing parameters' in resp.json['err_desc']
51

  
52
    Call.objects.all().delete()
53

  
54
    resp = app.get(start_endpoint, status=200, params={'apikey': '123',
55
                                                       'callee': '42',
56
                                                       'caller': '0612345678'})
57
    assert resp.json['err'] == 0
58
    assert resp.json['data']['callee'] == '42'
59
    assert resp.json['data']['caller'] == '0612345678'
60
    assert 'start' in resp.json['data']
61
    assert resp.json['data']['end'] is None
62
    assert resp.json['data']['is_current'] is True
63
    assert 'duration' in resp.json['data']
64
    assert resp.json['data']['details'] == {}
65
    assert Call.objects.count() == 1
66
    call = Call.objects.first()
67
    assert call.callee == '42'
68
    assert call.caller == '0612345678'
69
    assert call.end_timestamp is None
70
    assert call.details == {}
71
    json_start = resp.json['data']['start']
72
    start = call.start_timestamp
73

  
74
    # same call, do nothing
75
    resp = app.get(start_endpoint, status=200, params={'apikey': '123',
76
                                                       'callee': '42',
77
                                                       'caller': '0612345678'})
78
    assert resp.json['err'] == 0
79
    assert resp.json['data']['callee'] == '42'
80
    assert resp.json['data']['caller'] == '0612345678'
81
    assert resp.json['data']['start'] == json_start
82
    assert resp.json['data']['end'] is None
83
    assert resp.json['data']['is_current'] is True
84
    assert Call.objects.count() == 1
85
    call = Call.objects.first()
86
    assert call.callee == '42'
87
    assert call.caller == '0612345678'
88
    assert call.end_timestamp is None
89
    assert call.start_timestamp == start
90
    assert call.details == {}
91
    resp = app.get(start_endpoint, status=200, params={'apikey': '123',
92
                                                       'callee': '42',
93
                                                       'caller': '0612345678',
94
                                                       'foo': 'bar'})  # add details
95
    assert resp.json['err'] == 0
96
    assert resp.json['data']['callee'] == '42'
97
    assert resp.json['data']['caller'] == '0612345678'
98
    assert resp.json['data']['start'] == json_start
99
    assert resp.json['data']['end'] is None
100
    assert resp.json['data']['is_current'] is True
101
    assert resp.json['data']['details'] == {'foo': 'bar'}
102
    assert Call.objects.count() == 1
103
    call = Call.objects.first()
104
    assert call.callee == '42'
105
    assert call.caller == '0612345678'
106
    assert call.end_timestamp is None
107
    assert call.start_timestamp == start
108
    assert call.details == {'foo': 'bar'}
109

  
110
    resp = app.get(calls_endpoint, status=200, params={'apikey': '123'})
111
    assert resp.json['err'] == 0
112
    assert len(resp.json['data']['current']) == 1
113
    assert len(resp.json['data']['past']) == 0
114
    resp = app.get(calls_endpoint, status=200, params={'apikey': '123', 'callee': '42'})
115
    assert resp.json['err'] == 0
116
    assert len(resp.json['data']['current']) == 1
117
    assert len(resp.json['data']['past']) == 0
118
    resp = app.get(calls_endpoint, status=200, params={'apikey': '123', 'callee': '43'})
119
    assert resp.json['err'] == 0
120
    assert len(resp.json['data']['current']) == 0
121
    assert len(resp.json['data']['past']) == 0
122

  
123
    resp = app.get(start_endpoint, status=200, params={'apikey': '123',
124
                                                       'callee': '43',
125
                                                       'caller': '0687654321'})
126
    assert resp.json['err'] == 0
127
    assert resp.json['data']['callee'] == '43'
128
    assert resp.json['data']['caller'] == '0687654321'
129
    assert Call.objects.count() == 2
130
    assert Call.objects.filter(end_timestamp__isnull=True).count() == 2
131

  
132
    resp = app.get(calls_endpoint, status=200, params={'apikey': '123', 'callee': '42'})
133
    assert resp.json['err'] == 0
134
    assert len(resp.json['data']['current']) == 1
135
    assert len(resp.json['data']['past']) == 0
136
    resp = app.get(calls_endpoint, status=200, params={'apikey': '123', 'callee': '43'})
137
    assert resp.json['err'] == 0
138
    assert len(resp.json['data']['current']) == 1
139
    assert len(resp.json['data']['past']) == 0
140

  
141
    resp = app.get(stop_endpoint, status=200, params={'apikey': '123',
142
                                                      'callee': '43',
143
                                                      'caller': '0687654321'})
144
    assert resp.json['err'] == 0
145
    assert len(resp.json['data']) == 1
146
    assert resp.json['data'][0]['callee'] == '43'
147
    assert resp.json['data'][0]['caller'] == '0687654321'
148
    assert resp.json['data'][0]['start'] is not None
149
    assert resp.json['data'][0]['end'] is not None
150
    assert resp.json['data'][0]['duration'] is not None
151
    assert resp.json['data'][0]['is_current'] is False
152
    assert Call.objects.count() == 2
153
    assert Call.objects.filter(end_timestamp__isnull=True).count() == 1
154
    assert Call.objects.filter(end_timestamp__isnull=False).count() == 1
155

  
156
    # calls by callee
157
    resp = app.get(calls_endpoint, status=200, params={'apikey': '123', 'callee': '42'})
158
    assert resp.json['err'] == 0
159
    assert len(resp.json['data']['current']) == 1
160
    assert len(resp.json['data']['past']) == 0
161
    resp = app.get(calls_endpoint, status=200, params={'apikey': '123', 'callee': '43'})
162
    assert resp.json['err'] == 0
163
    assert len(resp.json['data']['current']) == 0
164
    assert len(resp.json['data']['past']) == 1
165
    resp = app.get(calls_endpoint, status=200, params={'apikey': '123'})
166
    assert resp.json['err'] == 0
167
    assert len(resp.json['data']['current']) == 1
168
    assert len(resp.json['data']['past']) == 1
169
    resp = app.get(calls_endpoint, status=200, params={'apikey': '123', 'callee': 'foo'})
170
    assert resp.json['err'] == 0
171
    assert len(resp.json['data']['current']) == 0
172
    assert len(resp.json['data']['past']) == 0
173

  
174
    # calls by caller
175
    resp = app.get(calls_endpoint, status=200, params={'apikey': '123',
176
                                                       'caller': '0612345678'})
177
    assert resp.json['err'] == 0
178
    assert len(resp.json['data']['current']) == 1
179
    assert len(resp.json['data']['past']) == 0
180
    resp = app.get(calls_endpoint, status=200, params={'apikey': '123',
181
                                                       'caller': '0687654321'})
182
    assert resp.json['err'] == 0
183
    assert len(resp.json['data']['current']) == 0
184
    assert len(resp.json['data']['past']) == 1
185
    resp = app.get(calls_endpoint, status=200, params={'apikey': '123',
186
                                                       'caller': 'foo'})
187
    assert resp.json['err'] == 0
188
    assert len(resp.json['data']['current']) == 0
189
    assert len(resp.json['data']['past']) == 0
190

  
191
    # create a "too long" current call (> 120 minutes == phonecalls.max_call_duration)
192
    assert Call.objects.count() == 2
193
    assert Call.objects.filter(end_timestamp__isnull=True).count() == 1
194
    assert Call.objects.filter(end_timestamp__isnull=False).count() == 1
195
    current_call = Call.objects.filter(end_timestamp__isnull=True).first()
196
    current_call.start_timestamp = now() - timedelta(minutes=200)
197
    current_call.save()
198
    # close too long calls
199
    phonecalls.hourly()
200
    assert Call.objects.count() == 2
201
    assert Call.objects.filter(end_timestamp__isnull=True).count() == 0
202
    assert Call.objects.filter(end_timestamp__isnull=False).count() == 2
203

  
204
    # create a "too old" call (> 60 days == phonecalls.data_retention_period)
205
    old_call = Call.objects.first()
206
    old_call.start_timestamp = old_call.start_timestamp - timedelta(days=100)
207
    old_call.end_timestamp = old_call.end_timestamp - timedelta(days=100)
208
    old_call.save()
209
    # remove old calls
210
    phonecalls.daily()
211
    assert Call.objects.count() == 1
212
    assert Call.objects.filter(end_timestamp__isnull=True).count() == 0
213
    assert Call.objects.filter(end_timestamp__isnull=False).count() == 1
0
-