Compare commits
110 Commits
Author | SHA1 | Date |
---|---|---|
Frédéric Péters | cfeb7c7ae8 | |
Yann Weber | 6c2c412cfc | |
Yann Weber | 1aca9c2a66 | |
Frédéric Péters | 1ef0ba26af | |
Frédéric Péters | 4c14a32d82 | |
Frédéric Péters | 32d0a0c44b | |
Valentin Deniaud | 733cdfc9a9 | |
Lauréline Guérin | 77f3373820 | |
Valentin Deniaud | 169dc0a69a | |
Lauréline Guérin | 88d8feacd8 | |
Thomas NOËL | 6ee8fbf78d | |
Lauréline Guérin | 3403295d3d | |
Yann Weber | 0563e0642d | |
Yann Weber | 5a90c4851b | |
Yann Weber | d03e1e7940 | |
Yann Weber | b0f956c223 | |
Yann Weber | 570cf81c8e | |
Benjamin Dauvergne | 5fa96e62a8 | |
Lauréline Guérin | 7c91b91d89 | |
Lauréline Guérin | a167a91cde | |
Lauréline Guérin | 56b794468f | |
Lauréline Guérin | 184cf83dd7 | |
Yann Weber | 4e6f41c4de | |
Benjamin Dauvergne | 42f73e2626 | |
Valentin Deniaud | 3576928b2c | |
Frédéric Péters | ae55827939 | |
Lauréline Guérin | d733e91135 | |
Lauréline Guérin | 4b8c3412e4 | |
Valentin Deniaud | be975cfa29 | |
Lauréline Guérin | f7e224ba9b | |
Thomas Jund | 41cadbcfa9 | |
Lauréline Guérin | a34d55879e | |
Lauréline Guérin | 43c42c507c | |
Lauréline Guérin | 886afb206e | |
Lauréline Guérin | 2c30eec6ac | |
Lauréline Guérin | 1896c33f29 | |
Lauréline Guérin | 1f23f85b3d | |
Lauréline Guérin | 393a20b87b | |
Lauréline Guérin | df0e356e75 | |
Frédéric Péters | 07512150e8 | |
Lauréline Guérin | 2576350aae | |
Lauréline Guérin | 2187bf3dde | |
Lauréline Guérin | 4add868dd9 | |
Valentin Deniaud | 9c19321fb9 | |
Lauréline Guérin | a88db00e04 | |
Lauréline Guérin | eecbb80809 | |
Lauréline Guérin | e0f1d9541d | |
Lauréline Guérin | 701733da57 | |
Lauréline Guérin | d709fa9bc7 | |
Lauréline Guérin | 1d00c5fce8 | |
Lauréline Guérin | bd06f2b82f | |
Lauréline Guérin | ecf0ffd96e | |
Lauréline Guérin | 1b1bc13c82 | |
Lauréline Guérin | 06ab6f12b7 | |
Lauréline Guérin | 024b34b34f | |
Lauréline Guérin | 0ea056dcd5 | |
Lauréline Guérin | 3cef873ce4 | |
Lauréline Guérin | 966d93829f | |
Lauréline Guérin | 03f9172c98 | |
Lauréline Guérin | 176d23aa4b | |
Lauréline Guérin | 9331b06e04 | |
Lauréline Guérin | 3f8146c092 | |
Lauréline Guérin | f6a0b58167 | |
Lauréline Guérin | e6db17f145 | |
Lauréline Guérin | 84581ed02e | |
Lauréline Guérin | 4f13f936e2 | |
Frédéric Péters | 7df4de695d | |
Yann Weber | 095057839a | |
Frédéric Péters | 69f9877ba5 | |
Lauréline Guérin | 895758c70c | |
Lauréline Guérin | 3071fab8f8 | |
Lauréline Guérin | a4e5721dad | |
Lauréline Guérin | 068e5fe467 | |
Yann Weber | a36369ae1c | |
Yann Weber | 3bfa450f97 | |
Yann Weber | 917c918422 | |
Frédéric Péters | 9a1b37a5f7 | |
Frédéric Péters | 5204fcda47 | |
Yann Weber | 9945568a57 | |
Yann Weber | d428ef8385 | |
Frédéric Péters | 9c660e7a1e | |
Yann Weber | 47e7558298 | |
Yann Weber | f2285f7880 | |
Benjamin Dauvergne | 5a9379a7b8 | |
Benjamin Dauvergne | f749c5e9cb | |
Benjamin Dauvergne | f61d07f586 | |
Yann Weber | 154fe0ccea | |
Yann Weber | 14e7998895 | |
Lauréline Guérin | 8e35a25ad9 | |
Pierre Ducroquet | 5db20c9434 | |
Frédéric Péters | eeca5783dd | |
Lauréline Guérin | 3c052b467b | |
Frédéric Péters | 888c0638d0 | |
Lauréline Guérin | 05aa65e72a | |
Valentin Deniaud | e83bfee4c3 | |
Valentin Deniaud | 7bea1c912b | |
Valentin Deniaud | 526f255ee5 | |
Valentin Deniaud | 1740ebe572 | |
Lauréline Guérin | 698bbfc7a4 | |
Valentin Deniaud | d02210ab66 | |
Nicolas Roche | c4ecd1900a | |
Valentin Deniaud | 7096938cda | |
Lauréline Guérin | ee557adbcc | |
Valentin Deniaud | ce96e674c2 | |
Valentin Deniaud | 5501b88c34 | |
Valentin Deniaud | 440d02d505 | |
Valentin Deniaud | f8748710bc | |
Valentin Deniaud | 6804b08cc6 | |
Benjamin Dauvergne | aad10c71ee | |
Benjamin Dauvergne | 2831272e56 |
|
@ -6,7 +6,7 @@ pipeline {
|
|||
stages {
|
||||
stage('Unit Tests') {
|
||||
steps {
|
||||
sh 'tox -rv -- --numprocesses 3'
|
||||
sh 'NUMPROCESSES=3 tox -rv'
|
||||
}
|
||||
post {
|
||||
always {
|
||||
|
|
|
@ -9,6 +9,7 @@ recursive-include chrono/api/templates *.html
|
|||
recursive-include chrono/agendas/templates *.html *.txt
|
||||
recursive-include chrono/manager/templates *.html *.txt
|
||||
recursive-include chrono/apps/ants_hub/templates *.html
|
||||
recursive-include chrono/apps/journal/templates *.html
|
||||
|
||||
# sql (migrations)
|
||||
recursive-include chrono/agendas/sql *.sql
|
||||
|
|
|
@ -18,7 +18,7 @@ import copy
|
|||
from urllib.parse import urljoin
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.mail import send_mail
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.transaction import atomic
|
||||
from django.template.loader import render_to_string
|
||||
|
@ -72,4 +72,12 @@ class Command(BaseCommand):
|
|||
with atomic():
|
||||
setattr(event, status + '_notification_timestamp', timestamp)
|
||||
event.save()
|
||||
send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, recipients, html_message=html_body)
|
||||
mail_msg = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=body,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
to=[settings.DEFAULT_FROM_EMAIL],
|
||||
bcc=recipients,
|
||||
)
|
||||
mail_msg.attach_alternative(html_body, 'text/html')
|
||||
mail_msg.send()
|
||||
|
|
|
@ -11,7 +11,7 @@ class Migration(migrations.Migration):
|
|||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='meetingtype',
|
||||
options={'ordering': ['duration', 'label']},
|
||||
options={'ordering': ['duration', 'label'], 'verbose_name': 'Meeting type'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='timeperiodexception',
|
||||
|
|
|
@ -17,6 +17,6 @@ class Migration(migrations.Migration):
|
|||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='url',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='URL'),
|
||||
field=models.URLField(blank=True, null=True, verbose_name='URL'),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -10,6 +10,6 @@ class Migration(migrations.Migration):
|
|||
migrations.AddField(
|
||||
model_name='agenda',
|
||||
name='desk_simple_management',
|
||||
field=models.BooleanField(default=False),
|
||||
field=models.BooleanField(default=False, verbose_name='Global desk management'),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 3.2.21 on 2023-12-06 09:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('agendas', '0167_bookingcheck_max_2_checks_on_booking'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='booking',
|
||||
name='from_recurring_fillslots',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,37 @@
|
|||
# Generated by Django 3.2.16 on 2023-12-22 08:46
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('agendas', '0168_booking_from_recurring_fillslots'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='booking',
|
||||
name='absence_callback_url',
|
||||
field=models.URLField(blank=True, max_length=500),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='booking',
|
||||
name='backoffice_url',
|
||||
field=models.URLField(blank=True, max_length=500),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='booking',
|
||||
name='cancel_callback_url',
|
||||
field=models.URLField(blank=True, max_length=500),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='booking',
|
||||
name='form_url',
|
||||
field=models.URLField(blank=True, max_length=500),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='booking',
|
||||
name='presence_callback_url',
|
||||
field=models.URLField(blank=True, max_length=500),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 3.2.18 on 2024-01-22 10:55
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('agendas', '0169_urlfield_maxlength_increase'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='agenda',
|
||||
name='events_type',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name='agendas',
|
||||
to='agendas.eventstype',
|
||||
verbose_name='Events type',
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,118 @@
|
|||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('snapshot', '0002_snapshot_models'),
|
||||
('agendas', '0170_alter_agenda_events_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='agenda',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='agenda',
|
||||
name='snapshot',
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='temporary_instance',
|
||||
to='snapshot.agendasnapshot',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='agenda',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='category',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='category',
|
||||
name='snapshot',
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='temporary_instance',
|
||||
to='snapshot.categorysnapshot',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='category',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='eventstype',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='eventstype',
|
||||
name='snapshot',
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='temporary_instance',
|
||||
to='snapshot.eventstypesnapshot',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='eventstype',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='resource',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='resource',
|
||||
name='snapshot',
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='temporary_instance',
|
||||
to='snapshot.resourcesnapshot',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='resource',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='unavailabilitycalendar',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='unavailabilitycalendar',
|
||||
name='snapshot',
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='temporary_instance',
|
||||
to='snapshot.unavailabilitycalendarsnapshot',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='unavailabilitycalendar',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
]
|
File diff suppressed because it is too large
Load Diff
|
@ -89,11 +89,11 @@ class FillSlotSerializer(serializers.Serializer):
|
|||
exclude_user = serializers.BooleanField(default=False)
|
||||
events = serializers.CharField(max_length=16, allow_blank=True)
|
||||
bypass_delays = serializers.BooleanField(default=False)
|
||||
form_url = serializers.CharField(max_length=250, allow_blank=True)
|
||||
backoffice_url = serializers.URLField(allow_blank=True)
|
||||
cancel_callback_url = serializers.URLField(allow_blank=True)
|
||||
presence_callback_url = serializers.URLField(allow_blank=True)
|
||||
absence_callback_url = serializers.URLField(allow_blank=True)
|
||||
form_url = serializers.CharField(max_length=500, allow_blank=True)
|
||||
backoffice_url = serializers.URLField(allow_blank=True, max_length=500)
|
||||
cancel_callback_url = serializers.URLField(allow_blank=True, max_length=500)
|
||||
presence_callback_url = serializers.URLField(allow_blank=True, max_length=500)
|
||||
absence_callback_url = serializers.URLField(allow_blank=True, max_length=500)
|
||||
count = serializers.IntegerField(min_value=1)
|
||||
cancel_booking_id = serializers.CharField(max_length=250, allow_blank=True, allow_null=True)
|
||||
force_waiting_list = serializers.BooleanField(default=False)
|
||||
|
@ -358,7 +358,23 @@ class BookingSerializer(serializers.ModelSerializer):
|
|||
self.user_check.computed_start_time if self.user_check else None
|
||||
)
|
||||
self.instance.computed_end_time = self.user_check.computed_end_time if self.user_check else None
|
||||
for key in ['', 'user_check_', 'computed_']:
|
||||
# adjust start_time (in case of multi checks)
|
||||
self.instance.adjusted_start_time = self.instance.start_time
|
||||
if (
|
||||
self.instance.start_time
|
||||
and self.instance.computed_start_time
|
||||
and self.instance.start_time < self.instance.computed_start_time
|
||||
):
|
||||
self.instance.adjusted_start_time = self.instance.computed_start_time
|
||||
# and end_time
|
||||
self.instance.adjusted_end_time = self.instance.end_time
|
||||
if (
|
||||
self.instance.end_time
|
||||
and self.instance.computed_end_time
|
||||
and self.instance.end_time > self.instance.computed_end_time
|
||||
):
|
||||
self.instance.adjusted_end_time = self.instance.computed_end_time
|
||||
for key in ['', 'user_check_', 'computed_', 'adjusted_']:
|
||||
start_key, end_key, minutes_key = (
|
||||
'%sstart_time' % key,
|
||||
'%send_time' % key,
|
||||
|
@ -430,6 +446,11 @@ class ResizeSerializer(serializers.Serializer):
|
|||
count = serializers.IntegerField(min_value=1)
|
||||
|
||||
|
||||
class PartialBookingsCheckSerializer(serializers.Serializer):
|
||||
user_external_id = serializers.CharField(max_length=250, allow_blank=True)
|
||||
timestamp = serializers.DateTimeField(input_formats=['iso-8601', '%Y-%m-%d'])
|
||||
|
||||
|
||||
class StatisticsFiltersSerializer(serializers.Serializer):
|
||||
time_interval = serializers.ChoiceField(choices=('day', _('Day')), default='day')
|
||||
start = serializers.DateTimeField(required=False, input_formats=['iso-8601', '%Y-%m-%d'])
|
||||
|
@ -446,6 +467,7 @@ class DateRangeSerializer(DateRangeMixin, serializers.Serializer):
|
|||
|
||||
class DatetimesSerializer(DateRangeSerializer):
|
||||
min_places = serializers.IntegerField(min_value=1, default=1)
|
||||
max_places = serializers.IntegerField(min_value=1, default=None)
|
||||
user_external_id = serializers.CharField(required=False, max_length=250, allow_blank=True)
|
||||
exclude_user_external_id = serializers.CharField(required=False, max_length=250, allow_blank=True)
|
||||
events = serializers.CharField(required=False, max_length=32, allow_blank=True)
|
||||
|
@ -668,6 +690,7 @@ class AgendaSerializer(serializers.ModelSerializer):
|
|||
'slug',
|
||||
'label',
|
||||
'kind',
|
||||
'partial_bookings',
|
||||
'minimal_booking_delay',
|
||||
'minimal_booking_delay_in_working_days',
|
||||
'maximal_booking_delay',
|
||||
|
@ -718,6 +741,10 @@ class AgendaSerializer(serializers.ModelSerializer):
|
|||
)
|
||||
if attrs.get('events_type') and attrs.get('kind', 'events') != 'events':
|
||||
raise ValidationError({'events_type': _('Option not available on %s agenda') % attrs['kind']})
|
||||
if attrs.get('partial_bookings') and attrs.get('kind', 'events') != 'events':
|
||||
raise ValidationError(
|
||||
{'partial_bookings': _('Option not available on %s agenda') % attrs['kind']}
|
||||
)
|
||||
return attrs
|
||||
|
||||
|
||||
|
|
|
@ -133,6 +133,11 @@ urlpatterns = [
|
|||
views.subscription,
|
||||
name='api-agenda-subscription',
|
||||
),
|
||||
path(
|
||||
'agenda/<slug:agenda_identifier>/partial-bookings-check/',
|
||||
views.partial_bookings_check,
|
||||
name='api-partial-bookings-check',
|
||||
),
|
||||
path('bookings/', views.bookings, name='api-bookings'),
|
||||
path('bookings/ics/', views.bookings_ics, name='api-bookings-ics'),
|
||||
path('booking/<int:booking_pk>/', views.booking, name='api-booking'),
|
||||
|
@ -151,4 +156,5 @@ urlpatterns = [
|
|||
path('statistics/', views.statistics_list, name='api-statistics-list'),
|
||||
path('statistics/bookings/', views.bookings_statistics, name='api-statistics-bookings'),
|
||||
path('ants/', include('chrono.apps.ants_hub.api_urls')),
|
||||
path('user-preferences/', include('chrono.apps.user_preferences.api_urls')),
|
||||
]
|
||||
|
|
|
@ -20,7 +20,7 @@ import datetime
|
|||
import json
|
||||
import uuid
|
||||
|
||||
import vobject
|
||||
import icalendar
|
||||
from django.conf import settings
|
||||
from django.db import IntegrityError, transaction
|
||||
from django.db.models import BooleanField, Case, Count, ExpressionWrapper, F, Func, Prefetch, Q, When
|
||||
|
@ -57,6 +57,7 @@ from chrono.agendas.models import (
|
|||
)
|
||||
from chrono.api import serializers
|
||||
from chrono.api.utils import APIError, APIErrorBadRequest, Response
|
||||
from chrono.apps.journal.utils import audit
|
||||
from chrono.utils.publik_urls import translate_to_publik_url
|
||||
from chrono.utils.timezone import localtime, make_aware, now
|
||||
|
||||
|
@ -149,6 +150,7 @@ def get_event_places(event):
|
|||
def is_event_disabled(
|
||||
event,
|
||||
min_places=1,
|
||||
max_places=None,
|
||||
disable_booked=True,
|
||||
bookable_events=None,
|
||||
bypass_delays=False,
|
||||
|
@ -170,7 +172,9 @@ def is_event_disabled(
|
|||
):
|
||||
# event is out of minimal delay and we don't want to bypass delays
|
||||
return True
|
||||
if event.remaining_places < min_places and event.remaining_waiting_list_places < min_places:
|
||||
if max_places and max_places <= event.remaining_places:
|
||||
return True
|
||||
elif event.remaining_places < min_places and event.remaining_waiting_list_places < min_places:
|
||||
if enable_full_when_booked and getattr(event, 'user_places_count', 0) > 0:
|
||||
return False
|
||||
return True
|
||||
|
@ -210,6 +214,7 @@ def get_event_detail(
|
|||
booking=None,
|
||||
agenda=None,
|
||||
min_places=1,
|
||||
max_places=None,
|
||||
booked_user_external_id=None,
|
||||
bookable_events=None,
|
||||
multiple_agendas=False,
|
||||
|
@ -264,6 +269,7 @@ def get_event_detail(
|
|||
'disabled': is_event_disabled(
|
||||
event,
|
||||
min_places=min_places,
|
||||
max_places=max_places,
|
||||
disable_booked=disable_booked,
|
||||
bookable_events=bookable_events,
|
||||
bypass_delays=bypass_delays,
|
||||
|
@ -331,9 +337,11 @@ def get_short_event_detail(
|
|||
details = {
|
||||
'id': '%s@%s' % (agenda.slug, event.slug) if multiple_agendas else event.slug,
|
||||
'slug': event.slug, # kept for compatibility
|
||||
'primary_event': None,
|
||||
'text': get_event_text(event, agenda),
|
||||
'label': event.label or '',
|
||||
'agenda_label': agenda.label,
|
||||
'agenda_slug': agenda.slug,
|
||||
'date': format_response_date(event.start_datetime),
|
||||
'datetime': format_response_datetime(event.start_datetime),
|
||||
'end_datetime': format_response_datetime(event.end_datetime) if event.end_datetime else '',
|
||||
|
@ -345,6 +353,12 @@ def get_short_event_detail(
|
|||
'check_locked': event.check_locked,
|
||||
'invoiced': event.invoiced,
|
||||
}
|
||||
if event.primary_event:
|
||||
details['primary_event'] = (
|
||||
'%s@%s' % (agenda.slug, event.primary_event.slug)
|
||||
if multiple_agendas
|
||||
else event.primary_event.slug
|
||||
)
|
||||
for key, value in event.get_custom_fields().items():
|
||||
details['custom_field_%s' % key] = value
|
||||
return details
|
||||
|
@ -355,6 +369,7 @@ def get_events_meta_detail(
|
|||
events,
|
||||
agenda=None,
|
||||
min_places=1,
|
||||
max_places=0,
|
||||
bookable_events=None,
|
||||
multiple_agendas=False,
|
||||
bypass_delays=False,
|
||||
|
@ -365,7 +380,11 @@ def get_events_meta_detail(
|
|||
for event in events:
|
||||
bookable_datetimes_number_total += 1
|
||||
if not is_event_disabled(
|
||||
event, min_places=min_places, bookable_events=bookable_events, bypass_delays=bypass_delays
|
||||
event,
|
||||
min_places=min_places,
|
||||
max_places=max_places,
|
||||
bookable_events=bookable_events,
|
||||
bypass_delays=bypass_delays,
|
||||
):
|
||||
bookable_datetimes_number_available += 1
|
||||
if not first_bookable_slot:
|
||||
|
@ -374,6 +393,7 @@ def get_events_meta_detail(
|
|||
event,
|
||||
agenda=agenda,
|
||||
min_places=min_places,
|
||||
max_places=max_places,
|
||||
bookable_events=bookable_events,
|
||||
multiple_agendas=multiple_agendas,
|
||||
bypass_delays=bypass_delays,
|
||||
|
@ -470,7 +490,13 @@ def make_booking(
|
|||
color=None,
|
||||
request_uuid=None,
|
||||
previous_state=None,
|
||||
from_recurring_fillslots=False,
|
||||
):
|
||||
if 'start_time' in payload and payload['start_time'] < localtime(event.start_datetime).time():
|
||||
raise APIError(N_('booking start must be after opening time'))
|
||||
if 'end_time' in payload and payload['end_time'] > event.end_time:
|
||||
raise APIError(N_('booking end must be before closing time'))
|
||||
|
||||
out_of_min_delay = False
|
||||
if event.agenda.min_booking_datetime and event.start_datetime < event.agenda.min_booking_datetime:
|
||||
out_of_min_delay = True
|
||||
|
@ -499,6 +525,7 @@ def make_booking(
|
|||
color=color,
|
||||
request_uuid=request_uuid,
|
||||
previous_state=previous_state,
|
||||
from_recurring_fillslots=from_recurring_fillslots,
|
||||
)
|
||||
|
||||
|
||||
|
@ -564,6 +591,7 @@ class Agendas(APIView):
|
|||
if agenda.kind == 'events':
|
||||
desk = Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
|
||||
desk.import_timeperiod_exceptions_from_settings()
|
||||
agenda.take_snapshot(request=self.request, comment=pgettext('snapshot', 'created'))
|
||||
return Response({'err': 0, 'data': [get_agenda_detail(request, agenda)]})
|
||||
|
||||
|
||||
|
@ -588,6 +616,7 @@ class AgendaAPI(APIView):
|
|||
if has_bookings:
|
||||
raise APIError(_('This cannot be removed as there are bookings for a future date.'))
|
||||
|
||||
agenda.take_snapshot(request=self.request, deletion=True)
|
||||
agenda.delete()
|
||||
return Response({'err': 0})
|
||||
|
||||
|
@ -601,7 +630,8 @@ class AgendaAPI(APIView):
|
|||
if 'kind' in serializer.validated_data and serializer.validated_data['kind'] != agenda.kind:
|
||||
raise APIErrorBadRequest(N_('it is not possible to change kind value'))
|
||||
|
||||
serializer.save()
|
||||
agenda = serializer.save()
|
||||
agenda.take_snapshot(request=self.request)
|
||||
return self.get(request, agenda_identifier)
|
||||
|
||||
|
||||
|
@ -653,6 +683,7 @@ class Datetimes(APIView):
|
|||
entries = Event.annotate_queryset_for_user(entries, user_external_id)
|
||||
if lock_code:
|
||||
entries = Event.annotate_queryset_for_lock_code(entries, lock_code=lock_code)
|
||||
entries = entries.prefetch_related(Prefetch('primary_event', queryset=Event.objects.all().order_by()))
|
||||
entries = entries.order_by('start_datetime', 'duration', 'label')
|
||||
|
||||
if payload['hide_disabled']:
|
||||
|
@ -661,7 +692,8 @@ class Datetimes(APIView):
|
|||
for e in entries
|
||||
if not is_event_disabled(
|
||||
e,
|
||||
payload['min_places'],
|
||||
min_places=payload['min_places'],
|
||||
max_places=payload['max_places'],
|
||||
disable_booked=disable_booked,
|
||||
bookable_events=bookable_events,
|
||||
bypass_delays=payload.get('bypass_delays'),
|
||||
|
@ -675,6 +707,7 @@ class Datetimes(APIView):
|
|||
x,
|
||||
agenda=agenda,
|
||||
min_places=payload['min_places'],
|
||||
max_places=payload['max_places'],
|
||||
booked_user_external_id=payload.get('user_external_id'),
|
||||
bookable_events=bookable_events_raw,
|
||||
disable_booked=disable_booked,
|
||||
|
@ -687,6 +720,7 @@ class Datetimes(APIView):
|
|||
entries,
|
||||
agenda=agenda,
|
||||
min_places=payload['min_places'],
|
||||
max_places=payload['max_places'],
|
||||
bookable_events=bookable_events_raw,
|
||||
),
|
||||
}
|
||||
|
@ -733,6 +767,9 @@ class MultipleAgendasDatetimes(APIView):
|
|||
entries = Event.annotate_queryset_for_user(entries, user_external_id, with_status=with_status)
|
||||
if lock_code:
|
||||
Event.annotate_queryset_for_lock_code(entries, lock_code)
|
||||
entries = entries.prefetch_related(
|
||||
Prefetch('primary_event', queryset=Event.objects.all().order_by())
|
||||
)
|
||||
|
||||
if check_overlaps:
|
||||
entries = Event.annotate_queryset_with_overlaps(entries)
|
||||
|
@ -781,6 +818,7 @@ class MultipleAgendasDatetimes(APIView):
|
|||
request,
|
||||
x,
|
||||
min_places=payload['min_places'],
|
||||
max_places=payload['max_places'],
|
||||
booked_user_external_id=payload.get('user_external_id'),
|
||||
multiple_agendas=True,
|
||||
disable_booked=disable_booked,
|
||||
|
@ -791,7 +829,11 @@ class MultipleAgendasDatetimes(APIView):
|
|||
for x in entries
|
||||
],
|
||||
'meta': get_events_meta_detail(
|
||||
request, entries, min_places=payload['min_places'], multiple_agendas=True
|
||||
request,
|
||||
entries,
|
||||
min_places=payload['min_places'],
|
||||
max_places=payload['max_places'],
|
||||
multiple_agendas=True,
|
||||
),
|
||||
}
|
||||
return Response(response)
|
||||
|
@ -1296,7 +1338,7 @@ class EventsAgendaFillslot(APIView):
|
|||
|
||||
if to_cancel_booking:
|
||||
cancelled_booking_id = to_cancel_booking.pk
|
||||
to_cancel_booking.cancel()
|
||||
to_cancel_booking.cancel(request=request)
|
||||
|
||||
# now we have a list of events, book them.
|
||||
primary_booking = None
|
||||
|
@ -1309,6 +1351,17 @@ class EventsAgendaFillslot(APIView):
|
|||
in_waiting_list=in_waiting_list,
|
||||
)
|
||||
new_booking.save()
|
||||
audit(
|
||||
'booking:create',
|
||||
request=request,
|
||||
agenda=event.agenda,
|
||||
extra_data={
|
||||
'booking': new_booking,
|
||||
'event': event,
|
||||
'primary_booking_id': primary_booking.id if primary_booking else None,
|
||||
},
|
||||
)
|
||||
|
||||
if lock_code and not confirm_after_lock:
|
||||
Lease.objects.create(
|
||||
booking=new_booking,
|
||||
|
@ -1346,6 +1399,9 @@ class EventsAgendaFillslot(APIView):
|
|||
'suspend_url': request.build_absolute_uri(
|
||||
reverse('api-suspend-booking', kwargs={'booking_pk': primary_booking.pk})
|
||||
),
|
||||
'resize_url': request.build_absolute_uri(
|
||||
reverse('api-resize-booking', kwargs={'booking_pk': primary_booking.pk})
|
||||
),
|
||||
},
|
||||
}
|
||||
if to_cancel_booking:
|
||||
|
@ -1530,7 +1586,7 @@ class MeetingsAgendaFillslot(APIView):
|
|||
).delete()
|
||||
if to_cancel_booking:
|
||||
cancelled_booking_id = to_cancel_booking.pk
|
||||
to_cancel_booking.cancel()
|
||||
to_cancel_booking.cancel(request=request)
|
||||
|
||||
# book event
|
||||
event.save()
|
||||
|
@ -1543,6 +1599,16 @@ class MeetingsAgendaFillslot(APIView):
|
|||
color=color,
|
||||
)
|
||||
booking.save()
|
||||
audit(
|
||||
'booking:create',
|
||||
request=request,
|
||||
agenda=event.agenda,
|
||||
extra_data={
|
||||
'booking': booking,
|
||||
'event': event,
|
||||
},
|
||||
)
|
||||
|
||||
if lock_code and not confirm_after_lock:
|
||||
Lease.objects.create(
|
||||
booking=booking,
|
||||
|
@ -1832,6 +1898,7 @@ class RecurringFillslots(APIView):
|
|||
min_start=start_datetime,
|
||||
max_start=end_datetime,
|
||||
)
|
||||
events = events.prefetch_related(Prefetch('primary_event', queryset=Event.objects.all().order_by()))
|
||||
|
||||
return events
|
||||
|
||||
|
@ -1875,7 +1942,7 @@ class RecurringFillslots(APIView):
|
|||
)
|
||||
|
||||
def make_bookings(self, events, payload, extra_data):
|
||||
return [make_booking(event, payload, extra_data) for event in events]
|
||||
return [make_booking(event, payload, extra_data, from_recurring_fillslots=True) for event in events]
|
||||
|
||||
|
||||
recurring_fillslots = RecurringFillslots.as_view()
|
||||
|
@ -1890,7 +1957,7 @@ class RecurringFillslotsByDay(RecurringFillslots):
|
|||
payload['start_time'], payload['end_time'] = payload['hours_by_days'][
|
||||
event.start_datetime.isoweekday()
|
||||
]
|
||||
bookings.append(make_booking(event, payload, extra_data))
|
||||
bookings.append(make_booking(event, payload, extra_data, from_recurring_fillslots=True))
|
||||
return bookings
|
||||
|
||||
|
||||
|
@ -1990,6 +2057,7 @@ class EventsFillslots(APIView):
|
|||
output_field=BooleanField(),
|
||||
)
|
||||
)
|
||||
events = events.prefetch_related(Prefetch('primary_event', queryset=Event.objects.all().order_by()))
|
||||
waiting_list_event_ids = [event.pk for event in events if event.in_waiting_list]
|
||||
|
||||
extra_data = get_extra_data(request, payload)
|
||||
|
@ -2214,9 +2282,13 @@ class MultipleAgendasEventsFillslotsRevert(APIView):
|
|||
if booking.previous_state == 'cancelled':
|
||||
bookings_to_cancel.append(booking)
|
||||
|
||||
events = Event.objects.filter(
|
||||
pk__in=[b.event_id for b in bookings_to_cancel + bookings_to_book + bookings_to_delete]
|
||||
).prefetch_related('agenda')
|
||||
events = (
|
||||
Event.objects.filter(
|
||||
pk__in=[b.event_id for b in bookings_to_cancel + bookings_to_book + bookings_to_delete]
|
||||
)
|
||||
.prefetch_related('agenda')
|
||||
.prefetch_related(Prefetch('primary_event', queryset=Event.objects.all().order_by()))
|
||||
)
|
||||
events_by_id = {x.id: x for x in events}
|
||||
with transaction.atomic():
|
||||
cancellation_datetime = now()
|
||||
|
@ -2435,6 +2507,16 @@ class MultipleAgendasEventsCheckLock(APIView):
|
|||
for event in events:
|
||||
event.async_refresh_booking_computed_times()
|
||||
|
||||
for event in events:
|
||||
audit(
|
||||
'check:lock' if check_locked else 'check:unlock',
|
||||
request=request,
|
||||
agenda=event.agenda,
|
||||
extra_data={
|
||||
'event': event,
|
||||
},
|
||||
)
|
||||
|
||||
return Response({'err': 0})
|
||||
|
||||
|
||||
|
@ -2464,6 +2546,16 @@ class MultipleAgendasEventsInvoiced(APIView):
|
|||
)
|
||||
events.update(invoiced=invoiced)
|
||||
|
||||
for event in events:
|
||||
audit(
|
||||
'invoice:mark' if invoiced else 'invoice:unmark',
|
||||
request=request,
|
||||
agenda=event.agenda,
|
||||
extra_data={
|
||||
'event': event,
|
||||
},
|
||||
)
|
||||
|
||||
return Response({'err': 0})
|
||||
|
||||
|
||||
|
@ -2740,10 +2832,12 @@ class BookingsAPI(ListAPIView):
|
|||
return Response({'err': 0, 'data': data})
|
||||
|
||||
def get_queryset(self):
|
||||
event_queryset = Event.objects.all().prefetch_related(
|
||||
'agenda', 'desk', Prefetch('primary_event', queryset=Event.objects.all().order_by())
|
||||
)
|
||||
return (
|
||||
Booking.objects.filter(primary_booking__isnull=True, cancellation_datetime__isnull=True)
|
||||
.select_related('event', 'event__agenda', 'event__desk')
|
||||
.prefetch_related('user_checks')
|
||||
.prefetch_related('user_checks', Prefetch('event', queryset=event_queryset))
|
||||
.order_by('event__start_datetime', 'event__slug', 'event__agenda__slug', 'pk')
|
||||
)
|
||||
|
||||
|
@ -2761,14 +2855,14 @@ class BookingsICS(BookingsAPI):
|
|||
except ValidationError as e:
|
||||
raise APIErrorBadRequest(N_('invalid payload'), errors=e.detail)
|
||||
|
||||
ics = vobject.iCalendar()
|
||||
ics.add('prodid').value = '-//Entr\'ouvert//NON SGML Publik'
|
||||
cal = icalendar.Calendar()
|
||||
cal['propid'] = '-//Entr\'ouvert//NON SGML Publik'
|
||||
|
||||
for booking in bookings:
|
||||
vevent = booking.get_vevent_ics()
|
||||
ics.add(vevent)
|
||||
cal.add_component(vevent)
|
||||
|
||||
return HttpResponse(ics.serialize(), content_type='text/calendar')
|
||||
return HttpResponse(cal.to_ical(), content_type='text/calendar')
|
||||
|
||||
|
||||
bookings_ics = BookingsICS.as_view()
|
||||
|
@ -2782,19 +2876,10 @@ class BookingAPI(APIView):
|
|||
super().initial(request, *args, **kwargs)
|
||||
self.booking = get_object_or_404(Booking, pk=kwargs.get('booking_pk'))
|
||||
|
||||
def check_booking(self, check_waiting_list=False):
|
||||
if self.booking.cancellation_datetime:
|
||||
raise APIError(N_('booking is cancelled'))
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if self.booking.primary_booking is not None:
|
||||
raise APIError(N_('secondary booking'), err=2)
|
||||
|
||||
if check_waiting_list and self.booking.in_waiting_list:
|
||||
raise APIError(N_('booking is in waiting list'), err=3)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.check_booking()
|
||||
|
||||
user_checks = self.booking.user_checks.all()
|
||||
user_check = None
|
||||
if len(user_checks) == 1:
|
||||
|
@ -2806,18 +2891,31 @@ class BookingAPI(APIView):
|
|||
{
|
||||
'err': 0,
|
||||
'booking_id': self.booking.pk,
|
||||
'places_count': self.booking.secondary_booking_set.count() + 1,
|
||||
}
|
||||
)
|
||||
return Response(response)
|
||||
|
||||
def patch(self, request, *args, **kwargs):
|
||||
self.check_booking(check_waiting_list=True)
|
||||
if self.booking.cancellation_datetime:
|
||||
raise APIError(N_('booking is cancelled'))
|
||||
|
||||
if self.booking.primary_booking is not None:
|
||||
raise APIError(N_('secondary booking'), err=2)
|
||||
|
||||
serializer = self.serializer_class(self.booking, data=request.data, partial=True)
|
||||
|
||||
if not serializer.is_valid():
|
||||
raise APIErrorBadRequest(N_('invalid payload'), errors=serializer.errors, err=4)
|
||||
|
||||
if set(serializer.validated_data) & {
|
||||
'user_was_present',
|
||||
'user_absence_reason',
|
||||
'user_presence_reason',
|
||||
}:
|
||||
if self.booking.in_waiting_list:
|
||||
raise APIError(N_('booking is in waiting list'), err=3)
|
||||
|
||||
if self.booking.event.agenda.kind != 'events' and (
|
||||
'user_was_present' in request.data
|
||||
or 'user_absence_reason' in request.data
|
||||
|
@ -2889,9 +2987,13 @@ class BookingAPI(APIView):
|
|||
return Response(response)
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
self.check_booking()
|
||||
if self.booking.cancellation_datetime:
|
||||
raise APIError(N_('booking is cancelled'))
|
||||
|
||||
self.booking.cancel()
|
||||
if self.booking.primary_booking is not None:
|
||||
raise APIError(N_('secondary booking'), err=2)
|
||||
|
||||
self.booking.cancel(request=request)
|
||||
response = {'err': 0, 'booking_id': self.booking.pk}
|
||||
return Response(response)
|
||||
|
||||
|
@ -2915,7 +3017,7 @@ class CancelBooking(APIView):
|
|||
raise APIError(N_('already cancelled'))
|
||||
if booking.primary_booking is not None:
|
||||
raise APIError(N_('secondary booking'), err=2)
|
||||
booking.cancel()
|
||||
booking.cancel(request=request)
|
||||
response = {'err': 0, 'booking_id': booking.id}
|
||||
return Response(response)
|
||||
|
||||
|
@ -2944,6 +3046,16 @@ class AcceptBooking(APIView):
|
|||
raise APIError(N_('booking is not in waiting list'), err=3)
|
||||
booking.accept()
|
||||
event = booking.event
|
||||
audit(
|
||||
'booking:accept',
|
||||
request=request,
|
||||
agenda=event.agenda,
|
||||
extra_data={
|
||||
'booking_id': booking.id,
|
||||
'event': event,
|
||||
},
|
||||
)
|
||||
|
||||
response = {
|
||||
'err': 0,
|
||||
'booking_id': booking.pk,
|
||||
|
@ -2975,6 +3087,17 @@ class SuspendBooking(APIView):
|
|||
if booking.in_waiting_list:
|
||||
raise APIError(N_('booking is already in waiting list'), err=3)
|
||||
booking.suspend()
|
||||
event = booking.event
|
||||
audit(
|
||||
'booking:suspend',
|
||||
request=request,
|
||||
agenda=event.agenda,
|
||||
extra_data={
|
||||
'booking': booking,
|
||||
'event': event,
|
||||
},
|
||||
)
|
||||
|
||||
response = {'err': 0, 'booking_id': booking.pk}
|
||||
return Response(response)
|
||||
|
||||
|
@ -3091,6 +3214,84 @@ class ResizeBooking(APIView):
|
|||
resize_booking = ResizeBooking.as_view()
|
||||
|
||||
|
||||
class PartialBookingsCheckAPI(APIView):
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
serializer_class = serializers.PartialBookingsCheckSerializer
|
||||
|
||||
def post(self, request, agenda_identifier):
|
||||
agenda = get_object_or_404(Agenda, slug=agenda_identifier, kind='events', partial_bookings=True)
|
||||
|
||||
serializer = self.serializer_class(data=request.data)
|
||||
if not serializer.is_valid():
|
||||
raise APIErrorBadRequest(N_('invalid payload'), errors=serializer.errors)
|
||||
check_timestamp = serializer.validated_data['timestamp']
|
||||
|
||||
event = get_object_or_404(
|
||||
Event,
|
||||
Q(checked=False) | Q(agenda__disable_check_update=False),
|
||||
start_datetime__date=check_timestamp.date(),
|
||||
recurrence_days__isnull=True,
|
||||
agenda=agenda,
|
||||
check_locked=False,
|
||||
)
|
||||
|
||||
if not (event.start_datetime.time() < check_timestamp.time() < event.end_time):
|
||||
raise APIError(N_('check time outside of opening hours'))
|
||||
|
||||
bookings = (
|
||||
Booking.objects.filter(
|
||||
event=event, user_external_id=serializer.validated_data['user_external_id']
|
||||
)
|
||||
.prefetch_related(
|
||||
# ignore absence checks
|
||||
Prefetch('user_checks', queryset=BookingCheck.objects.filter(presence=True))
|
||||
)
|
||||
.order_by('start_time')
|
||||
)
|
||||
|
||||
if not bookings:
|
||||
subscription = get_object_or_404(
|
||||
Subscription,
|
||||
agenda=agenda,
|
||||
user_external_id=serializer.validated_data['user_external_id'],
|
||||
date_start__lte=event.start_datetime,
|
||||
date_end__gt=event.start_datetime,
|
||||
)
|
||||
|
||||
# create dummy booking to allow check
|
||||
booking = event.booking_set.create(
|
||||
user_external_id=subscription.user_external_id,
|
||||
user_last_name=subscription.user_last_name,
|
||||
user_first_name=subscription.user_first_name,
|
||||
user_email=subscription.user_email,
|
||||
user_phone_number=subscription.user_phone_number,
|
||||
extra_data=subscription.extra_data,
|
||||
)
|
||||
bookings = [booking]
|
||||
|
||||
for booking in bookings:
|
||||
user_checks = booking.user_checks.all()
|
||||
|
||||
# create partial check for unchecked booking
|
||||
if not user_checks:
|
||||
BookingCheck.objects.create(booking=booking, presence=True, start_time=check_timestamp)
|
||||
break
|
||||
|
||||
# complete existing partial check
|
||||
user_check = user_checks[0]
|
||||
if not user_check.end_time:
|
||||
user_check.end_time = check_timestamp
|
||||
user_check.save()
|
||||
break
|
||||
else:
|
||||
raise APIError(N_('no booking to check'))
|
||||
|
||||
return Response({'err': 0})
|
||||
|
||||
|
||||
partial_bookings_check = PartialBookingsCheckAPI.as_view()
|
||||
|
||||
|
||||
class EventsAPI(APIView):
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
serializer_class = serializers.EventSerializer
|
||||
|
@ -3105,6 +3306,7 @@ class EventsAPI(APIView):
|
|||
event = serializer.save()
|
||||
if event.recurrence_days:
|
||||
event.create_all_recurrences()
|
||||
agenda.take_snapshot(request=self.request, comment=_('added event (%s)') % event)
|
||||
return Response({'err': 0, 'data': get_event_detail(request, event)})
|
||||
|
||||
|
||||
|
@ -3163,6 +3365,7 @@ class EventAPI(APIView):
|
|||
changed_data, payload, protected_fields, protected_fields + ['recurrence_end_date']
|
||||
):
|
||||
event = serializer.save()
|
||||
event.agenda.take_snapshot(request=self.request, comment=_('changed event (%s)') % event)
|
||||
return Response({'err': 0, 'data': get_event_detail(request, event)})
|
||||
|
||||
def delete(self, request, agenda_identifier, event_identifier):
|
||||
|
@ -3178,6 +3381,7 @@ class EventAPI(APIView):
|
|||
raise APIError(_('This cannot be removed as there are bookings for a future date.'))
|
||||
|
||||
event.delete()
|
||||
event.agenda.take_snapshot(request=self.request, comment=_('removed event (%s)') % event)
|
||||
return Response({'err': 0})
|
||||
|
||||
|
||||
|
@ -3232,6 +3436,14 @@ class EventCheck(APIView):
|
|||
if not event.checked:
|
||||
event.checked = True
|
||||
event.save(update_fields=['checked'])
|
||||
audit(
|
||||
'check:mark',
|
||||
request=request,
|
||||
agenda=event.agenda,
|
||||
extra_data={
|
||||
'event': event,
|
||||
},
|
||||
)
|
||||
event.async_notify_checked()
|
||||
response = {
|
||||
'err': 0,
|
||||
|
@ -3495,12 +3707,20 @@ class BookingsStatistics(APIView):
|
|||
)
|
||||
|
||||
def get_subfilters(self, agendas):
|
||||
extra_data_keys = (
|
||||
Booking.objects.filter(
|
||||
Q(event__agenda__in=agendas) | Q(event__agenda__virtual_agendas__in=agendas)
|
||||
)
|
||||
agenda_bookings = (
|
||||
Booking.objects.filter(event__agenda__in=agendas)
|
||||
.annotate(extra_data_keys=Func('extra_data', function='jsonb_object_keys'))
|
||||
.distinct('extra_data_keys')
|
||||
.values_list('extra_data_keys', flat=True)
|
||||
)
|
||||
virtual_bookings = (
|
||||
Booking.objects.filter(event__agenda__virtual_agendas__in=agendas)
|
||||
.annotate(extra_data_keys=Func('extra_data', function='jsonb_object_keys'))
|
||||
.distinct('extra_data_keys')
|
||||
.values_list('extra_data_keys', flat=True)
|
||||
)
|
||||
extra_data_keys = (
|
||||
agenda_bookings.union(virtual_bookings)
|
||||
.order_by('extra_data_keys')
|
||||
.values_list('extra_data_keys', flat=True)
|
||||
)
|
||||
|
|
|
@ -208,6 +208,7 @@ class Place(models.Model):
|
|||
'annulation_url': self.cancel_url,
|
||||
'plages': list(self.iter_open_dates()),
|
||||
'rdvs': list(self.iter_predemandes()),
|
||||
'logo_url': self.logo_url,
|
||||
}
|
||||
return payload
|
||||
|
||||
|
|
|
@ -14,19 +14,21 @@
|
|||
# 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 io
|
||||
import json
|
||||
import tarfile
|
||||
|
||||
from django.contrib.auth.models import Group
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import permissions
|
||||
from rest_framework.generics import GenericAPIView
|
||||
from rest_framework.response import Response
|
||||
|
||||
from chrono.agendas.models import Agenda, Category, EventsType, Resource, UnavailabilityCalendar
|
||||
from chrono.api.utils import APIErrorBadRequest
|
||||
from chrono.apps.export_import.models import Application, ApplicationElement
|
||||
from chrono.manager.utils import import_site
|
||||
|
||||
|
@ -34,18 +36,52 @@ klasses = {
|
|||
klass.application_component_type: klass
|
||||
for klass in [Agenda, Category, EventsType, Resource, UnavailabilityCalendar]
|
||||
}
|
||||
klasses['roles'] = Group
|
||||
klasses_translation = {
|
||||
'agendas_categories': 'categories', # categories type is already used in wcs for FormDef Category
|
||||
}
|
||||
klasses_translation_reverse = {v: k for k, v in klasses_translation.items()}
|
||||
|
||||
compare_urls = {
|
||||
'agendas': 'chrono-manager-agenda-history-compare',
|
||||
'categories': 'chrono-manager-category-history-compare',
|
||||
'events_types': 'chrono-manager-events-type-history-compare',
|
||||
'resources': 'chrono-manager-resource-history-compare',
|
||||
'unavailability_calendars': 'chrono-manager-unavailability-calendar-history-compare',
|
||||
}
|
||||
|
||||
|
||||
def get_klass_from_component_type(component_type):
|
||||
try:
|
||||
return klasses[component_type]
|
||||
except KeyError:
|
||||
raise Http404
|
||||
|
||||
|
||||
class Index(GenericAPIView):
|
||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
|
||||
permission_classes = (permissions.IsAdminUser,)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
data = []
|
||||
for klass in klasses.values():
|
||||
if klass == Group:
|
||||
data.append(
|
||||
{
|
||||
'id': 'roles',
|
||||
'text': _('Roles'),
|
||||
'singular': _('Role'),
|
||||
'urls': {
|
||||
'list': request.build_absolute_uri(
|
||||
reverse(
|
||||
'api-export-import-components-list',
|
||||
kwargs={'component_type': 'roles'},
|
||||
)
|
||||
),
|
||||
},
|
||||
'minor': True,
|
||||
}
|
||||
)
|
||||
continue
|
||||
component_type = {
|
||||
'id': klass.application_component_type,
|
||||
'text': klass.application_label_plural,
|
||||
|
@ -70,6 +106,16 @@ index = Index.as_view()
|
|||
|
||||
|
||||
def get_component_bundle_entry(request, component):
|
||||
if isinstance(component, Group):
|
||||
return {
|
||||
'id': component.role.slug if hasattr(component, 'role') else component.id,
|
||||
'text': component.name,
|
||||
'type': 'roles',
|
||||
'urls': {},
|
||||
# include uuid in object reference, this is not used for applification API but is useful
|
||||
# for authentic creating its role summary page.
|
||||
'uuid': component.role.uuid if hasattr(component, 'role') else None,
|
||||
}
|
||||
return {
|
||||
'id': str(component.slug),
|
||||
'text': component.label,
|
||||
|
@ -107,11 +153,14 @@ def get_component_bundle_entry(request, component):
|
|||
|
||||
|
||||
class ListComponents(GenericAPIView):
|
||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
|
||||
permission_classes = (permissions.IsAdminUser,)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
klass = klasses[kwargs['component_type']]
|
||||
response = [get_component_bundle_entry(request, x) for x in klass.objects.order_by('slug')]
|
||||
klass = get_klass_from_component_type(kwargs['component_type'])
|
||||
order_by = 'slug'
|
||||
if klass == Group:
|
||||
order_by = 'name'
|
||||
response = [get_component_bundle_entry(request, x) for x in klass.objects.order_by(order_by)]
|
||||
return Response({'data': response})
|
||||
|
||||
|
||||
|
@ -119,11 +168,11 @@ list_components = ListComponents.as_view()
|
|||
|
||||
|
||||
class ExportComponent(GenericAPIView):
|
||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
|
||||
permission_classes = (permissions.IsAdminUser,)
|
||||
|
||||
def get(self, request, slug, *args, **kwargs):
|
||||
klass = klasses[kwargs['component_type']]
|
||||
serialisation = klass.objects.get(slug=slug).export_json()
|
||||
klass = get_klass_from_component_type(kwargs['component_type'])
|
||||
serialisation = get_object_or_404(klass, slug=slug).export_json()
|
||||
return Response({'data': serialisation})
|
||||
|
||||
|
||||
|
@ -131,23 +180,13 @@ export_component = ExportComponent.as_view()
|
|||
|
||||
|
||||
class ComponentDependencies(GenericAPIView):
|
||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
|
||||
permission_classes = (permissions.IsAdminUser,)
|
||||
|
||||
def get(self, request, slug, *args, **kwargs):
|
||||
klass = klasses[kwargs['component_type']]
|
||||
component = klass.objects.get(slug=slug)
|
||||
klass = get_klass_from_component_type(kwargs['component_type'])
|
||||
component = get_object_or_404(klass, slug=slug)
|
||||
|
||||
def dependency_dict(element):
|
||||
if isinstance(element, Group):
|
||||
return {
|
||||
'id': element.role.slug if hasattr(element, 'role') else element.id,
|
||||
'text': element.name,
|
||||
'type': 'roles',
|
||||
'urls': {},
|
||||
# include uuid in object reference, this is not used for applification API but is useful
|
||||
# for authentic creating its role summary page.
|
||||
'uuid': element.role.uuid if hasattr(element, 'role') else None,
|
||||
}
|
||||
return get_component_bundle_entry(request, element)
|
||||
|
||||
dependencies = [dependency_dict(x) for x in component.get_dependencies() if x]
|
||||
|
@ -158,49 +197,200 @@ component_dependencies = ComponentDependencies.as_view()
|
|||
|
||||
|
||||
def component_redirect(request, component_type, slug):
|
||||
klass = klasses[component_type]
|
||||
klass = get_klass_from_component_type(component_type)
|
||||
component = get_object_or_404(klass, slug=slug)
|
||||
|
||||
if component_type not in klasses or component_type == 'roles':
|
||||
raise Http404
|
||||
|
||||
if (
|
||||
'compare' in request.GET
|
||||
and request.GET.get('application')
|
||||
and request.GET.get('version1')
|
||||
and request.GET.get('version2')
|
||||
):
|
||||
component_type = klasses_translation.get(component_type, component_type)
|
||||
return redirect(
|
||||
'%s?version1=%s&version2=%s&application=%s'
|
||||
% (
|
||||
reverse(compare_urls[component_type], args=[component.pk]),
|
||||
request.GET['version1'],
|
||||
request.GET['version2'],
|
||||
request.GET['application'],
|
||||
)
|
||||
)
|
||||
|
||||
if klass == Agenda:
|
||||
return redirect(reverse('chrono-manager-agenda-view', kwargs={'pk': component.pk}))
|
||||
if klass == Category:
|
||||
return redirect(reverse('chrono-manager-category-list'))
|
||||
if klass == EventsType:
|
||||
return redirect(reverse('chrono-manager-events-type-list'))
|
||||
if klass == Resource:
|
||||
return redirect(reverse('chrono-manager-resource-view', kwargs={'pk': component.pk}))
|
||||
if klass == UnavailabilityCalendar:
|
||||
return redirect(reverse('chrono-manager-unavailability-calendar-view', kwargs={'pk': component.pk}))
|
||||
raise Http404
|
||||
|
||||
|
||||
class BundleCheck(GenericAPIView):
|
||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
|
||||
permission_classes = (permissions.IsAdminUser,)
|
||||
|
||||
def put(self, request, *args, **kwargs):
|
||||
return Response({'err': 0, 'data': {}})
|
||||
def post(self, request, *args, **kwargs):
|
||||
bundle = request.FILES['bundle']
|
||||
try:
|
||||
with tarfile.open(fileobj=bundle) as tar:
|
||||
try:
|
||||
manifest = json.loads(tar.extractfile('manifest.json').read().decode())
|
||||
except KeyError:
|
||||
raise APIErrorBadRequest(_('Invalid tar file, missing manifest'))
|
||||
application_slug = manifest.get('slug')
|
||||
application_version = manifest.get('version_number')
|
||||
if not application_slug or not application_version:
|
||||
return Response({'data': {}})
|
||||
|
||||
differences = []
|
||||
unknown_elements = []
|
||||
no_history_elements = []
|
||||
legacy_elements = []
|
||||
content_types = ContentType.objects.get_for_models(
|
||||
*[v for k, v in klasses.items() if k != 'roles']
|
||||
)
|
||||
for element in manifest.get('elements'):
|
||||
component_type = element['type']
|
||||
if component_type not in klasses or component_type == 'roles':
|
||||
continue
|
||||
klass = klasses[component_type]
|
||||
component_type = klasses_translation.get(component_type, component_type)
|
||||
try:
|
||||
component = klass.objects.get(slug=element['slug'])
|
||||
except klass.DoesNotExist:
|
||||
unknown_elements.append(
|
||||
{
|
||||
'type': component_type,
|
||||
'slug': element['slug'],
|
||||
}
|
||||
)
|
||||
continue
|
||||
elements_qs = ApplicationElement.objects.filter(
|
||||
application__slug=application_slug,
|
||||
content_type=content_types[klass],
|
||||
object_id=component.pk,
|
||||
)
|
||||
if not elements_qs.exists():
|
||||
# object exists, but not linked to the application
|
||||
legacy_elements.append(
|
||||
{
|
||||
'type': component.application_component_type,
|
||||
'slug': str(component.slug),
|
||||
# information needed here, Relation objects may not exist yet in hobo
|
||||
'text': component.label,
|
||||
'url': reverse(
|
||||
'api-export-import-component-redirect',
|
||||
kwargs={
|
||||
'slug': str(component.slug),
|
||||
'component_type': component.application_component_type,
|
||||
},
|
||||
),
|
||||
}
|
||||
)
|
||||
continue
|
||||
snapshot_for_app = (
|
||||
klass.get_snapshot_model()
|
||||
.objects.filter(
|
||||
instance=component,
|
||||
application_slug=application_slug,
|
||||
application_version=application_version,
|
||||
)
|
||||
.order_by('timestamp')
|
||||
.last()
|
||||
)
|
||||
if not snapshot_for_app:
|
||||
# no snapshot for this bundle
|
||||
no_history_elements.append(
|
||||
{
|
||||
'type': element['type'],
|
||||
'slug': element['slug'],
|
||||
}
|
||||
)
|
||||
continue
|
||||
last_snapshot = (
|
||||
klass.get_snapshot_model().objects.filter(instance=component).latest('timestamp')
|
||||
)
|
||||
if snapshot_for_app.pk != last_snapshot.pk:
|
||||
differences.append(
|
||||
{
|
||||
'type': element['type'],
|
||||
'slug': element['slug'],
|
||||
'url': '%s?version1=%s&version2=%s'
|
||||
% (
|
||||
request.build_absolute_uri(
|
||||
reverse(compare_urls[component_type], args=[component.pk])
|
||||
),
|
||||
snapshot_for_app.pk,
|
||||
last_snapshot.pk,
|
||||
),
|
||||
}
|
||||
)
|
||||
except tarfile.TarError:
|
||||
raise APIErrorBadRequest(_('Invalid tar file'))
|
||||
|
||||
return Response(
|
||||
{
|
||||
'data': {
|
||||
'differences': differences,
|
||||
'unknown_elements': unknown_elements,
|
||||
'no_history_elements': no_history_elements,
|
||||
'legacy_elements': legacy_elements,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
bundle_check = BundleCheck.as_view()
|
||||
|
||||
|
||||
class BundleImport(GenericAPIView):
|
||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
|
||||
permission_classes = (permissions.IsAdminUser,)
|
||||
install = True
|
||||
|
||||
def put(self, request, *args, **kwargs):
|
||||
tar_io = io.BytesIO(request.read())
|
||||
def post(self, request, *args, **kwargs):
|
||||
bundle = request.FILES['bundle']
|
||||
components = {}
|
||||
with tarfile.open(fileobj=tar_io) as tar:
|
||||
manifest = json.loads(tar.extractfile('manifest.json').read().decode())
|
||||
self.application = Application.update_or_create_from_manifest(
|
||||
manifest,
|
||||
tar,
|
||||
editable=not self.install,
|
||||
)
|
||||
|
||||
for element in manifest.get('elements'):
|
||||
component_type = element['type']
|
||||
if component_type not in klasses or element['type'] == 'roles':
|
||||
continue
|
||||
component_type = klasses_translation.get(component_type, component_type)
|
||||
if component_type not in components:
|
||||
components[component_type] = []
|
||||
component_content = (
|
||||
tar.extractfile('%s/%s' % (element['type'], element['slug'])).read().decode()
|
||||
try:
|
||||
with tarfile.open(fileobj=bundle) as tar:
|
||||
try:
|
||||
manifest = json.loads(tar.extractfile('manifest.json').read().decode())
|
||||
except KeyError:
|
||||
raise APIErrorBadRequest(_('Invalid tar file, missing manifest'))
|
||||
self.application = Application.update_or_create_from_manifest(
|
||||
manifest,
|
||||
tar,
|
||||
editable=not self.install,
|
||||
)
|
||||
components[component_type].append(json.loads(component_content).get('data'))
|
||||
|
||||
for element in manifest.get('elements'):
|
||||
component_type = element['type']
|
||||
if component_type not in klasses or element['type'] == 'roles':
|
||||
continue
|
||||
component_type = klasses_translation.get(component_type, component_type)
|
||||
if component_type not in components:
|
||||
components[component_type] = []
|
||||
try:
|
||||
component_content = (
|
||||
tar.extractfile('%s/%s' % (element['type'], element['slug'])).read().decode()
|
||||
)
|
||||
except KeyError:
|
||||
raise APIErrorBadRequest(
|
||||
_(
|
||||
'Invalid tar file, missing component %s/%s'
|
||||
% (element['type'], element['slug'])
|
||||
)
|
||||
)
|
||||
components[component_type].append(json.loads(component_content).get('data'))
|
||||
except tarfile.TarError:
|
||||
raise APIErrorBadRequest(_('Invalid tar file'))
|
||||
|
||||
# init cache of application elements, from manifest
|
||||
self.application_elements = set()
|
||||
# import agendas
|
||||
|
@ -229,6 +419,11 @@ class BundleImport(GenericAPIView):
|
|||
self.application, existing_component
|
||||
)
|
||||
self.application_elements.add(element.content_object)
|
||||
if self.install is True:
|
||||
existing_component.take_snapshot(
|
||||
comment=_('Application (%s)') % self.application,
|
||||
application=self.application,
|
||||
)
|
||||
|
||||
def unlink_obsolete_objects(self):
|
||||
known_elements = ApplicationElement.objects.filter(application=self.application)
|
||||
|
@ -252,7 +447,7 @@ bundle_declare = BundleDeclare.as_view()
|
|||
|
||||
|
||||
class BundleUnlink(GenericAPIView):
|
||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
|
||||
permission_classes = (permissions.IsAdminUser,)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if request.POST.get('application'):
|
||||
|
|
|
@ -21,6 +21,14 @@ from django.contrib.contenttypes.models import ContentType
|
|||
from django.db import models
|
||||
|
||||
|
||||
class WithApplicationMixin:
|
||||
@property
|
||||
def applications(self):
|
||||
if getattr(self, '_applications', None) is None:
|
||||
Application.load_for_object(self)
|
||||
return self._applications
|
||||
|
||||
|
||||
class Application(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
slug = models.SlugField(max_length=100, unique=True)
|
||||
|
@ -47,10 +55,10 @@ class Application(models.Model):
|
|||
slug=manifest.get('slug'), defaults={'editable': editable}
|
||||
)
|
||||
application.name = manifest.get('application')
|
||||
application.description = manifest.get('description')
|
||||
application.documentation_url = manifest.get('documentation_url')
|
||||
application.description = manifest.get('description') or ''
|
||||
application.documentation_url = manifest.get('documentation_url') or ''
|
||||
application.version_number = manifest.get('version_number') or 'unknown'
|
||||
application.version_notes = manifest.get('version_notes')
|
||||
application.version_notes = manifest.get('version_notes') or ''
|
||||
if not editable:
|
||||
application.editable = editable
|
||||
application.visible = manifest.get('visible', True)
|
||||
|
@ -71,34 +79,23 @@ class Application(models.Model):
|
|||
@classmethod
|
||||
def populate_objects(cls, object_class, objects):
|
||||
content_type = ContentType.objects.get_for_model(object_class)
|
||||
elements = ApplicationElement.objects.filter(content_type=content_type)
|
||||
elements = ApplicationElement.objects.filter(
|
||||
content_type=content_type, application__visible=True
|
||||
).prefetch_related('application')
|
||||
elements_by_objects = collections.defaultdict(list)
|
||||
for element in elements:
|
||||
elements_by_objects[element.content_object].append(element)
|
||||
applications_by_ids = {
|
||||
a.pk: a for a in cls.objects.filter(pk__in=elements.values('application'), visible=True)
|
||||
}
|
||||
elements_by_objects[element.object_id].append(element)
|
||||
for obj in objects:
|
||||
applications = []
|
||||
elements = elements_by_objects.get(obj) or []
|
||||
for element in elements:
|
||||
application = applications_by_ids.get(element.application_id)
|
||||
if application:
|
||||
applications.append(application)
|
||||
applications = [element.application for element in elements_by_objects.get(obj.pk) or []]
|
||||
obj._applications = sorted(applications, key=lambda a: a.name)
|
||||
|
||||
@classmethod
|
||||
def load_for_object(cls, obj):
|
||||
content_type = ContentType.objects.get_for_model(obj.__class__)
|
||||
elements = ApplicationElement.objects.filter(content_type=content_type, object_id=obj.pk)
|
||||
applications_by_ids = {
|
||||
a.pk: a for a in cls.objects.filter(pk__in=elements.values('application'), visible=True)
|
||||
}
|
||||
applications = []
|
||||
for element in elements:
|
||||
application = applications_by_ids.get(element.application_id)
|
||||
if application:
|
||||
applications.append(application)
|
||||
elements = ApplicationElement.objects.filter(
|
||||
content_type=content_type, object_id=obj.pk, application__visible=True
|
||||
).prefetch_related('application')
|
||||
applications = [element.application for element in elements]
|
||||
obj._applications = sorted(applications, key=lambda a: a.name)
|
||||
|
||||
def get_objects_for_object_class(self, object_class):
|
||||
|
@ -106,6 +103,12 @@ class Application(models.Model):
|
|||
elements = ApplicationElement.objects.filter(content_type=content_type, application=self)
|
||||
return object_class.objects.filter(pk__in=elements.values('object_id'))
|
||||
|
||||
@classmethod
|
||||
def get_orphan_objects_for_object_class(cls, object_class):
|
||||
content_type = ContentType.objects.get_for_model(object_class)
|
||||
elements = ApplicationElement.objects.filter(content_type=content_type, application__visible=True)
|
||||
return object_class.objects.exclude(pk__in=elements.values('object_id'))
|
||||
|
||||
|
||||
class ApplicationElement(models.Model):
|
||||
application = models.ForeignKey(Application, on_delete=models.CASCADE)
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
# chrono - agendas system
|
||||
# Copyright (C) 2016-2024 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# 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 datetime
|
||||
|
||||
import django_filters
|
||||
from django.forms.widgets import DateInput
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from chrono.agendas.models import Agenda
|
||||
|
||||
from .models import AuditEntry
|
||||
|
||||
|
||||
class DateWidget(DateInput):
|
||||
input_type = 'date'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['format'] = '%Y-%m-%d'
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class DayFilter(django_filters.DateFilter):
|
||||
def filter(self, qs, value):
|
||||
if value:
|
||||
qs = qs.filter(timestamp__gte=value, timestamp__lt=value + datetime.timedelta(days=1))
|
||||
return qs
|
||||
|
||||
|
||||
class JournalFilterSet(django_filters.FilterSet):
|
||||
timestamp = DayFilter(widget=DateWidget())
|
||||
agenda = django_filters.ModelChoiceFilter(queryset=Agenda.objects.all())
|
||||
action_type = django_filters.ChoiceFilter(
|
||||
choices=(
|
||||
('booking', _('Booking')),
|
||||
('check', _('Checking')),
|
||||
('invoice', _('Invoicing')),
|
||||
)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = AuditEntry
|
||||
fields = []
|
|
@ -0,0 +1,6 @@
|
|||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = []
|
||||
operations = []
|
|
@ -0,0 +1,52 @@
|
|||
# Generated by Django 3.2.16 on 2024-04-23 11:49
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('agendas', '0171_snapshot_models'),
|
||||
('journal', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AuditEntry',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
('timestamp', models.DateTimeField(auto_now_add=True, verbose_name='Date')),
|
||||
('action_type', models.CharField(max_length=100, verbose_name='Action type')),
|
||||
('action_code', models.CharField(max_length=100, verbose_name='Action code')),
|
||||
('extra_data', models.JSONField(blank=True, default=dict)),
|
||||
(
|
||||
'agenda',
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name='audit_entries',
|
||||
to='agendas.agenda',
|
||||
),
|
||||
),
|
||||
(
|
||||
'user',
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name='User',
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'ordering': ('-timestamp',),
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,57 @@
|
|||
# chrono - agendas system
|
||||
# Copyright (C) 2016-2024 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# 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.conf import settings
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
MESSAGES = {
|
||||
'booking:accept': _('acceptation of booking (%(booking_id)s) in event "%(event)s"'),
|
||||
'booking:cancel': _('cancellation of booking (%(booking_id)s) in event "%(event)s"'),
|
||||
'booking:create': _('created booking (%(booking_id)s) for event %(event)s'),
|
||||
'booking:suspend': _('suspension of booking (%(booking_id)s) in event "%(event)s"'),
|
||||
'check:mark': _('marked event %(event)s as checked'),
|
||||
'check:mark-unchecked-absent': _('marked unchecked users as absent in %(event)s'),
|
||||
'check:reset': _('reset check of %(user_name)s in %(event)s'),
|
||||
'check:lock': _('marked event %(event)s as locked for checks'),
|
||||
'check:unlock': _('unmarked event %(event)s as locked for checks'),
|
||||
'check:absence': _('marked absence of %(user_name)s in %(event)s'),
|
||||
'check:presence': _('marked presence of %(user_name)s in %(event)s'),
|
||||
'invoice:mark': _('marked event %(event)s as invoiced'),
|
||||
'invoice:unmark': _('unmarked event %(event)s as invoiced'),
|
||||
}
|
||||
|
||||
|
||||
class AuditEntry(models.Model):
|
||||
timestamp = models.DateTimeField(verbose_name=_('Date'), auto_now_add=True)
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, verbose_name=_('User'), on_delete=models.SET_NULL, null=True
|
||||
)
|
||||
action_type = models.CharField(verbose_name=_('Action type'), max_length=100)
|
||||
action_code = models.CharField(verbose_name=_('Action code'), max_length=100)
|
||||
agenda = models.ForeignKey(
|
||||
'agendas.Agenda', on_delete=models.SET_NULL, null=True, related_name='audit_entries'
|
||||
)
|
||||
extra_data = models.JSONField(blank=True, default=dict)
|
||||
|
||||
class Meta:
|
||||
ordering = ('-timestamp',)
|
||||
|
||||
def get_action_text(self):
|
||||
try:
|
||||
return MESSAGES[f'{self.action_type}:{self.action_code}'] % self.extra_data
|
||||
except KeyError:
|
||||
return _('Unknown entry (%s:%s)') % (self.action_type, self.action_code)
|
|
@ -0,0 +1,49 @@
|
|||
{% extends "chrono/manager_base.html" %}
|
||||
{% load gadjo i18n %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url 'chrono-manager-audit-journal' %}">{% trans "Audit journal" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans "Audit journal" %}</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<table class="main">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Date" %}</th>
|
||||
<th>{% trans "User" %}</th>
|
||||
<th>{% trans "Agenda" %}</th>
|
||||
<th>{% trans "Action" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for line in object_list %}
|
||||
<tr>
|
||||
<td>{{ line.timestamp }}</td>
|
||||
<td>{{ line.user.get_full_name }}</td>
|
||||
<td>{{ line.agenda }}</td>
|
||||
<td>{{ line.get_action_text }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% include "gadjo/pagination.html" %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
<aside id="sidebar">
|
||||
<h3>{% trans "Search" %}</h3>
|
||||
<form action=".">
|
||||
{{ filter.form|with_template }}
|
||||
<div class="buttons">
|
||||
<button>{% trans "Search" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</aside>
|
||||
{% endblock %}
|
|
@ -0,0 +1,24 @@
|
|||
# chrono - agendas system
|
||||
# Copyright (C) 2016-2024 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# 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.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.journal_home, name='chrono-manager-audit-journal'),
|
||||
]
|
|
@ -0,0 +1,39 @@
|
|||
# chrono - agendas system
|
||||
# Copyright (C) 2016-2024 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# 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.contrib.auth import get_user_model
|
||||
|
||||
from .models import AuditEntry
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
def audit(action, request=None, user=None, agenda=None, extra_data=None):
|
||||
action_type, action_code = action.split(':', 1)
|
||||
extra_data = extra_data or {}
|
||||
if 'event' in extra_data:
|
||||
extra_data['event_id'] = extra_data['event'].id
|
||||
extra_data['event'] = extra_data['event'].get_journal_label()
|
||||
if 'booking' in extra_data:
|
||||
extra_data['booking_id'] = extra_data['booking'].id
|
||||
extra_data['booking'] = extra_data['booking'].get_journal_label()
|
||||
return AuditEntry.objects.create(
|
||||
user=request.user if request and isinstance(request.user, User) else user,
|
||||
action_type=action_type,
|
||||
action_code=action_code,
|
||||
agenda=agenda,
|
||||
extra_data=extra_data,
|
||||
)
|
|
@ -0,0 +1,42 @@
|
|||
# chrono - agendas system
|
||||
# Copyright (C) 2016-2024 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# 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.core.exceptions import PermissionDenied
|
||||
from django.views.generic import ListView
|
||||
|
||||
from .forms import JournalFilterSet
|
||||
|
||||
|
||||
class JournalHomeView(ListView):
|
||||
template_name = 'chrono/journal/home.html'
|
||||
paginate_by = 10
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not request.user.is_staff:
|
||||
raise PermissionDenied()
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
self.filterset = JournalFilterSet(self.request.GET)
|
||||
return self.filterset.qs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['filter'] = self.filterset
|
||||
return context
|
||||
|
||||
|
||||
journal_home = JournalHomeView.as_view()
|
|
@ -0,0 +1,30 @@
|
|||
# chrono - agendas system
|
||||
# Copyright (C) 2016-2024 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# 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 datetime
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils.timezone import now
|
||||
|
||||
from chrono.agendas.models import Agenda, Category, EventsType, Resource, UnavailabilityCalendar
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Clear obsolete snapshot instances'
|
||||
|
||||
def handle(self, **options):
|
||||
for model in [Agenda, Category, EventsType, Resource, UnavailabilityCalendar]:
|
||||
model.snapshots.filter(updated_at__lte=now() - datetime.timedelta(days=1)).delete()
|
|
@ -0,0 +1,7 @@
|
|||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = []
|
||||
|
||||
operations = []
|
|
@ -0,0 +1,186 @@
|
|||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('agendas', '0170_alter_agenda_events_type'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('snapshot', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='UnavailabilityCalendarSnapshot',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||
('comment', models.TextField(blank=True, null=True)),
|
||||
('serialization', models.JSONField(blank=True, default=dict)),
|
||||
('label', models.CharField(blank=True, max_length=150, verbose_name='Label')),
|
||||
('application_slug', models.CharField(max_length=100, null=True)),
|
||||
('application_version', models.CharField(max_length=100, null=True)),
|
||||
(
|
||||
'instance',
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to='agendas.unavailabilitycalendar',
|
||||
related_name='instance_snapshots',
|
||||
),
|
||||
),
|
||||
(
|
||||
'user',
|
||||
models.ForeignKey(
|
||||
null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'ordering': ('-timestamp',),
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ResourceSnapshot',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||
('comment', models.TextField(blank=True, null=True)),
|
||||
('serialization', models.JSONField(blank=True, default=dict)),
|
||||
('label', models.CharField(blank=True, max_length=150, verbose_name='Label')),
|
||||
('application_slug', models.CharField(max_length=100, null=True)),
|
||||
('application_version', models.CharField(max_length=100, null=True)),
|
||||
(
|
||||
'instance',
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to='agendas.resource',
|
||||
related_name='instance_snapshots',
|
||||
),
|
||||
),
|
||||
(
|
||||
'user',
|
||||
models.ForeignKey(
|
||||
null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'ordering': ('-timestamp',),
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EventsTypeSnapshot',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||
('comment', models.TextField(blank=True, null=True)),
|
||||
('serialization', models.JSONField(blank=True, default=dict)),
|
||||
('label', models.CharField(blank=True, max_length=150, verbose_name='Label')),
|
||||
('application_slug', models.CharField(max_length=100, null=True)),
|
||||
('application_version', models.CharField(max_length=100, null=True)),
|
||||
(
|
||||
'instance',
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to='agendas.eventstype',
|
||||
related_name='instance_snapshots',
|
||||
),
|
||||
),
|
||||
(
|
||||
'user',
|
||||
models.ForeignKey(
|
||||
null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'ordering': ('-timestamp',),
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CategorySnapshot',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||
('comment', models.TextField(blank=True, null=True)),
|
||||
('serialization', models.JSONField(blank=True, default=dict)),
|
||||
('label', models.CharField(blank=True, max_length=150, verbose_name='Label')),
|
||||
('application_slug', models.CharField(max_length=100, null=True)),
|
||||
('application_version', models.CharField(max_length=100, null=True)),
|
||||
(
|
||||
'instance',
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to='agendas.category',
|
||||
related_name='instance_snapshots',
|
||||
),
|
||||
),
|
||||
(
|
||||
'user',
|
||||
models.ForeignKey(
|
||||
null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'ordering': ('-timestamp',),
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AgendaSnapshot',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||
('comment', models.TextField(blank=True, null=True)),
|
||||
('serialization', models.JSONField(blank=True, default=dict)),
|
||||
('label', models.CharField(blank=True, max_length=150, verbose_name='Label')),
|
||||
('application_slug', models.CharField(max_length=100, null=True)),
|
||||
('application_version', models.CharField(max_length=100, null=True)),
|
||||
(
|
||||
'instance',
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to='agendas.agenda',
|
||||
related_name='instance_snapshots',
|
||||
),
|
||||
),
|
||||
(
|
||||
'user',
|
||||
models.ForeignKey(
|
||||
null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'ordering': ('-timestamp',),
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,182 @@
|
|||
# chrono - agendas system
|
||||
# Copyright (C) 2016-2024 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# 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.conf import settings
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class WithSnapshotManager(models.Manager):
|
||||
snapshots = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.snapshots = kwargs.pop('snapshots', False)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
if self.snapshots:
|
||||
return queryset.filter(snapshot__isnull=False)
|
||||
else:
|
||||
return queryset.filter(snapshot__isnull=True)
|
||||
|
||||
|
||||
class WithSnapshotMixin:
|
||||
@classmethod
|
||||
def get_snapshot_model(cls):
|
||||
return cls._meta.get_field('snapshot').related_model
|
||||
|
||||
def take_snapshot(self, *args, **kwargs):
|
||||
return self.get_snapshot_model().take(self, *args, **kwargs)
|
||||
|
||||
|
||||
class AbstractSnapshot(models.Model):
|
||||
timestamp = models.DateTimeField(auto_now_add=True)
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True)
|
||||
comment = models.TextField(blank=True, null=True)
|
||||
serialization = models.JSONField(blank=True, default=dict)
|
||||
label = models.CharField(_('Label'), max_length=150, blank=True)
|
||||
application_slug = models.CharField(max_length=100, null=True)
|
||||
application_version = models.CharField(max_length=100, null=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
ordering = ('-timestamp',)
|
||||
|
||||
@classmethod
|
||||
def get_instance_model(cls):
|
||||
return cls._meta.get_field('instance').related_model
|
||||
|
||||
@classmethod
|
||||
def take(cls, instance, request=None, comment=None, deletion=False, label=None, application=None):
|
||||
snapshot = cls(instance=instance, comment=comment, label=label or '')
|
||||
if request and not request.user.is_anonymous:
|
||||
snapshot.user = request.user
|
||||
if not deletion:
|
||||
snapshot.serialization = instance.export_json()
|
||||
else:
|
||||
snapshot.serialization = {}
|
||||
snapshot.comment = comment or _('deletion')
|
||||
if application:
|
||||
snapshot.application_slug = application.slug
|
||||
snapshot.application_version = application.version_number
|
||||
snapshot.save()
|
||||
return snapshot
|
||||
|
||||
def get_instance(self):
|
||||
try:
|
||||
# try reusing existing instance
|
||||
instance = self.get_instance_model().snapshots.get(snapshot=self)
|
||||
except self.get_instance_model().DoesNotExist:
|
||||
instance = self.load_instance(self.serialization, snapshot=self)
|
||||
instance.slug = self.serialization['slug'] # restore slug
|
||||
return instance
|
||||
|
||||
def load_instance(self, json_instance, snapshot=None):
|
||||
return self.get_instance_model().import_json(json_instance, snapshot=snapshot)[1]
|
||||
|
||||
def load_history(self):
|
||||
if self.instance is None:
|
||||
self._history = []
|
||||
return
|
||||
history = type(self).objects.filter(instance=self.instance)
|
||||
self._history = [s.id for s in history]
|
||||
|
||||
@property
|
||||
def previous(self):
|
||||
if not hasattr(self, '_history'):
|
||||
self.load_history()
|
||||
|
||||
try:
|
||||
idx = self._history.index(self.id)
|
||||
except ValueError:
|
||||
return None
|
||||
if idx == 0:
|
||||
return None
|
||||
return self._history[idx - 1]
|
||||
|
||||
@property
|
||||
def next(self):
|
||||
if not hasattr(self, '_history'):
|
||||
self.load_history()
|
||||
|
||||
try:
|
||||
idx = self._history.index(self.id)
|
||||
except ValueError:
|
||||
return None
|
||||
try:
|
||||
return self._history[idx + 1]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def first(self):
|
||||
if not hasattr(self, '_history'):
|
||||
self.load_history()
|
||||
|
||||
return self._history[0]
|
||||
|
||||
@property
|
||||
def last(self):
|
||||
if not hasattr(self, '_history'):
|
||||
self.load_history()
|
||||
|
||||
return self._history[-1]
|
||||
|
||||
|
||||
class AgendaSnapshot(AbstractSnapshot):
|
||||
instance = models.ForeignKey(
|
||||
'agendas.Agenda',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
related_name='instance_snapshots',
|
||||
)
|
||||
|
||||
|
||||
class CategorySnapshot(AbstractSnapshot):
|
||||
instance = models.ForeignKey(
|
||||
'agendas.Category',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
related_name='instance_snapshots',
|
||||
)
|
||||
|
||||
|
||||
class EventsTypeSnapshot(AbstractSnapshot):
|
||||
instance = models.ForeignKey(
|
||||
'agendas.EventsType',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
related_name='instance_snapshots',
|
||||
)
|
||||
|
||||
|
||||
class ResourceSnapshot(AbstractSnapshot):
|
||||
instance = models.ForeignKey(
|
||||
'agendas.Resource',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
related_name='instance_snapshots',
|
||||
)
|
||||
|
||||
|
||||
class UnavailabilityCalendarSnapshot(AbstractSnapshot):
|
||||
instance = models.ForeignKey(
|
||||
'agendas.UnavailabilityCalendar',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
related_name='instance_snapshots',
|
||||
)
|
|
@ -0,0 +1,213 @@
|
|||
# chrono - agendas system
|
||||
# Copyright (C) 2016-2024 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# 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 difflib
|
||||
import json
|
||||
import re
|
||||
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template import loader
|
||||
from django.urls import reverse
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import DetailView, ListView
|
||||
from lxml.html.diff import htmldiff
|
||||
from pyquery import PyQuery as pq
|
||||
|
||||
from chrono.utils.timezone import localtime
|
||||
|
||||
|
||||
class InstanceWithSnapshotHistoryView(ListView):
|
||||
def get_queryset(self):
|
||||
self.instance = get_object_or_404(self.model.get_instance_model(), pk=self.kwargs['pk'])
|
||||
return self.instance.instance_snapshots.all().select_related('user')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs[self.instance_context_key] = self.instance
|
||||
kwargs['object'] = self.instance
|
||||
current_date = None
|
||||
context = super().get_context_data(**kwargs)
|
||||
day_snapshot = None
|
||||
for snapshot in context['object_list']:
|
||||
if snapshot.timestamp.date() != current_date:
|
||||
current_date = snapshot.timestamp.date()
|
||||
snapshot.new_day = True
|
||||
snapshot.day_other_count = 0
|
||||
day_snapshot = snapshot
|
||||
else:
|
||||
day_snapshot.day_other_count += 1
|
||||
return context
|
||||
|
||||
|
||||
class InstanceWithSnapshotHistoryCompareView(DetailView):
|
||||
def get_snapshots_from_application(self):
|
||||
version1 = self.request.GET.get('version1')
|
||||
version2 = self.request.GET.get('version2')
|
||||
if not version1 or not version2:
|
||||
raise Http404
|
||||
|
||||
snapshot_for_app1 = (
|
||||
self.model.get_snapshot_model()
|
||||
.objects.filter(
|
||||
instance=self.object,
|
||||
application_slug=self.request.GET['application'],
|
||||
application_version=self.request.GET['version1'],
|
||||
)
|
||||
.order_by('timestamp')
|
||||
.last()
|
||||
)
|
||||
snapshot_for_app2 = (
|
||||
self.model.get_snapshot_model()
|
||||
.objects.filter(
|
||||
instance=self.object,
|
||||
application_slug=self.request.GET['application'],
|
||||
application_version=self.request.GET['version2'],
|
||||
)
|
||||
.order_by('timestamp')
|
||||
.last()
|
||||
)
|
||||
return snapshot_for_app1, snapshot_for_app2
|
||||
|
||||
def get_snapshots(self):
|
||||
if 'application' in self.request.GET:
|
||||
return self.get_snapshots_from_application()
|
||||
|
||||
id1 = self.request.GET.get('version1')
|
||||
id2 = self.request.GET.get('version2')
|
||||
if not id1 or not id2:
|
||||
raise Http404
|
||||
|
||||
snapshot1 = get_object_or_404(self.model.get_snapshot_model(), pk=id1, instance=self.object)
|
||||
snapshot2 = get_object_or_404(self.model.get_snapshot_model(), pk=id2, instance=self.object)
|
||||
|
||||
return snapshot1, snapshot2
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs[self.instance_context_key] = self.object
|
||||
|
||||
mode = self.request.GET.get('mode') or 'json'
|
||||
if mode not in ['json', 'inspect']:
|
||||
raise Http404
|
||||
|
||||
snapshot1, snapshot2 = self.get_snapshots()
|
||||
if not snapshot1 or not snapshot2:
|
||||
return redirect(reverse(self.history_view, args=[self.object.pk]))
|
||||
if snapshot1.timestamp > snapshot2.timestamp:
|
||||
snapshot1, snapshot2 = snapshot2, snapshot1
|
||||
|
||||
kwargs['mode'] = mode
|
||||
kwargs['snapshot1'] = snapshot1
|
||||
kwargs['snapshot2'] = snapshot2
|
||||
kwargs['fromdesc'] = self.get_snapshot_desc(snapshot1)
|
||||
kwargs['todesc'] = self.get_snapshot_desc(snapshot2)
|
||||
kwargs.update(getattr(self, 'get_compare_%s_context' % mode)(snapshot1, snapshot2))
|
||||
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
context = self.get_context_data(object=self.object)
|
||||
if isinstance(context, HttpResponseRedirect):
|
||||
return context
|
||||
return self.render_to_response(context)
|
||||
|
||||
def get_compare_inspect_context(self, snapshot1, snapshot2):
|
||||
instance1 = snapshot1.get_instance()
|
||||
instance2 = snapshot2.get_instance()
|
||||
|
||||
def get_context(instance):
|
||||
return {
|
||||
'object': instance,
|
||||
}
|
||||
|
||||
def fix_result(panel_diff):
|
||||
if not panel_diff:
|
||||
return panel_diff
|
||||
panel = pq(panel_diff)
|
||||
# remove "Link" added by htmldiff
|
||||
for link in panel.find('a'):
|
||||
d = pq(link)
|
||||
text = d.html()
|
||||
new_text = re.sub(r' Link: .*$', '', text)
|
||||
d.html(new_text)
|
||||
# remove empty ins and del tags
|
||||
for elem in panel.find('ins, del'):
|
||||
d = pq(elem)
|
||||
if not (d.html() or '').strip():
|
||||
d.remove()
|
||||
# prevent auto-closing behaviour of pyquery .html() method
|
||||
for elem in panel.find('span, ul, div'):
|
||||
d = pq(elem)
|
||||
if not d.html():
|
||||
d.html(' ')
|
||||
return panel.html()
|
||||
|
||||
inspect1 = loader.render_to_string(self.inspect_template_name, get_context(instance1), self.request)
|
||||
d1 = pq(str(inspect1))
|
||||
inspect2 = loader.render_to_string(self.inspect_template_name, get_context(instance2), self.request)
|
||||
d2 = pq(str(inspect2))
|
||||
panels_attrs = [tab.attrib for tab in d1('[role="tabpanel"]')]
|
||||
panels1 = list(d1('[role="tabpanel"]'))
|
||||
panels2 = list(d2('[role="tabpanel"]'))
|
||||
|
||||
# build tab list (merge version 1 and version2)
|
||||
tabs1 = d1.find('[role="tab"]')
|
||||
tabs2 = d2.find('[role="tab"]')
|
||||
tabs_order = [t.get('id') for t in panels_attrs]
|
||||
tabs = {}
|
||||
for tab in tabs1 + tabs2:
|
||||
tab_id = pq(tab).attr('aria-controls')
|
||||
tabs[tab_id] = pq(tab).outer_html()
|
||||
tabs = [tabs[k] for k in tabs_order if k in tabs]
|
||||
|
||||
# build diff of each panel
|
||||
panels_diff = list(map(htmldiff, panels1, panels2))
|
||||
panels_diff = [fix_result(t) for t in panels_diff]
|
||||
|
||||
return {
|
||||
'tabs': tabs,
|
||||
'panels': zip(panels_attrs, panels_diff),
|
||||
'tab_class_names': d1('.pk-tabs').attr('class'),
|
||||
}
|
||||
|
||||
def get_compare_json_context(self, snapshot1, snapshot2):
|
||||
s1 = json.dumps(snapshot1.serialization, sort_keys=True, indent=2)
|
||||
s2 = json.dumps(snapshot2.serialization, sort_keys=True, indent=2)
|
||||
diff_serialization = difflib.HtmlDiff(wrapcolumn=160).make_table(
|
||||
fromlines=s1.splitlines(True),
|
||||
tolines=s2.splitlines(True),
|
||||
)
|
||||
|
||||
return {
|
||||
'diff_serialization': diff_serialization,
|
||||
}
|
||||
|
||||
def get_snapshot_desc(self, snapshot):
|
||||
label_or_comment = ''
|
||||
if snapshot.label:
|
||||
label_or_comment = snapshot.label
|
||||
elif snapshot.comment:
|
||||
label_or_comment = snapshot.comment
|
||||
if snapshot.application_version:
|
||||
label_or_comment += ' (%s)' % _('Version %s') % snapshot.application_version
|
||||
return '{name} ({pk}) - {label_or_comment} ({user}{timestamp})'.format(
|
||||
name=_('Snapshot'),
|
||||
pk=snapshot.id,
|
||||
label_or_comment=label_or_comment,
|
||||
user='%s ' % snapshot.user if snapshot.user_id else '',
|
||||
timestamp=date_format(localtime(snapshot.timestamp), format='DATETIME_FORMAT'),
|
||||
)
|
|
@ -0,0 +1,23 @@
|
|||
# chrono - agendas system
|
||||
# Copyright (C) 2024 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# 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.urls import path
|
||||
|
||||
from . import api_views
|
||||
|
||||
urlpatterns = [
|
||||
path('save', api_views.save_preference, name='api-user-preferences'),
|
||||
]
|
|
@ -0,0 +1,43 @@
|
|||
# chrono - agendas system
|
||||
# Copyright (C) 2024 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# 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 json
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpResponse, HttpResponseBadRequest
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from . import models
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@login_required
|
||||
def save_preference(request):
|
||||
user_pref, dummy = models.UserPreferences.objects.get_or_create(user=request.user)
|
||||
|
||||
if len(request.body) > 1000:
|
||||
return HttpResponseBadRequest(_('Payload is too large'))
|
||||
try:
|
||||
prefs = json.loads(request.body)
|
||||
except json.JSONDecodeError:
|
||||
return HttpResponseBadRequest(_('Bad format'))
|
||||
if not isinstance(prefs, dict) or len(prefs) != 1:
|
||||
return HttpResponseBadRequest(_('Bad format'))
|
||||
|
||||
user_pref.preferences.update(prefs)
|
||||
user_pref.save()
|
||||
return HttpResponse('', status=204)
|
|
@ -0,0 +1,32 @@
|
|||
# Generated by Django 3.2.18 on 2024-04-11 15:30
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='UserPreferences',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
('preferences', models.JSONField(default=dict, verbose_name='Preferences')),
|
||||
(
|
||||
'user',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -0,0 +1,24 @@
|
|||
# chrono - agendas system
|
||||
# Copyright (C) 2024 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# 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.conf import settings
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class UserPreferences(models.Model):
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||
preferences = models.JSONField(_('Preferences'), default=dict)
|
File diff suppressed because it is too large
Load Diff
|
@ -82,7 +82,7 @@ class AgendaAddForm(forms.ModelForm):
|
|||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if settings.PARTIAL_BOOKINGS_ENABLED:
|
||||
if 'kind' in self.fields and settings.PARTIAL_BOOKINGS_ENABLED:
|
||||
self.fields['kind'].choices += [('partial-bookings', _('Partial bookings'))]
|
||||
|
||||
class Meta:
|
||||
|
@ -522,6 +522,20 @@ class BookingCheckFilterSet(django_filters.FilterSet):
|
|||
)
|
||||
self.filters['booking-status'].parent = self
|
||||
|
||||
if self.agenda.partial_bookings:
|
||||
self.filters['display'] = django_filters.MultipleChoiceFilter(
|
||||
label=_('Display'),
|
||||
choices=[
|
||||
('booked', _('Booked periods')),
|
||||
('checked', _('Checked periods')),
|
||||
('computed', _('Computed periods')),
|
||||
],
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
method='do_nothing',
|
||||
initial=['booked', 'checked', 'computed'],
|
||||
)
|
||||
self.filters['display'].parent = self
|
||||
|
||||
def filter_booking_status(self, queryset, name, value):
|
||||
if value == 'not-booked':
|
||||
return queryset.none()
|
||||
|
@ -575,12 +589,17 @@ class BookingCheckPresenceForm(forms.Form):
|
|||
|
||||
def __init__(self, *args, **kwargs):
|
||||
agenda = kwargs.pop('agenda')
|
||||
subscription = kwargs.pop('subscription', False)
|
||||
super().__init__(*args, **kwargs)
|
||||
check_types = get_agenda_check_types(agenda)
|
||||
self.presence_check_types = [ct for ct in check_types if ct.kind == 'presence']
|
||||
self.fields['check_type'].choices = [('', '---------')] + [
|
||||
(ct.slug, ct.label) for ct in self.presence_check_types
|
||||
]
|
||||
if not self.initial and subscription:
|
||||
unexpected_presences = [ct for ct in check_types if ct.unexpected_presence]
|
||||
if unexpected_presences:
|
||||
self.initial['check_type'] = unexpected_presences[0].slug
|
||||
|
||||
|
||||
class PartialBookingCheckForm(forms.ModelForm):
|
||||
|
@ -612,6 +631,7 @@ class PartialBookingCheckForm(forms.ModelForm):
|
|||
|
||||
def __init__(self, *args, first_check_form=None, **kwargs):
|
||||
agenda = kwargs.pop('agenda')
|
||||
self.event = kwargs.pop('event')
|
||||
self.first_check_form = first_check_form
|
||||
super().__init__(*args, **kwargs)
|
||||
self.check_types = get_agenda_check_types(agenda)
|
||||
|
@ -667,6 +687,20 @@ class PartialBookingCheckForm(forms.ModelForm):
|
|||
|
||||
return self.cleaned_data['presence']
|
||||
|
||||
def clean_start_time(self):
|
||||
start_time = self.cleaned_data['start_time']
|
||||
if start_time and start_time < localtime(self.event.start_datetime).time():
|
||||
raise ValidationError(_('Arrival must be after opening time.'))
|
||||
|
||||
return start_time
|
||||
|
||||
def clean_end_time(self):
|
||||
end_time = self.cleaned_data['end_time']
|
||||
if end_time and end_time > self.event.end_time:
|
||||
raise ValidationError(_('Departure must be before closing time.'))
|
||||
|
||||
return end_time
|
||||
|
||||
def save(self):
|
||||
booking = self.instance.booking
|
||||
if self.cleaned_data['presence'] is None:
|
||||
|
@ -743,17 +777,29 @@ class EventsTimesheetForm(forms.Form):
|
|||
],
|
||||
initial='portrait',
|
||||
)
|
||||
booking_filter = forms.ChoiceField(
|
||||
label=_('Filter by status'),
|
||||
choices=[
|
||||
('all', _('All')),
|
||||
('with_booking', _('With booking')),
|
||||
('without_booking', _('Without booking')),
|
||||
],
|
||||
initial='all',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.agenda = kwargs.pop('agenda')
|
||||
self.event = kwargs.pop('event', None)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.with_subscriptions = self.agenda.subscriptions.exists()
|
||||
if self.event is not None:
|
||||
del self.fields['date_start']
|
||||
del self.fields['date_end']
|
||||
del self.fields['date_display']
|
||||
del self.fields['custom_nb_dates_per_page']
|
||||
del self.fields['activity_display']
|
||||
if not self.with_subscriptions:
|
||||
del self.fields['booking_filter']
|
||||
|
||||
def get_slots(self):
|
||||
extra_data = self.cleaned_data['extra_data'].split(',')
|
||||
|
@ -821,20 +867,21 @@ class EventsTimesheetForm(forms.Form):
|
|||
)
|
||||
|
||||
users = {}
|
||||
subscriptions = self.agenda.subscriptions.filter(date_start__lt=max_start, date_end__gt=min_start)
|
||||
for subscription in subscriptions:
|
||||
if subscription.user_external_id in users:
|
||||
continue
|
||||
users[subscription.user_external_id] = {
|
||||
'user_id': subscription.user_external_id,
|
||||
'user_first_name': subscription.user_first_name,
|
||||
'user_last_name': subscription.user_last_name,
|
||||
'extra_data': {k: (subscription.extra_data or {}).get(k) or '' for k in all_extra_data},
|
||||
'events': copy.deepcopy(event_slots),
|
||||
}
|
||||
if self.with_subscriptions:
|
||||
subscriptions = self.agenda.subscriptions.filter(date_start__lt=max_start, date_end__gt=min_start)
|
||||
for subscription in subscriptions:
|
||||
if subscription.user_external_id in users:
|
||||
continue
|
||||
users[subscription.user_external_id] = {
|
||||
'user_id': subscription.user_external_id,
|
||||
'user_first_name': subscription.user_first_name,
|
||||
'user_last_name': subscription.user_last_name,
|
||||
'extra_data': {k: (subscription.extra_data or {}).get(k) or '' for k in all_extra_data},
|
||||
'events': copy.deepcopy(event_slots),
|
||||
}
|
||||
|
||||
booking_qs_kwargs = {}
|
||||
if not self.agenda.subscriptions.exists():
|
||||
if not self.with_subscriptions:
|
||||
booking_qs_kwargs = {'cancellation_datetime__isnull': True}
|
||||
booked_qs = (
|
||||
Booking.objects.filter(
|
||||
|
@ -870,6 +917,19 @@ class EventsTimesheetForm(forms.Form):
|
|||
participants += 1
|
||||
break
|
||||
|
||||
if self.cleaned_data.get('booking_filter') == 'with_booking':
|
||||
# remove subscribed users without booking
|
||||
users = {
|
||||
k: user for k, user in users.items() if any(any(e['dates'].values()) for e in user['events'])
|
||||
}
|
||||
elif self.cleaned_data.get('booking_filter') == 'without_booking':
|
||||
# remove subscribed users with booking
|
||||
users = {
|
||||
k: user
|
||||
for k, user in users.items()
|
||||
if not any(any(e['dates'].values()) for e in user['events'])
|
||||
}
|
||||
|
||||
if self.cleaned_data['sort'] == 'lastname,firstname':
|
||||
sort_fields = ['user_last_name', 'user_first_name']
|
||||
else:
|
||||
|
|
|
@ -201,6 +201,18 @@ table.agenda-table {
|
|||
text-align: center;
|
||||
}
|
||||
&.booking {
|
||||
&.lease {
|
||||
background:
|
||||
repeating-linear-gradient(
|
||||
135deg,
|
||||
hsla(10, 10%, 75%, 0.7) 0,
|
||||
hsla(10, 10%, 80%, 0.55) 10px,
|
||||
transparent 11px,
|
||||
transparent 20px);
|
||||
// color: currentColor;
|
||||
color: hsla(0, 0%, 0%, 0.7);
|
||||
}
|
||||
|
||||
left: 0;
|
||||
color: hsl(210, 84%, 40%);
|
||||
padding: 1ex;
|
||||
|
@ -562,6 +574,18 @@ div.agenda-settings .pk-tabs--container {
|
|||
|
||||
#event_details {
|
||||
margin: 1em 0;
|
||||
.objects-list .lease {
|
||||
background:
|
||||
repeating-linear-gradient(
|
||||
135deg,
|
||||
hsla(10, 10%, 75%, 0.7) 0,
|
||||
hsla(10, 10%, 80%, 0.55) 10px,
|
||||
transparent 11px,
|
||||
transparent 20px);
|
||||
}
|
||||
.objects-list .lease span {
|
||||
padding: 0 0.5ex 0 2ex;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
|
@ -635,6 +659,85 @@ div#main-content.partial-booking-dayview {
|
|||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
&--occupation-rate-list {
|
||||
position: static;
|
||||
display: grid;
|
||||
grid-template-rows: 40px auto;
|
||||
align-items: end;
|
||||
margin-top: 0.33rem;
|
||||
margin-bottom: 1rem;
|
||||
border-top: 3px solid var(--zebra-color);
|
||||
grid-template-columns: repeat(var(--nb-hours), 1fr);
|
||||
@media (min-width: 761px) {
|
||||
grid-template-columns: var(--registrant-name-width) repeat(var(--nb-hours), 1fr);
|
||||
}
|
||||
}
|
||||
.occupation-rate-list--title {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: normal;
|
||||
justify-self: end;
|
||||
align-self: end;
|
||||
padding: .66rem;
|
||||
padding-bottom: 0;
|
||||
@media (max-width: 760px) {
|
||||
grid-column: 1/-1;
|
||||
grid-row: 2/3;
|
||||
}
|
||||
}
|
||||
.occupation-rate {
|
||||
@function linear-progress($from, $to) {
|
||||
$ratio: #{($to - $from) / 100};
|
||||
@return "calc(#{$ratio} * var(--rate-percent) + #{$from})";
|
||||
}
|
||||
--hue: #{linear-progress(40, 10)};
|
||||
--saturation: #{linear-progress(50%, 75%)};
|
||||
--luminosity: #{linear-progress(65%, 50%)};
|
||||
background-color: hsl(var(--hue), var(--saturation), var(--luminosity));
|
||||
height: calc(1% * var(--rate-percent));
|
||||
margin: 0;
|
||||
opacity: 80%;
|
||||
position: relative;
|
||||
&--info {
|
||||
display: block;
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
padding: .33em .66em;
|
||||
text-align: center;
|
||||
background-color: var(--font-color);
|
||||
color: white;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
margin-top: .5em;
|
||||
font-weight: bold;
|
||||
filter: drop-shadow(0 0 3px white);
|
||||
&::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: .5em solid transparent;
|
||||
border-bottom-color: var(--font-color);
|
||||
}
|
||||
}
|
||||
&:not(:hover) .occupation-rate--info {
|
||||
display: none;
|
||||
}
|
||||
&:hover {
|
||||
opacity: 100%;
|
||||
z-index: 4;
|
||||
}
|
||||
&.overbooked {
|
||||
--hue: 0;
|
||||
--saturation: 95%;
|
||||
--luminosity: 40%;
|
||||
}
|
||||
}
|
||||
|
||||
&--registrant-items {
|
||||
margin-top: 0.5rem;
|
||||
position: relative;
|
||||
|
@ -704,6 +807,10 @@ div#main-content.partial-booking-dayview {
|
|||
}
|
||||
&.booking {
|
||||
--bar-color: #1066bc;
|
||||
.occasional {
|
||||
font-style: italic;
|
||||
font-size: 90%;
|
||||
}
|
||||
}
|
||||
&.check.present, &.computed.present {
|
||||
--bar-color: var(--green);
|
||||
|
@ -763,20 +870,73 @@ div#main-content.partial-booking-dayview {
|
|||
background-color: var(--red);
|
||||
z-index: 3;
|
||||
}
|
||||
}
|
||||
|
||||
.agenda-table.partial-bookings .booking {
|
||||
height: 70%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 15%;
|
||||
background: #1066bc;
|
||||
&.present {
|
||||
background: hsl(120, 57%, 35%);
|
||||
}
|
||||
&.absent {
|
||||
background: hsl(355, 80%, 45%);
|
||||
// Month view, table element
|
||||
&-month {
|
||||
width: 100%;
|
||||
border-spacing: 0;
|
||||
& col.we {
|
||||
background-color: var(--zebra-color);
|
||||
}
|
||||
& col.today {
|
||||
background-image: linear-gradient(
|
||||
135deg,
|
||||
hsl(65, 65%, 94%) 20%,
|
||||
hsl(65, 55%, 92%) 70%,
|
||||
hsl(65, 50%, 90%) 90%);
|
||||
}
|
||||
&--day {
|
||||
padding: .33em;
|
||||
a {
|
||||
color: var(--font-color);
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
&.today a {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
& .registrant {
|
||||
&--name {
|
||||
box-sizing: border-box;
|
||||
text-align: right;
|
||||
padding: .66rem;
|
||||
font-size: 130%;
|
||||
color: #505050;
|
||||
font-weight: normal;
|
||||
width: var(--registrant-name-width);
|
||||
}
|
||||
&--day-cell {
|
||||
border-left: var(--separator-size) solid var(--separator-color);
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
padding: .33em;
|
||||
line-height: 0;
|
||||
& .booking {
|
||||
display: inline-block;
|
||||
width: Min(100%, 1.75em);
|
||||
height: 1.75em;
|
||||
--booking-color: #1066bc;
|
||||
background-color: var(--booking-color);
|
||||
&.present {
|
||||
background: var(--green);
|
||||
}
|
||||
&.absent {
|
||||
background: var(--red);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&--registrant:nth-child(odd) {
|
||||
& th, & td {
|
||||
background-color: var(--zebra-color);
|
||||
}
|
||||
}
|
||||
&--registrant:nth-child(even) {
|
||||
& th, & td {
|
||||
--separator-color: var(--zebra-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -834,3 +994,22 @@ ul.objects-list.single-links li.ants-setting-not-configured a.edit {
|
|||
|
||||
/* used for the city-edit link */
|
||||
.icon-edit::before { content: "\f044"; }
|
||||
|
||||
a.button.button-paragraph {
|
||||
text-align: left;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 150%;
|
||||
padding-top: 0.8em;
|
||||
padding-bottom: 0.8em;
|
||||
}
|
||||
|
||||
.application-logo, .application-icon {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.snapshots-list .collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,23 @@
|
|||
$(function() {
|
||||
const foldableClassObserver = new MutationObserver((mutations) => {
|
||||
mutations.forEach(mu => {
|
||||
const old_folded = (mu.oldValue.indexOf('folded') != -1);
|
||||
const new_folded = mu.target.classList.contains('folded')
|
||||
if (old_folded == new_folded) { return; }
|
||||
var pref_message = Object();
|
||||
pref_message[mu.target.dataset.sectionFoldedPrefName] = new_folded;
|
||||
fetch('/api/user-preferences/save', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {'Accept': 'application/json', 'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(pref_message)
|
||||
});
|
||||
});
|
||||
});
|
||||
document.querySelectorAll('[data-section-folded-pref-name]').forEach(
|
||||
elt => foldableClassObserver.observe(elt, {attributes: true, attributeFilter: ['class'], attributeOldValue: true})
|
||||
);
|
||||
|
||||
$('[data-total]').each(function() {
|
||||
var total = $(this).data('total');
|
||||
var booked = $(this).data('booked');
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
{% load thumbnail %}
|
||||
{% if application %}
|
||||
<h2>
|
||||
{% thumbnail application.icon '64x64' format='PNG' as im %}
|
||||
<img src="{{ im.url }}" alt="" class="application-logo" />
|
||||
{% endthumbnail %}
|
||||
{{ application }}
|
||||
</h2>
|
||||
{% elif no_application %}
|
||||
<h2>{{ title_no_application }}</h2>
|
||||
{% else %}
|
||||
<h2>{{ title_object_list }}</h2>
|
||||
{% endif %}
|
|
@ -0,0 +1,5 @@
|
|||
{% if application %}
|
||||
<a href="{{ object_list_url }}?application={{ application.slug }}">{{ application }}</a>
|
||||
{% elif no_application %}
|
||||
<a href="{{ object_list_url }}?no-application">{{ title_no_application }}</a>
|
||||
{% endif %}
|
|
@ -0,0 +1,12 @@
|
|||
{% load i18n thumbnail %}
|
||||
{% if object.applications %}
|
||||
<h3>{% trans "Applications" %}</h3>
|
||||
{% for application in object.applications %}
|
||||
<a class="button button-paragraph" href="{{ object_list_url }}?application={{ application.slug }}">
|
||||
{% thumbnail application.icon '16x16' format='PNG' as im %}
|
||||
<img src="{{ im.url }}" alt="" class="application-icon" width="16" />
|
||||
{% endthumbnail %}
|
||||
{{ application }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% endif %}
|
|
@ -0,0 +1,8 @@
|
|||
{% load thumbnail %}
|
||||
{% if not application and not no_application %}
|
||||
{% for application in object.applications %}
|
||||
{% thumbnail application.icon '16x16' format='PNG' as im %}
|
||||
<img src="{{ im.url }}" alt="" class="application-icon" width="16" />
|
||||
{% endthumbnail %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
|
@ -0,0 +1,15 @@
|
|||
{% load i18n thumbnail %}
|
||||
{% if applications %}
|
||||
<h3>{% trans "Applications" %}</h3>
|
||||
{% for application in applications %}
|
||||
<a class="button button-paragraph" href="?application={{ application.slug }}">
|
||||
{% thumbnail application.icon '16x16' format='PNG' as im %}
|
||||
<img src="{{ im.url }}" alt="" class="application-icon" width="16" />
|
||||
{% endthumbnail %}
|
||||
{{ application }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
<a class="button button-paragraph" href="?no-application">
|
||||
{{ title_no_application }}
|
||||
</a>
|
||||
{% endif %}
|
|
@ -0,0 +1,20 @@
|
|||
<p class="snapshot-description">{{ fromdesc|safe }} ➔ {{ todesc|safe }}</p>
|
||||
<div class="snapshot-diff">
|
||||
{% if mode == 'json' %}
|
||||
{{ diff_serialization|safe }}
|
||||
{% else %}
|
||||
<div class="{{ tab_class_names }}">
|
||||
<div class="pk-tabs--tab-list" role="tablist">
|
||||
{% for tab in tabs %}{{ tab|safe }}{% endfor %}
|
||||
{{ tab_list|safe }}
|
||||
</div>
|
||||
<div class="pk-tabs--container">
|
||||
{% for attrs, panel in panels %}
|
||||
<div{% for k, v in attrs.items %} {{ k }}="{{ v }}"{% endfor %}>
|
||||
{{ panel|safe }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
|
@ -0,0 +1,63 @@
|
|||
{% load i18n %}
|
||||
<div>
|
||||
<form action="{{ compare_url }}" method="get">
|
||||
{% if object_list|length > 1 %}
|
||||
<p><button>{% trans "Show differences" %}</button></p>
|
||||
{% endif %}
|
||||
<table class="main">
|
||||
<thead>
|
||||
<th>{% trans 'Identifier' %}</th>
|
||||
<th>{% trans 'Compare' %}</th>
|
||||
<th>{% trans 'Date' %}</th>
|
||||
<th>{% trans 'Description' %}</th>
|
||||
<th>{% trans 'User' %}</th>
|
||||
<th>{% trans 'Actions' %}</th>
|
||||
</thead>
|
||||
<tbody class="snapshots-list">
|
||||
{% for snapshot in object_list %}
|
||||
<tr data-day="{{ snapshot.timestamp|date:"Y-m-d" }}" class="{% if snapshot.new_day %}new-day{% else %}collapsed{% endif %}">
|
||||
<td><span class="counter">#{{ snapshot.pk }}</span></td>
|
||||
<td>
|
||||
{% if object_list|length > 1 %}
|
||||
{% if not forloop.last %}<input type="radio" name="version1" value="{{ snapshot.pk }}" {% if forloop.first %}checked="checked"{% endif %} />{% else %} {% endif %}
|
||||
{% if not forloop.first %}<input type="radio" name="version2" value="{{ snapshot.pk }}" {% if forloop.counter == 2 %}checked="checked"{% endif %}/>{% else %} {% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ snapshot.timestamp }}
|
||||
{% if snapshot.new_day and snapshot.day_other_count %} — <a class="reveal" href="#day-{{ snapshot.timestamp|date:"Y-m-d"}}">
|
||||
{% if snapshot.day_other_count >= 50 %}<strong>{% endif %}
|
||||
{% blocktrans trimmed count counter=snapshot.day_other_count %}
|
||||
1 other this day
|
||||
{% plural %}
|
||||
{{ counter }} others
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if snapshot.label %}
|
||||
<strong>{{ snapshot.label }}</strong>
|
||||
{% elif snapshot.comment %}
|
||||
{{ snapshot.comment }}
|
||||
{% endif %}
|
||||
{% if snapshot.application_version %}({% blocktrans with version=snapshot.application_version %}Version {{ version }}{% endblocktrans %}){% endif %}
|
||||
</td>
|
||||
<td>{% if snapshot.user %} {{ snapshot.user.get_full_name }}{% endif %}</td>
|
||||
<td>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
$('tr.new-day a.reveal').on('click', function() {
|
||||
var day = $(this).parents('tr.new-day').data('day');
|
||||
$('.snapshots-list tr[data-day="' + day + '"]:not(.new-day)').toggleClass('collapsed');
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,19 @@
|
|||
{% extends "chrono/manager_agenda_settings.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans 'Agenda history' %} - {{ agenda }}</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url 'chrono-manager-agenda-history' pk=agenda.pk %}">{% trans "History" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% url 'chrono-manager-agenda-history-compare' pk=agenda.pk as compare_url %}
|
||||
{% include 'chrono/includes/snapshot_history_fragment.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,29 @@
|
|||
{% extends "chrono/manager_agenda_history.html" %}
|
||||
{% load gadjo static i18n %}
|
||||
|
||||
{% block css %}
|
||||
{{ block.super }}
|
||||
<link rel="stylesheet" type="text/css" media="all" href="{% static "css/gadjo.snapshotdiff.css" %}?{% start_timestamp %}"/>
|
||||
{% endblock %}
|
||||
|
||||
{% block extrascripts %}
|
||||
{{ block.super }}
|
||||
<script src="{% static "js/gadjo.snapshotdiff.js" %}?{% start_timestamp %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans 'Compare snapshots' %} ({% if mode == 'json' %}{% trans "JSON" %}{% else %}{% trans "Inspect" %}{% endif %})</h2>
|
||||
<span class="actions">
|
||||
<a href="?version1={{ snapshot1.pk }}&version2={{ snapshot2.pk }}&mode=inspect">{% trans "Compare inspect" %}</a>
|
||||
<a href="?version1={{ snapshot1.pk }}&version2={{ snapshot2.pk }}&mode=json">{% trans "Compare JSON" %}</a>
|
||||
</span>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url 'chrono-manager-agenda-history-compare' pk=agenda.pk %}">{% trans "Compare snapshots" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'chrono/includes/snapshot_compare_fragment.html' %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,19 @@
|
|||
{% extends "chrono/manager_agenda_settings.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans 'Inspect' %}</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url 'chrono-manager-agenda-inspect' pk=agenda.pk %}">{% trans "Inspect" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
{% include 'chrono/manager_agenda_inspect_fragment.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,322 @@
|
|||
{% load i18n %}
|
||||
<div class="pk-tabs">
|
||||
<div class="pk-tabs--tab-list" role="tablist">
|
||||
<button aria-controls="panel-information" aria-selected="true" id="tab-information" role="tab" tabindex="0">{% trans "Information" %}</button>
|
||||
<button aria-controls="panel-settings" aria-selected="false" id="tab-settings" role="tab" tabindex="-1">{% trans "Settings" %}</button>
|
||||
<button aria-controls="panel-permissions" aria-selected="false" id="tab-permissions" role="tab" tabindex="-1">{% trans "Permissions" %}</button>
|
||||
{% if object.kind == 'events' %}
|
||||
<button aria-controls="panel-events" aria-selected="false" id="tab-events" role="tab" tabindex="-1">{% trans "Events" %}</button>
|
||||
<button aria-controls="panel-exceptions" aria-selected="false" id="tab-exceptions" role="tab" tabindex="-1">{% trans "Recurrence exceptions" %}</button>
|
||||
{% elif object.kind == 'meetings' %}
|
||||
<button aria-controls="panel-meeting-types" aria-selected="false" id="tab-meeting-types" role="tab" tabindex="-1">{% trans "Meeting Types" %}</button>
|
||||
<button aria-controls="panel-desks" aria-selected="false" id="tab-desks" role="tab" tabindex="-1">{% trans "Desks" %}</button>
|
||||
<button aria-controls="panel-resources" aria-selected="false" id="tab-resources" role="tab" tabindex="-1">{% trans "Resources" %}</button>
|
||||
{% elif object.kind == 'virtual' %}
|
||||
<button aria-controls="panel-agendas" aria-selected="false" id="tab-agendas" role="tab" tabindex="-1">{% trans "Included Agendas" %}</button>
|
||||
<button aria-controls="panel-time-periods" aria-selected="false" id="tab-time-periods" role="tab" tabindex="-1">{% trans "Excluded Periods" %}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="pk-tabs--container">
|
||||
|
||||
<div aria-labelledby="tab-information" id="panel-information" role="tabpanel" tabindex="0">
|
||||
<div class="section">
|
||||
<ul>
|
||||
{% for label, value in object.get_inspect_fields %}
|
||||
<li class="parameter-{{ label|slugify }}">
|
||||
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
|
||||
{{ value }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div aria-labelledby="tab-settings" hidden id="panel-settings" role="tabpanel" tabindex="0">
|
||||
<div class="section">
|
||||
|
||||
{% if object.kind != 'virtual' %}
|
||||
<h4>{% trans "Display options" %}</h4>
|
||||
<ul>
|
||||
{% for label, value in object.get_display_inspect_fields %}
|
||||
<li class="parameter-{{ label|slugify }}">
|
||||
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
|
||||
{{ value }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% if object.kind == 'events' %}
|
||||
<h4>{% trans "Booking check options" %}</h4>
|
||||
<ul>
|
||||
{% for label, value in object.get_booking_check_inspect_fields %}
|
||||
<li class="parameter-{{ label|slugify }}">
|
||||
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
|
||||
{{ value }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% if object.kind == 'events' %}
|
||||
{% if agenda.partial_bookings %}
|
||||
<h4>{% trans "Invoicing options" %}</h4>
|
||||
<ul>
|
||||
{% for label, value in object.get_invoicing_inspect_fields %}
|
||||
<li class="parameter-{{ label|slugify }}">
|
||||
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
|
||||
{{ value }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<h4>{% trans "Management notifications" %}</h4>
|
||||
<ul>
|
||||
{% for label, value in object.get_notifications_inspect_fields %}
|
||||
<li class="parameter-{{ label|slugify }}">
|
||||
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
|
||||
{{ value }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if object.kind != 'virtual' and not object.partial_bookings %}
|
||||
<h4>{% trans "Booking reminders" %}</h4>
|
||||
<ul>
|
||||
{% for label, value in object.get_reminder_inspect_fields %}
|
||||
<li class="parameter-{{ label|slugify }}">
|
||||
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
|
||||
{{ value }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<h4>{% trans "Booking Delays" %}</h4>
|
||||
<ul>
|
||||
{% for label, value in object.get_booking_delays_inspect_fields %}
|
||||
<li class="parameter-{{ label|slugify }}">
|
||||
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
|
||||
{{ value }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div aria-labelledby="tab-permissions" hidden id="panel-permissions" role="tabpanel" tabindex="0">
|
||||
<div class="section">
|
||||
<ul>
|
||||
{% for label, value in object.get_permissions_inspect_fields %}
|
||||
<li class="parameter-{{ label|slugify }}">
|
||||
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
|
||||
{{ value }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if object.kind == 'events' %}
|
||||
|
||||
<div aria-labelledby="tab-events" hidden id="panel-events" role="tabpanel" tabindex="0">
|
||||
<div class="section">
|
||||
{% for event in object.event_set.all %}
|
||||
<h4>{{ event }}</h4>
|
||||
<ul>
|
||||
{% for label, value in event.get_inspect_fields %}
|
||||
<li class="parameter-{{ label|slugify }}">
|
||||
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
|
||||
{{ value }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% if object.events_type %}
|
||||
<li class="parameter-custom-fields">
|
||||
<span class="parameter">{% trans "Custom fields:" %}</span>
|
||||
<ul>
|
||||
{% for value in object.events_type.get_custom_fields %}
|
||||
<li class="parameter-custom-field-{{ value.varname }}">
|
||||
<span class="parameter">{% blocktrans with label=value.label %}{{ label }}:{% endblocktrans %}</span>
|
||||
{{ event.get_custom_fields|get:value.varname|default:"" }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div aria-labelledby="tab-exceptions" hidden id="panel-exceptions" role="tabpanel" tabindex="0">
|
||||
<div class="section">
|
||||
{% for desk in object.desk_set.all %}{% if desk.slug == '_exceptions_holder' %}
|
||||
<h4>{% trans "Unavailability calendars" %}</h4>
|
||||
<ul>
|
||||
{% for unavailability_calendar in desk.unavailability_calendars.all %}
|
||||
<li class="parameter-unavailability-calendar }}">
|
||||
{{ unavailability_calendar }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<h4>{% trans "Exception sources" %}</h4>
|
||||
{% for source in desk.timeperiodexceptionsource_set.all %}
|
||||
<h5>{{ source }}</h5>
|
||||
<ul>
|
||||
{% for label, value in source.get_inspect_fields %}
|
||||
<li class="parameter-{{ label|slugify }}">
|
||||
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
|
||||
{{ value }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endfor %}
|
||||
|
||||
<h4>{% trans "Exceptions" %}</h4>
|
||||
{% for exception in desk.timeperiodexception_set.all %}
|
||||
<h5>{{ exception }}</h5>
|
||||
<ul>
|
||||
{% for label, value in exception.get_inspect_fields %}
|
||||
<li class="parameter-{{ label|slugify }}">
|
||||
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
|
||||
{{ value }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endfor %}
|
||||
{% endif %}{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% elif object.kind == 'meetings' %}
|
||||
|
||||
<div aria-labelledby="tab-meeting-types" hidden id="panel-meeting-types" role="tabpanel" tabindex="0">
|
||||
<div class="section">
|
||||
{% for meeting_type in object.meetingtype_set.all %}
|
||||
<h4>{{ meeting_type }}</h4>
|
||||
<ul>
|
||||
{% for label, value in meeting_type.get_inspect_fields %}
|
||||
<li class="parameter-{{ label|slugify }}">
|
||||
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
|
||||
{{ value }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div aria-labelledby="tab-desks" hidden id="panel-desks" role="tabpanel" tabindex="0">
|
||||
<div class="section">
|
||||
{% for desk in object.desk_set.all %}
|
||||
<h4>{{ desk }}</h4>
|
||||
<ul>
|
||||
{% for label, value in desk.get_inspect_fields %}
|
||||
<li class="parameter-{{ label|slugify }}">
|
||||
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
|
||||
{{ value }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<h5>{% trans "Opening hours" %}</h5>
|
||||
{% for time_period in desk.timeperiod_set.all %}
|
||||
<h6>{{ time_period }}</h6>
|
||||
<ul>
|
||||
{% for label, value in time_period.get_inspect_fields %}
|
||||
<li class="parameter-{{ label|slugify }}">
|
||||
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
|
||||
{{ value }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endfor %}
|
||||
|
||||
<h5>{% trans "Unavailability calendars" %}</h5>
|
||||
<ul>
|
||||
{% for unavailability_calendar in desk.unavailability_calendars.all %}
|
||||
<li class="parameter-unavailability-calendar }}">
|
||||
{{ unavailability_calendar }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<h5>{% trans "Exception sources" %}</h5>
|
||||
{% for source in desk.timeperiodexceptionsource_set.all %}
|
||||
<h6>{{ source }}</h6>
|
||||
<ul>
|
||||
{% for label, value in source.get_inspect_fields %}
|
||||
<li class="parameter-{{ label|slugify }}">
|
||||
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
|
||||
{{ value }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endfor %}
|
||||
|
||||
<h5>{% trans "Exceptions" %}</h5>
|
||||
{% for exception in desk.timeperiodexception_set.all %}
|
||||
<h6>{{ exception }}</h6>
|
||||
<ul>
|
||||
{% for label, value in exception.get_inspect_fields %}
|
||||
<li class="parameter-{{ label|slugify }}">
|
||||
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
|
||||
{{ value }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endfor %}
|
||||
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div aria-labelledby="tab-resources" hidden id="panel-resources" role="tabpanel" tabindex="0">
|
||||
<div class="section">
|
||||
<ul>
|
||||
{% for resource in object.resources.all %}
|
||||
<li class="parameter-resource }}">
|
||||
{{ resource }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% elif object.kind == "virtual" %}
|
||||
|
||||
<div aria-labelledby="tab-agendas" hidden id="panel-agendas" role="tabpanel" tabindex="0">
|
||||
<div class="section">
|
||||
<ul>
|
||||
{% for agenda in object.real_agendas.all %}
|
||||
<li class="parameter-agenda }}">
|
||||
{{ agenda }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div aria-labelledby="tab-time-periods" hidden id="panel-time-periods" role="tabpanel" tabindex="0">
|
||||
<div class="section">
|
||||
{% for time_period in object.excluded_timeperiods.all %}
|
||||
<h4>{{ time_period }}</h4>
|
||||
<ul>
|
||||
{% for label, value in time_period.get_inspect_fields %}
|
||||
<li class="parameter-{{ label|slugify }}">
|
||||
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
|
||||
{{ value }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
|
@ -16,14 +16,8 @@
|
|||
</h2>
|
||||
<span class="actions">
|
||||
<a class="extra-actions-menu-opener"></a>
|
||||
{% block agenda-extra-management-actions %}
|
||||
{% endblock %}
|
||||
<ul class="extra-actions-menu">
|
||||
<li><a rel="popup" href="{% url 'chrono-manager-agenda-edit' pk=object.id %}">{% trans 'Options' %}</a></li>
|
||||
{% block agenda-extra-menu-actions %}{% endblock %}
|
||||
{% if user.is_staff %}
|
||||
<li><a rel="popup" class="action-duplicate" href="{% url 'chrono-manager-agenda-duplicate' pk=object.pk %}">{% trans 'Duplicate' %}</a></li>
|
||||
{% endif %}
|
||||
<li><a download href="{% url 'chrono-manager-agenda-export' pk=object.id %}">{% trans 'Export Configuration (JSON)' %}</a></li>
|
||||
{% if object.kind == 'events' %}
|
||||
<li><a download href="{% url 'chrono-manager-agenda-export-events' pk=object.pk %}">{% trans 'Export Events (CSV)' %}</a></li>
|
||||
|
@ -115,3 +109,25 @@
|
|||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
<aside id="sidebar">
|
||||
|
||||
<h3>{% trans "Actions" %}</h3>
|
||||
{% block agenda-extra-management-actions %}{% endblock %}
|
||||
<a class="button button-paragraph" rel="popup" href="{% url 'chrono-manager-agenda-edit' pk=object.id %}">{% trans 'Options' %}</a>
|
||||
{% if user.is_staff %}
|
||||
<a class="button button-paragraph" rel="popup" class="action-duplicate" href="{% url 'chrono-manager-agenda-duplicate' pk=object.pk %}">{% trans 'Duplicate' %}</a>
|
||||
{% endif %}
|
||||
|
||||
<h3>{% trans "Navigation" %}</h3>
|
||||
{% if show_history %}
|
||||
<a class="button button-paragraph" href="{% url 'chrono-manager-agenda-history' pk=agenda.pk %}">{% trans 'History' %}</a>
|
||||
{% endif %}
|
||||
<a class="button button-paragraph" href="{% url 'chrono-manager-agenda-inspect' pk=agenda.pk %}">{% trans 'Inspect' %}</a>
|
||||
{% block agenda-extra-navigation-actions %}{% endblock %}
|
||||
|
||||
{% url 'chrono-manager-homepage' as object_list_url %}
|
||||
{% include 'chrono/includes/application_detail_fragment.html' %}
|
||||
</aside>
|
||||
{% endblock %}
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
{% trans 'Agendas' as default_site_title %}
|
||||
{% firstof site_title default_site_title %}
|
||||
{% endblock %}
|
||||
{% block footer %}Chrono — Copyright © Entr'ouvert{% endblock %}
|
||||
|
||||
{% block homepage-url %}{{portal_agent_url}}{% endblock %}
|
||||
{% block homepage-title %}{{portal_agent_title}}{% endblock %}
|
||||
|
|
|
@ -1,12 +1,33 @@
|
|||
{% extends "chrono/manager_home.html" %}
|
||||
{% extends "chrono/manager_category_list.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page-title-extra-label %}
|
||||
{% if object.pk %}{{ object.label }}{% else %}{{ block.super }}{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
{% if object.pk %}
|
||||
<a href="{% url 'chrono-manager-category-edit' pk=object.pk %}">{{ object.label }}</a>
|
||||
{% else %}
|
||||
<a href="{% url 'chrono-manager-category-add' %}">{% trans "New Category" %}</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block appbar %}
|
||||
{% if object.id %}
|
||||
<h2>{% trans "Edit Category" %}</h2>
|
||||
{% else %}
|
||||
<h2>{% trans "New Category" %}</h2>
|
||||
{% endif %}
|
||||
{% if category %}
|
||||
<span class="actions">
|
||||
<a href="{% url 'chrono-manager-category-inspect' pk=category.pk %}">{% trans 'Inspect' %}</a>
|
||||
{% if show_history %}
|
||||
<a href="{% url 'chrono-manager-category-history' pk=category.pk %}">{% trans 'History' %}</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -20,3 +41,6 @@
|
|||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
{% extends "chrono/manager_category_form.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans 'Category history' %} - {{ category }}</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url 'chrono-manager-category-history' pk=category.pk %}">{% trans "History" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% url 'chrono-manager-category-history-compare' pk=category.pk as compare_url %}
|
||||
{% include 'chrono/includes/snapshot_history_fragment.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,29 @@
|
|||
{% extends "chrono/manager_category_history.html" %}
|
||||
{% load gadjo static i18n %}
|
||||
|
||||
{% block css %}
|
||||
{{ block.super }}
|
||||
<link rel="stylesheet" type="text/css" media="all" href="{% static "css/gadjo.snapshotdiff.css" %}?{% start_timestamp %}"/>
|
||||
{% endblock %}
|
||||
|
||||
{% block extrascripts %}
|
||||
{{ block.super }}
|
||||
<script src="{% static "js/gadjo.snapshotdiff.js" %}?{% start_timestamp %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans 'Compare snapshots' %} ({% if mode == 'json' %}{% trans "JSON" %}{% else %}{% trans "Inspect" %}{% endif %})</h2>
|
||||
<span class="actions">
|
||||
<a href="?version1={{ snapshot1.pk }}&version2={{ snapshot2.pk }}&mode=inspect">{% trans "Compare inspect" %}</a>
|
||||
<a href="?version1={{ snapshot1.pk }}&version2={{ snapshot2.pk }}&mode=json">{% trans "Compare JSON" %}</a>
|
||||
</span>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url 'chrono-manager-category-history-compare' pk=category.pk %}">{% trans "Compare snapshots" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'chrono/includes/snapshot_compare_fragment.html' %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,18 @@
|
|||
{% extends "chrono/manager_category_form.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans 'Inspect' %}</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url 'chrono-manager-category-inspect' pk=category.pk %}">{% trans "Inspect" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'chrono/manager_category_inspect_fragment.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,21 @@
|
|||
{% load i18n %}
|
||||
<div class="pk-tabs">
|
||||
<div class="pk-tabs--tab-list" role="tablist">
|
||||
<button aria-controls="panel-information" aria-selected="true" id="tab-information" role="tab" tabindex="0">{% trans "Information" %}</button>
|
||||
</div>
|
||||
<div class="pk-tabs--container">
|
||||
|
||||
<div aria-labelledby="tab-information" id="panel-information" role="tabpanel" tabindex="0">
|
||||
<div class="section">
|
||||
<ul>
|
||||
{% for label, value in object.get_inspect_fields %}
|
||||
<li class="parameter-{{ label|slugify }}">
|
||||
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
|
||||
{{ value }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,32 +1,37 @@
|
|||
{% extends "chrono/manager_base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page-title-extra-label %}
|
||||
{% trans "Categories" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url 'chrono-manager-category-list' %}">{% trans "Categories" %}</a>
|
||||
{% url 'chrono-manager-category-list' as object_list_url %}
|
||||
<a href="{{ object_list_url }}">{% trans "Categories" %}</a>
|
||||
{% include 'chrono/includes/application_breadcrumb_fragment.html' with title_no_application=_('Categories outside applications') %}
|
||||
{% endblock %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans 'Categories' %}</h2>
|
||||
<span class="actions">
|
||||
<a rel="popup" href="{% url 'chrono-manager-category-add' %}">{% trans 'New' %}</a>
|
||||
</span>
|
||||
{% include 'chrono/includes/application_appbar_fragment.html' with title_no_application=_('Categories outside applications') title_object_list=_('Categories') %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
{% if object_list %}
|
||||
<div>
|
||||
<ul class="objects-list single-links">
|
||||
{% for object in object_list %}
|
||||
<li>
|
||||
<a href="{% url 'chrono-manager-category-edit' pk=object.pk %}">{{ object.label }} ({{ object.slug }})</a>
|
||||
<a href="{% url 'chrono-manager-category-edit' pk=object.pk %}">
|
||||
{% include 'chrono/includes/application_icon_fragment.html' %}
|
||||
{{ object.label }} ({{ object.slug }})
|
||||
</a>
|
||||
<a rel="popup" class="delete" href="{% url 'chrono-manager-category-delete' pk=object.id %}">{% trans "remove" %}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% else %}
|
||||
{% elif not no_application %}
|
||||
<div class="big-msg-info">
|
||||
{% blocktrans trimmed %}
|
||||
This site doesn't have any category yet. Click on the "New" button in the top
|
||||
|
@ -35,3 +40,14 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% if not application and not no_application %}
|
||||
<aside id="sidebar">
|
||||
<h3>{% trans "Actions" %}</h3>
|
||||
<a class="button button-paragraph" rel="popup" href="{% url 'chrono-manager-category-add' %}">{% trans 'New category' %}</a>
|
||||
|
||||
{% include 'chrono/includes/application_list_fragment.html' with title_no_application=_('Categories outside applications') %}
|
||||
</aside>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -29,3 +29,6 @@
|
|||
{% include "gadjo/pagination.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -28,12 +28,16 @@
|
|||
|
||||
<ul class="objects-list single-links">
|
||||
{% for booking in booked %}
|
||||
<li>
|
||||
<a {% if booking.get_backoffice_url %}href="{{ booking.get_backoffice_url }}"{% endif %}>{{ booking.get_user_block }}, {{ booking.creation_datetime|date:"DATETIME_FORMAT" }}</a>
|
||||
{% if not booking.primary_booking %}
|
||||
<a rel="popup" class="delete" href="{% url 'chrono-manager-booking-cancel' pk=agenda.id booking_pk=booking.id %}?next={{ request.path }}">{% trans "Cancel" %}</a>
|
||||
<li{% if booking.lease %} class="lease"{% endif %}>
|
||||
{% if booking.lease %}
|
||||
<span>{% trans "Currently being booked..." %}</span>
|
||||
{% else %}
|
||||
<a class="delete disabled" title="{% trans "Can not cancel a secondary booking" %}" href="#">{% trans "Cancel" %}</a>
|
||||
<a {% if booking.get_backoffice_url %}href="{{ booking.get_backoffice_url }}"{% endif %}>{{ booking.get_user_block }}, {{ booking.creation_datetime|date:"DATETIME_FORMAT" }}</a>
|
||||
{% if not booking.primary_booking %}
|
||||
<a rel="popup" class="delete" href="{% url 'chrono-manager-booking-cancel' pk=agenda.id booking_pk=booking.id %}?next={{ request.path }}">{% trans "Cancel" %}</a>
|
||||
{% else %}
|
||||
<a class="delete disabled" title="{% trans "Can not cancel a secondary booking" %}" href="#">{% trans "Cancel" %}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
@ -53,7 +57,7 @@
|
|||
<div>
|
||||
<ul class="objects-list single-links">
|
||||
{% for booking in waiting %}
|
||||
<li><a {% if booking.get_backoffice_url %}href="{{ booking.get_backoffice_url }}"{% endif %}>{{ booking.get_user_block }}, {{ booking.creation_datetime|date:"DATETIME_FORMAT" }}</a></li>
|
||||
<li{% if booking.lease %} class="lease"{% endif %}><a {% if booking.get_backoffice_url %}href="{{ booking.get_backoffice_url }}"{% endif %}>{% if booking.lease %}{% trans "Currently being booked..." %}{% else %}{{ booking.get_user_block }}, {{ booking.creation_datetime|date:"DATETIME_FORMAT" }}{% endif %}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -2,13 +2,13 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block agenda-extra-management-actions %}
|
||||
<a rel="popup" href="{% url 'chrono-manager-agenda-import-events' pk=object.id %}">{% trans 'Import Events' %}</a>
|
||||
<a rel="popup" href="{% url 'chrono-manager-agenda-add-event' pk=object.id %}">{% trans 'New Event' %}</a>
|
||||
<a class="button button-paragraph" rel="popup" href="{% url 'chrono-manager-agenda-import-events' pk=object.id %}">{% trans 'Import Events' %}</a>
|
||||
<a class="button button-paragraph" rel="popup" href="{% url 'chrono-manager-agenda-add-event' pk=object.id %}">{% trans 'New Event' %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block agenda-extra-menu-actions %}
|
||||
{% block agenda-extra-navigation-actions %}
|
||||
{% with lingo_url=object.get_lingo_url %}{% if lingo_url %}
|
||||
<li><a href="{{ lingo_url }}">{% trans 'Pricing' context 'pricing' %}</a></li>
|
||||
<a class="button button-paragraph" href="{{ lingo_url }}">{% trans 'Pricing' context 'pricing' %}</a>
|
||||
{% endif %}{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
{% extends "chrono/manager_events_type_list.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page-title-extra-label %}
|
||||
{% if object.pk %}{{ object.label }}{% else %}{{ block.super }}{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
{% if object.pk %}
|
||||
|
@ -16,6 +20,14 @@
|
|||
{% else %}
|
||||
<h2>{% trans "New events type" %}</h2>
|
||||
{% endif %}
|
||||
{% if object.pk %}
|
||||
<span class="actions">
|
||||
<a href="{% url 'chrono-manager-events-type-inspect' pk=object.pk %}">{% trans 'Inspect' %}</a>
|
||||
{% if show_history %}
|
||||
<a href="{% url 'chrono-manager-events-type-history' pk=object.pk %}">{% trans 'History' %}</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -55,3 +67,6 @@
|
|||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
{% extends "chrono/manager_events_type_form.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans 'Events type history' %} - {{ events_type }}</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url 'chrono-manager-events-type-history' pk=events_type.pk %}">{% trans "History" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% url 'chrono-manager-events-type-history-compare' pk=events_type.pk as compare_url %}
|
||||
{% include 'chrono/includes/snapshot_history_fragment.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,29 @@
|
|||
{% extends "chrono/manager_events_type_history.html" %}
|
||||
{% load gadjo static i18n %}
|
||||
|
||||
{% block css %}
|
||||
{{ block.super }}
|
||||
<link rel="stylesheet" type="text/css" media="all" href="{% static "css/gadjo.snapshotdiff.css" %}?{% start_timestamp %}"/>
|
||||
{% endblock %}
|
||||
|
||||
{% block extrascripts %}
|
||||
{{ block.super }}
|
||||
<script src="{% static "js/gadjo.snapshotdiff.js" %}?{% start_timestamp %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans 'Compare snapshots' %} ({% if mode == 'json' %}{% trans "JSON" %}{% else %}{% trans "Inspect" %}{% endif %})</h2>
|
||||
<span class="actions">
|
||||
<a href="?version1={{ snapshot1.pk }}&version2={{ snapshot2.pk }}&mode=inspect">{% trans "Compare inspect" %}</a>
|
||||
<a href="?version1={{ snapshot1.pk }}&version2={{ snapshot2.pk }}&mode=json">{% trans "Compare JSON" %}</a>
|
||||
</span>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url 'chrono-manager-events-type-history-compare' pk=events_type.pk %}">{% trans "Compare snapshots" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'chrono/includes/snapshot_compare_fragment.html' %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,18 @@
|
|||
{% extends "chrono/manager_events_type_form.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans 'Inspect' %}</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url 'chrono-manager-events-type-inspect' pk=events_type.pk %}">{% trans "Inspect" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'chrono/manager_events_type_inspect_fragment.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,47 @@
|
|||
{% load i18n %}
|
||||
<div class="pk-tabs">
|
||||
<div class="pk-tabs--tab-list" role="tablist">
|
||||
<button aria-controls="panel-information" aria-selected="true" id="tab-information" role="tab" tabindex="0">{% trans "Information" %}</button>
|
||||
<button aria-controls="panel-custom-fields" aria-selected="false" id="tab-custom-fields" role="tab" tabindex="-1">{% trans "Custom fields" %}</button>
|
||||
</div>
|
||||
<div class="pk-tabs--container">
|
||||
|
||||
<div aria-labelledby="tab-information" id="panel-information" role="tabpanel" tabindex="0">
|
||||
<div class="section">
|
||||
<ul>
|
||||
{% for label, value in object.get_inspect_fields %}
|
||||
<li class="parameter-{{ label|slugify }}">
|
||||
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
|
||||
{{ value }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div aria-labelledby="tab-custom-fields" hidden id="panel-custom-fields" role="tabpanel" tabindex="0">
|
||||
<div class="section">
|
||||
{% for value in object.get_custom_fields %}
|
||||
<h4>{{ value.label }}</h4>
|
||||
<ul>
|
||||
<li class="parameter-varname">
|
||||
<span class="parameter">{% trans "Field slug:" %}</span>
|
||||
{{ value.varname }}
|
||||
</li>
|
||||
<li class="parameter-label">
|
||||
<span class="parameter">{% trans "Field label:" %}</span>
|
||||
{{ value.label }}
|
||||
</li>
|
||||
<li class="parameter-field-type">
|
||||
<span class="parameter">{% trans "Field type:" %}</span>
|
||||
{% if value.field_type == 'text' %}{% trans "Text" %}{% endif %}
|
||||
{% if value.field_type == 'textarea' %}{% trans "Textarea" %}{% endif %}
|
||||
{% if value.field_type == 'textbool' %}{% trans "Boolean" %}{% endif %}
|
||||
</li>
|
||||
</ul>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
|
@ -1,16 +1,19 @@
|
|||
{% extends "chrono/manager_base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page-title-extra-label %}
|
||||
{% trans "Events types" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url 'chrono-manager-events-type-list' %}">{% trans "Events types" %}</a>
|
||||
{% url 'chrono-manager-events-type-list' as object_list_url %}
|
||||
<a href="{{ object_list_url }}">{% trans "Events types" %}</a>
|
||||
{% include 'chrono/includes/application_breadcrumb_fragment.html' with title_no_application=_('Events types outside applications') %}
|
||||
{% endblock %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans 'Events types' %}</h2>
|
||||
<span class="actions">
|
||||
<a rel="popup" href="{% url 'chrono-manager-events-type-add' %}">{% trans 'New' %}</a>
|
||||
</span>
|
||||
{% include 'chrono/includes/application_appbar_fragment.html' with title_no_application=_('Events types outside applications') title_object_list=_('Events types') %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -22,13 +25,16 @@
|
|||
<ul class="objects-list single-links">
|
||||
{% for object in object_list %}
|
||||
<li>
|
||||
<a href="{% url 'chrono-manager-events-type-edit' pk=object.pk %}">{{ object.label }} ({{ object.slug }})</a>
|
||||
<a href="{% url 'chrono-manager-events-type-edit' pk=object.pk %}">
|
||||
{% include 'chrono/includes/application_icon_fragment.html' %}
|
||||
{{ object.label }} ({{ object.slug }})
|
||||
</a>
|
||||
<a rel="popup" class="delete" href="{% url 'chrono-manager-events-type-delete' pk=object.pk %}">{% trans "remove" %}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% else %}
|
||||
{% elif not no_application %}
|
||||
<div class="big-msg-info">
|
||||
{% blocktrans trimmed %}
|
||||
This site doesn't have any events type yet. Click on the "New" button in the top
|
||||
|
@ -37,3 +43,14 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% if not application and not no_application %}
|
||||
<aside id="sidebar">
|
||||
<h3>{% trans "Actions" %}</h3>
|
||||
<a class="button button-paragraph" rel="popup" href="{% url 'chrono-manager-events-type-add' %}">{% trans 'New events type' %}</a>
|
||||
|
||||
{% include 'chrono/includes/application_list_fragment.html' with title_no_application=_('Events types outside applications') %}
|
||||
</aside>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,36 +1,14 @@
|
|||
{% extends "chrono/manager_base.html" %}
|
||||
{% load i18n %}
|
||||
{% load i18n thumbnail chrono %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans 'Agendas' %}</h2>
|
||||
<span class="actions">
|
||||
{% if user.is_staff or has_access_to_unavailability_calendars %}
|
||||
<a class="extra-actions-menu-opener"></a>
|
||||
<ul class="extra-actions-menu">
|
||||
{% if user.is_staff %}
|
||||
<li><a rel="popup" href="{% url 'chrono-manager-agendas-import' %}">{% trans 'Import' %}</a></li>
|
||||
<li><a rel="popup" href="{% url 'chrono-manager-agendas-export' %}" data-autoclose-dialog="true">{% trans 'Export' %}</a></li>
|
||||
<li><a href="{% url 'chrono-manager-events-type-list' %}">{% trans 'Events types' %}</a></li>
|
||||
{% if shared_custody_enabled %}
|
||||
<li><a rel="popup" href="{% url 'chrono-manager-shared-custody-settings' %}">{% trans 'Shared custody' %}</a></li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if has_access_to_unavailability_calendars %}
|
||||
<li><a href="{% url 'chrono-manager-unavailability-calendar-list' %}">{% trans 'Unavailability calendars' %}</a></li>
|
||||
{% endif %}
|
||||
{% if user.is_staff %}
|
||||
<li><a href="{% url 'chrono-manager-resource-list' %}">{% trans 'Resources' %}</a></li>
|
||||
{% endif %}
|
||||
{% if ants_hub_enabled and user.is_staff %}
|
||||
<li><a href="{% url 'chrono-manager-ants-hub' %}">{% trans 'ANTS Hub' %}</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if user.is_staff %}
|
||||
<a href="{% url 'chrono-manager-category-list' %}">{% trans 'Categories' %}</a>
|
||||
<a rel="popup" href="{% url 'chrono-manager-agenda-add' %}">{% trans 'New' %}</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% include 'chrono/includes/application_appbar_fragment.html' with title_no_application=_('Agendas outside applications') title_object_list=_('Agendas') %}
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
{% url 'chrono-manager-homepage' as object_list_url %}
|
||||
{% include 'chrono/includes/application_breadcrumb_fragment.html' with title_no_application=_('Agendas outside applications') %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -38,16 +16,26 @@
|
|||
{% if object_list %}
|
||||
{% regroup object_list by category as agenda_groups %}
|
||||
{% for group in agenda_groups %}
|
||||
<div class="section">
|
||||
{% if group.grouper %}<h3>{{ group.grouper }}</h3>{% elif not forloop.first %}<h3>{% trans "Misc" %}</h3>{% endif %}
|
||||
<ul class="objects-list single-links">
|
||||
{% for object in group.list %}
|
||||
<li><a href="{% url 'chrono-manager-agenda-view' pk=object.id %}"><span class="badge">{{ object.get_real_kind_display }}</span> {{ object.label }}{% if user.is_staff %} <span class="identifier">[{% trans "identifier:" %} {{ object.slug }}]{% endif %}</span></a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% with i=group.grouper.id|stringformat:"s" %}
|
||||
{% with foldname='foldable-manager-category-group-'|add:i %}
|
||||
<div class="section foldable {% if user|get_preference:foldname %}folded{% endif %}" data-section-folded-pref-name="{{foldname}}">
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% if group.grouper %}<h3>{{ group.grouper }}</h3>{% elif not forloop.first %}<h3>{% trans "Misc" %}</h3>{% endif %}
|
||||
<ul class="objects-list single-links">
|
||||
{% for object in group.list %}
|
||||
<li>
|
||||
<a href="{% url 'chrono-manager-agenda-view' pk=object.id %}">
|
||||
<span class="badge">{{ object.get_real_kind_display }}</span>
|
||||
{% include 'chrono/includes/application_icon_fragment.html' %}
|
||||
{{ object.label }}{% if user.is_staff %} <span class="identifier">[{% trans "identifier:" %} {{ object.slug }}]{% endif %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% elif not no_application %}
|
||||
<div class="big-msg-info">
|
||||
{% blocktrans trimmed %}
|
||||
This site doesn't have any agenda yet. Click on the "New" button in the top
|
||||
|
@ -57,3 +45,42 @@
|
|||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% if with_sidebar and not application and not no_application %}
|
||||
<aside id="sidebar">
|
||||
|
||||
{% if user.is_staff %}
|
||||
<h3>{% trans "Actions" %}</h3>
|
||||
<a class="button button-paragraph" rel="popup" href="{% url 'chrono-manager-agenda-add' %}">{% trans 'New agenda' %}</a>
|
||||
<a class="button button-paragraph" rel="popup" href="{% url 'chrono-manager-agendas-export' %}" data-autoclose-dialog="true">{% trans 'Export site' %}</a>
|
||||
<a class="button button-paragraph" rel="popup" href="{% url 'chrono-manager-agendas-import' %}">{% trans 'Import site' %}</a>
|
||||
{% endif %}
|
||||
|
||||
{% if user.is_staff or has_access_to_unavailability_calendars %}
|
||||
<h3>{% trans "Navigation" %}</h3>
|
||||
{% if user.is_staff %}
|
||||
<a class="button button-paragraph" href="{% url 'chrono-manager-category-list' %}">{% trans 'Categories' %}</a>
|
||||
<a class="button button-paragraph" href="{% url 'chrono-manager-events-type-list' %}">{% trans 'Events types' %}</a>
|
||||
{% if shared_custody_enabled %}
|
||||
<a class="button button-paragraph" rel="popup" href="{% url 'chrono-manager-shared-custody-settings' %}">{% trans 'Shared custody' %}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if has_access_to_unavailability_calendars %}
|
||||
<a class="button button-paragraph" href="{% url 'chrono-manager-unavailability-calendar-list' %}">{% trans 'Unavailability calendars' %}</a>
|
||||
{% endif %}
|
||||
{% if user.is_staff %}
|
||||
<a class="button button-paragraph" href="{% url 'chrono-manager-resource-list' %}">{% trans 'Resources' %}</a>
|
||||
{% if ants_hub_enabled %}
|
||||
<a class="button button-paragraph" href="{% url 'chrono-manager-ants-hub' %}">{% trans 'ANTS Hub' %}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if user.is_staff and audit_journal_enabled %}
|
||||
<a class="button button-paragraph" href="{% url 'chrono-manager-audit-journal' %}">{% trans 'Audit journal' %}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% include 'chrono/includes/application_list_fragment.html' with title_no_application=_('Agendas outside applications') %}
|
||||
</aside>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -55,3 +55,6 @@
|
|||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -38,12 +38,16 @@
|
|||
{% endif %}
|
||||
|
||||
{% for booking in desk_info.bookings %}
|
||||
<div class="booking{% if booking.color %} booking-color-{{ booking.color.index }}{% endif %}"
|
||||
<div class="booking{% if booking.color %} booking-color-{{ booking.color.index }}{% endif %}{% if booking.lease %} lease{% endif %}"
|
||||
style="height: {{ booking.css_height }}%; min-height: {{ booking.css_height }}%; top: {{ booking.css_top }}%;"
|
||||
><span class="start-time">{{booking.event.start_datetime|date:"TIME_FORMAT"}}</span>
|
||||
<a {% if booking.get_backoffice_url %}href="{{booking.get_backoffice_url}}"{% endif %}>{{ booking.get_user_block }}</a>
|
||||
<a rel="popup" class="cancel" href="{% url 'chrono-manager-booking-cancel' pk=booking.event.agenda_id booking_pk=booking.pk %}?next={{ request.path }}">{% trans "Cancel" %}</a>
|
||||
{% if booking.color %}<span class="booking-color-label booking-bg-color-{{ booking.color.index }}">{{ booking.color }}</span>{% endif %}
|
||||
{% if booking.lease %}
|
||||
{% trans "Currently being booked..." %}
|
||||
{% else %}
|
||||
<a {% if booking.get_backoffice_url %}href="{{booking.get_backoffice_url}}"{% endif %}>{{ booking.get_user_block }}</a>
|
||||
<a rel="popup" class="cancel" href="{% url 'chrono-manager-booking-cancel' pk=booking.event.agenda_id booking_pk=booking.pk %}?next={{ request.path }}">{% trans "Cancel" %}</a>
|
||||
{% if booking.color %}<span class="booking-color-label booking-bg-color-{{ booking.color.index }}">{{ booking.color }}</span>{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</td>
|
||||
|
|
|
@ -31,12 +31,16 @@
|
|||
{% endfor %}
|
||||
|
||||
{% for slot in day.infos.booked_slots %}
|
||||
<div class="booking{% if slot.booking.color %} booking-color-{{ slot.booking.color.index }}{% endif %}" style="left:{{ slot.css_left|stringformat:".1f" }}%;height:{{ slot.css_height|stringformat:".1f" }}%;min-height:{{ slot.css_height|stringformat:".1f" }}%;top:{{ slot.css_top|stringformat:".1f" }}%;width:{{ slot.css_width|stringformat:".1f" }}%;">
|
||||
<div class="booking{% if slot.booking.color %} booking-color-{{ slot.booking.color.index }}{% endif %}{% if slot.booking.lease %} lease{% endif %}" style="left:{{ slot.css_left|stringformat:".1f" }}%;height:{{ slot.css_height|stringformat:".1f" }}%;min-height:{{ slot.css_height|stringformat:".1f" }}%;top:{{ slot.css_top|stringformat:".1f" }}%;width:{{ slot.css_width|stringformat:".1f" }}%;">
|
||||
<span class="start-time">{{slot.booking.event.start_datetime|date:"TIME_FORMAT"}}</span>
|
||||
<a {% if slot.booking.get_backoffice_url %}href="{{slot.booking.get_backoffice_url}}"{% endif %}>{{ slot.booking.get_user_block }}</a>
|
||||
<a rel="popup" class="cancel" href="{% url 'chrono-manager-booking-cancel' pk=slot.booking.event.agenda_id booking_pk=slot.booking.id %}?next={{ request.path }}">{% trans "Cancel" %}</a>
|
||||
{% if not single_desk %}<span class="desk">{{ slot.desk }}</span>{% endif %}
|
||||
{% if slot.booking.color %}<span class="booking-color-label booking-bg-color-{{ slot.booking.color.index }}">{{ slot.booking.color }}</span>{% endif %}
|
||||
{% if slot.booking.lease %}
|
||||
{% trans "Currently being booked..." %}
|
||||
{% else %}
|
||||
<a {% if slot.booking.get_backoffice_url %}href="{{slot.booking.get_backoffice_url}}"{% endif %}>{{ slot.booking.get_user_block }}</a>
|
||||
<a rel="popup" class="cancel" href="{% url 'chrono-manager-booking-cancel' pk=slot.booking.event.agenda_id booking_pk=slot.booking.id %}?next={{ request.path }}">{% trans "Cancel" %}</a>
|
||||
{% if not single_desk %}<span class="desk">{{ slot.desk }}</span>{% endif %}
|
||||
{% if slot.booking.color %}<span class="booking-color-label booking-bg-color-{{ slot.booking.color.index }}">{{ slot.booking.color }}</span>{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
|
|
@ -72,11 +72,28 @@
|
|||
</script>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="partial-booking--hours-list" aria-hidden="true">
|
||||
<div class="partial-booking--hours-list">
|
||||
{% for hour in hours %}
|
||||
<div class="partial-booking--hour">{{ hour|time:"H" }} h</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="partial-booking--occupation-rate-list">
|
||||
<h3 class="occupation-rate-list--title">{% trans "Occupation rate" %}</h3>
|
||||
{% for rate in occupation_rates %}
|
||||
<p
|
||||
class="occupation-rate {% if rate.overbooked %}overbooked{% endif %}"
|
||||
style="--rate-percent: {{ rate.height_percent }};"
|
||||
aria-label="{% blocktrans trimmed with start=rate.start_time|time:"H:i" end=rate.end_time|time:"H:i" %}
|
||||
From {{ start }} to {{ end }}:
|
||||
{% endblocktrans %}
|
||||
{{ rate.percent }}% ({{ rate.booked_places }}/{{ event.places }})"
|
||||
>
|
||||
<span class="occupation-rate--info">
|
||||
{{ rate.percent }}% <br> ({{ rate.booked_places }}/{{ event.places }})
|
||||
</span>
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="partial-booking--registrant-items">
|
||||
{% for user in users %}
|
||||
|
@ -95,56 +112,65 @@
|
|||
</h3>
|
||||
{% endspaceless %}
|
||||
<div class="registrant--datas">
|
||||
<div class="registrant--bar-container">
|
||||
{% for booking in user.bookings %}
|
||||
{% if booking.start_time %}
|
||||
<p
|
||||
class="registrant--bar booking"
|
||||
title="{% trans "Booked period" %}"
|
||||
style="left: {{ booking.css_left }}%; width: {{ booking.css_width }}%;"
|
||||
>
|
||||
<strong class="sr-only">{% trans "Booked period:" %}</strong>
|
||||
<time class="start-time" datetime="{{ booking.start_time|time:"H:i" }}">{{ booking.start_time|time:"H:i" }}</time>
|
||||
<time class="end-time" datetime="{{ booking.end_time|time:"H:i" }}">{{ booking.end_time|time:"H:i" }}</time>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if user.bookings %}
|
||||
{% if not filterset.form.cleaned_data or 'booked' in filterset.form.cleaned_data.display %}
|
||||
<div class="registrant--bar-container">
|
||||
{% for check in user.booking_checks %}
|
||||
<p
|
||||
class="registrant--bar check {{ check.css_class }}"
|
||||
title="{% trans "Checked period" %}"
|
||||
style="left: {{ check.css_left }}%;{% if check.css_width %} width: {{ check.css_width }}%;{% endif %}"
|
||||
>
|
||||
<strong class="sr-only">{% trans "Checked period:" %}</strong>
|
||||
{% 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 %}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="registrant--bar-container">
|
||||
{% for check in user.booking_checks %}
|
||||
{% if check.computed_start_time and check.computed_end_time %}
|
||||
{% for booking in user.bookings %}
|
||||
{% if booking.start_time %}
|
||||
<p
|
||||
class="registrant--bar computed {{ check.css_class }}"
|
||||
title="{% trans "Computed period" %}"
|
||||
style="left: {{ check.computed_css_left }}%; width: {{ check.computed_css_width }}%;"
|
||||
class="registrant--bar booking"
|
||||
title="{% trans "Booked period" %}"
|
||||
style="left: {{ booking.css_left }}%; width: {{ booking.css_width }}%;"
|
||||
>
|
||||
<strong class="sr-only">{% trans "Computed period:" %}</strong>
|
||||
<time class="start-time" datetime="{{ check.computed_start_time|time:"H:i" }}">{{ check.computed_start_time|time:"H:i" }}</time>
|
||||
<time class="end-time" datetime="{{ check.computed_end_time|time:"H:i" }}">{{ check.computed_end_time|time:"H:i" }}</time>
|
||||
<strong class="sr-only">{% trans "Booked period:" %}</strong>
|
||||
<time class="start-time" datetime="{{ booking.start_time|time:"H:i" }}">{{ booking.start_time|time:"H:i" }}</time>
|
||||
<time class="end-time" datetime="{{ booking.end_time|time:"H:i" }}">{{ booking.end_time|time:"H:i" }}</time>
|
||||
{% if not booking.from_recurring_fillslots %}
|
||||
<span class="occasional">{% trans "occasional" %}</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if user.bookings %}
|
||||
{% if not filterset.form.cleaned_data or 'checked' in filterset.form.cleaned_data.display %}
|
||||
<div class="registrant--bar-container">
|
||||
{% for check in user.booking_checks %}
|
||||
<p
|
||||
class="registrant--bar check {{ check.css_class }}"
|
||||
title="{% trans "Checked period" %}"
|
||||
style="left: {{ check.css_left }}%;{% if check.css_width %} width: {{ check.css_width }}%;{% endif %}"
|
||||
>
|
||||
<strong class="sr-only">{% trans "Checked period:" %}</strong>
|
||||
{% 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 %}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not filterset.form.cleaned_data or 'computed' in filterset.form.cleaned_data.display %}
|
||||
<div class="registrant--bar-container">
|
||||
{% for check in user.booking_checks %}
|
||||
{% if check.computed_start_time and check.computed_end_time %}
|
||||
<p
|
||||
class="registrant--bar computed {{ check.css_class }}"
|
||||
title="{% trans "Computed period" %}"
|
||||
style="left: {{ check.computed_css_left }}%; width: {{ check.computed_css_width }}%;"
|
||||
>
|
||||
<strong class="sr-only">{% trans "Computed period:" %}</strong>
|
||||
<time class="start-time" datetime="{{ check.computed_start_time|time:"H:i" }}">{{ check.computed_start_time|time:"H:i" }}</time>
|
||||
<time class="end-time" datetime="{{ check.computed_end_time|time:"H:i" }}">{{ check.computed_end_time|time:"H:i" }}</time>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
|
|
@ -3,33 +3,54 @@
|
|||
|
||||
{% block content %}
|
||||
|
||||
<table class="agenda-table partial-bookings">
|
||||
|
||||
<thead>
|
||||
<tr>
|
||||
<td></td>
|
||||
<div class="pk-table-wrapper">
|
||||
<table class="partial-booking partial-booking-month">
|
||||
<colgroup>
|
||||
<col class="name" />
|
||||
{% for day in days %}
|
||||
<th>
|
||||
<a href="{% url 'chrono-manager-agenda-day-view' pk=agenda.pk year=day.date|date:"Y" month=day.date|date:"m" day=day.date|date:"d" %}">{{ day|date:"d" }}</a>
|
||||
</th>
|
||||
<col class="{% if day|date:"w" == "0" or day|date:"w" == "6" %}we{% endif %}
|
||||
{% if today == day.date %}today{% endif %}
|
||||
" />
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
</colgroup>
|
||||
|
||||
<tbody>
|
||||
{% for booking_info in user_booking_info %}
|
||||
<tr class="{% cycle 'odd' 'even' %}">
|
||||
<th>{{ booking_info.user_name }}</th>
|
||||
{% for booking in booking_info.bookings %}
|
||||
<td class="day-cell">
|
||||
{% if booking %}
|
||||
<span class="booking {{ booking.check_css_class }}"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<thead>
|
||||
<tr class="partial-booking-month--day-list">
|
||||
<td></td>
|
||||
{% for day in days %}
|
||||
<th scope="col" class="partial-booking-month--day{% if today == day.date %} today{% endif %}">
|
||||
<a href="{% url 'chrono-manager-agenda-day-view' pk=agenda.pk year=day.date|date:"Y" month=day.date|date:"m" day=day.date|date:"d" %}">
|
||||
<time datetime="{{ day|date:"Y-m-d" }}">{{ day|date:"d" }}</time>
|
||||
</a>
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</thead>
|
||||
|
||||
</table>
|
||||
<tbody class="partial-booking-month--registrant-items">
|
||||
{% for booking_info in user_booking_info %}
|
||||
<tr class="partial-booking-month--registrant">
|
||||
<th class="registrant--name" scope="row">{{ booking_info.user_name }}</th>
|
||||
{% for booking in booking_info.bookings %}
|
||||
<td class="registrant--day-cell">
|
||||
{% if booking %}
|
||||
{% if booking.check_css_class == 'present' %}
|
||||
{% trans "Present" as booking_status %}
|
||||
{% elif booking.check_css_class == 'absent' %}
|
||||
{% trans "Absent" as booking_status %}
|
||||
{% else %}
|
||||
{% trans "Not checked" as booking_status %}
|
||||
{% endif %}
|
||||
<span title="{{ booking_status }}" class="booking {{ booking.check_css_class }}">
|
||||
<span class="sr-only">{{ booking_status }}</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -59,3 +59,6 @@
|
|||
{% endfor %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block page-title-extra-label %}
|
||||
- {{ resource.label }}
|
||||
{{ resource.label }}
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
|
@ -17,8 +17,10 @@
|
|||
<span class="actions">
|
||||
{% block appbar-extras %}
|
||||
{% if request.user.is_staff %}
|
||||
<a rel="popup" href="{% url 'chrono-manager-resource-edit' pk=resource.pk %}">{% trans 'Edit' %}</a>
|
||||
<a rel="popup" href="{% url 'chrono-manager-resource-delete' pk=resource.pk %}">{% trans 'Delete' %}</a>
|
||||
<a class="extra-actions-menu-opener"></a>
|
||||
<ul class="extra-actions-menu">
|
||||
<li><a rel="popup" href="{% url 'chrono-manager-resource-delete' pk=resource.pk %}">{% trans 'Delete' %}</a></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% include "chrono/manager_resource_view_buttons_fragment.html" with no_today=True no_opened=True %}
|
||||
{% endblock %}
|
||||
|
@ -53,3 +55,21 @@
|
|||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
<aside id="sidebar">
|
||||
{% if request.user.is_staff %}
|
||||
<h3>{% trans "Actions" %}</h3>
|
||||
<a class="button button-paragraph" rel="popup" href="{% url 'chrono-manager-resource-edit' pk=resource.pk %}">{% trans 'Edit' %}</a>
|
||||
{% endif %}
|
||||
|
||||
<h3>{% trans "Navigation" %}</h3>
|
||||
<a class="button button-paragraph" href="{% url 'chrono-manager-resource-inspect' pk=resource.pk %}">{% trans 'Inspect' %}</a>
|
||||
{% if show_history %}
|
||||
<a class="button button-paragraph" href="{% url 'chrono-manager-resource-history' pk=resource.pk %}">{% trans 'History' %}</a>
|
||||
{% endif %}
|
||||
|
||||
{% url 'chrono-manager-resource-list' as object_list_url %}
|
||||
{% include 'chrono/includes/application_detail_fragment.html' %}
|
||||
</aside>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,6 +1,15 @@
|
|||
{% extends "chrono/manager_home.html" %}
|
||||
{% extends "chrono/manager_resource_list.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
{% if object.pk %}
|
||||
<a href="{% url 'chrono-manager-resource-edit' pk=object.pk %}">{{ object.label }}</a>
|
||||
{% else %}
|
||||
<a href="{% url 'chrono-manager-resource-add' %}">{% trans "New Resource" %}</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block appbar %}
|
||||
{% if object.id %}
|
||||
<h2>{% trans "Edit Resource" %}</h2>
|
||||
|
@ -20,3 +29,6 @@
|
|||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
{% extends "chrono/manager_resource_detail.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans 'Resource history' %} - {{ resource }}</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url 'chrono-manager-resource-history' pk=resource.pk %}">{% trans "History" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% url 'chrono-manager-resource-history-compare' pk=resource.pk as compare_url %}
|
||||
{% include 'chrono/includes/snapshot_history_fragment.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,29 @@
|
|||
{% extends "chrono/manager_resource_history.html" %}
|
||||
{% load gadjo static i18n %}
|
||||
|
||||
{% block css %}
|
||||
{{ block.super }}
|
||||
<link rel="stylesheet" type="text/css" media="all" href="{% static "css/gadjo.snapshotdiff.css" %}?{% start_timestamp %}"/>
|
||||
{% endblock %}
|
||||
|
||||
{% block extrascripts %}
|
||||
{{ block.super }}
|
||||
<script src="{% static "js/gadjo.snapshotdiff.js" %}?{% start_timestamp %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans 'Compare snapshots' %} ({% if mode == 'json' %}{% trans "JSON" %}{% else %}{% trans "Inspect" %}{% endif %})</h2>
|
||||
<span class="actions">
|
||||
<a href="?version1={{ snapshot1.pk }}&version2={{ snapshot2.pk }}&mode=inspect">{% trans "Compare inspect" %}</a>
|
||||
<a href="?version1={{ snapshot1.pk }}&version2={{ snapshot2.pk }}&mode=json">{% trans "Compare JSON" %}</a>
|
||||
</span>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url 'chrono-manager-resource-history-compare' pk=resource.pk %}">{% trans "Compare snapshots" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'chrono/includes/snapshot_compare_fragment.html' %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,18 @@
|
|||
{% extends "chrono/manager_resource_detail.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans 'Inspect' %}</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url 'chrono-manager-resource-inspect' pk=resource.pk %}">{% trans "Inspect" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'chrono/manager_resource_inspect_fragment.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,22 @@
|
|||
{% load i18n %}
|
||||
<div class="pk-tabs">
|
||||
<div class="pk-tabs--tab-list" role="tablist">
|
||||
<button aria-controls="panel-information" aria-selected="true" id="tab-information" role="tab" tabindex="0">{% trans "Information" %}</button>
|
||||
</div>
|
||||
<div class="pk-tabs--container">
|
||||
|
||||
<div aria-labelledby="tab-information" id="panel-information" role="tabpanel" tabindex="0">
|
||||
<div class="section">
|
||||
<ul>
|
||||
{% for label, value in object.get_inspect_fields %}
|
||||
<li class="parameter-{{ label|slugify }}">
|
||||
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
|
||||
{{ value }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
|
@ -1,16 +1,19 @@
|
|||
{% extends "chrono/manager_base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page-title-extra-label %}
|
||||
{% trans "Resources" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url 'chrono-manager-resource-list' %}">{% trans "Resources" %}</a>
|
||||
{% url 'chrono-manager-resource-list' as object_list_url %}
|
||||
<a href="{{ object_list_url }}">{% trans "Resources" %}</a>
|
||||
{% include 'chrono/includes/application_breadcrumb_fragment.html' with title_no_application=_('Resources outside applications') %}
|
||||
{% endblock %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans 'Resources' %}</h2>
|
||||
<span class="actions">
|
||||
<a rel="popup" href="{% url 'chrono-manager-resource-add' %}">{% trans 'New' %}</a>
|
||||
</span>
|
||||
{% include 'chrono/includes/application_appbar_fragment.html' with title_no_application=_('Resources outside applications') title_object_list=_('Resources') %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
@ -20,12 +23,15 @@
|
|||
<ul class="objects-list single-links">
|
||||
{% for object in object_list %}
|
||||
<li>
|
||||
<a href="{% url 'chrono-manager-resource-view' pk=object.pk %}">{{ object.label }} ({{ object.slug }})</a>
|
||||
<a href="{% url 'chrono-manager-resource-view' pk=object.pk %}">
|
||||
{% include 'chrono/includes/application_icon_fragment.html' %}
|
||||
{{ object.label }} ({{ object.slug }})
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% else %}
|
||||
{% elif not no_application %}
|
||||
<div class="big-msg-info">
|
||||
{% blocktrans trimmed %}
|
||||
This site doesn't have any resource yet. Click on the "New" button in the top
|
||||
|
@ -34,3 +40,14 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% if not application and not no_application %}
|
||||
<aside id="sidebar">
|
||||
<h3>{% trans "Actions" %}</h3>
|
||||
<a class="button button-paragraph" rel="popup" href="{% url 'chrono-manager-resource-add' %}">{% trans 'New resource' %}</a>
|
||||
|
||||
{% include 'chrono/includes/application_list_fragment.html' with title_no_application=_('Resources outside applications') %}
|
||||
</aside>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -32,3 +32,6 @@
|
|||
{% block content %}
|
||||
{% include "chrono/manager_resource_week_timetable_fragment.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -59,3 +59,6 @@
|
|||
{% block content %}
|
||||
{% include "chrono/manager_resource_week_timetable_fragment.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -68,3 +68,6 @@
|
|||
</script>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -33,3 +33,6 @@
|
|||
{% include "gadjo/pagination.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block page-title-extra-label %}
|
||||
- {{ unavailability_calendar.label }}
|
||||
{{ unavailability_calendar.label }}
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
|
@ -49,3 +49,6 @@
|
|||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -31,3 +31,6 @@
|
|||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
{% extends "chrono/manager_unavailability_calendar_settings.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans 'UnavailabilityCalendarSnapshot calendar history' %} - {{ unavailability_calendar }}</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url 'chrono-manager-unavailability-calendar-history' pk=unavailability_calendar.pk %}">{% trans "History" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% url 'chrono-manager-unavailability-calendar-history-compare' pk=unavailability_calendar.pk as compare_url %}
|
||||
{% include 'chrono/includes/snapshot_history_fragment.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,29 @@
|
|||
{% extends "chrono/manager_unavailability_calendar_history.html" %}
|
||||
{% load gadjo static i18n %}
|
||||
|
||||
{% block css %}
|
||||
{{ block.super }}
|
||||
<link rel="stylesheet" type="text/css" media="all" href="{% static "css/gadjo.snapshotdiff.css" %}?{% start_timestamp %}"/>
|
||||
{% endblock %}
|
||||
|
||||
{% block extrascripts %}
|
||||
{{ block.super }}
|
||||
<script src="{% static "js/gadjo.snapshotdiff.js" %}?{% start_timestamp %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans 'Compare snapshots' %} ({% if mode == 'json' %}{% trans "JSON" %}{% else %}{% trans "Inspect" %}{% endif %})</h2>
|
||||
<span class="actions">
|
||||
<a href="?version1={{ snapshot1.pk }}&version2={{ snapshot2.pk }}&mode=inspect">{% trans "Compare inspect" %}</a>
|
||||
<a href="?version1={{ snapshot1.pk }}&version2={{ snapshot2.pk }}&mode=json">{% trans "Compare JSON" %}</a>
|
||||
</span>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url 'chrono-manager-unavailability-calendar-history-compare' pk=unavailability_calendar.pk %}">{% trans "Compare snapshots" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'chrono/includes/snapshot_compare_fragment.html' %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,18 @@
|
|||
{% extends "chrono/manager_unavailability_calendar_settings.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans 'Inspect' %}</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url 'chrono-manager-unavailability-calendar-inspect' pk=unavailability_calendar.pk %}">{% trans "Inspect" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'chrono/manager_unavailability_calendar_inspect_fragment.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,53 @@
|
|||
{% load i18n %}
|
||||
<div class="pk-tabs">
|
||||
<div class="pk-tabs--tab-list" role="tablist">
|
||||
<button aria-controls="panel-information" aria-selected="true" id="tab-information" role="tab" tabindex="0">{% trans "Information" %}</button>
|
||||
<button aria-controls="panel-permissions" aria-selected="false" id="tab-permissions" role="tab" tabindex="-1">{% trans "Permissions" %}</button>
|
||||
<button aria-controls="panel-exceptions" aria-selected="false" id="tab-exceptions" role="tab" tabindex="-1">{% trans "Exceptions" %}</button>
|
||||
</div>
|
||||
<div class="pk-tabs--container">
|
||||
|
||||
<div aria-labelledby="tab-information" id="panel-information" role="tabpanel" tabindex="0">
|
||||
<div class="section">
|
||||
<ul>
|
||||
{% for label, value in object.get_inspect_fields %}
|
||||
<li class="parameter-{{ label|slugify }}">
|
||||
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
|
||||
{{ value }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div aria-labelledby="tab-permissions" hidden id="panel-permissions" role="tabpanel" tabindex="0">
|
||||
<div class="section">
|
||||
<ul>
|
||||
{% for label, value in object.get_permissions_inspect_fields %}
|
||||
<li class="parameter-{{ label|slugify }}">
|
||||
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
|
||||
{{ value }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div aria-labelledby="tab-exceptions" hidden id="panel-exceptions" role="tabpanel" tabindex="0">
|
||||
<div class="section">
|
||||
{% for exception in object.timeperiodexception_set.all %}
|
||||
<h4>{{ exception }}</h4>
|
||||
<ul>
|
||||
{% for label, value in exception.get_inspect_fields %}
|
||||
<li class="parameter-{{ label|slugify }}">
|
||||
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
|
||||
{{ value }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
|
@ -1,18 +1,19 @@
|
|||
{% extends "chrono/manager_base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page-title-extra-label %}
|
||||
{% trans "Unavailability Calendars" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url 'chrono-manager-unavailability-calendar-list' %}">{% trans "Unavailability Calendars" %}</a>
|
||||
{% url 'chrono-manager-unavailability-calendar-list' as object_list_url %}
|
||||
<a href="{{ object_list_url }}">{% trans "Unavailability Calendars" %}</a>
|
||||
{% include 'chrono/includes/application_breadcrumb_fragment.html' with title_no_application=_('Unavailability Calendars outside applications') %}
|
||||
{% endblock %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans 'Unavailability Calendars' %}</h2>
|
||||
{% if user.is_staff %}
|
||||
<span class="actions">
|
||||
<a rel="popup" href="{% url 'chrono-manager-unavailability-calendar-add' %}">{% trans 'New' %}</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% include 'chrono/includes/application_appbar_fragment.html' with title_no_application=_('Unavailability Calendars outside applications') title_object_list=_('Unavailability Calendars') %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
@ -22,12 +23,15 @@
|
|||
<ul class="objects-list single-links">
|
||||
{% for object in object_list %}
|
||||
<li>
|
||||
<a href="{% url 'chrono-manager-unavailability-calendar-view' pk=object.pk %}">{{ object.label }} ({{ object.slug }})</a>
|
||||
<a href="{% url 'chrono-manager-unavailability-calendar-view' pk=object.pk %}">
|
||||
{% include 'chrono/includes/application_icon_fragment.html' %}
|
||||
{{ object.label }} ({{ object.slug }})
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% else %}
|
||||
{% elif not no_application %}
|
||||
<div class="big-msg-info">
|
||||
{% blocktrans trimmed %}
|
||||
This site doesn't have any unavailability calendar yet. Click on the "New" button in the top
|
||||
|
@ -36,3 +40,16 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% if not application and not no_application %}
|
||||
<aside id="sidebar">
|
||||
{% if user.is_staff %}
|
||||
<h3>{% trans "Actions" %}</h3>
|
||||
<a class="button button-paragraph" rel="popup" href="{% url 'chrono-manager-unavailability-calendar-add' %}">{% trans 'New unavailability calendar' %}</a>
|
||||
{% endif %}
|
||||
|
||||
{% include 'chrono/includes/application_list_fragment.html' with title_no_application=_('Unavailability Calendars outside applications') %}
|
||||
</aside>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -11,16 +11,8 @@
|
|||
</h2>
|
||||
<span class="actions">
|
||||
<a class="extra-actions-menu-opener"></a>
|
||||
{% block agenda-extra-management-actions %}
|
||||
<a rel="popup" href="{% url 'chrono-manager-unavailability-calendar-import-unavailabilities' pk=unavailability_calendar.id %}">{% trans 'Manage unavailabilities from ICS' %}</a>
|
||||
<a rel="popup" href="{% url 'chrono-manager-unavailability-calendar-add-unavailability' pk=unavailability_calendar.id %}">{% trans 'Add Unavailability' %}</a>
|
||||
{% endblock %}
|
||||
<ul class="extra-actions-menu">
|
||||
<li><a rel="popup" href="{% url 'chrono-manager-unavailability-calendar-edit' pk=unavailability_calendar.id %}">{% trans 'Options' %}</a></li>
|
||||
<li><a download href="{% url 'chrono-manager-unavailability-calendar-export' pk=unavailability_calendar.id %}">{% trans 'Export Configuration (JSON)' %}</a></li>
|
||||
{% if user.is_staff %}
|
||||
<li><a rel="popup" href="{% url 'chrono-manager-unavailability-calendar-delete' pk=unavailability_calendar.id %}">{% trans 'Delete' %}</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</span>
|
||||
{% endblock %}
|
||||
|
@ -50,5 +42,25 @@
|
|||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
<aside id="sidebar">
|
||||
<h3>{% trans "Actions" %}</h3>
|
||||
<a class="button button-paragraph" rel="popup" href="{% url 'chrono-manager-unavailability-calendar-import-unavailabilities' pk=unavailability_calendar.id %}">{% trans 'Manage unavailabilities from ICS' %}</a>
|
||||
<a class="button button-paragraph" rel="popup" href="{% url 'chrono-manager-unavailability-calendar-add-unavailability' pk=unavailability_calendar.id %}">{% trans 'Add Unavailability' %}</a>
|
||||
<a class="button button-paragraph" rel="popup" href="{% url 'chrono-manager-unavailability-calendar-edit' pk=unavailability_calendar.id %}">{% trans 'Options' %}</a>
|
||||
{% if user.is_staff %}
|
||||
<a class="button button-paragraph" rel="popup" href="{% url 'chrono-manager-unavailability-calendar-delete' pk=unavailability_calendar.id %}">{% trans 'Delete' %}</a>
|
||||
{% endif %}
|
||||
|
||||
<h3>{% trans "Navigation" %}</h3>
|
||||
{% if show_history %}
|
||||
<a class="button button-paragraph" href="{% url 'chrono-manager-unavailability-calendar-history' pk=unavailability_calendar.pk %}">{% trans 'History' %}</a>
|
||||
{% endif %}
|
||||
<a class="button button-paragraph" href="{% url 'chrono-manager-unavailability-calendar-inspect' pk=unavailability_calendar.pk %}">{% trans 'Inspect' %}</a>
|
||||
|
||||
{% url 'chrono-manager-unavailability-calendar-list' as object_list_url %}
|
||||
{% include 'chrono/includes/application_detail_fragment.html' %}
|
||||
</aside>
|
||||
{% endblock %}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue