booking calendar: display availablilities in rolling days (#19368)

This commit is contained in:
Josue Kouka 2017-10-31 17:13:54 +01:00
parent 872e23a659
commit 2b44768f5c
10 changed files with 224 additions and 114 deletions

View File

@ -29,7 +29,7 @@ class BookingCalendarForm(forms.ModelForm):
model = BookingCalendar
fields = (
'title', 'agenda_reference', 'formdef_reference',
'slot_duration', 'minimal_booking_duration')
'slot_duration', 'minimal_booking_duration', 'days_displayed')
def __init__(self, *args, **kwargs):
super(BookingCalendarForm, self).__init__(*args, **kwargs)

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('calendar', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='bookingcalendar',
name='days_displayed',
field=models.PositiveSmallIntegerField(default=7, verbose_name='Number of days to display'),
),
]

View File

@ -18,11 +18,11 @@ import datetime
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from combo.data.models import CellBase
from combo.data.library import register_cell_class
from .utils import is_chrono_enabled, is_wcs_enabled, get_calendar
from .utils import (is_chrono_enabled, is_wcs_enabled,
add_paginated_calendar_to_context)
@register_cell_class
@ -37,6 +37,7 @@ class BookingCalendar(CellBase):
minimal_booking_duration = models.DurationField(
_('Minimal booking duration'), default=datetime.timedelta(hours=1),
help_text=_('Format is hours:minutes:seconds'))
days_displayed = models.PositiveSmallIntegerField(_('Number of days to display'), default=7)
template_name = 'calendar/booking_calendar_cell.html'
@ -52,16 +53,5 @@ class BookingCalendar(CellBase):
return is_chrono_enabled() and is_wcs_enabled()
def render(self, context):
request = context['request']
page = request.GET.get('week_%s' % self.pk, 1)
# get calendar
calendar = get_calendar(self.agenda_reference, self.slot_duration)
paginator = Paginator(calendar, 1)
try:
cal_page = paginator.page(page)
except PageNotAnInteger:
cal_page = paginator.page(1)
except (EmptyPage,):
cal_page = paginator.page(paginator.num_pages)
context['calendar'] = cal_page
context = add_paginated_calendar_to_context(context)
return super(BookingCalendar, self).render(context)

View File

@ -1,65 +1,8 @@
{% load i18n calendar %}
<div id="cal-{{cell.id}}">
{% if cell.title %}
<h2>{{cell.title}}</h2>
{% endif %}
<div>
{% if calendar.has_other_pages %}
<p class="paginator">
{% if calendar.has_previous %}
<a class="previous" href="?week_{{cell.id}}={{ calendar.previous_page_number }}">{% trans "previous week" %}</a>
{% else %}
<span class="previous">{% trans "previous week" %}</span>
{% endif %}
<span class="current">
{{ calendar.number }} / {{ calendar.paginator.num_pages }}
</span>
{% if calendar.has_next %}
<a class="next" href="?week_{{cell.id}}={{ calendar.next_page_number }}">{% trans "next week" %}</a>
{% else %}
<span class="next">{% trans "next week" %}</span>
{% endif %}
</p>
{% endif %}
{% if calendar %}
<form method="POST" action="{% url 'calendar-booking' cell.id %}?week_{{cell.id}}={{calendar.number}}">
{% csrf_token %}
<table id="cal-table-{{cell.id}}">
{% for cal in calendar %}
<thead>
<tr>
<th></th>
{% for day in cal.get_days %}
<th>{{day|date:"SHORT_DATE_FORMAT"}}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for slot in cal.get_slots %}
<tr>
<th>{{slot|date:"TIME_FORMAT"}}</th>
{% for day in cal.get_days %}
{% get_day_slot cal day=day slot=slot as value %}
{% if not value.exist %}
<td class="absent"></td>
{% elif value.available %}
<td class="available">
<input type="checkbox" name="slots" value="{{value.label}}" id="slot-{{cell.id}}-{{value.label}}"/>
<label for="slot-{{cell.id}}-{{value.label}}"></label>
</td>
{% else %}
<td class="unavailable"></td>
{% endif %}
{% endfor %}
</tr>
{% endfor %}
</tbody>
{% endfor %}
</table>
<button class="submit-button">{% trans "Book" context "booking" %}</button>
</form>
{% endif %}
</div>
{% if cell.title %}
<h2>{{cell.title}}</h2>
{% endif %}
<div class="calcontent">
{% include 'calendar/booking_calendar_content.html' %}
</div>

View File

@ -0,0 +1,57 @@
{% load i18n calendar %}
{% if calendar_days.has_other_pages %}
<p class="paginator">
{% if calendar_days.has_previous %}
<a class="previous calchunk" href="?chunk_{{ cell.pk }}={{ calendar_days.previous_page_number }}" data-content-url="{% url 'ajax-calendar-content' pk=cell.pk %}?chunk_{{cell.pk}}={{ calendar_days.previous_page_number }}">{% trans "previous" %}</a>
{% else %}
<span class="previous">{% trans "previous" %}</span>
{% endif %}
<span class="current">
{{ calendar_days.number }} / {{ calendar_days.paginator.num_pages }}
</span>
{% if calendar_days.has_next %}
<a class="next calchunk" href="?chunk_{{ cell.pk }}={{ calendar_days.next_page_number }}" data-content-url="{% url 'ajax-calendar-content' pk=cell.pk %}?chunk_{{cell.pk}}={{ calendar_days.next_page_number }}">{% trans "next" %}</a>
{% else %}
<span class="next">{% trans "next" %}</span>
{% endif %}
</p>
{% endif %}
{% if calendar_days %}
<form method="POST" action="{% url 'calendar-booking' pk=cell.pk %}?chunk_{{cell.pk}}={{calendar_days.number}}">
{% csrf_token %}
<table id="cal-table-{{cell.pk}}">
<thead>
<tr>
<th></th>
{% for day in calendar_days %}
<th>{{day|date:"SHORT_DATE_FORMAT"}}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for slot in calendar_slots %}
<tr>
<th>{{slot|date:"TIME_FORMAT"}}</th>
{% for day in calendar_days %}
{% get_day_slot calendar day=day slot=slot as value %}
{% if not value.exist %}
<td class="absent"></td>
{% elif value.available %}
<td class="available">
<input type="checkbox" name="slots" value="{{value.label}}" id="slot-{{cell.pk}}-{{value.label}}"/>
<label for="slot-{{cell.pk}}-{{value.label}}"></label>
</td>
{% else %}
<td class="unavailable"></td>
{% endif %}
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
<button class="submit-button">{% trans "Book" context "booking" %}</button>
</form>
{% endif %}

View File

@ -16,8 +16,9 @@
from django.conf.urls import url
from .views import BookingView
from .views import BookingView, CalendarContentAjaxView
urlpatterns = [
url(r'^calendar/book/(?P<pk>[\w,-]+)/', BookingView.as_view(), name='calendar-booking'),
url(r'^ajax/calendar/content/(?P<pk>\w+)/$', CalendarContentAjaxView.as_view(), name='ajax-calendar-content'),
]

View File

@ -14,10 +14,13 @@
# 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/>.
import urllib
import datetime
import math
import urllib
from django.conf import settings
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.utils.dateparse import parse_datetime
from combo.utils import requests
@ -74,28 +77,41 @@ def get_chrono_events(agenda_reference):
return result.get('data', [])
def get_calendar(agenda_reference, offset):
def add_paginated_calendar_to_context(context):
request = context['request']
cell = context['cell']
page = request.GET.get('chunk_%s' % cell.pk, 1)
calendar = get_calendar(cell.agenda_reference, cell.slot_duration, cell.days_displayed)
paginator = Paginator(calendar.get_computed_days(), cell.days_displayed)
try:
cal_page = paginator.page(page)
except PageNotAnInteger:
cal_page = paginator.page(1)
except (EmptyPage,):
cal_page = paginator.page(paginator.num_pages)
context['calendar'] = calendar
context['calendar_days'] = cal_page
context['calendar_slots'] = calendar.get_slots()
return context
def get_calendar(agenda_reference, offset, days_displayed):
if not agenda_reference:
return []
events = get_chrono_events(agenda_reference)
calendar = {}
calendar = Calendar(offset, days_displayed)
for event in events:
event_datetime = parse_datetime(event['datetime'])
week_ref = event_datetime.isocalendar()[1]
if week_ref not in calendar:
calendar[week_ref] = WeekCalendar(week_ref, offset)
week_cal = calendar[week_ref]
# add day to week calendar
if not week_cal.has_day(event_datetime.date()):
if not calendar.has_day(event_datetime.date()):
day = WeekDay(event_datetime.date())
week_cal.days.append(day)
calendar.days.append(day)
else:
day = week_cal.get_day(event_datetime.date())
day = calendar.get_day(event_datetime.date())
# add slots to day
day.add_slots(DaySlot(
event_datetime, True if not event.get('disabled', True) else False))
return sorted(calendar.values(), key=lambda x: x.week)
return calendar
def get_form_url_with_params(cell, data):
@ -152,15 +168,15 @@ class WeekDay(object):
return max(self.slots, key=lambda x: x.date_time.time())
class WeekCalendar(object):
class Calendar(object):
def __init__(self, week, offset):
self.week = week
def __init__(self, offset, days_displayed):
self.offset = offset
self.days_displayed = days_displayed
self.days = []
def __repr__(self):
return '<WeekCalendar - %s>' % self.week
return '<Calendar>'
def get_slots(self):
start = self.get_minimum_slot()
@ -171,15 +187,18 @@ class WeekCalendar(object):
datetime.date.today(), start) + self.offset
start = start.time()
def get_days(self):
if self.days:
base_day = self.days[0].date
else:
base_day = datetime.datetime.today()
# only week days
for index in range(0, 5):
day = base_day + datetime.timedelta(days=index - base_day.weekday())
yield day
def get_computed_days(self):
if not self.days:
return []
computed_days = []
base_day = self.days[0].date
days_diff = (self.days[-1].date - self.days[0].date).days
# find a number which ensures calendar days are equally chunked
days_range = int(self.days_displayed * math.ceil(float(days_diff + 1) / self.days_displayed))
for index in range(days_range):
day = base_day + datetime.timedelta(days=index)
computed_days.append(day)
return computed_days
def get_day(self, date):
for day in self.days:

View File

@ -14,14 +14,14 @@
# 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.http import HttpResponseRedirect
from django.views.generic import View
from django.views.generic.detail import SingleObjectMixin
from django.contrib import messages
from django.http import HttpResponseRedirect
from django.views.generic import View, DetailView
from django.views.generic.detail import SingleObjectMixin
from .models import BookingCalendar
from .utils import get_form_url_with_params
from .forms import BookingForm
from .models import BookingCalendar
from .utils import get_form_url_with_params, add_paginated_calendar_to_context
class BookingView(SingleObjectMixin, View):
@ -42,3 +42,15 @@ class BookingView(SingleObjectMixin, View):
data = form.cleaned_data
url = get_form_url_with_params(cell, data)
return HttpResponseRedirect(url)
class CalendarContentAjaxView(DetailView):
model = BookingCalendar
template_name = 'calendar/booking_calendar_content.html'
def get_context_data(self, **kwargs):
context = super(CalendarContentAjaxView, self).get_context_data(**kwargs)
context['cell'] = self.object
context['request'] = self.request
context = add_paginated_calendar_to_context(context)
return context

View File

@ -206,9 +206,29 @@ $(function() {
});
});
$(function() {
$('body').on('click', 'a.calchunk', function(event){
event.preventDefault();
var $elem = $(this);
var url = $elem.data('content-url');
$.ajax({
url: url,
async: true,
dataType: 'html',
crossDomain: true,
success: function(data){
$elem.closest('div.calcontent').html(data);
},
error: function(error){
console.log(':(', error);
}
});
});
});
$('.bookingcalendar table').each(function(idx, elem) { set_booking_calendar_sensitivity(elem); });
$('.bookingcalendar input').on('change', function() {
$('body').on('change', '.bookingcalendar input', function() {
set_booking_calendar_sensitivity($(this).parents('table'));
});
});

View File

@ -5,7 +5,6 @@ import datetime
import pytest
import mock
from django.utils.dateparse import parse_time
from django.contrib.auth.models import User
from combo.data.models import Page
@ -222,12 +221,12 @@ def test_cell_rendering(mocked_get, client, cell):
@mock.patch('combo.apps.calendar.utils.requests.get', side_effect=mocked_requests_get)
def test_calendar(mocked_get, cell):
cal = get_calendar('default:whatever', cell.slot_duration)[0]
cal = get_calendar('default:whatever', cell.slot_duration, 7)
assert len(cal.days) == 3
for day in cal.get_days():
for day in cal.get_computed_days():
assert day in [
str2datetime('2017-06-12T08:00:00').date() + datetime.timedelta(days=i)
for i in range(0, 5)]
str2datetime('2017-06-13T08:00:00').date() + datetime.timedelta(days=i)
for i in range(0, 7)]
min_slot = str2datetime('2017-06-13T08:00:00')
max_slot = str2datetime('2017-06-14T15:00:00')
for slot in cal.get_slots():
@ -237,3 +236,53 @@ def test_calendar(mocked_get, cell):
assert cal.get_minimum_slot() == min_slot.time()
assert cal.get_maximum_slot() == max_slot.time()
assert cal.get_day(max_slot.date()).slots[-1].available is False
@mock.patch('combo.apps.calendar.utils.requests.get', side_effect=mocked_requests_get)
def test_cell_pagination(mocked_get, client, cell):
cell.days_displayed = 2
cell.save()
page = client.get('/booking/')
# first page
table = page.html.find('table')
thead_th = table.thead.findChildren('th')
assert len(thead_th) == 3
for th in thead_th:
if th.text:
assert th.text in ('06/13/2017', '06/14/2017')
links = page.html.findAll('a')
assert len(links) == 2
next_page_link = links[1]
assert next_page_link.text == 'next'
assert next_page_link.attrs['href'] == '?chunk_%d=2' % cell.pk
assert next_page_link.attrs['data-content-url'] == '/ajax/calendar/content/%d/?chunk_%d=2' % (cell.pk, cell.pk)
# second page without ajax call
page2 = client.get('/booking/?chunk_%d=2' % cell.pk)
table = page2.html.find('table')
thead_th = table.thead.findChildren('th')
assert len(thead_th) == 3
for th in thead_th:
if th.text:
assert th.text in ('06/15/2017', '06/16/2017')
links = page2.html.findAll('a')
assert len(links) == 2
previous_page_link = links[1]
assert previous_page_link.text == 'previous'
assert previous_page_link.attrs['href'] == '?chunk_%d=1' % cell.pk
assert previous_page_link.attrs['data-content-url'] == '/ajax/calendar/content/%d/?chunk_%d=1' % (cell.pk, cell.pk)
# second page through ajax call
page = client.get('/booking/?chunk_%d=1')
page2 = client.get(next_page_link.attrs['data-content-url'])
table = page2.html.find('table')
thead_th = table.thead.findChildren('th')
assert len(thead_th) == 3
for th in thead_th:
if th.text:
assert th.text in ('06/15/2017', '06/16/2017')
links = page2.html.findAll('a')
assert len(links) == 1
previous_page_link = links[0]
assert previous_page_link.text == 'previous'
assert previous_page_link.attrs['data-content-url'] == '/ajax/calendar/content/%d/?chunk_%d=1' % (cell.pk, cell.pk)