add phone calls connector (#29829)
This commit is contained in:
parent
da4e91d4a0
commit
154e689b3b
|
@ -0,0 +1,54 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.17 on 2019-01-21 14:14
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import jsonfield.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('base', '0010_loggingparameters_trace_emails'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Call',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('callee', models.CharField(max_length=64)),
|
||||
('caller', models.CharField(max_length=64)),
|
||||
('start_timestamp', models.DateTimeField(auto_now_add=True)),
|
||||
('end_timestamp', models.DateTimeField(default=None, null=True)),
|
||||
('details', jsonfield.fields.JSONField(default={})),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-start_timestamp'],
|
||||
'verbose_name': 'Phone Call',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PhoneCalls',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=50, verbose_name='Title')),
|
||||
('description', models.TextField(verbose_name='Description')),
|
||||
('slug', models.SlugField(unique=True, verbose_name='Identifier')),
|
||||
('max_call_duration', models.PositiveIntegerField(default=120, help_text='Each hour, too long calls are closed.', verbose_name='Maximum duration of a call, in minutes.')),
|
||||
('data_retention_period', models.PositiveIntegerField(default=60, help_text='Each day, old calls are removed.', verbose_name='Data retention period, in days.')),
|
||||
('users', models.ManyToManyField(blank=True, to='base.ApiUser')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Phone Calls',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='call',
|
||||
name='resource',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='phonecalls.PhoneCalls'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,146 @@
|
|||
# passerelle - uniform access to multiple data sources and services
|
||||
# Copyright (C) 2019 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# 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.db import models
|
||||
from django.utils.timezone import now, timedelta, make_naive
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from jsonfield import JSONField
|
||||
|
||||
from passerelle.base.models import BaseResource
|
||||
from passerelle.utils.api import endpoint
|
||||
|
||||
|
||||
class PhoneCalls(BaseResource):
|
||||
category = _('Telephony')
|
||||
|
||||
max_call_duration = models.PositiveIntegerField(
|
||||
_('Maximum duration of a call, in minutes.'),
|
||||
help_text=_('Each hour, too long calls are closed.'),
|
||||
default=120)
|
||||
data_retention_period = models.PositiveIntegerField(
|
||||
_('Data retention period, in days.'),
|
||||
help_text=_('Each day, old calls are removed.'),
|
||||
default=60)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Phone Calls')
|
||||
|
||||
@endpoint(name='call-start',
|
||||
perm='can_access',
|
||||
parameters={
|
||||
'callee': {'description': _('Callee number'),
|
||||
'example_value': '142'},
|
||||
'caller': {'description': _('Caller number'),
|
||||
'example_value': '0143350135'},
|
||||
})
|
||||
def call_start(self, request, callee, caller, **kwargs):
|
||||
existing_call = Call.objects.filter(resource=self,
|
||||
callee=callee, caller=caller,
|
||||
end_timestamp=None).last()
|
||||
if existing_call:
|
||||
existing_call.details = kwargs
|
||||
existing_call.save()
|
||||
return {'data': existing_call.json()}
|
||||
new_call = Call(resource=self, callee=callee, caller=caller, details=kwargs)
|
||||
new_call.save()
|
||||
return {'data': new_call.json()}
|
||||
|
||||
@endpoint(name='call-stop',
|
||||
perm='can_access',
|
||||
parameters={
|
||||
'callee': {'description': _('Callee number'),
|
||||
'example_value': '142'},
|
||||
'caller': {'description': _('Caller number'),
|
||||
'example_value': '0143350135'},
|
||||
})
|
||||
def call_stop(self, request, callee, caller, **kwargs):
|
||||
# close all current callee/caller calls
|
||||
data = []
|
||||
for current_call in Call.objects.filter(resource=self,
|
||||
callee=callee, caller=caller,
|
||||
end_timestamp=None):
|
||||
current_call.end_timestamp = now()
|
||||
current_call.save()
|
||||
data.append(current_call.json())
|
||||
return {'data': data}
|
||||
|
||||
@endpoint(name='calls',
|
||||
perm='can_access',
|
||||
parameters={
|
||||
'callee': {'description': _('Callee number'),
|
||||
'example_value': '142'},
|
||||
'limit': {'description': _('Maximal number of results')},
|
||||
})
|
||||
def calls(self, request, callee=None, caller=None, limit=30):
|
||||
calls = Call.objects.filter(resource=self)
|
||||
if callee:
|
||||
calls = calls.filter(callee=callee)
|
||||
if caller:
|
||||
calls = calls.filter(caller=caller)
|
||||
|
||||
def json_list(calls):
|
||||
return [call.json() for call in calls[:limit]]
|
||||
return {
|
||||
'data': {
|
||||
'current': json_list(calls.filter(end_timestamp__isnull=True)),
|
||||
'past': json_list(calls.filter(end_timestamp__isnull=False)),
|
||||
}
|
||||
}
|
||||
|
||||
def hourly(self):
|
||||
super(PhoneCalls, self).hourly()
|
||||
# close unfinished long calls
|
||||
maximal_time = now() - timedelta(minutes=self.max_call_duration)
|
||||
Call.objects.filter(resource=self, end_timestamp=None,
|
||||
start_timestamp__lt=maximal_time).update(end_timestamp=now())
|
||||
|
||||
def daily(self):
|
||||
super(PhoneCalls, self).daily()
|
||||
# remove finished old calls
|
||||
maximal_time = now() - timedelta(days=self.data_retention_period)
|
||||
Call.objects.filter(resource=self, end_timestamp__isnull=False,
|
||||
end_timestamp__lt=maximal_time).delete()
|
||||
|
||||
|
||||
class Call(models.Model):
|
||||
resource = models.ForeignKey(PhoneCalls)
|
||||
callee = models.CharField(blank=False, max_length=64)
|
||||
caller = models.CharField(blank=False, max_length=64)
|
||||
start_timestamp = models.DateTimeField(auto_now_add=True)
|
||||
end_timestamp = models.DateTimeField(null=True, default=None)
|
||||
details = JSONField(default={})
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Phone Call')
|
||||
ordering = ['-start_timestamp']
|
||||
|
||||
def json(self):
|
||||
# We use make_naive to send localtime, because this API will be used
|
||||
# by javascript, which will not be comfortable with UTC datetimes
|
||||
if self.end_timestamp:
|
||||
is_current = False
|
||||
end_timestamp = make_naive(self.end_timestamp)
|
||||
else:
|
||||
is_current = True
|
||||
end_timestamp = None
|
||||
return {
|
||||
'caller': self.caller,
|
||||
'callee': self.callee,
|
||||
'start': make_naive(self.start_timestamp),
|
||||
'end': end_timestamp,
|
||||
'is_current': is_current,
|
||||
'details': self.details,
|
||||
}
|
|
@ -138,6 +138,7 @@ INSTALLED_APPS = (
|
|||
'passerelle.apps.ovh',
|
||||
'passerelle.apps.oxyd',
|
||||
'passerelle.apps.pastell',
|
||||
'passerelle.apps.phonecalls',
|
||||
'passerelle.apps.solis',
|
||||
'passerelle.apps.arpege_ecp',
|
||||
'passerelle.apps.vivaticket',
|
||||
|
|
|
@ -0,0 +1,211 @@
|
|||
import pytest
|
||||
import utils
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.timezone import now, timedelta
|
||||
|
||||
from passerelle.apps.phonecalls.models import PhoneCalls, Call
|
||||
from passerelle.base.models import ApiUser, AccessRight
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def phonecalls(db):
|
||||
phonecalls = PhoneCalls.objects.create(slug='test')
|
||||
apikey = ApiUser.objects.create(username='all', keytype='API', key='123')
|
||||
obj_type = ContentType.objects.get_for_model(phonecalls)
|
||||
AccessRight.objects.create(codename='can_access', apiuser=apikey,
|
||||
resource_type=obj_type,
|
||||
resource_pk=phonecalls.pk)
|
||||
return phonecalls
|
||||
|
||||
|
||||
def test_phonecalls_start_stop(app, phonecalls):
|
||||
start_endpoint = utils.generic_endpoint_url('phonecalls', 'call-start',
|
||||
slug=phonecalls.slug)
|
||||
assert start_endpoint == '/phonecalls/test/call-start'
|
||||
stop_endpoint = utils.generic_endpoint_url('phonecalls', 'call-stop',
|
||||
slug=phonecalls.slug)
|
||||
assert stop_endpoint == '/phonecalls/test/call-stop'
|
||||
calls_endpoint = utils.generic_endpoint_url('phonecalls', 'calls',
|
||||
slug=phonecalls.slug)
|
||||
assert calls_endpoint == '/phonecalls/test/calls'
|
||||
|
||||
resp = app.get(start_endpoint, status=403)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['err_class'] == 'django.core.exceptions.PermissionDenied'
|
||||
resp = app.get(stop_endpoint, status=403)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['err_class'] == 'django.core.exceptions.PermissionDenied'
|
||||
resp = app.get(calls_endpoint, status=403)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['err_class'] == 'django.core.exceptions.PermissionDenied'
|
||||
|
||||
resp = app.get(start_endpoint, params={'apikey': '123'}, status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['err_class'] == 'passerelle.views.WrongParameter'
|
||||
assert 'missing parameters' in resp.json['err_desc']
|
||||
resp = app.get(stop_endpoint, params={'apikey': '123'}, status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['err_class'] == 'passerelle.views.WrongParameter'
|
||||
assert 'missing parameters' in resp.json['err_desc']
|
||||
|
||||
Call.objects.all().delete()
|
||||
|
||||
resp = app.get(start_endpoint, status=200, params={'apikey': '123',
|
||||
'callee': '42',
|
||||
'caller': '0612345678'})
|
||||
assert resp.json['err'] == 0
|
||||
assert resp.json['data']['callee'] == '42'
|
||||
assert resp.json['data']['caller'] == '0612345678'
|
||||
assert 'start' in resp.json['data']
|
||||
assert resp.json['data']['end'] is None
|
||||
assert resp.json['data']['is_current'] is True
|
||||
assert resp.json['data']['details'] == {}
|
||||
assert Call.objects.count() == 1
|
||||
call = Call.objects.first()
|
||||
assert call.callee == '42'
|
||||
assert call.caller == '0612345678'
|
||||
assert call.end_timestamp is None
|
||||
assert call.details == {}
|
||||
json_start = resp.json['data']['start']
|
||||
start = call.start_timestamp
|
||||
|
||||
# same call, do nothing
|
||||
resp = app.get(start_endpoint, status=200, params={'apikey': '123',
|
||||
'callee': '42',
|
||||
'caller': '0612345678'})
|
||||
assert resp.json['err'] == 0
|
||||
assert resp.json['data']['callee'] == '42'
|
||||
assert resp.json['data']['caller'] == '0612345678'
|
||||
assert resp.json['data']['start'] == json_start
|
||||
assert resp.json['data']['end'] is None
|
||||
assert resp.json['data']['is_current'] is True
|
||||
assert Call.objects.count() == 1
|
||||
call = Call.objects.first()
|
||||
assert call.callee == '42'
|
||||
assert call.caller == '0612345678'
|
||||
assert call.end_timestamp is None
|
||||
assert call.start_timestamp == start
|
||||
assert call.details == {}
|
||||
resp = app.get(start_endpoint, status=200, params={'apikey': '123',
|
||||
'callee': '42',
|
||||
'caller': '0612345678',
|
||||
'foo': 'bar'}) # add details
|
||||
assert resp.json['err'] == 0
|
||||
assert resp.json['data']['callee'] == '42'
|
||||
assert resp.json['data']['caller'] == '0612345678'
|
||||
assert resp.json['data']['start'] == json_start
|
||||
assert resp.json['data']['end'] is None
|
||||
assert resp.json['data']['is_current'] is True
|
||||
assert resp.json['data']['details'] == {'foo': 'bar'}
|
||||
assert Call.objects.count() == 1
|
||||
call = Call.objects.first()
|
||||
assert call.callee == '42'
|
||||
assert call.caller == '0612345678'
|
||||
assert call.end_timestamp is None
|
||||
assert call.start_timestamp == start
|
||||
assert call.details == {'foo': 'bar'}
|
||||
|
||||
resp = app.get(calls_endpoint, status=200, params={'apikey': '123'})
|
||||
assert resp.json['err'] == 0
|
||||
assert len(resp.json['data']['current']) == 1
|
||||
assert len(resp.json['data']['past']) == 0
|
||||
resp = app.get(calls_endpoint, status=200, params={'apikey': '123', 'callee': '42'})
|
||||
assert resp.json['err'] == 0
|
||||
assert len(resp.json['data']['current']) == 1
|
||||
assert len(resp.json['data']['past']) == 0
|
||||
resp = app.get(calls_endpoint, status=200, params={'apikey': '123', 'callee': '43'})
|
||||
assert resp.json['err'] == 0
|
||||
assert len(resp.json['data']['current']) == 0
|
||||
assert len(resp.json['data']['past']) == 0
|
||||
|
||||
resp = app.get(start_endpoint, status=200, params={'apikey': '123',
|
||||
'callee': '43',
|
||||
'caller': '0687654321'})
|
||||
assert resp.json['err'] == 0
|
||||
assert resp.json['data']['callee'] == '43'
|
||||
assert resp.json['data']['caller'] == '0687654321'
|
||||
assert Call.objects.count() == 2
|
||||
assert Call.objects.filter(end_timestamp__isnull=True).count() == 2
|
||||
|
||||
resp = app.get(calls_endpoint, status=200, params={'apikey': '123', 'callee': '42'})
|
||||
assert resp.json['err'] == 0
|
||||
assert len(resp.json['data']['current']) == 1
|
||||
assert len(resp.json['data']['past']) == 0
|
||||
resp = app.get(calls_endpoint, status=200, params={'apikey': '123', 'callee': '43'})
|
||||
assert resp.json['err'] == 0
|
||||
assert len(resp.json['data']['current']) == 1
|
||||
assert len(resp.json['data']['past']) == 0
|
||||
|
||||
resp = app.get(stop_endpoint, status=200, params={'apikey': '123',
|
||||
'callee': '43',
|
||||
'caller': '0687654321'})
|
||||
assert resp.json['err'] == 0
|
||||
assert len(resp.json['data']) == 1
|
||||
assert resp.json['data'][0]['callee'] == '43'
|
||||
assert resp.json['data'][0]['caller'] == '0687654321'
|
||||
assert resp.json['data'][0]['start'] is not None
|
||||
assert resp.json['data'][0]['end'] is not None
|
||||
assert resp.json['data'][0]['is_current'] is False
|
||||
assert Call.objects.count() == 2
|
||||
assert Call.objects.filter(end_timestamp__isnull=True).count() == 1
|
||||
assert Call.objects.filter(end_timestamp__isnull=False).count() == 1
|
||||
|
||||
# calls by callee
|
||||
resp = app.get(calls_endpoint, status=200, params={'apikey': '123', 'callee': '42'})
|
||||
assert resp.json['err'] == 0
|
||||
assert len(resp.json['data']['current']) == 1
|
||||
assert len(resp.json['data']['past']) == 0
|
||||
resp = app.get(calls_endpoint, status=200, params={'apikey': '123', 'callee': '43'})
|
||||
assert resp.json['err'] == 0
|
||||
assert len(resp.json['data']['current']) == 0
|
||||
assert len(resp.json['data']['past']) == 1
|
||||
resp = app.get(calls_endpoint, status=200, params={'apikey': '123'})
|
||||
assert resp.json['err'] == 0
|
||||
assert len(resp.json['data']['current']) == 1
|
||||
assert len(resp.json['data']['past']) == 1
|
||||
resp = app.get(calls_endpoint, status=200, params={'apikey': '123', 'callee': 'foo'})
|
||||
assert resp.json['err'] == 0
|
||||
assert len(resp.json['data']['current']) == 0
|
||||
assert len(resp.json['data']['past']) == 0
|
||||
|
||||
# calls by caller
|
||||
resp = app.get(calls_endpoint, status=200, params={'apikey': '123',
|
||||
'caller': '0612345678'})
|
||||
assert resp.json['err'] == 0
|
||||
assert len(resp.json['data']['current']) == 1
|
||||
assert len(resp.json['data']['past']) == 0
|
||||
resp = app.get(calls_endpoint, status=200, params={'apikey': '123',
|
||||
'caller': '0687654321'})
|
||||
assert resp.json['err'] == 0
|
||||
assert len(resp.json['data']['current']) == 0
|
||||
assert len(resp.json['data']['past']) == 1
|
||||
resp = app.get(calls_endpoint, status=200, params={'apikey': '123',
|
||||
'caller': 'foo'})
|
||||
assert resp.json['err'] == 0
|
||||
assert len(resp.json['data']['current']) == 0
|
||||
assert len(resp.json['data']['past']) == 0
|
||||
|
||||
# create a "too long" current call (> 120 minutes == phonecalls.max_call_duration)
|
||||
assert Call.objects.count() == 2
|
||||
assert Call.objects.filter(end_timestamp__isnull=True).count() == 1
|
||||
assert Call.objects.filter(end_timestamp__isnull=False).count() == 1
|
||||
current_call = Call.objects.filter(end_timestamp__isnull=True).first()
|
||||
current_call.start_timestamp = now() - timedelta(minutes=200)
|
||||
current_call.save()
|
||||
# close too long calls
|
||||
phonecalls.hourly()
|
||||
assert Call.objects.count() == 2
|
||||
assert Call.objects.filter(end_timestamp__isnull=True).count() == 0
|
||||
assert Call.objects.filter(end_timestamp__isnull=False).count() == 2
|
||||
|
||||
# create a "too old" call (> 60 days == phonecalls.data_retention_period)
|
||||
old_call = Call.objects.first()
|
||||
old_call.start_timestamp = old_call.start_timestamp - timedelta(days=100)
|
||||
old_call.end_timestamp = old_call.end_timestamp - timedelta(days=100)
|
||||
old_call.save()
|
||||
# remove old calls
|
||||
phonecalls.daily()
|
||||
assert Call.objects.count() == 1
|
||||
assert Call.objects.filter(end_timestamp__isnull=True).count() == 0
|
||||
assert Call.objects.filter(end_timestamp__isnull=False).count() == 1
|
Loading…
Reference in New Issue