Projet

Général

Profil

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

Josué Kouka, 19 mai 2017 14:24

Télécharger (18,8 ko)

Voir les différences:

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

 combo/apps/chrono/README                         |  10 +++
 combo/apps/chrono/__init__.py                    |  28 +++++++
 combo/apps/chrono/models.py                      |  72 ++++++++++++++++
 combo/apps/chrono/static/chrono/js/calendar.js   |  91 ++++++++++++++++++++
 combo/apps/chrono/templates/chrono/calendar.html |  10 +++
 combo/apps/chrono/urls.py                        |  24 ++++++
 combo/apps/chrono/utils.py                       |  61 ++++++++++++++
 combo/apps/chrono/views.py                       |  53 ++++++++++++
 combo/settings.py                                |   2 +
 tests/test_calendar.py                           | 102 +++++++++++++++++++++++
 10 files changed, 453 insertions(+)
 create mode 100644 combo/apps/chrono/README
 create mode 100644 combo/apps/chrono/__init__.py
 create mode 100644 combo/apps/chrono/models.py
 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
session_vars are defineed such as:
7
{
8
    "my_session_var1_name": "value",
9
    "my_session_var2_name": "ref_to_received_key",
10
}
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/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
import json
18
from datetime import datetime
19

  
20
from django.db import models
21
from django.utils.translation import ugettext_lazy as _
22
from django.utils.dateparse import parse_time
23
from django.core.urlresolvers import reverse
24

  
25
from combo.data.models import CellBase
26
from combo.data.library import register_cell_class
27
from .utils import is_chrono_enabled
28

  
29
from jsonfield import JSONField
30

  
31

  
32
@register_cell_class
33
class CalendarCell(CellBase):
34

  
35
    title = models.CharField(_('Title'), max_length=128)
36
    events_source = models.URLField(_('Events source URL'))
37
    form_url = models.URLField(_('Application form URL'))
38
    session_vars = JSONField(_('Session vars'))
39
    slot_duration = models.TimeField(
40
        _('Slot duration'), default=parse_time('00:30'))
41
    minimal_event_duration = models.TimeField(
42
        _('Minimal event duration'), default=parse_time('02:00'))
43
    business_hours_start = models.TimeField(
44
        _('Business hour start'), default=parse_time('08:00'))
45
    business_hours_end = models.TimeField(
46
        _('Business hour end'), default=parse_time('18:00'))
47
    event_default_title = models.CharField(
48
        _('Event default title'), max_length=32)
49

  
50
    template_name = 'chrono/calendar.html'
51

  
52
    class Meta:
53
        verbose_name = _('Calendar Cell')
54

  
55
    class Media:
56
        js = (
57
            'xstatic/lib/moment.min.js',
58
            'xstatic/fullcalendar.min.js',
59
            'chrono/js/calendar.js'
60
        )
61
        css = {'all': ('xstatic/fullcalendar.min.css',)}
62

  
63
    @classmethod
64
    def is_enabled(cls):
65
        return is_chrono_enabled()
66

  
67
    def render(self, context):
68
        include = ['title', 'slot_duration', 'business_hours_start', 'business_hours_end',
69
                   'minimal_event_duration', 'event_default_title']
70
        context['chrono'] = json.dumps({key: str(value) for key, value in vars(self).items()
71
                                       if key in include})
72
        return super(CalendarCell, self).render(context)
combo/apps/chrono/static/chrono/js/calendar.js
1
$(function() {
2

  
3
    function renderCal(cal_id){
4
        $(cal_id).fullCalendar({
5
            lang: "fr",
6
            header: {
7
                left: "prev,next today",
8
                center: "title",
9
                right: "agendaWeek,agendaDay"
10
            },
11
            defaultView: "agendaWeek",
12
            weekends: false,
13
            allDaySlot: false,
14
            slotDuration: chrono.slot_duration,
15
            forceEventDuration: true,
16
            defaultTimedEventDuration: chrono.default_event_duration,
17
            scrollTime: "08:00",
18
            businessHours: {
19
                start: chrono.business_hours_start,
20
                end: chrono.business_hours_end
21
            },
22
            events: events_source_url,
23
            eventOverlap: function(stillEvent, movingEvent) {
24
                return true;
25
            },
26
            editable: true,
27
            selectable: true,
28
            selectHelper: true,
29
            unselectAuto: false,
30
            selectConstraint: {
31
                 start: chrono.business_hours_start,
32
                 end: chrono.business_hours_end,
33
                id: 'available'
34
            },
35
            select: function(start, end) {
36
                // can't select in the past
37
                if (start.isBefore(moment())){
38
                    $(cal_id).fullCalendar("unselect");
39
                    return false;
40
                }
41
                var title = chrono.event_default_title;
42
                var eventData;
43
                if (title && title.trim()) {
44
                    eventData = {
45
                        title: title,
46
                        start: start,
47
                        end: end
48
                    };
49
                    $(cal_id).fullCalendar("renderEvent", eventData);
50
                }
51
                $(cal_id).fullCalendar("unselect");
52
            },
53
            eventRender: function(event, element) {
54
                var start = moment(event.start).fromNow();
55
                element.attr("title", start);
56
            },
57
            loading: function() {
58
            },
59
            eventClick: function(calEvent, jsEvent, view){
60
                if (calEvent.source){
61
                    console.log(calEvent.source);
62
                    return false;
63
                }
64
                params = {
65
                    start: calEvent.start.format(),
66
                    end: calEvent.end.format()
67
                }
68
                form_url = events_booking_url + "?" + $.param(params)
69
                $.ajax({
70
                    type: "POST",
71
                    url: events_booking_url,
72
                    data: JSON.stringify({
73
                        start: calEvent.start.format(),
74
                        end: calEvent.end.format()
75
                    }),
76
                    dataType: "json",
77
                    success: function(response){
78
                        console.log(response.url);
79
                        window.location = response.url;
80
                    },
81
                    error: function(xhr, status, error){
82
                        console.log(JSON.stringify(status));
83
                    }
84
                })
85
            },
86
        });
87
    }
88
    $(".calendar").each(function(i, cal){
89
        renderCal("#" + cal.id);
90
    });
91
});
combo/apps/chrono/templates/chrono/calendar.html
1
<h2>{{title}}</h2>
2

  
3
<script type="text/javascript">
4
    var events_source_url = "{% url 'chrono-events' cell.pk %}";
5
    var events_booking_url = "{% url 'chrono-booking' cell.pk %}";
6
    var chrono = JSON.parse(unescape('{{chrono | safe }}'));
7
</script>
8

  
9
<div id="calendar_{{cell.pk}}" class="calendar">
10
</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

  
17
from django.conf import settings
18
from combo.utils import requests
19

  
20

  
21
def get_chrono_service():
22
    if hasattr(settings, 'KNOWN_SERVICES') and settings.KNOWN_SERVICES.get('chrono'):
23
        return settings.KNOWN_SERVICES['chrono'].values()[0]
24

  
25

  
26
def is_chrono_enabled():
27
    return get_chrono_service()
28

  
29

  
30
def convert_widget_format(events):
31
    data = []
32
    for event in events.get('data'):
33
        data.append({
34
            'id': 'available',
35
            'title': event.get('text'),
36
            'start': event.get('datetime'),
37
            'editable': False,
38
            'durationEditable': False,
39
            'rendering': 'inverse-background'
40
        })
41
    return data
42

  
43

  
44
def get_chrono_events(chrono_url, **kwargs):
45
    response = requests.get(
46
        chrono_url,
47
        headers={'accept': 'application/json'},
48
        **kwargs)
49
    data = convert_widget_format(response.json())
50
    return data
51

  
52

  
53
def build_session_vars(cell, data):
54
    session_vars = {}
55
    for key, value in cell.session_vars.items():
56
        key = 'session_var_%s' % key
57
        if data.get(value):
58
            session_vars.update({key: data[value]})
59
        else:
60
            session_vars.update({key: value})
61
    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 json
18
from django.http import JsonResponse
19
from django.views.decorators.csrf import csrf_exempt
20
from django.views.generic import View
21
from django.views.generic.detail import SingleObjectMixin
22

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

  
26

  
27
class EventsView(SingleObjectMixin, View):
28

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

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

  
37

  
38
class BookingView(SingleObjectMixin, View):
39

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

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

  
47
    def post(self, request, *args, **kwargs):
48
        data = json.loads(request.body)
49
        cell = self.get_object()
50
        params = build_session_vars(cell, data)
51
        params = '&'.join(['%s=%s' % (key, value) for key, value in params.items()])
52
        url = '%s?%s' % (cell.form_url, params)
53
        return JsonResponse({'url': url}, safe=False)
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)
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
-