diff --git a/tests/test_source_phone.py b/tests/test_source_phone.py
new file mode 100644
index 0000000..0bbc124
--- /dev/null
+++ b/tests/test_source_phone.py
@@ -0,0 +1,185 @@
+# welco - multichannel request processing
+# Copyright (C) 2015 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 .
+
+import json
+
+import pytest
+
+from django.core.urlresolvers import reverse
+
+from welco.sources.phone import models
+
+pytestmark = pytest.mark.django_db
+
+
+@pytest.fixture
+def user():
+ from django.contrib.auth.models import User
+
+ user = User.objects.create(username='toto')
+ user.set_password('toto')
+ user.save()
+ return user
+
+
+def test_call_start_stop(client):
+ assert models.PhoneCall.objects.count() == 0
+ payload = {
+ 'event': 'start',
+ 'caller': '0033699999999',
+ 'callee': '102',
+ 'data': {
+ 'user': 'boby.lapointe',
+ }
+ }
+ response = client.post(reverse('phone-call-event'), json.dumps(payload),
+ content_type='application/json')
+ assert response.status_code == 200
+ assert response['content-type'] == 'application/json'
+ assert json.loads(response.content) == {'err': 0}
+ assert models.PhoneCall.objects.count() == 1
+ assert models.PhoneCall.objects.filter(
+ caller='0033699999999',
+ callee='102',
+ data=json.dumps(payload['data']), stop__isnull=True).count() == 1
+ # new start event
+ response = client.post(reverse('phone-call-event'), json.dumps(payload),
+ content_type='application/json')
+ assert response.status_code == 200
+ assert response['content-type'] == 'application/json'
+ assert json.loads(response.content) == {'err': 0}
+ assert models.PhoneCall.objects.count() == 2
+ assert models.PhoneCall.objects.filter(
+ caller='0033699999999',
+ callee='102',
+ data=json.dumps(payload['data']), stop__isnull=True).count() == 1
+ # first call has been closed
+ assert models.PhoneCall.objects.filter(
+ caller='0033699999999',
+ callee='102',
+ data=json.dumps(payload['data']), stop__isnull=False).count() == 1
+ payload['event'] = 'stop'
+ response = client.post(reverse('phone-call-event'), json.dumps(payload),
+ content_type='application/json')
+ assert response.status_code == 200
+ assert response['content-type'] == 'application/json'
+ assert json.loads(response.content) == {'err': 0}
+ assert models.PhoneCall.objects.count() == 2
+ assert models.PhoneCall.objects.filter(
+ caller='0033699999999',
+ callee='102',
+ data=json.dumps(payload['data']), stop__isnull=False).count() == 2
+ # stop is idempotent
+ response = client.post(reverse('phone-call-event'), json.dumps(payload),
+ content_type='application/json')
+ assert response.status_code == 200
+ assert response['content-type'] == 'application/json'
+ assert json.loads(response.content) == {'err': 0}
+ assert models.PhoneCall.objects.count() == 2
+ assert models.PhoneCall.objects.filter(
+ caller='0033699999999',
+ callee='102',
+ data=json.dumps(payload['data']), stop__isnull=False).count() == 2
+
+
+def test_current_calls(user, client):
+ # create some calls
+ for number in range(0, 10):
+ payload = {
+ 'event': 'start',
+ 'caller': '00336999999%02d' % number,
+ 'callee': '1%02d' % number,
+ 'data': {
+ 'user': 'boby.lapointe',
+ }
+ }
+ response = client.post(reverse('phone-call-event'), json.dumps(payload),
+ content_type='application/json')
+ assert response.status_code == 200
+ assert response['content-type'] == 'application/json'
+ assert json.loads(response.content) == {'err': 0}
+
+ # register user to some lines
+ # then remove from some
+ for number in range(0, 10):
+ models.PhoneLine.take(callee='1%02d' % number, user=user)
+ for number in range(5, 10):
+ models.PhoneLine.release(callee='1%02d' % number, user=user)
+ client.login(username='toto', password='toto')
+ response = client.get(reverse('phone-current-calls'))
+ assert response.status_code == 200
+ assert response['content-type'] == 'application/json'
+ payload = json.loads(response.content)
+ assert isinstance(payload, dict)
+ assert set(payload.keys()) == set(['err', 'data'])
+ assert payload['err'] == 0
+ data = payload['data']
+ assert set(data.keys()) == set(['calls', 'lines', 'all-lines'])
+ assert isinstance(data['calls'], list)
+ assert isinstance(data['lines'], list)
+ assert isinstance(data['all-lines'], list)
+ assert len(data['calls']) == 5
+ assert len(data['lines']) == 5
+ assert len(data['all-lines']) == 10
+ for call in data['calls']:
+ assert set(call.keys()) <= set(['caller', 'callee', 'start', 'data'])
+ assert isinstance(call['caller'], unicode)
+ assert isinstance(call['callee'], unicode)
+ assert isinstance(call['start'], unicode)
+ if 'data' in call:
+ assert isinstance(call['data'], dict)
+ assert len([call for call in data['lines'] if isinstance(call, unicode)]) == 5
+ assert len([call for call in data['all-lines'] if isinstance(call, unicode)]) == 10
+
+ # unregister user to all remaining lines
+ for number in range(0, 5):
+ models.PhoneLine.release(callee='1%02d' % number, user=user)
+ response = client.get(reverse('phone-current-calls'))
+ assert response.status_code == 200
+ assert response['content-type'] == 'application/json'
+ payload = json.loads(response.content)
+ assert isinstance(payload, dict)
+ assert set(payload.keys()) == set(['err', 'data'])
+ assert payload['err'] == 0
+ assert set(payload['data'].keys()) == set(['calls', 'lines', 'all-lines'])
+ assert len(payload['data']['calls']) == 0
+ assert len(payload['data']['lines']) == 0
+ assert len(payload['data']['all-lines']) == 10
+
+
+def test_take_release_line(user, client):
+ client.login(username='toto', password='toto')
+
+ assert models.PhoneLine.objects.count() == 0
+ payload = {
+ 'callee': '102',
+ }
+ response = client.post(reverse('phone-take-line'), json.dumps(payload),
+ content_type='application/json')
+ assert response.status_code == 200
+ assert response['content-type'] == 'application/json'
+ assert json.loads(response.content) == {'err': 0}
+ assert models.PhoneLine.objects.count() == 1
+ assert models.PhoneLine.objects.filter(
+ users=user, callee='102').count() == 1
+ response = client.post(reverse('phone-release-line'), json.dumps(payload),
+ content_type='application/json')
+ assert response.status_code == 200
+ assert response['content-type'] == 'application/json'
+ assert json.loads(response.content) == {'err': 0}
+ assert models.PhoneLine.objects.count() == 1
+ assert models.PhoneLine.objects.filter(
+ users=user, callee='102').count() == 0
diff --git a/welco/sources/phone/migrations/0001_initial.py b/welco/sources/phone/migrations/0001_initial.py
new file mode 100644
index 0000000..c64f164
--- /dev/null
+++ b/welco/sources/phone/migrations/0001_initial.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='PhoneCall',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('number', models.CharField(max_length=20, verbose_name='Number')),
+ ('creation_timestamp', models.DateTimeField(auto_now_add=True)),
+ ('last_update_timestamp', models.DateTimeField(auto_now=True)),
+ ],
+ options={
+ 'verbose_name': 'Phone Call',
+ },
+ ),
+ ]
diff --git a/welco/sources/phone/migrations/0002_auto_20151028_1635.py b/welco/sources/phone/migrations/0002_auto_20151028_1635.py
new file mode 100644
index 0000000..332b4e3
--- /dev/null
+++ b/welco/sources/phone/migrations/0002_auto_20151028_1635.py
@@ -0,0 +1,59 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+from django.utils.timezone import utc
+from django.utils.timezone import now
+import datetime
+from django.conf import settings
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('phone', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='PhoneLine',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('callee', models.CharField(unique=True, max_length=20, verbose_name='Callee')),
+ ('users', models.ManyToManyField(to=settings.AUTH_USER_MODEL, verbose_name='User')),
+ ],
+ ),
+ migrations.RemoveField(
+ model_name='phonecall',
+ name='number',
+ ),
+ migrations.AddField(
+ model_name='phonecall',
+ name='callee',
+ field=models.CharField(default='0', max_length=20, verbose_name='Callee'),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name='phonecall',
+ name='caller',
+ field=models.CharField(default='0', max_length=20, verbose_name='Caller'),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name='phonecall',
+ name='data',
+ field=models.TextField(verbose_name='Data', blank=True),
+ ),
+ migrations.AddField(
+ model_name='phonecall',
+ name='start',
+ field=models.DateTimeField(default=now, verbose_name='Start', auto_now_add=True),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name='phonecall',
+ name='stop',
+ field=models.DateTimeField(null=True, verbose_name='Stop', blank=True),
+ ),
+ ]
diff --git a/welco/sources/phone/migrations/__init__.py b/welco/sources/phone/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/welco/sources/phone/models.py b/welco/sources/phone/models.py
index bce7837..b0de123 100644
--- a/welco/sources/phone/models.py
+++ b/welco/sources/phone/models.py
@@ -14,23 +14,23 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-import subprocess
-
from django.core.urlresolvers import reverse
from django.db import models
-from django.db.models.signals import post_save
-from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _
+
class PhoneCall(models.Model):
class Meta:
verbose_name = _('Phone Call')
- number = models.CharField(_('Number'), max_length=20)
-
creation_timestamp = models.DateTimeField(auto_now_add=True)
last_update_timestamp = models.DateTimeField(auto_now=True)
+ caller = models.CharField(_('Caller'), max_length=20)
+ callee = models.CharField(_('Callee'), max_length=20)
+ start = models.DateTimeField(_('Start'), auto_now_add=True)
+ stop = models.DateTimeField(_('Stop'), null=True, blank=True)
+ data = models.TextField(_('Data'), blank=True)
@classmethod
def get_qualification_form_class(cls):
@@ -43,7 +43,38 @@ class PhoneCall(models.Model):
def get_qualification_form_submit_url(cls):
return reverse('qualif-phone-save')
+ @classmethod
+ def get_current_calls(cls, user):
+ return cls.objects.filter(callee__in=PhoneLine.get_callees(user),
+ stop__isnull=True).order_by('start')
+
+ @classmethod
+ def get_all_callees(cls):
+ return cls.objects.values_list('callee', flat=True).distinct()
+
def get_source_context(self, request):
return {
'channel': 'phone',
}
+
+
+class PhoneLine(models.Model):
+ callee = models.CharField(_('Callee'), unique=True, max_length=20)
+ users = models.ManyToManyField('auth.User', verbose_name=_('User'))
+
+ @classmethod
+ def take(cls, callee, user):
+ '''Take a line number'''
+ line, created = cls.objects.get_or_create(callee=callee)
+ line.users.add(user)
+
+ @classmethod
+ def release(cls, callee, user):
+ '''Release a line number'''
+ line, created = cls.objects.get_or_create(callee=callee)
+ line.users.remove(user)
+
+ @classmethod
+ def get_callees(cls, user):
+ '''Return all line numbers watched by user'''
+ return PhoneLine.objects.filter(users=user).values_list('callee', flat=True)
diff --git a/welco/sources/phone/urls.py b/welco/sources/phone/urls.py
new file mode 100644
index 0000000..990d852
--- /dev/null
+++ b/welco/sources/phone/urls.py
@@ -0,0 +1,27 @@
+# welco - multichannel request processing
+# Copyright (C) 2015 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 .
+
+from django.conf.urls import patterns, url
+
+from . import views
+
+urlpatterns = patterns(
+ '',
+ url(r'^api/phone/call-event/$', views.call_event, name='phone-call-event'),
+ url(r'^api/phone/current-calls/$', views.current_calls, name='phone-current-calls'),
+ url(r'^api/phone/take-line/$', views.take_line, name='phone-take-line'),
+ url(r'^api/phone/release-line/$', views.release_line, name='phone-release-line'),
+)
diff --git a/welco/sources/phone/views.py b/welco/sources/phone/views.py
index 01928ff..836a81f 100644
--- a/welco/sources/phone/views.py
+++ b/welco/sources/phone/views.py
@@ -14,11 +14,19 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+import json
+import logging
+
from django import template
from django.contrib.contenttypes.models import ContentType
from django.template import RequestContext
+from django.views.decorators.csrf import csrf_exempt
+from django.contrib.auth.decorators import login_required
+from django.http import HttpResponseBadRequest, HttpResponse
+from django.utils.timezone import now
+
+from .models import PhoneCall, PhoneLine
-from .models import PhoneCall
class Home(object):
source_key = 'phone'
@@ -31,3 +39,145 @@ class Home(object):
context['source_type'] = ContentType.objects.get_for_model(PhoneCall)
tmpl = template.loader.get_template('welco/phone_home.html')
return tmpl.render(context)
+
+
+@csrf_exempt
+def call_event(request):
+ '''Log a new call start or stop, input is JSON:
+
+ {
+ 'event': 'start' or 'stop',
+ 'caller': '003399999999',
+ 'callee': '102',
+ 'data': {
+ 'user': 'zozo',
+ },
+ }
+ '''
+ logger = logging.getLogger(__name__)
+ try:
+ payload = json.loads(request.body)
+ assert isinstance(payload, dict), 'payload is not a JSON object'
+ assert set(payload.keys()) <= set(['event', 'caller', 'callee', 'data']), \
+ 'payload keys must be "event", "caller", "callee" and optionnaly "data"'
+ assert set(['event', 'caller', 'callee']) <= set(payload.keys()), \
+ 'payload keys must be "event", "caller", "callee" and optionnaly "data"'
+ assert payload['event'] in ('start', 'stop'), 'event must be "start" or "stop"'
+ assert isinstance(payload['caller'], unicode), 'caller must be a string'
+ assert isinstance(payload['callee'], unicode), 'callee must be a string'
+ if 'data' in payload:
+ assert isinstance(payload['data'], dict), 'data must be a JSON object'
+ except (TypeError, ValueError, AssertionError), e:
+ return HttpResponseBadRequest(json.dumps({'err': 1, 'msg':
+ unicode(e)}),
+ content_type='application/json')
+ # terminate all existing calls between these two endpoints
+ PhoneCall.objects.filter(caller=payload['caller'],
+ callee=payload['callee'], stop__isnull=True) \
+ .update(stop=now())
+ if payload['event'] == 'start':
+ # start a new call
+ kwargs = {
+ 'caller': payload['caller'],
+ 'callee': payload['callee'],
+ }
+ if 'data' in payload:
+ kwargs['data'] = json.dumps(payload['data'])
+ PhoneCall.objects.create(**kwargs)
+ logger.info('start call from %s to %s', payload['caller'], payload['callee'])
+ else:
+ logger.info('stop call from %s to %s', payload['caller'], payload['callee'])
+ return HttpResponse(json.dumps({'err': 0}), content_type='application/json')
+
+
+@login_required
+def current_calls(request):
+ '''Returns the list of current calls for current user as JSON:
+
+ {
+ 'err': 0,
+ 'data': {
+ 'calls': [
+ {
+ 'caller': '00334545445',
+ 'callee': '102',
+ 'data': { ... },
+ },
+ ...
+ ],
+ 'lines': [
+ '102',
+ ],
+ 'all-lines': [
+ '102',
+ ],
+ }
+ }
+
+ lines are number the user is currently watching, all-lines is all
+ registered numbers.
+ '''
+ all_callees = PhoneCall.get_all_callees()
+ callees = PhoneLine.get_callees(request.user)
+ phonecalls = PhoneCall.get_current_calls(request.user)
+
+ calls = []
+ payload = {
+ 'err': 0,
+ 'data': {
+ 'calls': calls,
+ 'lines': list(callees),
+ 'all-lines': list(all_callees),
+ },
+ }
+ for call in phonecalls:
+ calls.append({
+ 'caller': call.caller,
+ 'callee': call.callee,
+ 'start': call.start.isoformat('T').split('.')[0],
+ })
+ if call.data:
+ calls[-1]['data'] = json.loads(call.data)
+ response = HttpResponse(content_type='application/json')
+ json.dump(payload, response, indent=2)
+ return response
+
+
+@login_required
+def take_line(request):
+ '''Take a line, input is JSON:
+
+ { 'callee': '003369999999' }
+ '''
+ logger = logging.getLogger(__name__)
+ try:
+ payload = json.loads(request.body)
+ assert isinstance(payload, dict), 'payload is not a JSON object'
+ assert payload.keys() == ['callee'], 'payload must have only one key: callee'
+ except (TypeError, ValueError, AssertionError), e:
+ return HttpResponseBadRequest(json.dumps({'err': 1, 'msg':
+ unicode(e)}),
+ content_type='application/json')
+ PhoneLine.take(payload['callee'], request.user)
+ logger.info(u'user %s took line %s', request.user, payload['callee'])
+ return HttpResponse(json.dumps({'err': 0}), content_type='application/json')
+
+
+@login_required
+def release_line(request):
+ '''Release a line, input is JSON:
+
+ { 'callee': '003369999999' }
+ '''
+ logger = logging.getLogger(__name__)
+ try:
+ payload = json.loads(request.body)
+ assert isinstance(payload, dict), 'payload is not a JSON object'
+ assert payload.keys() == ['callee'], 'payload must have only one key: callee'
+ except (TypeError, ValueError, AssertionError), e:
+ return HttpResponseBadRequest(json.dumps({'err': 1, 'msg':
+ unicode(e)}),
+ content_type='application/json')
+ PhoneLine.release(payload['callee'], request.user)
+ logger.info(u'user %s released line %s', request.user, payload['callee'])
+ return HttpResponse(json.dumps({'err': 0}), content_type='application/json')
diff --git a/welco/urls.py b/welco/urls.py
index 6a94e8b..9e26ab2 100644
--- a/welco/urls.py
+++ b/welco/urls.py
@@ -24,6 +24,7 @@ from . import apps
urlpatterns = patterns('',
url(r'^$', 'welco.views.home', name='home'),
url(r'^phone/$', 'welco.views.home_phone', name='home-phone'),
+ url(r'^', include('welco.sources.phone.urls')),
url(r'^ajax/qualification$', 'welco.views.qualification', name='qualif-zone'),
url(r'^ajax/qualification-done$', 'welco.views.qualification_done', name='qualif-done'),
url(r'^ajax/remove-association/(?P\w+)$',