Compare commits
2 Commits
main
...
wip/80047-
Author | SHA1 | Date |
---|---|---|
Thomas Jund | e176e8e968 | |
Valentin Deniaud | 08bd4538f5 |
|
@ -18,8 +18,8 @@ class Migration(migrations.Migration):
|
|||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
('presence', models.BooleanField()),
|
||||
('start_time', models.TimeField(null=True, verbose_name='Arrival')),
|
||||
('end_time', models.TimeField(null=True, verbose_name='Departure')),
|
||||
('start_time', models.TimeField(null=True, blank=True, verbose_name='Arrival')),
|
||||
('end_time', models.TimeField(null=True, blank=True, verbose_name='Departure')),
|
||||
('computed_end_time', models.TimeField(null=True)),
|
||||
('computed_start_time', models.TimeField(null=True)),
|
||||
('type_slug', models.CharField(blank=True, max_length=160, null=True)),
|
||||
|
|
|
@ -3158,8 +3158,8 @@ class BookingCheck(models.Model):
|
|||
|
||||
presence = models.BooleanField()
|
||||
|
||||
start_time = models.TimeField(_('Arrival'), null=True)
|
||||
end_time = models.TimeField(_('Departure'), null=True)
|
||||
start_time = models.TimeField(_('Arrival'), null=True, blank=True)
|
||||
end_time = models.TimeField(_('Departure'), null=True, blank=True)
|
||||
computed_start_time = models.TimeField(null=True)
|
||||
computed_end_time = models.TimeField(null=True)
|
||||
|
||||
|
@ -3262,7 +3262,12 @@ class BookingCheck(models.Model):
|
|||
if len(booking_checks) > 1:
|
||||
raise ValueError('too many booking checks') # should not happen
|
||||
|
||||
return bool(start_time < booking_checks[0].end_time and end_time > booking_checks[0].start_time)
|
||||
booking_check = booking_checks[0]
|
||||
|
||||
if not start_time or not end_time:
|
||||
return bool(booking_check.start_time < (start_time or end_time) < booking_check.end_time)
|
||||
|
||||
return bool(start_time < booking_check.end_time and end_time > booking_check.start_time)
|
||||
|
||||
|
||||
OpeningHour = collections.namedtuple('OpeningHour', ['begin', 'end'])
|
||||
|
|
|
@ -640,12 +640,16 @@ class PartialBookingCheckForm(forms.ModelForm):
|
|||
self.fields['presence'].widget.choices = presence_choices
|
||||
|
||||
def clean(self):
|
||||
if self.cleaned_data['end_time'] <= self.cleaned_data['start_time']:
|
||||
start_time = self.cleaned_data.get('start_time')
|
||||
end_time = self.cleaned_data.get('end_time')
|
||||
|
||||
if not start_time and not end_time:
|
||||
raise ValidationError(_('Both arrival and departure cannot not be empty.'))
|
||||
|
||||
if start_time and end_time and end_time <= start_time:
|
||||
raise ValidationError(_('Arrival must be before departure.'))
|
||||
|
||||
if self.instance.overlaps_existing_check(
|
||||
self.cleaned_data['start_time'], self.cleaned_data['end_time']
|
||||
):
|
||||
if self.instance.overlaps_existing_check(start_time, end_time):
|
||||
raise ValidationError(_('Booking check hours overlap existing check.'))
|
||||
|
||||
if self.cleaned_data['presence'] is not None:
|
||||
|
|
|
@ -684,33 +684,66 @@ div#main-content.partial-booking-dayview {
|
|||
margin: 0.33rem 0;
|
||||
}
|
||||
&--bar {
|
||||
--color: white;
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
padding: 0.33em 0.66em;
|
||||
background-color: var(--background);
|
||||
color: var(--color);
|
||||
background-color: var(--bar-color);
|
||||
color: white;
|
||||
border: none;
|
||||
&:not(:first-child) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
.start-time, .end-time {
|
||||
display: inline-block;
|
||||
padding: 0.33em 0.66em;
|
||||
}
|
||||
.end-time {
|
||||
float: right;
|
||||
margin-left: .66em;
|
||||
}
|
||||
&.booking {
|
||||
--background: #1066bc;
|
||||
--bar-color: #1066bc;
|
||||
}
|
||||
&.check.present, &.computed.present {
|
||||
--background: var(--green);
|
||||
--bar-color: var(--green);
|
||||
}
|
||||
&.check.absent, &.computed.absent {
|
||||
--background: var(--red);
|
||||
--bar-color: var(--red);
|
||||
}
|
||||
&.computed {
|
||||
opacity: 0.6;
|
||||
}
|
||||
&.end-only, &.start-only {
|
||||
background-color: transparent;
|
||||
.end-time, .start-time {
|
||||
background-color: var(--bar-color);
|
||||
position: relative;
|
||||
&::before {
|
||||
content:"?";
|
||||
color: var(--bar-color);
|
||||
font-weight: 800;
|
||||
line-height: 0;
|
||||
position: absolute;
|
||||
border: 0.75em solid transparent;
|
||||
width: 0;
|
||||
height: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
.start-time::before {
|
||||
left: 100%;
|
||||
border-left-color: var(--bar-color);
|
||||
text-indent: 0.25em;
|
||||
}
|
||||
.end-time::before {
|
||||
right: 100%;
|
||||
border-right-color: var(--bar-color);
|
||||
text-indent: -0.75em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -98,7 +98,7 @@
|
|||
{% for booking in user.bookings %}
|
||||
{% if booking.start_time %}
|
||||
<a
|
||||
class="registrant--bar clearfix booking"
|
||||
class="registrant--bar booking"
|
||||
title="{% trans "Booked period" %}"
|
||||
style="left: {{ booking.css_left }}%; width: {{ booking.css_width }}%;"
|
||||
{% if allow_check and not booking.user_check %}
|
||||
|
@ -117,17 +117,21 @@
|
|||
<div class="registrant--bar-container">
|
||||
{% for check in user.booking_checks %}
|
||||
<a
|
||||
class="registrant--bar clearfix check {{ check.css_class }}"
|
||||
class="registrant--bar check {{ check.css_class }}"
|
||||
title="{% trans "Checked period" %}"
|
||||
style="left: {{ check.css_left }}%; width: {{ check.css_width }}%;"
|
||||
style="left: {{ check.css_left }}%;{% if check.css_width %} width: {{ check.css_width }}%;{% endif %}"
|
||||
{% if allow_check %}
|
||||
rel="popup"
|
||||
href="{% url 'chrono-manager-partial-booking-update-check' pk=agenda.pk check_pk=check.pk %}"
|
||||
{% endif %}
|
||||
>
|
||||
<strong class="sr-only">{% trans "Checked period:" %}</strong>
|
||||
<time class="start-time" datetime="{{ check.start_time|time:"H:i" }}">{{ check.start_time|time:"H:i" }}</time>
|
||||
<time class="end-time" datetime="{{ check.end_time|time:"H:i" }}">{{ check.end_time|time:"H:i" }}</time>
|
||||
{% if check.start_time %}
|
||||
<time class="start-time" datetime="{{ check.start_time|time:"H:i" }}">{{ check.start_time|time:"H:i" }}</time>
|
||||
{% endif %}
|
||||
{% if check.end_time %}
|
||||
<time class="end-time" datetime="{{ check.end_time|time:"H:i" }}">{{ check.end_time|time:"H:i" }}</time>
|
||||
{% endif %}
|
||||
{% if check.type_label %}<span>{{ check.type_label }}</span>{% endif %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
|
@ -136,7 +140,7 @@
|
|||
{% for check in user.booking_checks %}
|
||||
{% if check.computed_start_time and check.computed_end_time %}
|
||||
<p
|
||||
class="registrant--bar clearfix computed {{ check.css_class }}"
|
||||
class="registrant--bar computed {{ check.css_class }}"
|
||||
title="{% trans "Computed period" %}"
|
||||
style="left: {{ check.computed_css_left }}%; width: {{ check.computed_css_width }}%;"
|
||||
>
|
||||
|
|
|
@ -1667,8 +1667,18 @@ class AgendaDayView(EventChecksMixin, AgendaDateView, DayArchiveView):
|
|||
booking.user_check_list = list(booking.user_checks.all()) # queryset is prefetched
|
||||
for check in booking.user_check_list:
|
||||
check.css_class = 'present' if check.presence else 'absent'
|
||||
check.css_left = get_time_ratio(check.start_time, start_time)
|
||||
check.css_width = get_time_ratio(check.end_time, check.start_time)
|
||||
|
||||
if not check.start_time:
|
||||
check.css_class += ' end-only'
|
||||
check.css_left = 0
|
||||
check.css_width = get_time_ratio(check.end_time, start_time)
|
||||
elif not check.end_time:
|
||||
check.css_class += ' start-only'
|
||||
check.css_left = get_time_ratio(check.start_time, start_time)
|
||||
else:
|
||||
check.css_left = get_time_ratio(check.start_time, start_time)
|
||||
check.css_width = get_time_ratio(check.end_time, check.start_time)
|
||||
|
||||
if check.computed_start_time and check.computed_end_time:
|
||||
check.computed_css_left = get_time_ratio(check.computed_start_time, start_time)
|
||||
check.computed_css_width = get_time_ratio(
|
||||
|
|
|
@ -536,6 +536,11 @@ def test_manager_partial_bookings_multiple_checks(app, admin_user):
|
|||
assert 'Add second booking check' not in resp.text
|
||||
|
||||
resp.form['start_time'] = '11:30'
|
||||
resp.form['end_time'] = ''
|
||||
resp = resp.form.submit()
|
||||
assert 'Booking check hours overlap existing check.' in resp.text
|
||||
|
||||
resp.form['end_time'] = '17:30'
|
||||
resp = resp.form.submit()
|
||||
assert 'Booking check hours overlap existing check.' in resp.text
|
||||
|
||||
|
@ -550,6 +555,76 @@ def test_manager_partial_bookings_multiple_checks(app, admin_user):
|
|||
assert resp.pyquery('.registrant--bar.check')[0].findall('time')[1].text == '12:00'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('subscription_only', (True, False))
|
||||
def test_manager_partial_bookings_incomplete_check(subscription_only, app, admin_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 = Event.objects.create(
|
||||
label='Event', start_datetime=start_datetime, end_time=datetime.time(18, 00), places=10, agenda=agenda
|
||||
)
|
||||
if subscription_only:
|
||||
Subscription.objects.create(
|
||||
agenda=agenda,
|
||||
user_external_id='xxx',
|
||||
user_first_name='Jane',
|
||||
user_last_name='Doe',
|
||||
date_start=event.start_datetime,
|
||||
date_end=event.start_datetime + datetime.timedelta(days=2),
|
||||
)
|
||||
else:
|
||||
booking = 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,
|
||||
)
|
||||
|
||||
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))
|
||||
if subscription_only:
|
||||
resp = resp.click('Jane Doe')
|
||||
else:
|
||||
resp = resp.click('Booked period')
|
||||
|
||||
resp.form['start_time'] = '11:01'
|
||||
resp.form['presence'] = 'True'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
booking = Booking.objects.get()
|
||||
assert booking.user_check.start_time == datetime.time(11, 1)
|
||||
assert booking.user_check.end_time is None
|
||||
assert booking.user_check.computed_start_time == datetime.time(11, 0)
|
||||
assert booking.user_check.computed_end_time is None
|
||||
|
||||
assert len(resp.pyquery('.registrant--bar.check.present')) == 1
|
||||
assert resp.pyquery('.registrant--bar.check.present')[0].attrib['style'] == 'left: 30.9%;'
|
||||
assert resp.pyquery('.registrant--bar.check time').text() == '11:01'
|
||||
|
||||
resp = resp.click('Checked period')
|
||||
resp.form['start_time'] = ''
|
||||
resp.form['end_time'] = '13:15'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
booking.refresh_from_db()
|
||||
assert booking.user_check.start_time is None
|
||||
assert booking.user_check.end_time == datetime.time(13, 15)
|
||||
assert booking.user_check.computed_start_time is None
|
||||
assert booking.user_check.computed_end_time == datetime.time(14, 0)
|
||||
|
||||
assert len(resp.pyquery('.registrant--bar.check.present')) == 1
|
||||
assert resp.pyquery('.registrant--bar.check.present')[0].attrib['style'] == 'left: 0%; width: 48.08%;'
|
||||
assert resp.pyquery('.registrant--bar.check time').text() == '13:15'
|
||||
|
||||
resp = resp.click('Checked period')
|
||||
resp.form['start_time'] = ''
|
||||
resp.form['end_time'] = ''
|
||||
resp = resp.form.submit()
|
||||
assert 'Both arrival and departure cannot not be empty.' in resp.text
|
||||
|
||||
|
||||
@mock.patch('chrono.manager.forms.get_agenda_check_types')
|
||||
def test_manager_partial_bookings_check_subscription(check_types, app, admin_user):
|
||||
check_types.return_value = []
|
||||
|
|
Loading…
Reference in New Issue