Projet

Général

Profil

0004-implement-telphony-models-and-web-services-fixes-878.patch

Benjamin Dauvergne, 28 octobre 2015 18:06

Télécharger (22,9 ko)

Voir les différences:

Subject: [PATCH 4/4] implement telphony models and web services (fixes #8789)

- 2 new models: PhoneCall, PhoneLine
- 4 web-services:
 - call_event
 - current_calls
 - take_line
 - release_line
 tests/test_source_phone.py                         | 185 +++++++++++++++++++++
 welco/sources/phone/migrations/0001_initial.py     |  25 +++
 .../phone/migrations/0002_auto_20151028_1635.py    |  67 ++++++++
 welco/sources/phone/migrations/__init__.py         |   0
 welco/sources/phone/models.py                      |  29 +++-
 welco/sources/phone/urls.py                        |  29 ++++
 welco/sources/phone/views.py                       | 153 ++++++++++++++++-
 welco/urls.py                                      |   1 +
 8 files changed, 480 insertions(+), 9 deletions(-)
 create mode 100644 tests/test_source_phone.py
 create mode 100644 welco/sources/phone/migrations/0001_initial.py
 create mode 100644 welco/sources/phone/migrations/0002_auto_20151028_1635.py
 create mode 100644 welco/sources/phone/migrations/__init__.py
 create mode 100644 welco/sources/phone/urls.py
tests/test_source_phone.py
1
# welco - multichannel request processing
2
# Copyright (C) 2015  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
import json
18

  
19
import pytest
20

  
21
from django.core.urlresolvers import reverse
22

  
23
from welco.sources.phone import models
24

  
25
pytestmark = pytest.mark.django_db
26

  
27

  
28
@pytest.fixture
29
def user():
30
    from django.contrib.auth.models import User
31

  
32
    user = User.objects.create(username='toto')
33
    user.set_password('toto')
34
    user.save()
35
    return user
36

  
37

  
38
def test_call_start_stop(client):
39
    assert models.PhoneCall.objects.count() == 0
40
    payload = {
41
        'event': 'start',
42
        'caller': '0033699999999',
43
        'callee': '102',
44
        'data': {
45
            'user': 'boby.lapointe',
46
        }
47
    }
48
    response = client.post(reverse('phone-call-event'), json.dumps(payload),
49
                           content_type='application/json')
50
    assert response.status_code == 200
51
    assert response['content-type'] == 'application/json'
52
    assert json.loads(response.content) == {'err': 0}
53
    assert models.PhoneCall.objects.count() == 1
54
    assert models.PhoneCall.objects.filter(
55
        caller='0033699999999',
56
        callee='102',
57
        data=json.dumps(payload['data']), stop__isnull=True).count() == 1
58
    # new start event
59
    response = client.post(reverse('phone-call-event'), json.dumps(payload),
60
                           content_type='application/json')
61
    assert response.status_code == 200
62
    assert response['content-type'] == 'application/json'
63
    assert json.loads(response.content) == {'err': 0}
64
    assert models.PhoneCall.objects.count() == 2
65
    assert models.PhoneCall.objects.filter(
66
        caller='0033699999999',
67
        callee='102',
68
        data=json.dumps(payload['data']), stop__isnull=True).count() == 1
69
    # first call has been closed
70
    assert models.PhoneCall.objects.filter(
71
        caller='0033699999999',
72
        callee='102',
73
        data=json.dumps(payload['data']), stop__isnull=False).count() == 1
74
    payload['event'] = 'stop'
75
    response = client.post(reverse('phone-call-event'), json.dumps(payload),
76
                           content_type='application/json')
77
    assert response.status_code == 200
78
    assert response['content-type'] == 'application/json'
79
    assert json.loads(response.content) == {'err': 0}
80
    assert models.PhoneCall.objects.count() == 2
81
    assert models.PhoneCall.objects.filter(
82
        caller='0033699999999',
83
        callee='102',
84
        data=json.dumps(payload['data']), stop__isnull=False).count() == 2
85
    # stop is idempotent
86
    response = client.post(reverse('phone-call-event'), json.dumps(payload),
87
                           content_type='application/json')
88
    assert response.status_code == 200
89
    assert response['content-type'] == 'application/json'
90
    assert json.loads(response.content) == {'err': 0}
91
    assert models.PhoneCall.objects.count() == 2
92
    assert models.PhoneCall.objects.filter(
93
        caller='0033699999999',
94
        callee='102',
95
        data=json.dumps(payload['data']), stop__isnull=False).count() == 2
96

  
97

  
98
def test_current_calls(user, client):
99
    # create some calls
100
    for number in range(0, 10):
101
        payload = {
102
            'event': 'start',
103
            'caller': '00336999999%02d' % number,
104
            'callee': '1%02d' % number,
105
            'data': {
106
                'user': 'boby.lapointe',
107
            }
108
        }
109
        response = client.post(reverse('phone-call-event'), json.dumps(payload),
110
                               content_type='application/json')
111
        assert response.status_code == 200
112
        assert response['content-type'] == 'application/json'
113
        assert json.loads(response.content) == {'err': 0}
114

  
115
    # register user to some lines
116
    # then remove from some
117
    for number in range(0, 10):
118
        models.PhoneLine.take(callee='1%02d' % number, user=user)
119
    for number in range(5, 10):
120
        models.PhoneLine.release(callee='1%02d' % number, user=user)
121
    client.login(username='toto', password='toto')
122
    response = client.get(reverse('phone-current-calls'))
123
    assert response.status_code == 200
124
    assert response['content-type'] == 'application/json'
125
    payload = json.loads(response.content)
126
    assert isinstance(payload, dict)
127
    assert set(payload.keys()) == set(['err', 'data'])
128
    assert payload['err'] == 0
129
    data = payload['data']
130
    assert set(data.keys()) == set(['calls', 'lines', 'all-lines'])
131
    assert isinstance(data['calls'], list)
132
    assert isinstance(data['lines'], list)
133
    assert isinstance(data['all-lines'], list)
134
    assert len(data['calls']) == 5
135
    assert len(data['lines']) == 5
136
    assert len(data['all-lines']) == 10
137
    for call in data['calls']:
138
        assert set(call.keys()) <= set(['caller', 'callee', 'start', 'data'])
139
        assert isinstance(call['caller'], unicode)
140
        assert isinstance(call['callee'], unicode)
141
        assert isinstance(call['start'], unicode)
142
        if 'data' in call:
143
            assert isinstance(call['data'], dict)
144
    assert len([call for call in data['lines'] if isinstance(call, unicode)]) == 5
145
    assert len([call for call in data['all-lines'] if isinstance(call, unicode)]) == 10
146

  
147
    # unregister user to all remaining lines
148
    for number in range(0, 5):
149
        models.PhoneLine.release(callee='1%02d' % number, user=user)
150
    response = client.get(reverse('phone-current-calls'))
151
    assert response.status_code == 200
152
    assert response['content-type'] == 'application/json'
153
    payload = json.loads(response.content)
154
    assert isinstance(payload, dict)
155
    assert set(payload.keys()) == set(['err', 'data'])
156
    assert payload['err'] == 0
157
    assert set(payload['data'].keys()) == set(['calls', 'lines', 'all-lines'])
158
    assert len(payload['data']['calls']) == 0
159
    assert len(payload['data']['lines']) == 0
160
    assert len(payload['data']['all-lines']) == 10
161

  
162

  
163
def test_take_release_line(user, client):
164
    client.login(username='toto', password='toto')
165

  
166
    assert models.PhoneLine.objects.count() == 0
167
    payload = {
168
        'callee': '102',
169
    }
170
    response = client.post(reverse('phone-take-line'), json.dumps(payload),
171
                           content_type='application/json')
172
    assert response.status_code == 200
173
    assert response['content-type'] == 'application/json'
174
    assert json.loads(response.content) == {'err': 0}
175
    assert models.PhoneLine.objects.count() == 1
176
    assert models.PhoneLine.objects.filter(
177
        users=user, callee='102').count() == 1
178
    response = client.post(reverse('phone-release-line'), json.dumps(payload),
179
                           content_type='application/json')
180
    assert response.status_code == 200
181
    assert response['content-type'] == 'application/json'
182
    assert json.loads(response.content) == {'err': 0}
183
    assert models.PhoneLine.objects.count() == 1
184
    assert models.PhoneLine.objects.filter(
185
        users=user, callee='102').count() == 0
welco/sources/phone/migrations/0001_initial.py
1
# -*- coding: utf-8 -*-
2
from __future__ import unicode_literals
3

  
4
from django.db import migrations, models
5

  
6

  
7
class Migration(migrations.Migration):
8

  
9
    dependencies = [
10
    ]
11

  
12
    operations = [
13
        migrations.CreateModel(
14
            name='PhoneCall',
15
            fields=[
16
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
17
                ('number', models.CharField(max_length=20, verbose_name='Number')),
18
                ('creation_timestamp', models.DateTimeField(auto_now_add=True)),
19
                ('last_update_timestamp', models.DateTimeField(auto_now=True)),
20
            ],
21
            options={
22
                'verbose_name': 'Phone Call',
23
            },
24
        ),
25
    ]
welco/sources/phone/migrations/0002_auto_20151028_1635.py
1
# -*- coding: utf-8 -*-
2
from __future__ import unicode_literals
3

  
4
from django.db import migrations, models
5
from django.utils.timezone import utc
6
from django.utils.timezone import now
7
import datetime
8
from django.conf import settings
9

  
10

  
11
class Migration(migrations.Migration):
12

  
13
    dependencies = [
14
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
15
        ('phone', '0001_initial'),
16
    ]
17

  
18
    operations = [
19
        migrations.CreateModel(
20
            name='PhoneLine',
21
            fields=[
22
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
23
                ('callee', models.CharField(unique=True, max_length=20, verbose_name='Callee')),
24
                ('users', models.ManyToManyField(to=settings.AUTH_USER_MODEL, verbose_name='User')),
25
            ],
26
        ),
27
        migrations.RemoveField(
28
            model_name='phonecall',
29
            name='creation_timestamp',
30
        ),
31
        migrations.RemoveField(
32
            model_name='phonecall',
33
            name='last_update_timestamp',
34
        ),
35
        migrations.RemoveField(
36
            model_name='phonecall',
37
            name='number',
38
        ),
39
        migrations.AddField(
40
            model_name='phonecall',
41
            name='callee',
42
            field=models.CharField(default='0', max_length=20, verbose_name='Callee'),
43
            preserve_default=False,
44
        ),
45
        migrations.AddField(
46
            model_name='phonecall',
47
            name='caller',
48
            field=models.CharField(default='0', max_length=20, verbose_name='Caller'),
49
            preserve_default=False,
50
        ),
51
        migrations.AddField(
52
            model_name='phonecall',
53
            name='data',
54
            field=models.TextField(verbose_name='Data', blank=True),
55
        ),
56
        migrations.AddField(
57
            model_name='phonecall',
58
            name='start',
59
            field=models.DateTimeField(default=now, verbose_name='Start', auto_now_add=True),
60
            preserve_default=False,
61
        ),
62
        migrations.AddField(
63
            model_name='phonecall',
64
            name='stop',
65
            field=models.DateTimeField(null=True, verbose_name='Stop', blank=True),
66
        ),
67
    ]
welco/sources/phone/models.py
14 14
# You should have received a copy of the GNU Affero General Public License
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
17
import subprocess
18

  
19 17
from django.core.urlresolvers import reverse
20 18
from django.db import models
21
from django.db.models.signals import post_save
22
from django.dispatch import receiver
23 19
from django.utils.translation import ugettext_lazy as _
24 20

  
21

  
25 22
class PhoneCall(models.Model):
26 23

  
27 24
    class Meta:
28 25
        verbose_name = _('Phone Call')
29 26

  
30
    number = models.CharField(_('Number'), max_length=20)
31

  
32
    creation_timestamp = models.DateTimeField(auto_now_add=True)
33
    last_update_timestamp = models.DateTimeField(auto_now=True)
27
    caller = models.CharField(_('Caller'), max_length=20)
28
    callee = models.CharField(_('Callee'), max_length=20)
29
    start = models.DateTimeField(_('Start'), auto_now_add=True)
30
    stop = models.DateTimeField(_('Stop'), null=True, blank=True)
31
    data = models.TextField(_('Data'), blank=True)
34 32

  
35 33
    @classmethod
36 34
    def get_qualification_form_class(cls):
......
47 45
        return {
48 46
            'channel': 'phone',
49 47
        }
48

  
49

  
50
class PhoneLine(models.Model):
51
    callee = models.CharField(_('Callee'), unique=True, max_length=20)
52
    users = models.ManyToManyField('auth.User', verbose_name=_('User'))
53

  
54
    @classmethod
55
    def take(cls, callee, user):
56
        line, created = cls.objects.get_or_create(callee=callee)
57
        line.users.add(user)
58

  
59
    @classmethod
60
    def release(cls, callee, user):
61
        line, created = cls.objects.get_or_create(callee=callee)
62
        line.users.remove(user)
welco/sources/phone/urls.py
1
# welco - multichannel request processing
2
# Copyright (C) 2015  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.conf.urls import patterns, url
18

  
19
from . import views
20

  
21
urlpatterns = patterns('',
22
                       url(r'^phone/call-event/$', views.call_event,
23
                           name='phone-call-event'),
24
                       url(r'^phone/current-calls/$', views.current_calls,
25
                           name='phone-current-calls'),
26
                       url(r'^phone/take-line/$', views.take_line,
27
                           name='phone-take-line'),
28
                       url(r'^phone/release-line/$', views.release_line,
29
                           name='phone-release-line'),)
welco/sources/phone/views.py
14 14
# You should have received a copy of the GNU Affero General Public License
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
17
import json
18
import logging
19

  
17 20
from django import template
18 21
from django.contrib.contenttypes.models import ContentType
19 22
from django.template import RequestContext
23
from django.views.decorators.csrf import csrf_exempt
24
from django.contrib.auth.decorators import login_required
25
from django.http import HttpResponseBadRequest, HttpResponse
26
from django.utils.timezone import now
27

  
28
from .models import PhoneCall, PhoneLine
20 29

  
21
from .models import PhoneCall
22 30

  
23 31
class Home(object):
24 32
    source_key = 'phone'
......
31 39
        context['source_type'] = ContentType.objects.get_for_model(PhoneCall)
32 40
        tmpl = template.loader.get_template('welco/phone_home.html')
33 41
        return tmpl.render(context)
42

  
43

  
44
@csrf_exempt
45
def call_event(request):
46
    '''Log a new call start or stop, input is JSON:
47

  
48
       {
49
         'event': 'start' or 'stop',
50
         'caller': '003399999999',
51
         'callee': '102',
52
         'data': {
53
           'user': 'zozo',
54
         },
55
       }
56
    '''
57
    logger = logging.getLogger(__name__)
58
    try:
59
        payload = json.loads(request.body)
60
        assert isinstance(payload, dict), 'payload is not a JSON object'
61
        assert set(payload.keys()) <= set(['event', 'caller', 'callee', 'data']), \
62
            'payload keys must be "event", "caller", "callee" and optionnaly "data"'
63
        assert set(['event', 'caller', 'callee']) <= set(payload.keys()), \
64
            'payload keys must be "event", "caller", "callee" and optionnaly "data"'
65
        assert payload['event'] in ('start', 'stop'), 'event must be "start" or "stop"'
66
        assert isinstance(payload['caller'], unicode), 'caller must be a string'
67
        assert isinstance(payload['callee'], unicode), 'callee must be a string'
68
        if 'data' in payload:
69
            assert isinstance(payload['data'], dict), 'data must be a JSON object'
70
    except (TypeError, ValueError, AssertionError), e:
71
        return HttpResponseBadRequest(json.dumps({'err': 1, 'msg':
72
                                                  unicode(e)}),
73
                                      content_type='application/json')
74
    # terminate all existing calls between these two endpoints
75
    PhoneCall.objects.filter(caller=payload['caller'],
76
                             callee=payload['callee'], stop__isnull=True) \
77
        .update(stop=now())
78
    if payload['event'] == 'start':
79
        # start a new call
80
        kwargs = {
81
            'caller': payload['caller'],
82
            'callee': payload['callee'],
83
        }
84
        if 'data' in payload:
85
            kwargs['data'] = json.dumps(payload['data'])
86
        PhoneCall.objects.create(**kwargs)
87
        logger.info('start call from %s to %s', payload['caller'], payload['callee'])
88
    else:
89
        logger.info('stop call from %s to %s', payload['caller'], payload['callee'])
90
    return HttpResponse(json.dumps({'err': 0}), content_type='application/json')
91

  
92

  
93
@login_required
94
def current_calls(request):
95
    '''Returns the list of current calls for current user as JSON:
96

  
97
       {
98
         'err': 0,
99
         'data': {
100
           'calls': [
101
              {
102
                'caller': '00334545445',
103
                'callee': '102',
104
                'data': { ... },
105
              },
106
              ...
107
           ],
108
           'lines': [
109
              '102',
110
           ],
111
           'all-lines': [
112
             '102',
113
           ],
114
         }
115
       }
116

  
117
       lines are number the user is currently watching, all-lines is all
118
       registered numbers.
119
    '''
120
    callees = PhoneLine.objects.filter(users=request.user) \
121
        .values_list('callee', flat=True)
122
    all_callees = PhoneCall.objects.values_list('callee', flat=True).distinct()
123
    phonecalls = PhoneCall.objects.filter(callee__in=callees,
124
                                          stop__isnull=True).order_by('start')
125
    calls = []
126
    payload = {
127
        'err': 0,
128
        'data': {
129
            'calls': calls,
130
            'lines': list(callees),
131
            'all-lines': list(all_callees),
132
        },
133
    }
134
    for call in phonecalls:
135
        calls.append({
136
            'caller': call.caller,
137
            'callee': call.callee,
138
            'start': call.start.isoformat('T').split('.')[0],
139
        })
140
        if call.data:
141
            calls[-1]['data'] = json.loads(call.data)
142
    response = HttpResponse(content_type='application/json')
143
    json.dump(payload, response, indent=2)
144
    return response
145

  
146

  
147
@login_required
148
def take_line(request):
149
    '''Take a line, input is JSON:
150

  
151
       { 'callee': '003369999999' }
152
    '''
153
    logger = logging.getLogger(__name__)
154
    try:
155
        payload = json.loads(request.body)
156
        assert isinstance(payload, dict), 'payload is not a JSON object'
157
        assert payload.keys() == ['callee'], 'payload must have only one key: callee'
158
    except (TypeError, ValueError, AssertionError), e:
159
        return HttpResponseBadRequest(json.dumps({'err': 1, 'msg':
160
                                                  unicode(e)}),
161
                                      content_type='application/json')
162
    PhoneLine.take(payload['callee'], request.user)
163
    logger.info(u'user %s took line %s', request.user, payload['callee'])
164
    return HttpResponse(json.dumps({'err': 0}), content_type='application/json')
165

  
166

  
167
@login_required
168
def release_line(request):
169
    '''Release a line, input is JSON:
170

  
171
       { 'callee': '003369999999' }
172
    '''
173
    logger = logging.getLogger(__name__)
174
    try:
175
        payload = json.loads(request.body)
176
        assert isinstance(payload, dict), 'payload is not a JSON object'
177
        assert payload.keys() == ['callee'], 'payload must have only one key: callee'
178
    except (TypeError, ValueError, AssertionError), e:
179
        return HttpResponseBadRequest(json.dumps({'err': 1, 'msg':
180
                                                  unicode(e)}),
181
                                      content_type='application/json')
182
    PhoneLine.release(payload['callee'], request.user)
183
    logger.info(u'user %s released line %s', request.user, payload['callee'])
184
    return HttpResponse(json.dumps({'err': 0}), content_type='application/json')
welco/urls.py
24 24
urlpatterns = patterns('',
25 25
    url(r'^$', 'welco.views.home', name='home'),
26 26
    url(r'^phone/$', 'welco.views.home_phone', name='home-phone'),
27
    url(r'^phone/', include('welco.sources.phone.urls')),
27 28
    url(r'^ajax/qualification$', 'welco.views.qualification', name='qualif-zone'),
28 29
    url(r'^ajax/qualification-done$', 'welco.views.qualification_done', name='qualif-done'),
29 30
    url(r'^ajax/remove-association/(?P<pk>\w+)$',
30
-