plages libres #89
|
@ -0,0 +1,32 @@
|
|||
# Generated by Django 3.2.18 on 2023-05-31 08:16
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('agendas', '0153_event_index'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='agenda',
|
||||
name='partial_bookings',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='booking',
|
||||
name='end_time',
|
||||
field=models.TimeField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='booking',
|
||||
name='start_time',
|
||||
field=models.TimeField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='end_time',
|
||||
field=models.TimeField(null=True, verbose_name='End time'),
|
||||
),
|
||||
]
|
|
@ -289,6 +289,7 @@ class Agenda(models.Model):
|
|||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
partial_bookings = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
ordering = ['label']
|
||||
|
@ -1934,6 +1935,7 @@ class Event(models.Model):
|
|||
|
||||
agenda = models.ForeignKey(Agenda, on_delete=models.CASCADE)
|
||||
start_datetime = models.DateTimeField(_('Date/time'))
|
||||
end_time = models.TimeField(_('End time'), null=True)
|
||||
recurrence_days = ArrayField(
|
||||
models.IntegerField(choices=WEEKDAY_CHOICES),
|
||||
verbose_name=_('Recurrence days'),
|
||||
|
@ -2289,6 +2291,11 @@ class Event(models.Model):
|
|||
|
||||
@property
|
||||
def end_datetime(self):
|
||||
if self.end_time:
|
||||
return localtime(self.start_datetime).replace(
|
||||
hour=self.end_time.hour, minute=self.end_time.minute
|
||||
)
|
||||
|
||||
if self.meeting_type:
|
||||
minutes = self.meeting_type.duration
|
||||
else:
|
||||
|
@ -2429,6 +2436,7 @@ class Event(models.Model):
|
|||
agenda=self.agenda,
|
||||
primary_event=self,
|
||||
slug=self.slug,
|
||||
end_time=self.end_time,
|
||||
duration=self.duration,
|
||||
places=self.places,
|
||||
waiting_list_places=self.waiting_list_places,
|
||||
|
@ -2665,6 +2673,9 @@ class Booking(models.Model):
|
|||
absence_callback_url = models.URLField(blank=True)
|
||||
color = models.ForeignKey(BookingColor, null=True, on_delete=models.SET_NULL, related_name='bookings')
|
||||
|
||||
start_time = models.TimeField(null=True)
|
||||
end_time = models.TimeField(null=True)
|
||||
|
||||
@property
|
||||
def user_name(self):
|
||||
return ('%s %s' % (self.user_first_name, self.user_last_name)).strip()
|
||||
|
|
|
@ -188,6 +188,18 @@ class RecurringFillslotsSerializer(MultipleAgendasEventsFillSlotsSerializer):
|
|||
check_overlaps = CommaSeparatedStringField(
|
||||
required=False, child=serializers.SlugField(max_length=160, allow_blank=False)
|
||||
)
|
||||
start_time = serializers.TimeField(required=False)
|
||||
end_time = serializers.TimeField(required=False)
|
||||
|
||||
def validate(self, attrs):
|
||||
super().validate(attrs)
|
||||
use_partial_bookings = any(agenda.partial_bookings for agenda in self.context['agendas'])
|
||||
if use_partial_bookings:
|
||||
if not attrs.get('start_time') or not attrs.get('end_time'):
|
||||
raise ValidationError(_('must include start_time and end_time for partial bookings agenda'))
|
||||
if attrs['start_time'] > attrs['end_time']:
|
||||
raise ValidationError(_('start_time must be before end_time'))
|
||||
return attrs
|
||||
|
||||
def validate_slots(self, value):
|
||||
super().validate_slots(value)
|
||||
|
@ -401,6 +413,14 @@ class AgendaOrSubscribedSlugsMixin(AgendaSlugsMixin):
|
|||
attrs['agenda_slugs'] = [agenda.slug for agenda in agendas]
|
||||
else:
|
||||
attrs['agenda_slugs'] = self.agenda_slugs
|
||||
|
||||
if any(
|
||||
agenda.partial_bookings != attrs['agendas'][0].partial_bookings for agenda in attrs['agendas']
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
{'agendas': _('Cannot mix partial bookings agendas with other kinds.')}
|
||||
)
|
||||
|
||||
return attrs
|
||||
|
||||
def validate_agendas(self, value):
|
||||
|
|
|
@ -461,6 +461,8 @@ def make_booking(event, payload, extra_data, primary_booking=None, in_waiting_li
|
|||
user_display_label=payload.get('user_display_label', ''),
|
||||
extra_emails=payload.get('extra_emails', []),
|
||||
extra_phone_numbers=payload.get('extra_phone_numbers', []),
|
||||
start_time=payload.get('start_time'),
|
||||
end_time=payload.get('end_time'),
|
||||
extra_data=extra_data,
|
||||
color=color,
|
||||
)
|
||||
|
|
|
@ -79,10 +79,22 @@ class AgendaAddForm(forms.ModelForm):
|
|||
edit_role = forms.ModelChoiceField(label=_('Edit Role'), required=False, queryset=get_role_queryset())
|
||||
view_role = forms.ModelChoiceField(label=_('View Role'), required=False, queryset=get_role_queryset())
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if settings.PARTIAL_BOOKINGS_ENABLED:
|
||||
self.fields['kind'].choices += [('partial-bookings', _('Partial bookings'))]
|
||||
|
||||
class Meta:
|
||||
model = Agenda
|
||||
fields = ['label', 'kind', 'category', 'edit_role', 'view_role']
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if self.cleaned_data.get('kind') == 'partial-bookings':
|
||||
self.cleaned_data['kind'] = 'events'
|
||||
self.instance.partial_bookings = True
|
||||
self.instance.default_view = 'day'
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
create = self.instance.pk is None
|
||||
super().save()
|
||||
|
@ -118,6 +130,8 @@ class AgendaEditForm(forms.ModelForm):
|
|||
else:
|
||||
if not EventsType.objects.exists():
|
||||
del self.fields['events_type']
|
||||
if kwargs['instance'].partial_bookings:
|
||||
del self.fields['default_view']
|
||||
|
||||
|
||||
class AgendaBookingDelaysForm(forms.ModelForm):
|
||||
|
@ -187,6 +201,7 @@ class NewEventForm(forms.ModelForm):
|
|||
fields = [
|
||||
'label',
|
||||
'start_datetime',
|
||||
'end_time',
|
||||
'frequency',
|
||||
'recurrence_days',
|
||||
'recurrence_week_interval',
|
||||
|
@ -199,14 +214,26 @@ class NewEventForm(forms.ModelForm):
|
|||
}
|
||||
widgets = {
|
||||
'recurrence_end_date': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
|
||||
'end_time': widgets.TimeWidget,
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.instance.agenda.partial_bookings:
|
||||
del self.fields['duration']
|
||||
else:
|
||||
del self.fields['end_time']
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if self.cleaned_data.get('frequency') == 'unique':
|
||||
self.cleaned_data['recurrence_days'] = None
|
||||
self.cleaned_data['recurrence_end_date'] = None
|
||||
|
||||
end_time = self.cleaned_data.get('end_time')
|
||||
if end_time and self.cleaned_data['start_datetime'].time() > end_time:
|
||||
self.add_error('end_time', _('End time must be greater than start time.'))
|
||||
|
||||
def clean_start_datetime(self):
|
||||
start_datetime = self.cleaned_data['start_datetime']
|
||||
if start_datetime.year < 2000:
|
||||
|
@ -242,6 +269,7 @@ class EventForm(NewEventForm):
|
|||
protected_fields = (
|
||||
'slug',
|
||||
'start_datetime',
|
||||
'end_time',
|
||||
'frequency',
|
||||
'recurrence_days',
|
||||
'recurrence_week_interval',
|
||||
|
@ -251,11 +279,13 @@ class EventForm(NewEventForm):
|
|||
model = Event
|
||||
widgets = {
|
||||
'recurrence_end_date': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
|
||||
'end_time': widgets.TimeWidget,
|
||||
}
|
||||
fields = [
|
||||
'label',
|
||||
'slug',
|
||||
'start_datetime',
|
||||
'end_time',
|
||||
'frequency',
|
||||
'recurrence_days',
|
||||
'recurrence_week_interval',
|
||||
|
@ -284,6 +314,8 @@ class EventForm(NewEventForm):
|
|||
)
|
||||
if self.instance.recurrence_days and self.instance.has_recurrences_booked():
|
||||
for field in self.protected_fields:
|
||||
if field not in self.fields:
|
||||
continue
|
||||
self.fields[field].disabled = True
|
||||
self.fields[field].help_text = _(
|
||||
'This field cannot be modified because some recurrences have bookings attached to them.'
|
||||
|
@ -1245,7 +1277,7 @@ class ImportEventsForm(forms.Form):
|
|||
raise ValidationError(_('Invalid file format. (duration, {event_no} event)'), i)
|
||||
|
||||
try:
|
||||
event.full_clean(exclude=['desk', 'meeting_type', 'primary_event'])
|
||||
event.full_clean(exclude=['desk', 'meeting_type', 'primary_event', 'end_time'])
|
||||
except ValidationError as e:
|
||||
errors = [_('Invalid file format:\n')]
|
||||
for label, field_errors in e.message_dict.items():
|
||||
|
|
|
@ -597,3 +597,10 @@ div#appbar a.active {
|
|||
background: #386ede;
|
||||
color: white;
|
||||
}
|
||||
|
||||
table.partial-bookings {
|
||||
border-spacing: 0;
|
||||
td.hour-cell {
|
||||
outline: solid 1px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,14 +3,16 @@
|
|||
{% now "m" as today_month %}
|
||||
{% now "j" as today_day %}
|
||||
{% now "Ymj" as today %}
|
||||
{% if not no_opened and agenda.kind == 'events' %}
|
||||
<a href="{% url 'chrono-manager-agenda-open-events-view' pk=agenda.pk %}">{% trans 'Open events' %}</a>
|
||||
{% if not agenda.partial_bookings %}
|
||||
{% if not no_opened and agenda.kind == 'events' %}
|
||||
<a href="{% url 'chrono-manager-agenda-open-events-view' pk=agenda.pk %}">{% trans 'Open events' %}</a>
|
||||
{% endif %}
|
||||
<span class="buttons-group">
|
||||
<a {% if active == 'day' %}class="active"{% endif%} href="{% url 'chrono-manager-agenda-day-view' pk=agenda.pk year=view.date|date:"Y"|default:today_year|default:today_year month=view.date|date:"m"|default:today_month day=view.date|date:"d"|default:today_day %}">{% trans 'Day' %}</a>
|
||||
<a {% if active == 'week' %}class="active"{% endif%} href="{% url 'chrono-manager-agenda-week-view' pk=agenda.pk year=view.date|date:"Y"|default:today_year|default:today_year month=view.date|date:"m"|default:today_month day=view.date|date:"d"|default:today_day %}">{% trans 'Week' %}</a>
|
||||
<a {% if active == 'month' %}class="active"{% endif%} href="{% url 'chrono-manager-agenda-month-view' pk=agenda.pk year=view.date|date:"Y"|default:today_year|default:today_year month=view.date|date:"m"|default:today_month day=view.date|date:"d"|default:today_day %}">{% trans 'Month' %}</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
<span class="buttons-group">
|
||||
<a {% if active == 'day' %}class="active"{% endif%} href="{% url 'chrono-manager-agenda-day-view' pk=agenda.pk year=view.date|date:"Y"|default:today_year|default:today_year month=view.date|date:"m"|default:today_month day=view.date|date:"d"|default:today_day %}">{% trans 'Day' %}</a>
|
||||
<a {% if active == 'week' %}class="active"{% endif%} href="{% url 'chrono-manager-agenda-week-view' pk=agenda.pk year=view.date|date:"Y"|default:today_year|default:today_year month=view.date|date:"m"|default:today_month day=view.date|date:"d"|default:today_day %}">{% trans 'Week' %}</a>
|
||||
<a {% if active == 'month' %}class="active"{% endif%} href="{% url 'chrono-manager-agenda-month-view' pk=agenda.pk year=view.date|date:"Y"|default:today_year|default:today_year month=view.date|date:"m"|default:today_month day=view.date|date:"d"|default:today_day %}">{% trans 'Month' %}</a>
|
||||
</span>
|
||||
{% if not no_today %}
|
||||
<a {% if active == 'day' and view.date|date:"Ymj" == today %}class="active"{% endif%} href="{% url 'chrono-manager-agenda-day-view' pk=agenda.pk year=today_year month=today_month day=today_day %}">{% trans 'Today' %}</a>
|
||||
{% endif %}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
{% extends "chrono/manager_agenda_day_view.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% if not hours %}
|
||||
<div>
|
||||
<p>{% trans "No opening hours this day." %}</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<table class="agenda-table partial-bookings">
|
||||
|
||||
<thead>
|
||||
<tr>
|
||||
<td></td>
|
||||
{% for hour in hours %}
|
||||
<th>{{ hour|date:"TIME_FORMAT" }}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for user, bookings in bookings_by_user.items %}
|
||||
<tr class="{% cycle 'odd' 'even' %}">
|
||||
<th>{{ bookings.0.get_user_block }}</th>
|
||||
{% for _ in hours %}
|
||||
<td class="hour-cell">
|
||||
{% if forloop.first %}
|
||||
{% for booking in bookings %}
|
||||
<div class="booking"
|
||||
style="left: {{ booking.css_left }}%; width: {{ booking.css_width }}%;"
|
||||
>{{ booking.start_time }} - {{ booking.end_time }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -1370,6 +1370,8 @@ class AgendaDayView(AgendaDateView, DayArchiveView):
|
|||
def get_template_names(self):
|
||||
if self.agenda.kind == 'virtual':
|
||||
return ['chrono/manager_meetings_agenda_day_view.html']
|
||||
if self.agenda.partial_bookings:
|
||||
return ['chrono/manager_partial_bookings_day_view.html']
|
||||
return ['chrono/manager_%s_agenda_day_view.html' % self.agenda.kind]
|
||||
|
||||
def get_previous_day_url(self):
|
||||
|
@ -1487,6 +1489,37 @@ class AgendaDayView(AgendaDateView, DayArchiveView):
|
|||
current_date += interval
|
||||
first = False
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
if self.agenda.partial_bookings:
|
||||
self.fill_partial_bookings_context(context)
|
||||
return context
|
||||
|
||||
def fill_partial_bookings_context(self, context):
|
||||
events = self.agenda.event_set.filter(start_datetime__date=self.date.date())
|
||||
if not events.exists():
|
||||
return
|
||||
|
||||
event_times = events.aggregate(Min('start_datetime'), Max('end_time'))
|
||||
min_time = localtime(event_times['start_datetime__min']).time()
|
||||
max_time = event_times['end_time__max']
|
||||
|
||||
start_time = datetime.time(min_time.hour - 1, 0)
|
||||
end_time = datetime.time(max_time.hour + 2, 0)
|
||||
context['hours'] = [datetime.time(hour=i) for i in range(start_time.hour, end_time.hour)]
|
||||
|
||||
def get_time_ratio(t1, t2):
|
||||
return 100 * ((t1.hour - t2.hour) * 60 + t1.minute - t2.minute) // 60
|
||||
|
||||
bookings = Booking.objects.filter(event__in=events)
|
||||
bookings_by_user = collections.defaultdict(list)
|
||||
for booking in bookings:
|
||||
booking.css_left = get_time_ratio(booking.start_time, start_time)
|
||||
booking.css_width = get_time_ratio(booking.end_time, booking.start_time)
|
||||
bookings_by_user[booking.user_external_id].append(booking)
|
||||
|
||||
context['bookings_by_user'] = dict(bookings_by_user)
|
||||
|
||||
|
||||
agenda_day_view = AgendaDayView.as_view()
|
||||
|
||||
|
|
|
@ -199,6 +199,7 @@ REST_FRAMEWORK = {'EXCEPTION_HANDLER': 'chrono.api.utils.exception_handler'}
|
|||
|
||||
SHARED_CUSTODY_ENABLED = False
|
||||
LEGACY_FILLSLOTS_ENABLED = False
|
||||
PARTIAL_BOOKINGS_ENABLED = False
|
||||
|
||||
local_settings_file = os.environ.get(
|
||||
'CHRONO_SETTINGS_FILE', os.path.join(os.path.dirname(__file__), 'local_settings.py')
|
||||
|
|
|
@ -1555,3 +1555,16 @@ def test_events_datetimes_max_booking_datetime_with_minimal_booking_time_to_none
|
|||
resp = app.get(api_url)
|
||||
assert resp.json['data'][-2]['datetime'] == '2023-04-05 09:00:00'
|
||||
assert resp.json['data'][-1]['datetime'] == '2023-04-05 11:00:00'
|
||||
|
||||
|
||||
@pytest.mark.freeze_time('2023-05-01')
|
||||
def test_events_datetime_partial_bookings_end_datetime(app, user):
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='events', partial_bookings=True)
|
||||
start_datetime = make_aware(datetime.datetime(2023, 5, 2, 8, 0))
|
||||
Event.objects.create(
|
||||
label='Event', start_datetime=start_datetime, end_time=datetime.time(18, 00), places=10, agenda=agenda
|
||||
)
|
||||
|
||||
resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug)
|
||||
assert resp.json['data'][0]['datetime'] == '2023-05-02 08:00:00'
|
||||
assert resp.json['data'][0]['end_datetime'] == '2023-05-02 18:00:00'
|
||||
|
|
|
@ -16,7 +16,7 @@ from chrono.agendas.models import (
|
|||
SharedCustodyRule,
|
||||
Subscription,
|
||||
)
|
||||
from chrono.utils.timezone import now
|
||||
from chrono.utils.timezone import make_aware, now
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
@ -1609,7 +1609,7 @@ def test_recurring_events_api_fillslots_overlapping_events(app, user):
|
|||
|
||||
|
||||
@pytest.mark.freeze_time('2021-09-06 12:00')
|
||||
def test_recurring_events_api_fillslots_overlapping_events_partial_booking(app, user):
|
||||
def test_recurring_events_api_fillslots_partly_overlapping_events(app, user):
|
||||
agenda = Agenda.objects.create(label='First Agenda', kind='events')
|
||||
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
|
||||
start, end = now(), now() + datetime.timedelta(days=30)
|
||||
|
@ -1677,3 +1677,63 @@ def test_recurring_events_api_fillslots_overlapping_events_partial_booking(app,
|
|||
'2021-09-28',
|
||||
'2021-10-05',
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.freeze_time('2023-05-01 10:00')
|
||||
def test_recurring_events_api_fillslots_partial_bookings(app, user):
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='events', partial_bookings=True)
|
||||
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
|
||||
start_datetime = make_aware(datetime.datetime(2023, 5, 2, 8, 0))
|
||||
event = Event.objects.create(
|
||||
label='Event 08-18',
|
||||
start_datetime=start_datetime,
|
||||
end_time=datetime.time(18, 00),
|
||||
places=2,
|
||||
recurrence_end_date=start_datetime + datetime.timedelta(days=30),
|
||||
recurrence_days=[1],
|
||||
agenda=agenda,
|
||||
)
|
||||
event.create_all_recurrences()
|
||||
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
|
||||
params = {
|
||||
'user_external_id': 'user_id',
|
||||
'slots': 'foo-bar@event-08-18:1',
|
||||
'start_time': '10:00',
|
||||
'end_time': '15:00',
|
||||
}
|
||||
fillslots_url = '/api/agendas/recurring-events/fillslots/?action=update&agendas=%s' % agenda.slug
|
||||
resp = app.post_json(fillslots_url, params=params)
|
||||
|
||||
assert Booking.objects.count() == 5
|
||||
for booking in Booking.objects.all():
|
||||
assert booking.start_time == datetime.time(10, 00)
|
||||
assert booking.end_time == datetime.time(15, 00)
|
||||
|
||||
# mix with other kind
|
||||
other_agenda = Agenda.objects.create(label='Not partial', kind='events')
|
||||
resp = app.post_json(fillslots_url + ',%s' % other_agenda.slug, params=params, status=400)
|
||||
assert resp.json['errors']['agendas'][0] == 'Cannot mix partial bookings agendas with other kinds.'
|
||||
|
||||
# missing start_time
|
||||
del params['start_time']
|
||||
resp = app.post_json(fillslots_url, params=params, status=400)
|
||||
assert (
|
||||
resp.json['errors']['non_field_errors'][0]
|
||||
== 'must include start_time and end_time for partial bookings agenda'
|
||||
)
|
||||
|
||||
# missing end_time
|
||||
params['start_time'] = '10:00'
|
||||
del params['end_time']
|
||||
resp = app.post_json(fillslots_url, params=params, status=400)
|
||||
assert (
|
||||
resp.json['errors']['non_field_errors'][0]
|
||||
== 'must include start_time and end_time for partial bookings agenda'
|
||||
)
|
||||
|
||||
# end before start
|
||||
params['end_time'] = '09:00'
|
||||
resp = app.post_json(fillslots_url, params=params, status=400)
|
||||
assert resp.json['errors']['non_field_errors'][0] == 'start_time must be before end_time'
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from chrono.agendas.models import Agenda, Booking, Event
|
||||
from chrono.utils.timezone import make_aware
|
||||
from tests.utils import login
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_manager_partial_bookings_add_agenda(app, admin_user, settings):
|
||||
app = login(app)
|
||||
resp = app.get('/manage/agendas/add/')
|
||||
assert 'partial-bookings' not in resp.text
|
||||
|
||||
settings.PARTIAL_BOOKINGS_ENABLED = True
|
||||
|
||||
resp = app.get('/manage/agendas/add/')
|
||||
resp.form['label'] = 'Foo bar'
|
||||
resp.form['kind'] = 'partial-bookings'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
agenda = Agenda.objects.get(label='Foo bar')
|
||||
assert agenda.kind == 'events'
|
||||
assert agenda.partial_bookings is True
|
||||
assert agenda.default_view == 'day'
|
||||
|
||||
resp = resp.click('Options')
|
||||
assert 'default_view' not in resp.form.fields
|
||||
|
||||
|
||||
def test_manager_partial_bookings_add_event(app, admin_user):
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='events', partial_bookings=True)
|
||||
|
||||
app = login(app)
|
||||
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
|
||||
resp = resp.click('New Event')
|
||||
assert 'duration' not in resp.form.fields
|
||||
|
||||
resp.form['start_datetime_0'] = '2023-02-15'
|
||||
resp.form['start_datetime_1'] = '08:00'
|
||||
resp.form['end_time'] = '18:00'
|
||||
resp.form['places'] = 10
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
event = Event.objects.get()
|
||||
assert event.end_time == datetime.time(18, 00)
|
||||
assert event.duration is None
|
||||
|
||||
resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.pk, event.pk))
|
||||
assert 'duration' not in resp.form.fields
|
||||
assert resp.form['end_time'].value == '18:00'
|
||||
|
||||
|
||||
def test_manager_partial_bookings_day_view(app, admin_user, freezer):
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='events', partial_bookings=True)
|
||||
start_datetime = make_aware(datetime.datetime(2023, 5, 2, 8, 0))
|
||||
event = Event.objects.create(
|
||||
label='Event', start_datetime=start_datetime, end_time=datetime.time(18, 00), places=10, agenda=agenda
|
||||
)
|
||||
Booking.objects.create(
|
||||
user_external_id='xxx',
|
||||
user_first_name='Jane',
|
||||
user_last_name='Doe',
|
||||
start_time=datetime.time(11, 00),
|
||||
end_time=datetime.time(13, 30),
|
||||
event=event,
|
||||
)
|
||||
Booking.objects.create(
|
||||
user_external_id='yyy',
|
||||
user_first_name='Jon',
|
||||
user_last_name='Doe',
|
||||
start_time=datetime.time(8, 00),
|
||||
end_time=datetime.time(10, 00),
|
||||
event=event,
|
||||
)
|
||||
Booking.objects.create(
|
||||
user_external_id='yyy',
|
||||
user_first_name='Jon',
|
||||
user_last_name='Doe',
|
||||
start_time=datetime.time(12, 00),
|
||||
end_time=datetime.time(14, 00),
|
||||
event=event,
|
||||
)
|
||||
|
||||
app = login(app)
|
||||
today = start_datetime.date()
|
||||
resp = app.get('/manage/agendas/%s/day/%d/%d/%d/' % (agenda.pk, today.year, today.month, today.day))
|
||||
assert 'Week' not in resp.text
|
||||
assert 'Month' not in resp.text
|
||||
|
||||
# time range from one hour before event start to one hour after end
|
||||
assert (
|
||||
resp.pyquery('thead th').text()
|
||||
== '7 a.m. 8 a.m. 9 a.m. 10 a.m. 11 a.m. noon 1 p.m. 2 p.m. 3 p.m. 4 p.m. 5 p.m. 6 p.m. 7 p.m.'
|
||||
)
|
||||
|
||||
assert len(resp.pyquery('tbody tr')) == 2
|
||||
assert resp.pyquery('tbody tr th')[0].text == 'Jane Doe'
|
||||
assert resp.pyquery('tbody tr th')[1].text == 'Jon Doe'
|
||||
|
||||
assert resp.pyquery('tbody tr div.booking')[0].text == '11 a.m. - 1:30 p.m.'
|
||||
assert resp.pyquery('tbody tr div.booking')[0].attrib['style'] == 'left: 400%; width: 250%;'
|
||||
assert resp.pyquery('tbody tr div.booking')[1].text == '8 a.m. - 10 a.m.'
|
||||
assert resp.pyquery('tbody tr div.booking')[1].attrib['style'] == 'left: 100%; width: 200%;'
|
||||
assert resp.pyquery('tbody tr div.booking')[2].text == 'noon - 2 p.m.'
|
||||
assert resp.pyquery('tbody tr div.booking')[2].attrib['style'] == 'left: 500%; width: 200%;'
|
||||
|
||||
resp = resp.click('Next day')
|
||||
assert 'No opening hours this day.' in resp.text
|
Loading…
Reference in New Issue