0001-add-phone-calls-connector-29829.patch
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 |
- |