Projet

Général

Profil

0001-add-calendar-cell-model-16393.patch

Josué Kouka, 31 mai 2017 18:11

Télécharger (24,2 ko)

Voir les différences:

Subject: [PATCH] add calendar cell model (#16393)

 combo/apps/chrono/README                         |  18 ++++
 combo/apps/chrono/__init__.py                    |  28 +++++++
 combo/apps/chrono/forms.py                       |  58 +++++++++++++
 combo/apps/chrono/migrations/0001_initial.py     |  43 ++++++++++
 combo/apps/chrono/migrations/__init__.py         |   0
 combo/apps/chrono/models.py                      |  71 ++++++++++++++++
 combo/apps/chrono/static/chrono/css/calendar.css |  23 +++++
 combo/apps/chrono/static/chrono/js/calendar.js   |  12 +++
 combo/apps/chrono/templates/chrono/calendar.html |  11 +++
 combo/apps/chrono/urls.py                        |  24 ++++++
 combo/apps/chrono/utils.py                       |  93 +++++++++++++++++++++
 combo/apps/chrono/views.py                       |  57 +++++++++++++
 combo/settings.py                                |   2 +
 debian/control                                   |   1 +
 setup.py                                         |   1 +
 tests/test_calendar.py                           | 102 +++++++++++++++++++++++
 16 files changed, 544 insertions(+)
 create mode 100644 combo/apps/chrono/README
 create mode 100644 combo/apps/chrono/__init__.py
 create mode 100644 combo/apps/chrono/forms.py
 create mode 100644 combo/apps/chrono/migrations/0001_initial.py
 create mode 100644 combo/apps/chrono/migrations/__init__.py
 create mode 100644 combo/apps/chrono/models.py
 create mode 100644 combo/apps/chrono/static/chrono/css/calendar.css
 create mode 100644 combo/apps/chrono/static/chrono/js/calendar.js
 create mode 100644 combo/apps/chrono/templates/chrono/calendar.html
 create mode 100644 combo/apps/chrono/urls.py
 create mode 100644 combo/apps/chrono/utils.py
 create mode 100644 combo/apps/chrono/views.py
 create mode 100644 tests/test_calendar.py
combo/apps/chrono/README
1
Combo calendar cell
2
=================
3

  
4
To be visible, this cell needs a 'chrono' entry in settings.KNOWN_SERVICES
5

  
6
Example of session_var definition
7

  
8
{
9
    "nusery_id": "southpark",
10
    "date_debut": "$start",
11
    "date_fin": "$end"
12
}
13

  
14
$start and $end will be replaced by event start_datetime and end_datetime
15
and all keys in the session_vars dick will be prefixed by a <session_var_[key]>.
16
The result will be:
17

  
18
session_var_nursery_id=southapark&dsession_var_date_debut=2017-05-19T13:30:00&session_var_date_fin=2017-05-19T15:30:00
combo/apps/chrono/__init__.py
1
# combo - content management system
2
# Copyright (C) 2017  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 django.apps
18

  
19

  
20
class AppConfig(django.apps.AppConfig):
21
    name = 'combo.apps.chrono'
22

  
23
    def get_before_urls(self):
24
        from . import urls
25
        return urls.urlpatterns
26

  
27

  
28
default_app_config = 'combo.apps.chrono.AppConfig'
combo/apps/chrono/forms.py
1
# combo - content management system
2
# Copyright (C) 2017  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 import forms
18

  
19
from .models import CalendarCell
20
from .utils import get_calendar, get_agendas, get_formsdef
21

  
22

  
23
class CalendarCellForm(forms.ModelForm):
24
    class Meta:
25
        model = CalendarCell
26
        fields = (
27
            'title', 'agenda_reference', 'formdef_reference',
28
            'session_vars', 'slot_duration', 'minimal_event_duration',
29
            'business_hour_start', 'business_hour_start')
30

  
31
    def __init__(self, *args, **kwargs):
32
        super(CalendarCellForm, self).__init__(*args, **kwargs)
33
        agenda_references = get_agendas()
34
        formdef_references = get_formsdef()
35
        print(formdef_references)
36
        self.fields['agenda_reference'].widget = forms.Select(choices=agenda_references)
37
        self.fields['formdef_reference'].widget = forms.Select(choices=formdef_references)
38

  
39

  
40
class CalenderForm(forms.Form):
41

  
42
    def __init__(self, *args, **kwargs):
43
        available_slots = kwargs.pop('available_slots')
44
        super(CalenderForm, self).__init__(*args, **kwargs)
45
        self.cleaned_data = {}
46
        calendar = get_calendar(available_slots)
47
        for index, (day, choices) in enumerate(calendar.iteritems()):
48
            self.fields['day_%s' % index] = forms.MultipleChoiceField(
49
                label=day,
50
                widget=forms.CheckboxSelectMultiple(),
51
                required=False, choices=choices)
52

  
53
    def as_div(self):
54
        return self._html_output(
55
            normal_row='<div><p%(html_class_attr)s>%(label)s %(field)s%(help_text)s</p></div>',
56
            error_row='%s', row_ender='</div>',
57
            help_text_html=' <span class="helptext">%s</span>',
58
            errors_on_separate_row=True)
combo/apps/chrono/migrations/0001_initial.py
1
# -*- coding: utf-8 -*-
2
from __future__ import unicode_literals
3

  
4
from django.db import migrations, models
5
import jsonfield.fields
6
import datetime
7

  
8

  
9
class Migration(migrations.Migration):
10

  
11
    dependencies = [
12
        ('auth', '0006_require_contenttypes_0002'),
13
        ('data', '0025_jsoncell_varnames_str'),
14
    ]
15

  
16
    operations = [
17
        migrations.CreateModel(
18
            name='CalendarCell',
19
            fields=[
20
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
21
                ('placeholder', models.CharField(max_length=20)),
22
                ('order', models.PositiveIntegerField()),
23
                ('slug', models.SlugField(verbose_name='Slug', blank=True)),
24
                ('extra_css_class', models.CharField(max_length=100, verbose_name='Extra classes for CSS styling', blank=True)),
25
                ('public', models.BooleanField(default=True, verbose_name='Public')),
26
                ('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')),
27
                ('last_update_timestamp', models.DateTimeField(auto_now=True)),
28
                ('title', models.CharField(max_length=128, verbose_name='Title')),
29
                ('agenda_reference', models.URLField(verbose_name='Events source URL')),
30
                ('formdef_reference', models.URLField(verbose_name='Application form URL')),
31
                ('session_vars', jsonfield.fields.JSONField(default=dict, verbose_name='Session vars')),
32
                ('slot_duration', models.TimeField(default=datetime.time(0, 30), verbose_name='Slot duration')),
33
                ('minimal_event_duration', models.TimeField(default=datetime.time(2, 0), verbose_name='Minimal event duration')),
34
                ('business_hour_start', models.TimeField(default=datetime.time(8, 0), verbose_name='Business hour start')),
35
                ('business_hour_end', models.TimeField(default=datetime.time(18, 0), verbose_name='Business hour end')),
36
                ('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)),
37
                ('page', models.ForeignKey(to='data.Page')),
38
            ],
39
            options={
40
                'verbose_name': 'Calendar Cell',
41
            },
42
        ),
43
    ]
combo/apps/chrono/models.py
1
# combo - content management system
2
# Copyright (C) 2017  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.translation import ugettext_lazy as _
19
from django.utils.dateparse import parse_time
20

  
21
from combo.data.models import CellBase
22
from combo.data.library import register_cell_class
23
from .utils import is_chrono_enabled, get_chrono_events
24

  
25
from jsonfield import JSONField
26

  
27

  
28
@register_cell_class
29
class CalendarCell(CellBase):
30

  
31
    title = models.CharField(_('Title'), max_length=128)
32
    agenda_reference = models.URLField(_('Events source URL'))
33
    formdef_reference = models.URLField(_('Application form URL'))
34
    session_vars = JSONField(_('Session vars'))
35
    slot_duration = models.TimeField(
36
        _('Slot duration'), default=parse_time('00:30'))
37
    minimal_event_duration = models.TimeField(
38
        _('Minimal event duration'), default=parse_time('02:00'))
39
    business_hour_start = models.TimeField(
40
        _('Business hour start'), default=parse_time('08:00'))
41
    business_hour_end = models.TimeField(
42
        _('Business hour end'), default=parse_time('18:00'))
43

  
44
    template_name = 'chrono/calendar.html'
45

  
46
    class Meta:
47
        verbose_name = _('Calendar Cell')
48

  
49
    def get_default_form_class(self):
50
        from .forms import CalendarCellForm
51
        return CalendarCellForm
52

  
53
    class Media:
54
        js = (
55
            'chrono/js/calendar.js'
56
        )
57
        css = {'all': ('chrono/css/calendar.css',)}
58

  
59
    @classmethod
60
    def is_enabled(cls):
61
        return is_chrono_enabled()
62

  
63
    def save(self, *args, **kwargs):
64
        return super(CalendarCell, self).save(*args, **kwargs)
65

  
66
    def render(self, context):
67
        from .forms import CalenderForm
68
        form = CalenderForm(
69
            available_slots=get_chrono_events(self.agenda_reference))
70
        context['form'] = form
71
        return super(CalendarCell, self).render(context)
combo/apps/chrono/static/chrono/css/calendar.css
1
div.calendarcell div {
2
    float: left;
3
}
4

  
5
div.calendarcell input[type=checkbox]{
6
    display: none;
7
}
8

  
9
div.calendarcell li {
10
    list-style-type: none;
11
    background-color: #ccc;
12
    margin: 5px;
13
    padding: 5px 5px;
14
}
15

  
16
div.calendarcell ul {
17
    margin: 0;
18
    padding: 0;
19
}
20

  
21
div,calendarcell input[checked]{
22

  
23
}
combo/apps/chrono/static/chrono/js/calendar.js
1
$(function(){
2
    // check if datetime are part of the same day
3
    var  datetimes = $('input:checked')
4

  
5
    var date = datetimes.first().attr('name');
6
    $('input:checked').each(function(index, value){
7
        if ($(this).name != date){
8
            // unchecked
9
           $(this).prop('checked', false);
10
        }
11
    });
12
});
combo/apps/chrono/templates/chrono/calendar.html
1
{% load i18n %}
2

  
3
<h2>{{title}}</h2>
4

  
5
<div id="calendar_{{cell.pk}}" data-calendar="{{cell.pk}}">
6
    <form method="POST" action="{% url 'chrono-booking' cell.pk %}">
7
        {% csrf_token %}
8
        {{ form.as_div }}
9
        <input type="submit" value="Submit"/>
10
    </form>
11
</div>
combo/apps/chrono/urls.py
1
# combo - content management system
2
# Copyright (C) 2017  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 url
18

  
19
from .views import EventsView, BookingView
20

  
21
urlpatterns = [
22
    url(r'^chrono/events/(?P<pk>[\w,-]+)/', EventsView.as_view(), name='chrono-events'),
23
    url(r'^chrono/book/(?P<pk>[\w,-]+)/', BookingView.as_view(), name='chrono-booking'),
24
]
combo/apps/chrono/utils.py
1
# combo - content management system
2
# Copyright (C) 2017 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
from django.conf import settings
17
from django.utils.dateparse import parse_datetime
18
from django.utils import formats
19

  
20

  
21
from combo.utils import requests
22
from combo.apps.wcs.utils import get_wcs_json
23

  
24

  
25
def get_service(service_name):
26
    if hasattr(settings, 'KNOWN_SERVICES') and settings.KNOWN_SERVICES.get(service_name):
27
        return settings.KNOWN_SERVICES[service_name].values()[0]
28

  
29

  
30
def get_chrono_service():
31
    return get_service('chrono')
32

  
33

  
34
def get_wcs_service():
35
    return get_service('wcs')
36

  
37

  
38
def is_chrono_enabled():
39
    return get_chrono_service()
40

  
41

  
42
def get_agendas():
43
    chrono = get_chrono_service()
44
    url = chrono['url'] + 'api/agenda/'
45
    response = requests.get(url, headers={'accept': 'application/json'})
46
    data = response.json()
47
    agendas = [(item['api']['datetimes_url'], item['text']) for item in data['data']]
48
    return agendas
49

  
50

  
51
def get_formsdef():
52
    wcs = get_wcs_service()
53
    url = wcs['url'] + '/api/formdefs/'
54
    response = requests.get(url, headers={'accept': 'application/json'})
55
    data = response.json()
56
    forms = [(item['url'], item['title']) for item in data]
57
    return forms
58

  
59

  
60
def get_chrono_events(chrono_url, **kwargs):
61
    response = requests.get(
62
        chrono_url,
63
        headers={'accept': 'application/json'},
64
        **kwargs)
65
    return response.json().get('data', [])
66

  
67

  
68
def get_calendar(events):
69
    calendar = {}
70
    for event in events:
71
        day = formats.date_format(parse_datetime(event['datetime']).date())
72
        if day not in calendar:
73
            calendar[day] = []
74
        event_datetime = parse_datetime(event['datetime'])
75
        calendar[day].append((event_datetime.isoformat(), event_datetime.strftime('%H:%M')))
76
    return calendar
77

  
78

  
79
def humanize_date(datetime):
80
    return formats.date_format(
81
        parse_datetime(datetime).date())
82

  
83

  
84
def build_session_vars(cell, data):
85
    session_vars = {}
86
    for key, value in cell.session_vars.items():
87
        key = 'session_var_%s' % key
88
        value = value.strip('$')
89
        if data.get(value):
90
            session_vars.update({key: data[value]})
91
        else:
92
            session_vars.update({key: value})
93
    return session_vars
combo/apps/chrono/views.py
1
# combo - content management system
2
# Copyright (C) 2017  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 urllib
18

  
19
from django.http import HttpResponseRedirect, JsonResponse
20
from django.views.decorators.csrf import csrf_exempt
21
from django.views.generic import View
22
from django.views.generic.detail import SingleObjectMixin
23

  
24
from .models import CalendarCell
25
from .utils import get_chrono_events, build_session_vars
26

  
27

  
28
class EventsView(SingleObjectMixin, View):
29

  
30
    http_method_names = ['get']
31
    model = CalendarCell
32

  
33
    def get(self, request, *args, **kwargs):
34
        cell = self.get_object()
35
        data = get_chrono_events(cell.events_source)
36
        return JsonResponse(data, safe=False)
37

  
38

  
39
class BookingView(SingleObjectMixin, View):
40

  
41
    http_method_names = ['post']
42
    model = CalendarCell
43

  
44
    @csrf_exempt
45
    def dispatch(self, request, *args, **kwargs):
46
        return super(BookingView, self).dispatch(request, *args, **kwargs)
47

  
48
    def post(self, request, *args, **kwargs):
49
        cell = self.get_object()
50
        data = [request.POST.getlist(key) for key in request.POST if key != 'csrfmiddlewaretoken']
51
        data = {
52
            'start': data[0][0],
53
            'end': data[0][-1]
54
        }
55
        params = build_session_vars(cell, data)
56
        url = '%s?%s' % (cell.formdef_reference, urllib.urlencode(params))
57
        return HttpResponseRedirect(url)
combo/settings.py
75 75
    'combo.apps.notifications',
76 76
    'combo.apps.search',
77 77
    'combo.apps.usersearch',
78
    'combo.apps.chrono',
78 79
    'haystack',
79 80
    'xstatic.pkg.chartnew_js',
81
    'xstatic.pkg.fullcalendar',
80 82
)
81 83

  
82 84
INSTALLED_APPS = plugins.register_plugins_apps(INSTALLED_APPS)
debian/control
18 18
    python-xstatic-chartnew-js,
19 19
    python-eopayment (>= 1.9),
20 20
    python-django-haystack (>= 2.4.0)
21
    python-xstatic-fullcalendar (>= 3.4.0)
21 22
Recommends: python-django-mellon, python-whoosh
22 23
Conflicts: python-lingo
23 24
Description: Portal Management System (Python module)
setup.py
116 116
        'djangorestframework>=3.3, <3.4',
117 117
        'django-haystack',
118 118
        'whoosh',
119
        'XStatic-fullcalendar>=3.4.0'
119 120
        ],
120 121
    zip_safe=False,
121 122
    cmdclass={
tests/test_calendar.py
1
import json
2
import urlparse
3

  
4
import pytest
5
import mock
6

  
7
from django.utils.dateparse import parse_time
8

  
9
from combo.data.models import Page
10
from combo.apps.chrono.models import CalendarCell
11

  
12
pytestmark = pytest.mark.django_db
13

  
14
CHRONO_EVENTS = {
15
    "data": [
16
        {
17
            "disabled": True,
18
            "text": "19 mai 2017 08:00",
19
            "api": {
20
                "fillslot_url": "http://example.net/api/agenda/whatever/fillslot/86/"
21
            },
22
            "id": 86,
23
            "datetime": "2017-05-19 08:00:00"
24
        },
25
        {
26
            "disabled": True,
27
            "text": "19 mai 2017 08:30",
28
            "api": {
29
                "fillslot_url": "http://example.net/api/agenda/whatever/fillslot/87/"
30
            },
31
            "id": 87,
32
            "datetime": "2017-05-19 08:30:00"
33
        },
34
        {
35
            "disabled": True,
36
            "text": "19 mai 2017 09:00",
37
            "api": {
38
                "fillslot_url": "http://example.net/api/agenda/whatever/fillslot/88/"
39
            },
40
            "id": 88,
41
            "datetime": "2017-05-19 09:00:00"
42
        },
43
        {
44
            "disabled": True,
45
            "text": "19 mai 2017 09:30",
46
            "api": {
47
                "fillslot_url": "http://example.net/api/agenda/whatever/fillslot/89/"
48
            },
49
            "id": 89,
50
            "datetime": "2017-05-19 09:30:00"
51
        }
52
    ]
53
}
54

  
55

  
56
class MockedRequestResponse(mock.Mock):
57

  
58
    def json(self):
59
        return json.loads(self.content)
60

  
61

  
62
@pytest.fixture
63
def cell(db):
64
    page = Page.objects.create()
65
    cell = CalendarCell(
66
        page=page, title='Example Of Calendar', order=1,
67
        events_source='http://example.net/api/events/',
68
        form_url='http://example.net/form/whatever/',
69
        session_vars={
70
            "start_dt": "$start", "end_dt": "$end",
71
            "whatever_slug": "whatever"
72
        },
73
        slot_duration=parse_time('00:30'),
74
        business_hours_start=parse_time('08:00'),
75
        business_hours_end=parse_time('18:00'),
76
        event_default_title='Available'
77
    )
78
    cell.save()
79
    return cell
80

  
81

  
82
@mock.patch('combo.apps.chrono.utils.requests.get')
83
def test_get_events(mocked_get, app, cell):
84
    mocked_get.return_value = MockedRequestResponse(content=json.dumps(CHRONO_EVENTS))
85
    resp = app.get('/chrono/events/%s/' % cell.pk)
86
    assert len(resp.json) == 4
87
    for datum in resp.json:
88
        assert datum['id'] == 'available'
89
        assert datum['rendering'] == 'inverse-background'
90

  
91

  
92
def test_redirection_session_vars(app, cell, settings):
93
    params = {
94
        'start': '2017-05-19T10:30:23',
95
        'end': '2017-05-19T12:30:14',
96
    }
97
    resp = app.post_json('/chrono/book/%s/' % cell.pk, params=params)
98
    parsed = urlparse.urlparse(resp.json['url'])
99
    qs = urlparse.parse_qs(parsed.query)
100
    assert qs['session_var_whatever_slug'] == ['whatever']
101
    assert qs['session_var_start_dt'] == [params['start']]
102
    assert qs['session_var_end_dt'] == [params['end']]
0
-