booking calendar: display availablilities in rolling days (#19368)
This commit is contained in:
parent
872e23a659
commit
2b44768f5c
|
@ -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)
|
||||
|
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
|
@ -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'),
|
||||
]
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue