Compare commits

..

19 Commits

Author SHA1 Message Date
Yann Weber 86e25f5fd2 manager: make agenda's groups foldable (#85616)
gitea/chrono/pipeline/head This commit looks good Details
2024-03-21 12:12:50 +01:00
Frédéric Péters 07512150e8 api: limit export/import APIs to admin users (#88439)
gitea/chrono/pipeline/head This commit looks good Details
2024-03-21 09:50:18 +01:00
Lauréline Guérin 2576350aae
translation update
gitea/chrono/pipeline/head This commit looks good Details
2024-03-21 08:36:35 +01:00
Lauréline Guérin 2187bf3dde export_import: unknown component in urls (#88085)
gitea/chrono/pipeline/head This commit looks good Details
2024-03-21 08:31:19 +01:00
Lauréline Guérin 4add868dd9 agendas: fix import of incorrect ics file (#88090)
gitea/chrono/pipeline/head This commit looks good Details
2024-03-21 08:30:58 +01:00
Valentin Deniaud 9c19321fb9 tests: fix typo in partial bookings feature flag (#88098)
gitea/chrono/pipeline/head This commit looks good Details
2024-03-18 16:25:34 +01:00
Lauréline Guérin a88db00e04
misc: add pyquery in dependencies (#88222)
gitea/chrono/pipeline/head This commit looks good Details
2024-03-15 12:11:59 +01:00
Lauréline Guérin eecbb80809
translation update
gitea/chrono/pipeline/head This commit looks good Details
2024-03-15 10:40:59 +01:00
Lauréline Guérin e0f1d9541d
manager: prefill presence check form with unexpected presence (#88039)
gitea/chrono/pipeline/head This commit looks good Details
2024-03-15 08:38:14 +01:00
Lauréline Guérin 701733da57
misc: tests with --dist loadfile options (#87751)
gitea/chrono/pipeline/head This commit looks good Details
2024-03-11 16:15:34 +01:00
Lauréline Guérin d709fa9bc7
misc: verbose tests (#87751) 2024-03-11 16:15:34 +01:00
Lauréline Guérin 1d00c5fce8
snapshots: compare inspect (#87751) 2024-03-11 16:15:34 +01:00
Lauréline Guérin bd06f2b82f
manager: inspect views (#87751) 2024-03-08 14:14:04 +01:00
Lauréline Guérin ecf0ffd96e
agendas: fix permissions for agenda history views (#87751) 2024-03-07 16:45:37 +01:00
Lauréline Guérin 1b1bc13c82
agendas: fix snapshot on role update (#87751) 2024-03-07 16:45:37 +01:00
Lauréline Guérin 06ab6f12b7
agendas: export resources only for meetings agenda (#87751) 2024-03-07 16:45:37 +01:00
Lauréline Guérin 024b34b34f
agendas: object history and compare (#87316)
gitea/chrono/pipeline/head This commit looks good Details
2024-03-07 16:44:19 +01:00
Lauréline Guérin 0ea056dcd5
agendas: fix missing options in agenda import/export (#87679)
gitea/chrono/pipeline/head This commit looks good Details
2024-03-04 17:32:20 +01:00
Lauréline Guérin 3cef873ce4
export_import: fix event agenda dependencies (#87627)
gitea/chrono/pipeline/head This commit looks good Details
2024-02-29 15:44:34 +01:00
54 changed files with 2488 additions and 155 deletions

View File

@ -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'),
),
]

View File

@ -62,13 +62,14 @@ from django.template import (
VariableDoesNotExist,
engines,
)
from django.template.defaultfilters import yesno
from django.urls import reverse
from django.utils import functional
from django.utils.dates import WEEKDAYS
from django.utils.encoding import force_str
from django.utils.formats import date_format
from django.utils.functional import cached_property
from django.utils.html import escape
from django.utils.html import escape, linebreaks
from django.utils.module_loading import import_string
from django.utils.safestring import mark_safe
from django.utils.text import slugify
@ -177,12 +178,35 @@ def booking_template_validator(value):
pass
class WithInspectMixin:
def get_inspect_fields(self, keys=None):
keys = keys or self.get_inspect_keys()
for key in keys:
field = self._meta.get_field(key)
get_value_method = 'get_%s_inspect_value' % key
get_display_method = 'get_%s_display' % key
if hasattr(self, get_value_method):
value = getattr(self, get_value_method)()
elif hasattr(self, get_display_method):
value = getattr(self, get_display_method)()
else:
value = getattr(self, key)
if value in [None, '']:
continue
if isinstance(value, bool):
value = yesno(value)
if isinstance(field, models.TextField):
value = mark_safe(linebreaks(value))
yield (field.verbose_name, value)
TimeSlot = collections.namedtuple(
'TimeSlot', ['start_datetime', 'end_datetime', 'full', 'desk', 'booked_for_external_user']
)
class Agenda(WithSnapshotMixin, WithApplicationMixin, models.Model):
# pylint: disable=too-many-public-methods
class Agenda(WithSnapshotMixin, WithApplicationMixin, WithInspectMixin, models.Model):
# mark temporarily restored snapshots
snapshot = models.ForeignKey(
AgendaSnapshot, on_delete=models.CASCADE, null=True, related_name='temporary_instance'
@ -250,7 +274,7 @@ class Agenda(WithSnapshotMixin, WithApplicationMixin, models.Model):
booking_form_url = models.CharField(
_('Booking form URL'), max_length=200, blank=True, validators=[django_template_validator]
)
desk_simple_management = models.BooleanField(default=False)
desk_simple_management = models.BooleanField(_('Global desk management'), default=False)
mark_event_checked_auto = models.BooleanField(
_('Automatically mark event as checked when all bookings have been checked'), default=False
)
@ -475,6 +499,7 @@ class Agenda(WithSnapshotMixin, WithApplicationMixin, models.Model):
yield from desk.get_dependencies()
if self.kind == 'events':
yield self.events_type
yield from self.desk_set.get().get_dependencies()
def export_json(self):
agenda = {
@ -484,11 +509,11 @@ class Agenda(WithSnapshotMixin, WithApplicationMixin, models.Model):
'category': self.category.slug if self.category else None,
'minimal_booking_delay': self.minimal_booking_delay,
'maximal_booking_delay': self.maximal_booking_delay,
'anonymize_delay': self.anonymize_delay,
'permissions': {
'view': self.view_role.name if self.view_role else None,
'edit': self.edit_role.name if self.edit_role else None,
},
'resources': [x.slug for x in self.resources.all()],
'default_view': self.default_view,
}
if hasattr(self, 'reminder_settings'):
@ -516,6 +541,7 @@ class Agenda(WithSnapshotMixin, WithApplicationMixin, models.Model):
agenda['meetingtypes'] = [x.export_json() for x in self.meetingtype_set.filter(deleted=False)]
agenda['desks'] = [desk.export_json() for desk in self.desk_set.all()]
agenda['desk_simple_management'] = self.desk_simple_management
agenda['resources'] = [x.slug for x in self.resources.all()]
elif self.kind == 'virtual':
agenda['excluded_timeperiods'] = [x.export_json() for x in self.excluded_timeperiods.all()]
agenda['real_agendas'] = [{'slug': x.slug, 'kind': x.kind} for x in self.real_agendas.all()]
@ -559,7 +585,8 @@ class Agenda(WithSnapshotMixin, WithApplicationMixin, models.Model):
slug = data.pop('slug')
qs_kwargs = {}
if snapshot:
qs_kwargs = {'snapshot': snapshot}
qs_kwargs = {'snapshot': snapshot} # don't take slug from snapshot: it has to be unique !
data['slug'] = str(uuid.uuid4()) # random slug
else:
qs_kwargs = {'slug': slug}
agenda, created = cls.objects.update_or_create(defaults=data, **qs_kwargs)
@ -623,6 +650,62 @@ class Agenda(WithSnapshotMixin, WithApplicationMixin, models.Model):
return created, agenda
def get_inspect_keys(self):
keys = ['label', 'slug', 'kind', 'category', 'anonymize_delay', 'default_view']
if self.kind == 'events':
keys += ['booking_form_url', 'events_type']
elif self.kind == 'meetings':
keys += ['desk_simple_management']
return keys
def get_permissions_inspect_fields(self):
yield from self.get_inspect_fields(keys=['edit_role', 'view_role'])
def get_display_inspect_fields(self):
keys = []
if self.kind == 'events':
keys += ['event_display_template']
keys += [
'booking_user_block_template',
]
yield from self.get_inspect_fields(keys=keys)
def get_booking_check_inspect_fields(self):
keys = [
'booking_check_filters',
'mark_event_checked_auto',
'disable_check_update',
'enable_check_for_future_events',
'booking_extra_user_block_template',
]
yield from self.get_inspect_fields(keys=keys)
def get_invoicing_inspect_fields(self):
keys = ['invoicing_unit', 'invoicing_tolerance']
yield from self.get_inspect_fields(keys=keys)
def get_notifications_inspect_fields(self):
if hasattr(self, 'notifications_settings'):
yield from self.notifications_settings.get_inspect_fields()
return []
def get_reminder_inspect_fields(self):
if hasattr(self, 'reminder_settings'):
yield from self.reminder_settings.get_inspect_fields()
return []
def get_booking_delays_inspect_fields(self):
keys = [
'minimal_booking_delay',
'minimal_booking_delay_in_working_days',
'maximal_booking_delay',
'minimal_booking_time',
]
yield from self.get_inspect_fields(keys=keys)
def get_kind_inspect_value(self):
return self.get_real_kind_display()
def duplicate(self, label=None):
# clone current agenda
new_agenda = copy.deepcopy(self)
@ -1759,7 +1842,7 @@ WEEK_CHOICES = [
]
class TimePeriod(models.Model):
class TimePeriod(WithInspectMixin, models.Model):
weekday = models.IntegerField(_('Week day'), choices=WEEKDAYS_LIST, null=True)
weekday_indexes = ArrayField(
models.IntegerField(choices=WEEK_CHOICES),
@ -1826,6 +1909,15 @@ class TimePeriod(models.Model):
'end_time': self.end_time.strftime('%H:%M'),
}
def get_inspect_keys(self):
return [
'weekday',
'weekday_indexes',
'date',
'start_time',
'end_time',
]
def duplicate(self, desk_target=None, agenda_target=None):
# clone current period
new_period = copy.deepcopy(self)
@ -2033,7 +2125,7 @@ class SharedTimePeriod:
start_datetime += datetime.timedelta(days=7)
class MeetingType(models.Model):
class MeetingType(WithInspectMixin, models.Model):
agenda = models.ForeignKey(Agenda, on_delete=models.CASCADE)
label = models.CharField(_('Label'), max_length=150)
slug = models.SlugField(_('Identifier'), max_length=160)
@ -2070,6 +2162,13 @@ class MeetingType(models.Model):
'duration': self.duration,
}
def get_inspect_keys(self):
return [
'label',
'slug',
'duration',
]
def duplicate(self, agenda_target=None):
new_meeting_type = copy.deepcopy(self)
new_meeting_type.pk = None
@ -2091,7 +2190,7 @@ class MeetingType(models.Model):
)
class Event(models.Model):
class Event(WithInspectMixin, models.Model):
id = models.BigAutoField(primary_key=True)
INTERVAL_CHOICES = [
(1, _('Every week')),
@ -2659,6 +2758,23 @@ class Event(models.Model):
'duration': self.duration,
}
def get_inspect_keys(self):
return [
'label',
'slug',
'description',
'start_datetime',
'duration',
'recurrence_days',
'recurrence_week_interval',
'recurrence_end_date',
'publication_datetime',
'places',
'waiting_list_places',
'url',
'pricing',
]
def duplicate(self, agenda_target=None, primary_event=None, label=None, start_datetime=None):
new_event = copy.deepcopy(self)
new_event.pk = None
@ -2843,7 +2959,7 @@ class Event(models.Model):
return custom_fields
class EventsType(WithSnapshotMixin, WithApplicationMixin, models.Model):
class EventsType(WithSnapshotMixin, WithApplicationMixin, WithInspectMixin, models.Model):
# mark temporarily restored snapshots
snapshot = models.ForeignKey(
EventsTypeSnapshot, on_delete=models.CASCADE, null=True, related_name='temporary_instance'
@ -2903,7 +3019,8 @@ class EventsType(WithSnapshotMixin, WithApplicationMixin, models.Model):
slug = data.pop('slug')
qs_kwargs = {}
if snapshot:
qs_kwargs = {'snapshot': snapshot}
qs_kwargs = {'snapshot': snapshot} # don't take slug from snapshot: it has to be unique !
data['slug'] = str(uuid.uuid4()) # random slug
else:
qs_kwargs = {'slug': slug}
events_type, created = cls.objects.update_or_create(defaults=data, **qs_kwargs)
@ -2916,6 +3033,9 @@ class EventsType(WithSnapshotMixin, WithApplicationMixin, models.Model):
'custom_fields': self.custom_fields,
}
def get_inspect_keys(self):
return ['label', 'slug']
class BookingColor(models.Model):
COLOR_COUNT = 8
@ -3310,7 +3430,7 @@ class BookingCheck(models.Model):
OpeningHour = collections.namedtuple('OpeningHour', ['begin', 'end'])
class Desk(models.Model):
class Desk(WithInspectMixin, models.Model):
agenda = models.ForeignKey(Agenda, on_delete=models.CASCADE)
label = models.CharField(_('Label'), max_length=150)
slug = models.SlugField(_('Identifier'), max_length=160)
@ -3373,6 +3493,12 @@ class Desk(models.Model):
'unavailability_calendars': [{'slug': x.slug} for x in self.unavailability_calendars.all()],
}
def get_inspect_keys(self):
return [
'label',
'slug',
]
def duplicate(self, label=None, agenda_target=None, reset_slug=True):
# clone current desk
new_desk = copy.deepcopy(self)
@ -3474,7 +3600,7 @@ class Desk(models.Model):
).delete() # source was not in settings anymore
class Resource(WithSnapshotMixin, WithApplicationMixin, models.Model):
class Resource(WithSnapshotMixin, WithApplicationMixin, WithInspectMixin, models.Model):
# mark temporarily restored snapshots
snapshot = models.ForeignKey(
ResourceSnapshot, on_delete=models.CASCADE, null=True, related_name='temporary_instance'
@ -3524,7 +3650,8 @@ class Resource(WithSnapshotMixin, WithApplicationMixin, models.Model):
slug = data.pop('slug')
qs_kwargs = {}
if snapshot:
qs_kwargs = {'snapshot': snapshot}
qs_kwargs = {'snapshot': snapshot} # don't take slug from snapshot: it has to be unique !
data['slug'] = str(uuid.uuid4()) # random slug
else:
qs_kwargs = {'slug': slug}
resource, created = cls.objects.update_or_create(defaults=data, **qs_kwargs)
@ -3537,8 +3664,11 @@ class Resource(WithSnapshotMixin, WithApplicationMixin, models.Model):
'description': self.description,
}
def get_inspect_keys(self):
return ['label', 'slug', 'description']
class Category(WithSnapshotMixin, WithApplicationMixin, models.Model):
class Category(WithSnapshotMixin, WithApplicationMixin, WithInspectMixin, models.Model):
# mark temporarily restored snapshots
snapshot = models.ForeignKey(
CategorySnapshot, on_delete=models.CASCADE, null=True, related_name='temporary_instance'
@ -3581,7 +3711,8 @@ class Category(WithSnapshotMixin, WithApplicationMixin, models.Model):
slug = data.pop('slug')
qs_kwargs = {}
if snapshot:
qs_kwargs = {'snapshot': snapshot}
qs_kwargs = {'snapshot': snapshot} # don't take slug from snapshot: it has to be unique !
data['slug'] = str(uuid.uuid4()) # random slug
else:
qs_kwargs = {'slug': slug}
category, created = cls.objects.update_or_create(defaults=data, **qs_kwargs)
@ -3593,12 +3724,15 @@ class Category(WithSnapshotMixin, WithApplicationMixin, models.Model):
'slug': self.slug,
}
def get_inspect_keys(self):
return ['label', 'slug']
def ics_directory_path(instance, filename):
return f'ics/{str(uuid.uuid4())}/{filename}'
class TimePeriodExceptionSource(models.Model):
class TimePeriodExceptionSource(WithInspectMixin, models.Model):
desk = models.ForeignKey(Desk, on_delete=models.CASCADE, null=True)
unavailability_calendar = models.ForeignKey('UnavailabilityCalendar', on_delete=models.CASCADE, null=True)
ics_filename = models.CharField(null=True, max_length=256)
@ -3841,15 +3975,18 @@ class TimePeriodExceptionSource(models.Model):
data = clean_import_data(cls, data)
if data.get('ics_file'):
data['ics_file'] = ContentFile(base64.b64decode(data['ics_file']), name=data['ics_filename'])
try:
data['ics_file'] = ContentFile(base64.b64decode(data['ics_file']), name=data['ics_filename'])
except base64.binascii.Error:
raise AgendaImportError(_('Bad ics file'))
desk = data.pop('desk')
settings_slug = data.pop('settings_slug')
ics_url = data.pop('ics_url', None)
ics_filename = data.pop('ics_filename', None)
source, _ = cls.objects.update_or_create(
source = cls.objects.update_or_create(
desk=desk, settings_slug=settings_slug, ics_filename=ics_filename, ics_url=ics_url, defaults=data
)
)[0]
if settings_slug:
if source.enabled:
source.enable()
@ -3869,8 +4006,18 @@ class TimePeriodExceptionSource(models.Model):
'enabled': self.enabled,
}
def get_inspect_keys(self):
return [
'ics_filename',
'ics_file',
'ics_url',
'settings_slug',
'settings_label',
'enabled',
]
class UnavailabilityCalendar(WithSnapshotMixin, WithApplicationMixin, models.Model):
class UnavailabilityCalendar(WithSnapshotMixin, WithApplicationMixin, WithInspectMixin, models.Model):
# mark temporarily restored snapshots
snapshot = models.ForeignKey(
UnavailabilityCalendarSnapshot, on_delete=models.CASCADE, null=True, related_name='temporary_instance'
@ -3966,7 +4113,8 @@ class UnavailabilityCalendar(WithSnapshotMixin, WithApplicationMixin, models.Mod
slug = data.pop('slug')
qs_kwargs = {}
if snapshot:
qs_kwargs = {'snapshot': snapshot}
qs_kwargs = {'snapshot': snapshot} # don't take slug from snapshot: it has to be unique !
data['slug'] = str(uuid.uuid4()) # random slug
else:
qs_kwargs = {'slug': slug}
unavailability_calendar, created = cls.objects.update_or_create(defaults=data, **qs_kwargs)
@ -3978,6 +4126,12 @@ class UnavailabilityCalendar(WithSnapshotMixin, WithApplicationMixin, models.Mod
return created, unavailability_calendar
def get_inspect_keys(self):
return ['label', 'slug']
def get_permissions_inspect_fields(self):
yield from self.get_inspect_fields(keys=['edit_role', 'view_role'])
class TimePeriodExceptionGroup(models.Model):
unavailability_calendar = models.ForeignKey(UnavailabilityCalendar, on_delete=models.CASCADE)
@ -3992,7 +4146,7 @@ class TimePeriodExceptionGroup(models.Model):
return self.label
class TimePeriodException(models.Model):
class TimePeriodException(WithInspectMixin, models.Model):
desk = models.ForeignKey(Desk, on_delete=models.CASCADE, null=True)
unavailability_calendar = models.ForeignKey(UnavailabilityCalendar, on_delete=models.CASCADE, null=True)
source = models.ForeignKey(TimePeriodExceptionSource, on_delete=models.CASCADE, null=True)
@ -4106,6 +4260,13 @@ class TimePeriodException(models.Model):
'update_datetime': export_datetime(self.update_datetime),
}
def get_inspect_keys(self):
return [
'label',
'start_datetime',
'end_datetime',
]
def duplicate(self, desk_target=None, source_target=None):
# clone current exception
new_exception = copy.deepcopy(self)
@ -4197,7 +4358,7 @@ class NotificationType:
return self.settings._meta.get_field(self.name).verbose_name
class AgendaNotificationsSettings(models.Model):
class AgendaNotificationsSettings(WithInspectMixin, models.Model):
EMAIL_FIELD = 'use-email-field'
VIEW_ROLE = 'view-role'
EDIT_ROLE = 'edit-role'
@ -4261,6 +4422,13 @@ class AgendaNotificationsSettings(models.Model):
'cancelled_event_emails': self.cancelled_event_emails,
}
def get_inspect_keys(self):
return [
'almost_full_event',
'full_event',
'cancelled_event',
]
def duplicate(self, agenda_target):
new_settings = copy.deepcopy(self)
new_settings.pk = None
@ -4269,7 +4437,7 @@ class AgendaNotificationsSettings(models.Model):
return new_settings
class AgendaReminderSettings(models.Model):
class AgendaReminderSettings(WithInspectMixin, models.Model):
ONE_DAY_BEFORE = 1
TWO_DAYS_BEFORE = 2
THREE_DAYS_BEFORE = 3
@ -4358,6 +4526,14 @@ class AgendaReminderSettings(models.Model):
'sms_extra_info': self.sms_extra_info,
}
def get_inspect_keys(self):
return [
'days_before_email',
'days_before_sms',
'email_extra_info',
'sms_extra_info',
]
def duplicate(self, agenda_target):
new_settings = copy.deepcopy(self)
new_settings.pk = None

View File

@ -43,7 +43,7 @@ klasses_translation_reverse = {v: k for k, v in klasses_translation.items()}
class Index(GenericAPIView):
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
permission_classes = (permissions.IsAdminUser,)
def get(self, request, *args, **kwargs):
data = []
@ -137,7 +137,7 @@ 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']]
@ -152,11 +152,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()
serialisation = get_object_or_404(klass, slug=slug).export_json()
return Response({'data': serialisation})
@ -164,11 +164,11 @@ 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)
component = get_object_or_404(klass, slug=slug)
def dependency_dict(element):
return get_component_bundle_entry(request, element)
@ -197,7 +197,7 @@ def component_redirect(request, component_type, slug):
class BundleCheck(GenericAPIView):
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
permission_classes = (permissions.IsAdminUser,)
def put(self, request, *args, **kwargs):
return Response({'err': 0, 'data': {}})
@ -207,7 +207,7 @@ bundle_check = BundleCheck.as_view()
class BundleImport(GenericAPIView):
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
permission_classes = (permissions.IsAdminUser,)
install = True
def put(self, request, *args, **kwargs):
@ -283,7 +283,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'):

View File

@ -40,7 +40,7 @@ class WithSnapshotMixin:
return cls._meta.get_field('snapshot').related_model
def take_snapshot(self, *args, **kwargs):
self.get_snapshot_model().take(self, *args, **kwargs)
return self.get_snapshot_model().take(self, *args, **kwargs)
class AbstractSnapshot(models.Model):
@ -74,17 +74,68 @@ class AbstractSnapshot(models.Model):
snapshot.application_slug = application.slug
snapshot.application_version = application.version_number
snapshot.save()
return snapshot
def get_instance(self):
try:
# try reusing existing instance
return self.get_instance_model().snapshots.get(snapshot=self)
instance = self.get_instance_model().snapshots.get(snapshot=self)
except self.get_instance_model().DoesNotExist:
return self.load_instance(self.serialization, snapshot=self)
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(

View File

@ -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/>.
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(self):
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'),
)

View File

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: chrono 0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-02-27 16:39+0100\n"
"POT-Creation-Date: 2024-03-21 08:36+0100\n"
"PO-Revision-Date: 2024-02-01 09:50+0100\n"
"Last-Translator: Frederic Peters <fpeters@entrouvert.com>\n"
"Language: French\n"
@ -41,6 +41,7 @@ msgid "in %s days"
msgstr "dans %s jours"
#: agendas/models.py
#: manager/templates/chrono/manager_agenda_inspect_fragment.html
#: manager/templates/chrono/manager_events_agenda_day_view.html
#: manager/templates/chrono/manager_events_agenda_month_view.html
#: manager/templates/chrono/manager_events_agenda_settings.html
@ -142,6 +143,7 @@ msgid "Label"
msgstr "Libellé"
#: agendas/models.py
#: manager/templates/chrono/includes/snapshot_history_fragment.html
msgid "Identifier"
msgstr "Identifiant"
@ -192,6 +194,10 @@ msgstr "Vue par défaut"
msgid "Booking form URL"
msgstr "Adresse de la démarche de réservation"
#: agendas/models.py
msgid "Global desk management"
msgstr "Gestion globale des guichets"
#: agendas/models.py
msgid "Automatically mark event as checked when all bookings have been checked"
msgstr ""
@ -380,6 +386,7 @@ msgid "Repeat"
msgstr "Répéter"
#: agendas/models.py manager/forms.py
#: manager/templates/chrono/includes/snapshot_history_fragment.html
msgid "Date"
msgstr "Date"
@ -461,6 +468,7 @@ msgid "Optional label to identify this date."
msgstr "Libellé optionnel pour identifier la date."
#: agendas/models.py
#: manager/templates/chrono/includes/snapshot_history_fragment.html
msgid "Description"
msgstr "Description"
@ -567,6 +575,7 @@ msgid "Resource"
msgstr "Ressource"
#: agendas/models.py manager/forms.py
#: manager/templates/chrono/manager_agenda_inspect_fragment.html
#: manager/templates/chrono/manager_home.html
#: manager/templates/chrono/manager_meetings_agenda_settings.html
#: manager/templates/chrono/manager_resource_list.html
@ -612,11 +621,16 @@ msgstr "Lévènement « %s » na pas de date de début."
msgid "Exception"
msgstr "Exception"
#: agendas/models.py
msgid "Bad ics file"
msgstr "Mauvais format de fichier ICS"
#: agendas/models.py
msgid "Unavailability calendar"
msgstr "Calendrier dindisponibilités"
#: agendas/models.py manager/forms.py
#: manager/templates/chrono/manager_agenda_inspect_fragment.html
#: manager/templates/chrono/manager_home.html
msgid "Unavailability calendars"
msgstr "Calendrier dindisponibilités"
@ -1938,6 +1952,15 @@ msgstr "Rôle"
msgid "deletion"
msgstr "suppression"
#: apps/snapshot/views.py
#, python-format
msgid "Version %s"
msgstr "Version %s"
#: apps/snapshot/views.py
msgid "Snapshot"
msgstr "Sauvegarde"
#: manager/forms.py
msgid "Desk 1"
msgstr "Guichet 1"
@ -2011,14 +2034,17 @@ msgid "Field type"
msgstr "Type du champ"
#: manager/forms.py
#: manager/templates/chrono/manager_events_type_inspect_fragment.html
msgid "Text"
msgstr "Texte"
#: manager/forms.py
#: manager/templates/chrono/manager_events_type_inspect_fragment.html
msgid "Textarea"
msgstr "Zone de texte"
#: manager/forms.py
#: manager/templates/chrono/manager_events_type_inspect_fragment.html
msgid "Boolean"
msgstr "Booléen"
@ -2441,12 +2467,49 @@ msgstr "Couleurs des rendez-vous:"
msgid "Applications"
msgstr "Applications"
#: manager/templates/chrono/includes/snapshot_history_fragment.html
msgid "Show differences"
msgstr "Afficher les différences"
#: manager/templates/chrono/includes/snapshot_history_fragment.html
msgid "Compare"
msgstr "Comparer"
#: manager/templates/chrono/includes/snapshot_history_fragment.html
msgid "User"
msgstr "Usager"
#: manager/templates/chrono/includes/snapshot_history_fragment.html
#: manager/templates/chrono/manager_agenda_settings.html
#: manager/templates/chrono/manager_category_list.html
#: manager/templates/chrono/manager_events_type_list.html
#: manager/templates/chrono/manager_home.html
#: manager/templates/chrono/manager_resource_detail.html
#: manager/templates/chrono/manager_resource_list.html
#: manager/templates/chrono/manager_unavailability_calendar_list.html
#: manager/templates/chrono/manager_unavailability_calendar_settings.html
msgid "Actions"
msgstr "Actions"
#: manager/templates/chrono/includes/snapshot_history_fragment.html
#, python-format
msgid "1 other this day"
msgid_plural "%(counter)s others"
msgstr[0] "1 autre ce jour"
msgstr[1] "%(counter)s autres ce jour"
#: manager/templates/chrono/includes/snapshot_history_fragment.html
#, python-format
msgid "Version %(version)s"
msgstr "Version %(version)s"
#: manager/templates/chrono/manager_agenda_add_form.html
msgid "New Agenda"
msgstr "Nouvel agenda"
#: manager/templates/chrono/manager_agenda_date_view.html
#: manager/templates/chrono/manager_agenda_form.html
#: manager/templates/chrono/manager_agenda_inspect_fragment.html
#: manager/templates/chrono/manager_agenda_notifications_form.html
#: manager/templates/chrono/manager_agenda_settings.html
#: manager/templates/chrono/manager_agenda_view.html
@ -2578,6 +2641,169 @@ msgstr "hors de la période dinscription"
msgid "Booking form"
msgstr "Démarche de réservation"
#: manager/templates/chrono/manager_agenda_history.html
msgid "Agenda history"
msgstr "Historique de lagenda"
#: manager/templates/chrono/manager_agenda_history.html
#: manager/templates/chrono/manager_agenda_settings.html
#: manager/templates/chrono/manager_category_form.html
#: manager/templates/chrono/manager_category_history.html
#: manager/templates/chrono/manager_events_type_form.html
#: manager/templates/chrono/manager_events_type_history.html
#: manager/templates/chrono/manager_resource_detail.html
#: manager/templates/chrono/manager_resource_history.html
#: manager/templates/chrono/manager_unavailability_calendar_history.html
#: manager/templates/chrono/manager_unavailability_calendar_settings.html
msgid "History"
msgstr "Historique"
#: manager/templates/chrono/manager_agenda_history_compare.html
#: manager/templates/chrono/manager_category_history_compare.html
#: manager/templates/chrono/manager_events_type_history_compare.html
#: manager/templates/chrono/manager_resource_history_compare.html
#: manager/templates/chrono/manager_unavailability_calendar_history_compare.html
msgid "Compare snapshots"
msgstr "Comparaison des sauvergardes"
#: manager/templates/chrono/manager_agenda_history_compare.html
#: manager/templates/chrono/manager_category_history_compare.html
#: manager/templates/chrono/manager_events_type_history_compare.html
#: manager/templates/chrono/manager_resource_history_compare.html
#: manager/templates/chrono/manager_unavailability_calendar_history_compare.html
msgid "JSON"
msgstr "JSON"
#: manager/templates/chrono/manager_agenda_history_compare.html
#: manager/templates/chrono/manager_agenda_inspect.html
#: manager/templates/chrono/manager_agenda_settings.html
#: manager/templates/chrono/manager_category_form.html
#: manager/templates/chrono/manager_category_history_compare.html
#: manager/templates/chrono/manager_category_inspect.html
#: manager/templates/chrono/manager_events_type_form.html
#: manager/templates/chrono/manager_events_type_history_compare.html
#: manager/templates/chrono/manager_events_type_inspect.html
#: manager/templates/chrono/manager_resource_detail.html
#: manager/templates/chrono/manager_resource_history_compare.html
#: manager/templates/chrono/manager_resource_inspect.html
#: manager/templates/chrono/manager_unavailability_calendar_history_compare.html
#: manager/templates/chrono/manager_unavailability_calendar_inspect.html
#: manager/templates/chrono/manager_unavailability_calendar_settings.html
msgid "Inspect"
msgstr "Inspecteur"
#: manager/templates/chrono/manager_agenda_history_compare.html
#: manager/templates/chrono/manager_category_history_compare.html
#: manager/templates/chrono/manager_events_type_history_compare.html
#: manager/templates/chrono/manager_resource_history_compare.html
#: manager/templates/chrono/manager_unavailability_calendar_history_compare.html
msgid "Compare inspect"
msgstr "Comparaison des inspecteurs"
#: manager/templates/chrono/manager_agenda_history_compare.html
#: manager/templates/chrono/manager_category_history_compare.html
#: manager/templates/chrono/manager_events_type_history_compare.html
#: manager/templates/chrono/manager_resource_history_compare.html
#: manager/templates/chrono/manager_unavailability_calendar_history_compare.html
msgid "Compare JSON"
msgstr "Comparaison JSON"
#: manager/templates/chrono/manager_agenda_inspect_fragment.html
#: manager/templates/chrono/manager_category_inspect_fragment.html
#: manager/templates/chrono/manager_events_type_inspect_fragment.html
#: manager/templates/chrono/manager_resource_inspect_fragment.html
#: manager/templates/chrono/manager_unavailability_calendar_inspect_fragment.html
msgid "Information"
msgstr "Informations"
#: manager/templates/chrono/manager_agenda_inspect_fragment.html
#: manager/templates/chrono/manager_agenda_settings.html
#: manager/templates/chrono/manager_unavailability_calendar_inspect_fragment.html
msgid "Permissions"
msgstr "Permissions"
#: manager/templates/chrono/manager_agenda_inspect_fragment.html
#: manager/templates/chrono/manager_events_agenda_settings.html
msgid "Recurrence exceptions"
msgstr "Exceptions aux récurrences"
#: manager/templates/chrono/manager_agenda_inspect_fragment.html
#: manager/templates/chrono/manager_meetings_agenda_settings.html
#: manager/templates/chrono/manager_virtual_agenda_settings.html
msgid "Meeting Types"
msgstr "Types de rendez-vous"
#: manager/templates/chrono/manager_agenda_inspect_fragment.html
#: manager/templates/chrono/manager_meetings_agenda_settings.html
msgid "Desks"
msgstr "Guichets"
#: manager/templates/chrono/manager_agenda_inspect_fragment.html
#: manager/templates/chrono/manager_virtual_agenda_settings.html
msgid "Included Agendas"
msgstr "Agendas inclus"
#: manager/templates/chrono/manager_agenda_inspect_fragment.html
#: manager/templates/chrono/manager_virtual_agenda_settings.html
msgid "Excluded Periods"
msgstr "Périodes exclues"
#: manager/templates/chrono/manager_agenda_inspect_fragment.html
#: manager/templates/chrono/manager_category_inspect_fragment.html
#: manager/templates/chrono/manager_events_type_inspect_fragment.html
#: manager/templates/chrono/manager_resource_inspect_fragment.html
#: manager/templates/chrono/manager_unavailability_calendar_inspect_fragment.html
#, python-format
msgid "%(label)s:"
msgstr "%(label)s :"
#: manager/templates/chrono/manager_agenda_inspect_fragment.html
#: manager/templates/chrono/manager_events_agenda_settings.html
#: manager/templates/chrono/manager_meetings_agenda_settings.html
msgid "Display options"
msgstr "Paramètres daffichage"
#: manager/templates/chrono/manager_agenda_inspect_fragment.html
#: manager/templates/chrono/manager_events_agenda_settings.html
msgid "Booking check options"
msgstr "Paramètres du pointage"
#: manager/templates/chrono/manager_agenda_inspect_fragment.html
#: manager/templates/chrono/manager_events_agenda_settings.html
msgid "Invoicing options"
msgstr "Paramètres de facturation"
#: manager/templates/chrono/manager_agenda_inspect_fragment.html
#: manager/templates/chrono/manager_events_agenda_settings.html
msgid "Management notifications"
msgstr "Notifications dadministration"
#: manager/templates/chrono/manager_agenda_inspect_fragment.html
#: manager/templates/chrono/manager_agenda_settings.html
msgid "Booking reminders"
msgstr "Rappels de réservation"
#: manager/templates/chrono/manager_agenda_inspect_fragment.html
#: manager/templates/chrono/manager_agenda_settings.html
msgid "Booking Delays"
msgstr "Délais de réservation"
#: manager/templates/chrono/manager_agenda_inspect_fragment.html
msgid "Exception sources"
msgstr "Sources dexceptions"
#: manager/templates/chrono/manager_agenda_inspect_fragment.html
#: manager/templates/chrono/manager_meetings_agenda_settings.html
#: manager/templates/chrono/manager_time_period_exception_list.html
#: manager/templates/chrono/manager_unavailability_calendar_inspect_fragment.html
msgid "Exceptions"
msgstr "Exceptions"
#: manager/templates/chrono/manager_agenda_inspect_fragment.html
#: manager/templates/chrono/manager_meetings_agenda_settings.html
msgid "Opening hours"
msgstr "Plages horaires"
#: manager/templates/chrono/manager_agenda_month_view.html
#: manager/templates/chrono/manager_resource_month_view.html
msgid "Previous month"
@ -2662,18 +2888,6 @@ msgstr "Exporter les évènements (CSV)"
msgid "Delete"
msgstr "Supprimer"
#: manager/templates/chrono/manager_agenda_settings.html
msgid "Booking reminders"
msgstr "Rappels de réservation"
#: manager/templates/chrono/manager_agenda_settings.html
msgid "Booking Delays"
msgstr "Délais de réservation"
#: manager/templates/chrono/manager_agenda_settings.html
msgid "Permissions"
msgstr "Permissions"
#: manager/templates/chrono/manager_agenda_settings.html
msgid "Reminders are disabled for this agenda."
msgstr "Les rappels sont désactivés pour cet agenda."
@ -2735,23 +2949,19 @@ msgstr "Rôle dédition :"
msgid "View Role:"
msgstr "Rôle de visualisation :"
#: manager/templates/chrono/manager_agenda_settings.html
#: manager/templates/chrono/manager_category_list.html
#: manager/templates/chrono/manager_events_type_list.html
#: manager/templates/chrono/manager_home.html
#: manager/templates/chrono/manager_resource_detail.html
#: manager/templates/chrono/manager_resource_list.html
#: manager/templates/chrono/manager_unavailability_calendar_list.html
#: manager/templates/chrono/manager_unavailability_calendar_settings.html
msgid "Actions"
msgstr "Actions"
#: manager/templates/chrono/manager_agenda_settings.html
#: manager/templates/chrono/manager_event_detail.html
#: manager/templates/chrono/manager_unavailability_calendar_settings.html
msgid "Options"
msgstr "Options"
#: manager/templates/chrono/manager_agenda_settings.html
#: manager/templates/chrono/manager_home.html
#: manager/templates/chrono/manager_resource_detail.html
#: manager/templates/chrono/manager_unavailability_calendar_settings.html
msgid "Navigation"
msgstr "Navigation"
#: manager/templates/chrono/manager_agenda_unavailability_calendar_form.html
msgid "Add unavailability calendar"
msgstr "Ajouter un calendrier dindisponibilités"
@ -2795,6 +3005,10 @@ msgstr "Nouvelle catégorie"
msgid "Edit Category"
msgstr "Modification de la catégorie"
#: manager/templates/chrono/manager_category_history.html
msgid "Category history"
msgstr "Historique de la catégorie"
#: manager/templates/chrono/manager_category_list.html
msgid "Categories outside applications"
msgstr "Catégories hors applications"
@ -3116,37 +3330,11 @@ msgstr "Ce mois na pas dévènements configurés."
msgid "Import Events"
msgstr "Importer des évènements"
#: manager/templates/chrono/manager_events_agenda_settings.html
#: manager/templates/chrono/manager_home.html
msgid "Navigation"
msgstr "Navigation"
#: manager/templates/chrono/manager_events_agenda_settings.html
msgctxt "pricing"
msgid "Pricing"
msgstr "Tarification"
#: manager/templates/chrono/manager_events_agenda_settings.html
msgid "Recurrence exceptions"
msgstr "Exceptions aux récurrences"
#: manager/templates/chrono/manager_events_agenda_settings.html
#: manager/templates/chrono/manager_meetings_agenda_settings.html
msgid "Display options"
msgstr "Paramètres daffichage"
#: manager/templates/chrono/manager_events_agenda_settings.html
msgid "Booking check options"
msgstr "Paramètres du pointage"
#: manager/templates/chrono/manager_events_agenda_settings.html
msgid "Invoicing options"
msgstr "Paramètres de facturation"
#: manager/templates/chrono/manager_events_agenda_settings.html
msgid "Management notifications"
msgstr "Notifications dadministration"
#: manager/templates/chrono/manager_events_agenda_settings.html
msgid ""
"This agenda doesn't have any event yet. Click on the \"New Event\" button in "
@ -3329,6 +3517,7 @@ msgid "Edit events type"
msgstr "Modification du type dévènement"
#: manager/templates/chrono/manager_events_type_form.html
#: manager/templates/chrono/manager_events_type_inspect_fragment.html
msgid "Custom fields"
msgstr "Champs personnalisés"
@ -3336,6 +3525,22 @@ msgstr "Champs personnalisés"
msgid "Add another custom field"
msgstr "Ajouter un autre champ"
#: manager/templates/chrono/manager_events_type_history.html
msgid "Events type history"
msgstr "Historique du type dévènement"
#: manager/templates/chrono/manager_events_type_inspect_fragment.html
msgid "Field slug:"
msgstr "Identifiant du champ :"
#: manager/templates/chrono/manager_events_type_inspect_fragment.html
msgid "Field label:"
msgstr "Libellé du champ :"
#: manager/templates/chrono/manager_events_type_inspect_fragment.html
msgid "Field type:"
msgstr "Type du champ :"
#: manager/templates/chrono/manager_events_type_list.html
msgid "Events types outside applications"
msgstr "Types dévènements hors applications"
@ -3430,19 +3635,6 @@ msgstr "Basculer en gestion unitaire des guichets"
msgid "Switch to global desk management"
msgstr "Basculer en gestion globale des guichets"
#: manager/templates/chrono/manager_meetings_agenda_settings.html
#: manager/templates/chrono/manager_virtual_agenda_settings.html
msgid "Meeting Types"
msgstr "Types de rendez-vous"
#: manager/templates/chrono/manager_meetings_agenda_settings.html
msgid "Desks"
msgstr "Guichets"
#: manager/templates/chrono/manager_meetings_agenda_settings.html
msgid "Opening hours"
msgstr "Plages horaires"
#: manager/templates/chrono/manager_meetings_agenda_settings.html
#: manager/templates/chrono/manager_virtual_agenda_settings.html
msgid "minutes"
@ -3468,11 +3660,6 @@ msgstr "Ajouter une plage horaire régulière"
msgid "Add a unique period"
msgstr "Ajouter une plage horaire unique"
#: manager/templates/chrono/manager_meetings_agenda_settings.html
#: manager/templates/chrono/manager_time_period_exception_list.html
msgid "Exceptions"
msgstr "Exceptions"
#: manager/templates/chrono/manager_meetings_agenda_settings.html
msgid "manage exceptions"
msgstr "gérer les exceptions"
@ -3602,6 +3789,10 @@ msgstr "Nouvelle ressource"
msgid "Edit Resource"
msgstr "Modification de la ressource"
#: manager/templates/chrono/manager_resource_history.html
msgid "Resource history"
msgstr "Historique de la ressource"
#: manager/templates/chrono/manager_resource_list.html
msgid "Resources outside applications"
msgstr "Ressources hors applications"
@ -3786,6 +3977,10 @@ msgstr "Modification du calendrier dindisponibilités"
msgid "New Unavailability Calendar"
msgstr "Nouveau calendrier dindisponibilités"
#: manager/templates/chrono/manager_unavailability_calendar_history.html
msgid "UnavailabilityCalendarSnapshot calendar history"
msgstr "Historique du calendrier dindisponibilités"
#: manager/templates/chrono/manager_unavailability_calendar_list.html
msgid "Unavailability Calendars"
msgstr "Calendriers dindisponibilités"
@ -3837,14 +4032,6 @@ msgstr "Ajouter une période dexclusion"
msgid "Include Agenda"
msgstr "Inclure un agenda"
#: manager/templates/chrono/manager_virtual_agenda_settings.html
msgid "Included Agendas"
msgstr "Agendas inclus"
#: manager/templates/chrono/manager_virtual_agenda_settings.html
msgid "Excluded Periods"
msgstr "Périodes exclues"
#: manager/templates/chrono/manager_virtual_agenda_settings.html
msgid ""
"This virtual agenda doesn't include any agenda yet. Click on the \"Include "

View File

@ -589,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):

View File

@ -956,3 +956,81 @@ a.button.button-paragraph {
.application-logo, .application-icon {
vertical-align: middle;
}
.snapshots-list .collapsed {
display: none;
}
p.snapshot-description {
font-size: 80%;
margin: 0;
}
div.diff {
margin: 1em 0;
h3 {
del, ins {
font-weight: bold;
background-color: transparent;
}
del {
color: #fbb6c2 !important;
}
ins {
color: #d4fcbc !important;
}
}
}
ins {
text-decoration: none;
background-color: #d4fcbc;
}
del {
text-decoration: line-through;
background-color: #fbb6c2;
color: #555;
}
table.diff {
background: white;
border: 1px solid #f3f3f3;
border-collapse: collapse;
width: 100%;
colgroup, thead, tbody, td {
border: 1px solid #f3f3f3;
}
tbody tr:nth-child(even) {
background: #fdfdfd;
}
th, td {
max-width: 30vw;
/* it will not actually limit width as the table is set to
* expand to 100% but it will prevent one side getting wider
*/
overflow: hidden;
text-overflow: ellipsis;
vertical-align: top;
}
.diff_header {
background: #f7f7f7;
}
td.diff_header {
text-align: right;
padding-right: 10px;
color: #606060;
}
.diff_next {
display: none;
}
.diff_add {
background-color: #aaffaa;
}
.diff_chg {
background-color: #ffff77;
}
.diff_sub {
background-color: #ffaaaa;
}
}

View File

@ -0,0 +1,20 @@
<p class="snapshot-description">{{ fromdesc|safe }} ➔ {{ todesc|safe }}</p>
<div class="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>

View File

@ -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 %}&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{% endif %}
{% if not forloop.first %}<input type="radio" name="version2" value="{{ snapshot.pk }}" {% if forloop.counter == 2 %}checked="checked"{% endif %}/>{% else %}&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{% 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>

View File

@ -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 %}

View File

@ -0,0 +1,19 @@
{% extends "chrono/manager_agenda_history.html" %}
{% load i18n %}
{% 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 %}

View File

@ -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 %}

View File

@ -0,0 +1,309 @@
{% 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 %}
</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>

View File

@ -120,6 +120,11 @@
<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 %}

View File

@ -20,6 +20,14 @@
{% 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 %}

View File

@ -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 %}

View File

@ -0,0 +1,19 @@
{% extends "chrono/manager_category_history.html" %}
{% load i18n %}
{% 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 %}

View File

@ -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 %}

View File

@ -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>

View File

@ -8,7 +8,6 @@
{% block agenda-extra-navigation-actions %}
{% with lingo_url=object.get_lingo_url %}{% if lingo_url %}
<h3>{% trans "Navigation" %}</h3>
<a class="button button-paragraph" href="{{ lingo_url }}">{% trans 'Pricing' context 'pricing' %}</a>
{% endif %}{% endwith %}
{% endblock %}

View File

@ -20,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 %}

View File

@ -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 %}

View File

@ -0,0 +1,19 @@
{% extends "chrono/manager_events_type_history.html" %}
{% load i18n %}
{% 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 %}

View File

@ -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 %}

View File

@ -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>

View File

@ -63,6 +63,12 @@
<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>

View File

@ -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 %}

View File

@ -0,0 +1,19 @@
{% extends "chrono/manager_resource_history.html" %}
{% load i18n %}
{% 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 %}

View File

@ -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 %}

View File

@ -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>

View File

@ -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 %}

View File

@ -0,0 +1,19 @@
{% extends "chrono/manager_unavailability_calendar_history.html" %}
{% load i18n %}
{% 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 %}

View File

@ -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 %}

View File

@ -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>

View File

@ -54,6 +54,12 @@
<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>

View File

@ -65,6 +65,21 @@ urlpatterns = [
views.unavailability_calendar_import_unavailabilities,
name='chrono-manager-unavailability-calendar-import-unavailabilities',
),
path(
'unavailability-calendar/<int:pk>/inspect/',
views.unavailability_calendar_inspect,
name='chrono-manager-unavailability-calendar-inspect',
),
path(
'unavailability-calendar/<int:pk>/history/',
views.unavailability_calendar_history,
name='chrono-manager-unavailability-calendar-history',
),
path(
'unavailability-calendar/<int:pk>/history/compare/',
views.unavailability_calendar_history_compare,
name='chrono-manager-unavailability-calendar-history-compare',
),
path('resources/', views.resource_list, name='chrono-manager-resource-list'),
path('resource/add/', views.resource_add, name='chrono-manager-resource-add'),
path('resource/<int:pk>/', views.resource_view, name='chrono-manager-resource-view'),
@ -90,10 +105,24 @@ urlpatterns = [
),
path('resource/<int:pk>/edit/', views.resource_edit, name='chrono-manager-resource-edit'),
path('resource/<int:pk>/delete/', views.resource_delete, name='chrono-manager-resource-delete'),
path('resource/<int:pk>/inspect/', views.resource_inspect, name='chrono-manager-resource-inspect'),
path('resource/<int:pk>/history/', views.resource_history, name='chrono-manager-resource-history'),
path(
'resource/<int:pk>/history/compare/',
views.resource_history_compare,
name='chrono-manager-resource-history-compare',
),
path('categories/', views.category_list, name='chrono-manager-category-list'),
path('category/add/', views.category_add, name='chrono-manager-category-add'),
path('category/<int:pk>/edit/', views.category_edit, name='chrono-manager-category-edit'),
path('category/<int:pk>/delete/', views.category_delete, name='chrono-manager-category-delete'),
path('category/<int:pk>/inspect/', views.category_inspect, name='chrono-manager-category-inspect'),
path('category/<int:pk>/history/', views.category_history, name='chrono-manager-category-history'),
path(
'category/<int:pk>/history/compare/',
views.category_history_compare,
name='chrono-manager-category-history-compare',
),
path('events-types/', views.events_type_list, name='chrono-manager-events-type-list'),
path('events-type/add/', views.events_type_add, name='chrono-manager-events-type-add'),
path('events-type/<int:pk>/edit/', views.events_type_edit, name='chrono-manager-events-type-edit'),
@ -102,6 +131,17 @@ urlpatterns = [
views.events_type_delete,
name='chrono-manager-events-type-delete',
),
path(
'events-type/<int:pk>/inspect/', views.events_type_inspect, name='chrono-manager-events-type-inspect'
),
path(
'events-type/<int:pk>/history/', views.events_type_history, name='chrono-manager-events-type-history'
),
path(
'events-type/<int:pk>/history/compare/',
views.events_type_history_compare,
name='chrono-manager-events-type-history-compare',
),
path('agendas/add/', views.agenda_add, name='chrono-manager-agenda-add'),
path('agendas/import/', views.agendas_import, name='chrono-manager-agendas-import'),
path('agendas/export/', views.agendas_export, name='chrono-manager-agendas-export'),
@ -449,6 +489,13 @@ urlpatterns = [
views.agenda_import_events_sample_csv,
name='chrono-manager-sample-events-csv',
),
path('agendas/<int:pk>/inspect/', views.agenda_inspect, name='chrono-manager-agenda-inspect'),
path('agendas/<int:pk>/history/', views.agenda_history, name='chrono-manager-agenda-history'),
path(
'agendas/<int:pk>/history/compare/',
views.agenda_history_compare,
name='chrono-manager-agenda-history-compare',
),
path(
'shared-custody/settings/',
views.shared_custody_settings,

View File

@ -30,7 +30,7 @@ from django.conf import settings
from django.contrib import messages
from django.core.exceptions import PermissionDenied
from django.db import IntegrityError, models, transaction
from django.db.models import BooleanField, Count, ExpressionWrapper, F, Func, Max, Min, Q, Value
from django.db.models import BooleanField, Count, ExpressionWrapper, F, Func, Max, Min, Prefetch, Q, Value
from django.db.models.deletion import ProtectedError
from django.db.models.functions import Cast
from django.http import Http404, HttpResponse, HttpResponseForbidden, HttpResponseRedirect
@ -92,6 +92,14 @@ from chrono.agendas.models import (
VirtualMember,
)
from chrono.apps.export_import.models import Application
from chrono.apps.snapshot.models import (
AgendaSnapshot,
CategorySnapshot,
EventsTypeSnapshot,
ResourceSnapshot,
UnavailabilityCalendarSnapshot,
)
from chrono.apps.snapshot.views import InstanceWithSnapshotHistoryCompareView, InstanceWithSnapshotHistoryView
from chrono.utils.date import get_weekday_index
from chrono.utils.timezone import localtime, make_aware, make_naive, now
@ -288,6 +296,7 @@ class ResourceDetailView(DetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['resource'] = self.object
context['show_history'] = settings.SNAPSHOTS_ENABLED
return context
@ -791,6 +800,49 @@ class ResourceDeleteView(DeleteView):
resource_delete = ResourceDeleteView.as_view()
class ResourceInspectView(DetailView):
template_name = 'chrono/manager_resource_inspect.html'
model = Resource
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
resource_inspect = ResourceInspectView.as_view()
class ResourceHistoryView(InstanceWithSnapshotHistoryView):
template_name = 'chrono/manager_resource_history.html'
model = ResourceSnapshot
instance_context_key = 'resource'
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
resource_history = ResourceHistoryView.as_view()
class ResourceHistoryCompareView(InstanceWithSnapshotHistoryCompareView):
template_name = 'chrono/manager_resource_history_compare.html'
inspect_template_name = 'chrono/manager_resource_inspect_fragment.html'
model = Resource
instance_context_key = 'resource'
history_view = 'chrono-manager-resource-history'
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
resource_history_compare = ResourceHistoryCompareView.as_view()
class CategoryListView(WithApplicationsMixin, ListView):
template_name = 'chrono/manager_category_list.html'
model = Category
@ -852,6 +904,10 @@ class CategoryEditView(UpdateView):
self.object.take_snapshot(request=self.request)
return response
def get_context_data(self, **kwargs):
kwargs['show_history'] = settings.SNAPSHOTS_ENABLED
return super().get_context_data(**kwargs)
category_edit = CategoryEditView.as_view()
@ -876,6 +932,49 @@ class CategoryDeleteView(DeleteView):
category_delete = CategoryDeleteView.as_view()
class CategoryInspectView(DetailView):
template_name = 'chrono/manager_category_inspect.html'
model = Category
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
category_inspect = CategoryInspectView.as_view()
class CategoryHistoryView(InstanceWithSnapshotHistoryView):
template_name = 'chrono/manager_category_history.html'
model = CategorySnapshot
instance_context_key = 'category'
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
category_history = CategoryHistoryView.as_view()
class CategoryHistoryCompareView(InstanceWithSnapshotHistoryCompareView):
template_name = 'chrono/manager_category_history_compare.html'
inspect_template_name = 'chrono/manager_category_inspect_fragment.html'
model = Category
instance_context_key = 'category'
history_view = 'chrono-manager-category-history'
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
category_history_compare = CategoryHistoryCompareView.as_view()
class EventsTypeListView(WithApplicationsMixin, ListView):
template_name = 'chrono/manager_events_type_list.html'
model = EventsType
@ -934,6 +1033,7 @@ class EventsTypeEditView(UpdateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['show_history'] = settings.SNAPSHOTS_ENABLED
data = None
if self.request.method == 'POST':
data = self.request.POST
@ -1020,6 +1120,50 @@ class EventsTypeDeleteView(DeleteView):
events_type_delete = EventsTypeDeleteView.as_view()
class EventsTypeInspectView(DetailView):
template_name = 'chrono/manager_events_type_inspect.html'
model = EventsType
context_object_name = 'events_type'
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
events_type_inspect = EventsTypeInspectView.as_view()
class EventsTypeHistoryView(InstanceWithSnapshotHistoryView):
template_name = 'chrono/manager_events_type_history.html'
model = EventsTypeSnapshot
instance_context_key = 'events_type'
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
events_type_history = EventsTypeHistoryView.as_view()
class EventsTypeHistoryCompareView(InstanceWithSnapshotHistoryCompareView):
template_name = 'chrono/manager_events_type_history_compare.html'
inspect_template_name = 'chrono/manager_events_type_inspect_fragment.html'
model = EventsType
instance_context_key = 'events_type'
history_view = 'chrono-manager-events-type-history'
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
events_type_history_compare = EventsTypeHistoryCompareView.as_view()
class AgendaAddView(CreateView):
template_name = 'chrono/manager_agenda_add_form.html'
model = Agenda
@ -1252,6 +1396,7 @@ class AgendaEditView(ManagedAgendaMixin, UpdateView):
def form_valid(self, *args, **kwargs):
response = super().form_valid(*args, **kwargs)
self.agenda = Agenda.objects.get(pk=self.agenda.pk) # refresh object, for M2M
self.agenda.take_snapshot(request=self.request, comment=self.comment)
return response
@ -1614,6 +1759,7 @@ class EventChecksMixin:
)
subscription.presence_form = BookingCheckPresenceForm(
agenda=self.agenda,
subscription=True,
)
# sort results
if (
@ -2430,6 +2576,7 @@ class AgendaSettings(ManagedAgendaMixin, DetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['show_history'] = settings.SNAPSHOTS_ENABLED
if self.agenda.accept_meetings():
context['meeting_types'] = self.object.iter_meetingtypes()
if self.agenda.kind == 'virtual':
@ -4052,6 +4199,60 @@ class TimePeriodExceptionSourceRefreshView(ManagedTimePeriodExceptionMixin, Deta
time_period_exception_source_refresh = TimePeriodExceptionSourceRefreshView.as_view()
class AgendaInspectView(ManagedAgendaMixin, DetailView):
template_name = 'chrono/manager_agenda_inspect.html'
model = Agenda
def set_agenda(self, **kwargs):
self.agenda = get_object_or_404(
Agenda.objects.select_related(
'category', 'events_type', 'edit_role', 'view_role'
).prefetch_related(
'resources',
Prefetch(
'desk_set',
queryset=Desk.objects.prefetch_related(
'timeperiod_set',
'timeperiodexceptionsource_set',
'unavailability_calendars',
Prefetch(
'timeperiodexception_set',
queryset=TimePeriodException.objects.filter(source__isnull=True),
),
),
),
Prefetch('event_set', queryset=Event.objects.filter(primary_event__isnull=True)),
),
id=kwargs.get('pk'),
)
def get_object(self):
return self.agenda
agenda_inspect = AgendaInspectView.as_view()
class AgendaHistoryView(ManagedAgendaMixin, InstanceWithSnapshotHistoryView):
template_name = 'chrono/manager_agenda_history.html'
model = AgendaSnapshot
instance_context_key = 'agenda'
agenda_history = AgendaHistoryView.as_view()
class AgendaHistoryCompareView(ManagedAgendaMixin, InstanceWithSnapshotHistoryCompareView):
template_name = 'chrono/manager_agenda_history_compare.html'
inspect_template_name = 'chrono/manager_agenda_inspect_fragment.html'
model = Agenda
instance_context_key = 'agenda'
history_view = 'chrono-manager-agenda-history'
agenda_history_compare = AgendaHistoryCompareView.as_view()
class BookingCancelView(ViewableAgendaMixin, UpdateView):
template_name = 'chrono/manager_confirm_booking_cancellation.html'
model = Booking
@ -4560,6 +4761,7 @@ class UnavailabilityCalendarSettings(ManagedUnavailabilityCalendarMixin, DetailV
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['unavailability_calendar'] = self.object
context['show_history'] = settings.SNAPSHOTS_ENABLED
return context
@ -4669,6 +4871,45 @@ class UnavailabilityCalendarImportUnavailabilitiesView(ManagedUnavailabilityCale
unavailability_calendar_import_unavailabilities = UnavailabilityCalendarImportUnavailabilitiesView.as_view()
class UnavailabilityCalendarInspectView(ManagedUnavailabilityCalendarMixin, DetailView):
template_name = 'chrono/manager_unavailability_calendar_inspect.html'
model = UnavailabilityCalendar
context_object_name = 'unavailability_calendar'
def set_unavailability_calendar(self, **kwargs):
self.unavailability_calendar = get_object_or_404(
UnavailabilityCalendar.objects.select_related('edit_role', 'view_role'), pk=kwargs.get('pk')
)
def get_object(self):
return self.unavailability_calendar
unavailability_calendar_inspect = UnavailabilityCalendarInspectView.as_view()
class UnavailabilityCalendarHistoryView(ManagedUnavailabilityCalendarMixin, InstanceWithSnapshotHistoryView):
template_name = 'chrono/manager_unavailability_calendar_history.html'
model = UnavailabilityCalendarSnapshot
instance_context_key = 'unavailability_calendar'
unavailability_calendar_history = UnavailabilityCalendarHistoryView.as_view()
class UnavailabilityCalendarHistoryCompareView(
ManagedUnavailabilityCalendarMixin, InstanceWithSnapshotHistoryCompareView
):
template_name = 'chrono/manager_unavailability_calendar_history_compare.html'
inspect_template_name = 'chrono/manager_unavailability_calendar_inspect_fragment.html'
model = UnavailabilityCalendar
instance_context_key = 'unavailability_calendar'
history_view = 'chrono-manager-unavailability-calendar-history'
unavailability_calendar_history_compare = UnavailabilityCalendarHistoryCompareView.as_view()
class SharedCustodyAgendaMixin:
agenda = None
tab_anchor = None

View File

@ -206,6 +206,7 @@ REST_FRAMEWORK = {'EXCEPTION_HANDLER': 'chrono.api.utils.exception_handler'}
SHARED_CUSTODY_ENABLED = False
PARTIAL_BOOKINGS_ENABLED = False
SNAPSHOTS_ENABLED = False
CHRONO_ANTS_HUB_URL = None

View File

@ -62,6 +62,7 @@ class CheckType:
slug: str
label: str
kind: str
unexpected_presence: bool = False
def get_agenda_check_types(agenda):
@ -73,5 +74,12 @@ def get_agenda_check_types(agenda):
check_types = []
for ct in result['data']:
check_types.append(CheckType(slug=ct['id'], label=ct['text'], kind=ct['kind']))
check_types.append(
CheckType(
slug=ct['id'],
label=ct['text'],
kind=ct['kind'],
unexpected_presence=ct.get('unexpected_presence') or False,
)
)
return check_types

2
debian/control vendored
View File

@ -14,7 +14,9 @@ Package: python3-chrono
Architecture: all
Depends: python3-django (>= 2:3.2),
python3-gadjo,
python3-lxml,
python3-publik-django-templatetags,
python3-pyquery,
python3-requests,
python3-uwsgidecorators,
${misc:Depends},

View File

@ -1,6 +1,7 @@
[MASTER]
persistent=yes
ignore=vendor,Bouncers,ezt.py
extension-pkg-allow-list=lxml
[MESSAGES CONTROL]
disable=

View File

@ -165,10 +165,12 @@ setup(
'django-filter<23.2',
'vobject',
'python-dateutil',
'pyquery',
'requests',
'workalendar',
'weasyprint',
'sorl-thumbnail',
'lxml',
],
zip_safe=False,
cmdclass={

View File

@ -13,8 +13,11 @@ from chrono.apps.export_import.models import Application, ApplicationElement
pytestmark = pytest.mark.django_db
def test_object_types(app, user):
def test_object_types(app, user, admin_user):
app.authorization = ('Basic', ('john.doe', 'password'))
app.get('/api/export-import/', status=403)
app.authorization = ('Basic', ('admin', 'admin'))
resp = app.get('/api/export-import/')
assert resp.json == {
'data': [
@ -63,8 +66,8 @@ def test_object_types(app, user):
}
def test_list(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
def test_list(app, admin_user):
app.authorization = ('Basic', ('admin', 'admin'))
Agenda.objects.create(label='Rdv', slug='rdv', kind='meetings')
Agenda.objects.create(label='Event', slug='event', kind='events')
Category.objects.create(slug='cat', label='Category')
@ -163,8 +166,8 @@ def test_list(app, user):
}
def test_export_agenda(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
def test_export_agenda(app, admin_user):
app.authorization = ('Basic', ('admin', 'admin'))
group1 = Group.objects.create(name='group1')
group2 = Group.objects.create(name='group2')
Agenda.objects.create(label='Rdv', slug='rdv', kind='meetings', edit_role=group1, view_role=group2)
@ -173,8 +176,8 @@ def test_export_agenda(app, user):
assert resp.json['data']['permissions'] == {'view': 'group2', 'edit': 'group1'}
def test_export_minor_components(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
def test_export_minor_components(app, admin_user):
app.authorization = ('Basic', ('admin', 'admin'))
Category.objects.create(slug='cat', label='Category')
Resource.objects.create(slug='foo', label='Foo')
EventsType.objects.create(slug='foo', label='Foo')
@ -189,9 +192,12 @@ def test_export_minor_components(app, user):
resp = app.get('/api/export-import/unavailability_calendars/foo/')
assert resp.json['data']['label'] == 'Foo'
# unknown component
app.get('/api/export-import/agendas/foo/', status=404)
def test_agenda_dependencies_category(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
def test_agenda_dependencies_category(app, admin_user):
app.authorization = ('Basic', ('admin', 'admin'))
category = Category.objects.create(slug='cat', label='Category')
Agenda.objects.create(label='Rdv', slug='rdv', kind='meetings', category=category)
resp = app.get('/api/export-import/agendas/rdv/dependencies/')
@ -212,8 +218,8 @@ def test_agenda_dependencies_category(app, user):
}
def test_agenda_dependencies_resources(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
def test_agenda_dependencies_resources(app, admin_user):
app.authorization = ('Basic', ('admin', 'admin'))
meetings_agenda = Agenda.objects.create(label='Rdv', slug='rdv', kind='meetings')
meetings_agenda.resources.add(Resource.objects.create(slug='foo', label='Foo'))
resp = app.get('/api/export-import/agendas/rdv/dependencies/')
@ -234,11 +240,12 @@ def test_agenda_dependencies_resources(app, user):
}
def test_agenda_dependencies_unavailability_calendars(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
def test_agenda_dependencies_unavailability_calendars(app, admin_user):
app.authorization = ('Basic', ('admin', 'admin'))
meetings_agenda = Agenda.objects.create(label='Rdv', slug='rdv', kind='meetings')
desk = Desk.objects.create(slug='foo', label='Foo', agenda=meetings_agenda)
desk.unavailability_calendars.add(UnavailabilityCalendar.objects.create(slug='foo', label='Foo'))
unavailability_calendar = UnavailabilityCalendar.objects.create(slug='foo', label='Foo')
desk.unavailability_calendars.add(unavailability_calendar)
resp = app.get('/api/export-import/agendas/rdv/dependencies/')
assert resp.json == {
'data': [
@ -255,10 +262,29 @@ def test_agenda_dependencies_unavailability_calendars(app, user):
],
'err': 0,
}
events_agenda = Agenda.objects.create(label='Evt', slug='evt', kind='events')
desk = Desk.objects.create(agenda=events_agenda, slug='_exceptions_holder')
desk.unavailability_calendars.add(unavailability_calendar)
resp = app.get('/api/export-import/agendas/evt/dependencies/')
assert resp.json == {
'data': [
{
'id': 'foo',
'text': 'Foo',
'type': 'unavailability_calendars',
'urls': {
'dependencies': 'http://testserver/api/export-import/unavailability_calendars/foo/dependencies/',
'export': 'http://testserver/api/export-import/unavailability_calendars/foo/',
'redirect': 'http://testserver/api/export-import/unavailability_calendars/foo/redirect/',
},
}
],
'err': 0,
}
def test_agenda_dependencies_groups(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
def test_agenda_dependencies_groups(app, admin_user):
app.authorization = ('Basic', ('admin', 'admin'))
group1 = Group.objects.create(name='group1')
group2 = Group.objects.create(name='group2')
Agenda.objects.create(label='Rdv', slug='rdv', kind='meetings', edit_role=group1, view_role=group2)
@ -274,8 +300,8 @@ def test_agenda_dependencies_groups(app, user):
}
def test_agenda_dependencies_virtual_agendas(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
def test_agenda_dependencies_virtual_agendas(app, admin_user):
app.authorization = ('Basic', ('admin', 'admin'))
rdv1 = Agenda.objects.create(label='Rdv1', slug='rdv1', kind='meetings')
rdv2 = Agenda.objects.create(label='Rdv2', slug='rdv2', kind='meetings')
virt = Agenda.objects.create(label='Virt', slug='virt', kind='virtual')
@ -309,10 +335,11 @@ def test_agenda_dependencies_virtual_agendas(app, user):
}
def test_agenda_dependencies_events_type(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
def test_agenda_dependencies_events_type(app, admin_user):
app.authorization = ('Basic', ('admin', 'admin'))
events_type = EventsType.objects.create(slug='foo', label='Foo')
Agenda.objects.create(label='Evt', slug='evt', kind='events', events_type=events_type)
events_agenda = Agenda.objects.create(label='Evt', slug='evt', kind='events', events_type=events_type)
Desk.objects.create(agenda=events_agenda, slug='_exceptions_holder')
resp = app.get('/api/export-import/agendas/evt/dependencies/')
assert resp.json == {
'data': [
@ -331,8 +358,13 @@ def test_agenda_dependencies_events_type(app, user):
}
def test_unknown_compoment_dependencies(app, admin_user):
app.authorization = ('Basic', ('admin', 'admin'))
app.get('/api/export-import/agendas/foo/dependencies/', status=404)
def test_redirect(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
app.authorization = ('Basic', ('john', 'doe'))
agenda = Agenda.objects.create(label='Rdv', slug='rdv', kind='meetings')
category = Category.objects.create(slug='cat', label='Category')
resource = Resource.objects.create(slug='foo', label='Foo')
@ -360,8 +392,8 @@ def test_redirect(app, user):
assert resp.location == f'/manage/unavailability-calendar/{unavailability_calendar.pk}/'
def create_bundle(app, user, visible=True, version_number='42.0'):
app.authorization = ('Basic', ('john.doe', 'password'))
def create_bundle(app, admin_user, visible=True, version_number='42.0'):
app.authorization = ('Basic', ('admin', 'admin'))
group, _ = Group.objects.get_or_create(name='plop')
category, _ = Category.objects.get_or_create(slug='foo', label='Foo')
@ -446,12 +478,12 @@ def bundle(app, user):
return create_bundle(app, user)
def test_bundle_import(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
def test_bundle_import(app, admin_user):
app.authorization = ('Basic', ('admin', 'admin'))
bundles = []
for version_number in ['42.0', '42.1']:
bundles.append(create_bundle(app, user, version_number=version_number))
bundles.append(create_bundle(app, admin_user, version_number=version_number))
Agenda.objects.all().delete()
Category.objects.all().delete()
@ -505,10 +537,10 @@ def test_bundle_import(app, user):
)
def test_bundle_declare(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
def test_bundle_declare(app, admin_user):
app.authorization = ('Basic', ('admin', 'admin'))
bundle = create_bundle(app, user, visible=False)
bundle = create_bundle(app, admin_user, visible=False)
resp = app.put('/api/export-import/bundle-declare/', bundle)
assert Agenda.objects.all().count() == 4
assert resp.json['err'] == 0
@ -525,7 +557,7 @@ def test_bundle_declare(app, user):
assert application.visible is False
assert ApplicationElement.objects.count() == 8
bundle = create_bundle(app, user, visible=True)
bundle = create_bundle(app, admin_user, visible=True)
# create link to element not present in manifest: it should be unlinked
last_page = Agenda.objects.latest('pk')
ApplicationElement.objects.create(
@ -543,8 +575,8 @@ def test_bundle_declare(app, user):
assert ApplicationElement.objects.count() == 4 # category, events_type, unavailability_calendar, resource
def test_bundle_unlink(app, user, bundle):
app.authorization = ('Basic', ('john.doe', 'password'))
def test_bundle_unlink(app, admin_user, bundle):
app.authorization = ('Basic', ('admin', 'admin'))
application = Application.objects.create(
name='Test',
@ -598,6 +630,6 @@ def test_bundle_unlink(app, user, bundle):
assert ApplicationElement.objects.count() == 2
def test_bundle_check(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
def test_bundle_check(app, admin_user):
app.authorization = ('Basic', ('admin', 'admin'))
assert app.put('/api/export-import/bundle-check/').json == {'err': 0, 'data': {}}

View File

@ -13,14 +13,17 @@ from django.test.utils import CaptureQueriesContext
from chrono.agendas.models import (
Agenda,
AgendaNotificationsSettings,
AgendaReminderSettings,
Booking,
Desk,
Event,
EventsType,
MeetingType,
Resource,
TimePeriod,
TimePeriodException,
TimePeriodExceptionSource,
UnavailabilityCalendar,
VirtualMember,
)
@ -478,13 +481,15 @@ def test_add_agenda_and_set_role(app, admin_user, manager_user):
resp = resp.form.submit().follow()
assert 'Edit Role: Managers' in resp.text
assert AgendaSnapshot.objects.count() == 2
snapshot = AgendaSnapshot.objects.latest('pk')
assert snapshot.serialization['permissions'] == {'edit': 'Managers', 'view': None}
# still only one desk
assert agenda.desk_set.count() == 1
def test_agenda_set_role_with_partial_booking(settings, app, admin_user):
settings.PARTIAL_BOOKING_ENABLED = True
settings.PARTIAL_BOOKINGS_ENABLED = True
group = Group.objects.create(name='testgroup')
agenda = Agenda.objects.create(label='Foobar')
@ -798,6 +803,78 @@ def test_options_agenda_as_manager(app, manager_user):
assert '<h2>Settings' in resp.text
def test_inspect_agenda(app, admin_user):
meetings_agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
meetings_agenda.resources.add(Resource.objects.create(slug='foo', label='Foo'))
desk = Desk.objects.create(slug='foo', label='Foo', agenda=meetings_agenda)
unavailability_calendar = UnavailabilityCalendar.objects.create(slug='foo', label='Foo')
desk.unavailability_calendars.add(unavailability_calendar)
MeetingType.objects.create(agenda=meetings_agenda, label='Meeting Type', duration=30)
tpx_start = make_aware(datetime.datetime(2017, 5, 22, 8, 0))
tpx_end = make_aware(datetime.datetime(2017, 5, 22, 12, 30))
TimePeriodException.objects.create(desk=desk, start_datetime=tpx_start, end_datetime=tpx_end)
TimePeriod.objects.create(
desk=desk, weekday=2, start_time=tpx_start.time(), end_time=tpx_end.time(), weekday_indexes=[1, 3]
)
TimePeriod.objects.create(
desk=desk, date=datetime.date(2022, 10, 24), start_time=tpx_start.time(), end_time=tpx_end.time()
)
TimePeriodExceptionSource.objects.create(desk=desk, ics_url='http://example.com/sample.ics')
AgendaNotificationsSettings.objects.create(
agenda=meetings_agenda,
full_event=AgendaNotificationsSettings.EMAIL_FIELD,
full_event_emails=['hop@entrouvert.com', 'top@entrouvert.com'],
)
AgendaReminderSettings.objects.create(agenda=meetings_agenda, days_before_email=1, email_extra_info='top')
events_agenda = Agenda.objects.create(label='Events', kind='events')
Event.objects.create(
agenda=events_agenda, start_datetime=make_aware(datetime.datetime(2020, 7, 21, 16, 42, 35)), places=10
)
exceptions_desk = Desk.objects.create(agenda=events_agenda, slug='_exceptions_holder')
tpx_start = make_aware(datetime.datetime(2017, 5, 22, 8, 0))
tpx_end = make_aware(datetime.datetime(2017, 5, 22, 12, 30))
TimePeriodException.objects.create(desk=exceptions_desk, start_datetime=tpx_start, end_datetime=tpx_end)
exceptions_desk.unavailability_calendars.add(unavailability_calendar)
virtual_agenda = Agenda.objects.create(label='Virtual', kind='virtual')
VirtualMember.objects.create(virtual_agenda=virtual_agenda, real_agenda=meetings_agenda)
TimePeriod.objects.create(
agenda=virtual_agenda, weekday=1, start_time=datetime.time(10, 0), end_time=datetime.time(11, 0)
)
app = login(app)
resp = app.get('/manage/agendas/%s/settings' % meetings_agenda.pk)
with CaptureQueriesContext(connection) as ctx:
resp = resp.click('Inspect')
assert len(ctx.captured_queries) == 12
resp = app.get('/manage/agendas/%s/settings' % events_agenda.pk)
with CaptureQueriesContext(connection) as ctx:
resp = resp.click('Inspect')
assert len(ctx.captured_queries) == 12
resp = app.get('/manage/agendas/%s/settings' % virtual_agenda.pk)
with CaptureQueriesContext(connection) as ctx:
resp = resp.click('Inspect')
assert len(ctx.captured_queries) == 8
def test_inspect_agenda_as_manager(app, manager_user):
agenda = Agenda.objects.create(slug='foo', label='Foo')
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
app = login(app, username='manager', password='manager')
agenda.view_role = manager_user.groups.all()[0]
agenda.save()
app.get('/manage/agendas/%s/inspect/' % agenda.pk, status=403)
agenda.edit_role = manager_user.groups.all()[0]
agenda.save()
app.get('/manage/agendas/%s/inspect/' % agenda.pk, status=200)
@mock.patch('chrono.agendas.models.Agenda.is_available_for_simple_management')
def test_agenda_options_desk_simple_management(available_mock, app, admin_user):
agenda = Agenda.objects.create(label='Foo bar', kind='meetings')

View File

@ -1,4 +1,6 @@
import pytest
from django.db import connection
from django.test.utils import CaptureQueriesContext
from chrono.agendas.models import Agenda, Category
from chrono.apps.snapshot.models import CategorySnapshot
@ -84,3 +86,14 @@ def test_delete_category_as_manager(app, manager_user):
category = Category.objects.create(label='Foo bar')
app = login(app, username='manager', password='manager')
app.get('/manage/category/%s/delete/' % category.pk, status=403)
def test_inspect_category(app, admin_user):
category = Category.objects.create(label='Foo bar')
app = login(app)
resp = app.get('/manage/category/%s/edit/' % category.pk)
with CaptureQueriesContext(connection) as ctx:
resp = resp.click('Inspect')
assert len(ctx.captured_queries) == 3

View File

@ -1026,7 +1026,7 @@ def test_import_events(app, admin_user):
)
with CaptureQueriesContext(connection) as ctx:
resp = resp.form.submit(status=302)
assert len(ctx.captured_queries) == 32
assert len(ctx.captured_queries) == 31
assert Event.objects.count() == 5
assert set(Event.objects.values_list('slug', flat=True)) == {
'labelb',
@ -2482,11 +2482,12 @@ def test_event_check_booking(check_types, app, admin_user):
check_types.return_value = [
CheckType(slug='foo-reason', label='Foo reason', kind='absence'),
CheckType(slug='bar-reason', label='Bar reason', kind='presence'),
CheckType(slug='bar-reason', label='Bar reason', kind='presence', unexpected_presence=True),
]
resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
assert len(resp.pyquery.find('td.booking-actions form.absence select')) == 1
assert len(resp.pyquery.find('td.booking-actions form.presence select')) == 1
assert resp.pyquery.find('td.booking-actions form.presence option:selected').text() == '---------'
# reset
_test_reset()
@ -2833,6 +2834,14 @@ def test_event_check_subscription(check_types, app, admin_user):
resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
assert '/manage/agendas/%s/subscriptions/%s/presence/%s' % (agenda.pk, subscription.pk, event.pk) in resp
assert '/manage/agendas/%s/subscriptions/%s/absence/%s' % (agenda.pk, subscription.pk, event.pk) in resp
assert resp.pyquery.find('td.booking-actions form.presence option:selected').text() == '---------'
check_types.return_value = [
CheckType(slug='foo-reason', label='Foo reason', kind='absence'),
CheckType(slug='bar-reason', label='Bar reason', kind='presence'),
CheckType(slug='baz-reason', label='Baz reason', kind='presence', unexpected_presence=True),
]
resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
assert resp.pyquery.find('td.booking-actions form.presence option:selected').text() == 'Baz reason'
app.post(
'/manage/agendas/%s/subscriptions/%s/presence/%s' % (agenda.pk, subscription.pk, event.pk),
params={'csrfmiddlewaretoken': token, 'check_type': 'bar-reason'},

View File

@ -1,4 +1,6 @@
import pytest
from django.db import connection
from django.test.utils import CaptureQueriesContext
from chrono.agendas.models import Agenda, EventsType
from chrono.apps.snapshot.models import EventsTypeSnapshot
@ -212,3 +214,14 @@ def test_delete_events_type_as_manager(app, manager_user):
events_type = EventsType.objects.create(label='Foo bar')
app = login(app, username='manager', password='manager')
app.get('/manage/events-type/%s/delete/' % events_type.pk, status=403)
def test_inspect_events_type(app, admin_user):
events_type = EventsType.objects.create(label='Foo bar')
app = login(app)
resp = app.get('/manage/events-type/%s/edit/' % events_type.pk)
with CaptureQueriesContext(connection) as ctx:
resp = resp.click('Inspect')
assert len(ctx.captured_queries) == 3

View File

@ -1043,3 +1043,14 @@ def test_resource_today_button(app, admin_user):
resp = app.get('/manage/resource/%s/week/%s/%s/%s/' % (resource.pk, today.year, today.month, today.day))
assert 'Today' not in resp.pyquery('a.active').text()
def test_inspect_resource(app, admin_user):
resource = Resource.objects.create(label='Foo bar')
app = login(app)
resp = app.get('/manage/resource/%s/' % resource.pk)
with CaptureQueriesContext(connection) as ctx:
resp = resp.click('Inspect')
assert len(ctx.captured_queries) == 3

View File

@ -0,0 +1,308 @@
import datetime
import pytest
from django.utils.timezone import now
from chrono.agendas.models import Agenda, Category, Desk, Event, EventsType, Resource, UnavailabilityCalendar
from chrono.apps.snapshot.models import (
AgendaSnapshot,
CategorySnapshot,
EventsTypeSnapshot,
ResourceSnapshot,
UnavailabilityCalendarSnapshot,
)
from tests.utils import login
pytestmark = pytest.mark.django_db
def test_agenda_history(settings, app, admin_user):
agenda = Agenda.objects.create(slug='foo', label='Foo')
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
snapshot1 = agenda.take_snapshot()
Event.objects.create(
agenda=agenda,
places=1,
start_datetime=now() - datetime.timedelta(days=60),
)
agenda.description = 'Foo Bar'
agenda.save()
snapshot2 = agenda.take_snapshot()
snapshot2.application_version = '42.0'
snapshot2.save()
assert AgendaSnapshot.objects.count() == 2
app = login(app)
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
assert 'History' not in resp
settings.SNAPSHOTS_ENABLED = True
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
resp = resp.click('History')
assert [x.attrib['class'] for x in resp.pyquery.find('.snapshots-list tr')] == [
'new-day',
'collapsed',
]
assert '(Version 42.0)' in resp.pyquery('tr:nth-child(1)').text()
for mode in ['json', 'inspect', '']:
resp = app.get(
'/manage/agendas/%s/history/compare/?version1=%s&version2=%s&mode=%s'
% (agenda.pk, snapshot1.pk, snapshot2.pk, mode)
)
assert 'Snapshot (%s)' % (snapshot1.pk) in resp
assert 'Snapshot (%s) - (Version 42.0)' % (snapshot2.pk) in resp
if mode == 'inspect':
assert resp.text.count('<ins>') == 6
assert resp.text.count('<del>') == 0
else:
assert resp.text.count('diff_sub') == 1
assert resp.text.count('diff_add') == 16
assert resp.text.count('diff_chg') == 0
resp = app.get(
'/manage/agendas/%s/history/compare/?version1=%s&version2=%s'
% (agenda.pk, snapshot2.pk, snapshot1.pk)
)
assert 'Snapshot (%s)' % (snapshot1.pk) in resp
assert 'Snapshot (%s) - (Version 42.0)' % (snapshot2.pk) in resp
assert resp.text.count('diff_sub') == 1
assert resp.text.count('diff_add') == 16
assert resp.text.count('diff_chg') == 0
def test_agenda_history_as_manager(app, manager_user):
agenda = Agenda.objects.create(slug='foo', label='Foo')
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
snapshot1 = agenda.take_snapshot()
snapshot2 = agenda.take_snapshot()
app = login(app, username='manager', password='manager')
agenda.view_role = manager_user.groups.all()[0]
agenda.save()
app.get('/manage/agendas/%s/history/' % agenda.pk, status=403)
app.get(
'/manage/agendas/%s/history/compare/?version1=%s&version2=%s'
% (agenda.pk, snapshot2.pk, snapshot1.pk),
status=403,
)
agenda.edit_role = manager_user.groups.all()[0]
agenda.save()
app.get('/manage/agendas/%s/history/' % agenda.pk, status=200)
app.get(
'/manage/agendas/%s/history/compare/?version1=%s&version2=%s'
% (agenda.pk, snapshot2.pk, snapshot1.pk),
status=200,
)
def test_category_history(settings, app, admin_user):
category = Category.objects.create(slug='foo', label='Foo')
snapshot1 = category.take_snapshot()
category.label = 'Bar'
category.save()
snapshot2 = category.take_snapshot()
snapshot2.application_version = '42.0'
snapshot2.save()
assert CategorySnapshot.objects.count() == 2
app = login(app)
resp = app.get('/manage/category/%s/edit/' % category.pk)
assert 'History' not in resp
settings.SNAPSHOTS_ENABLED = True
resp = app.get('/manage/category/%s/edit/' % category.pk)
resp = resp.click('History')
assert [x.attrib['class'] for x in resp.pyquery.find('.snapshots-list tr')] == [
'new-day',
'collapsed',
]
assert '(Version 42.0)' in resp.pyquery('tr:nth-child(1)').text()
for mode in ['json', 'inspect', '']:
resp = app.get(
'/manage/category/%s/history/compare/?version1=%s&version2=%s&mode=%s'
% (category.pk, snapshot1.pk, snapshot2.pk, mode)
)
assert 'Snapshot (%s)' % (snapshot1.pk) in resp
assert 'Snapshot (%s) - (Version 42.0)' % (snapshot2.pk) in resp
if mode == 'inspect':
assert resp.text.count('<ins>') == 1
assert resp.text.count('<del>') == 1
else:
assert resp.text.count('diff_sub') == 0
assert resp.text.count('diff_add') == 0
assert resp.text.count('diff_chg') == 2
resp = app.get(
'/manage/category/%s/history/compare/?version1=%s&version2=%s'
% (category.pk, snapshot2.pk, snapshot1.pk)
)
assert 'Snapshot (%s)' % (snapshot1.pk) in resp
assert 'Snapshot (%s) - (Version 42.0)' % (snapshot2.pk) in resp
assert resp.text.count('diff_sub') == 0
assert resp.text.count('diff_add') == 0
assert resp.text.count('diff_chg') == 2
def test_events_type_history(settings, app, admin_user):
events_type = EventsType.objects.create(slug='foo', label='Foo')
snapshot1 = events_type.take_snapshot()
events_type.label = 'Bar'
events_type.save()
snapshot2 = events_type.take_snapshot()
snapshot2.application_version = '42.0'
snapshot2.save()
assert EventsTypeSnapshot.objects.count() == 2
app = login(app)
resp = app.get('/manage/events-type/%s/edit/' % events_type.pk)
assert 'History' not in resp
settings.SNAPSHOTS_ENABLED = True
resp = app.get('/manage/events-type/%s/edit/' % events_type.pk)
resp = resp.click('History')
assert [x.attrib['class'] for x in resp.pyquery.find('.snapshots-list tr')] == [
'new-day',
'collapsed',
]
assert '(Version 42.0)' in resp.pyquery('tr:nth-child(1)').text()
for mode in ['json', 'inspect', '']:
resp = app.get(
'/manage/events-type/%s/history/compare/?version1=%s&version2=%s&mode=%s'
% (events_type.pk, snapshot1.pk, snapshot2.pk, mode)
)
assert 'Snapshot (%s)' % (snapshot1.pk) in resp
assert 'Snapshot (%s) - (Version 42.0)' % (snapshot2.pk) in resp
if mode == 'inspect':
assert resp.text.count('<ins>') == 1
assert resp.text.count('<del>') == 1
else:
assert resp.text.count('diff_sub') == 0
assert resp.text.count('diff_add') == 0
assert resp.text.count('diff_chg') == 2
resp = app.get(
'/manage/events-type/%s/history/compare/?version1=%s&version2=%s'
% (events_type.pk, snapshot2.pk, snapshot1.pk)
)
assert 'Snapshot (%s)' % (snapshot1.pk) in resp
assert 'Snapshot (%s) - (Version 42.0)' % (snapshot2.pk) in resp
assert resp.text.count('diff_sub') == 0
assert resp.text.count('diff_add') == 0
assert resp.text.count('diff_chg') == 2
def test_resource_history(settings, app, admin_user):
resource = Resource.objects.create(slug='foo', label='Foo')
snapshot1 = resource.take_snapshot()
resource.label = 'Bar'
resource.save()
snapshot2 = resource.take_snapshot()
snapshot2.application_version = '42.0'
snapshot2.save()
assert ResourceSnapshot.objects.count() == 2
app = login(app)
resp = app.get('/manage/resource/%s/' % resource.pk)
assert 'History' not in resp
settings.SNAPSHOTS_ENABLED = True
resp = app.get('/manage/resource/%s/' % resource.pk)
resp = resp.click('History')
assert [x.attrib['class'] for x in resp.pyquery.find('.snapshots-list tr')] == [
'new-day',
'collapsed',
]
assert '(Version 42.0)' in resp.pyquery('tr:nth-child(1)').text()
for mode in ['json', 'inspect', '']:
resp = app.get(
'/manage/resource/%s/history/compare/?version1=%s&version2=%s&mode=%s'
% (resource.pk, snapshot1.pk, snapshot2.pk, mode)
)
assert 'Snapshot (%s)' % (snapshot1.pk) in resp
assert 'Snapshot (%s) - (Version 42.0)' % (snapshot2.pk) in resp
if mode == 'inspect':
assert resp.text.count('<ins>') == 1
assert resp.text.count('<del>') == 1
else:
assert resp.text.count('diff_sub') == 0
assert resp.text.count('diff_add') == 0
assert resp.text.count('diff_chg') == 2
resp = app.get(
'/manage/resource/%s/history/compare/?version1=%s&version2=%s'
% (resource.pk, snapshot2.pk, snapshot1.pk)
)
assert 'Snapshot (%s)' % (snapshot1.pk) in resp
assert 'Snapshot (%s) - (Version 42.0)' % (snapshot2.pk) in resp
assert resp.text.count('diff_sub') == 0
assert resp.text.count('diff_add') == 0
assert resp.text.count('diff_chg') == 2
def test_unavailability_calendar_history(settings, app, admin_user):
unavailability_calendar = UnavailabilityCalendar.objects.create(slug='foo', label='Foo')
snapshot1 = unavailability_calendar.take_snapshot()
unavailability_calendar.label = 'Bar'
unavailability_calendar.save()
snapshot2 = unavailability_calendar.take_snapshot()
snapshot2.application_version = '42.0'
snapshot2.save()
assert UnavailabilityCalendarSnapshot.objects.count() == 2
app = login(app)
resp = app.get('/manage/unavailability-calendar/%s/settings' % unavailability_calendar.pk)
assert 'History' not in resp
settings.SNAPSHOTS_ENABLED = True
resp = app.get('/manage/unavailability-calendar/%s/settings' % unavailability_calendar.pk)
resp = resp.click('History')
assert [x.attrib['class'] for x in resp.pyquery.find('.snapshots-list tr')] == [
'new-day',
'collapsed',
]
assert '(Version 42.0)' in resp.pyquery('tr:nth-child(1)').text()
for mode in ['json', 'inspect', '']:
resp = app.get(
'/manage/unavailability-calendar/%s/history/compare/?version1=%s&version2=%s&mode=%s'
% (unavailability_calendar.pk, snapshot1.pk, snapshot2.pk, mode)
)
assert 'Snapshot (%s)' % (snapshot1.pk) in resp
assert 'Snapshot (%s) - (Version 42.0)' % (snapshot2.pk) in resp
if mode == 'inspect':
assert resp.text.count('<ins>') == 1
assert resp.text.count('<del>') == 1
else:
assert resp.text.count('diff_sub') == 0
assert resp.text.count('diff_add') == 0
assert resp.text.count('diff_chg') == 2
resp = app.get(
'/manage/unavailability-calendar/%s/history/compare/?version1=%s&version2=%s'
% (unavailability_calendar.pk, snapshot2.pk, snapshot1.pk)
)
assert 'Snapshot (%s)' % (snapshot1.pk) in resp
assert 'Snapshot (%s) - (Version 42.0)' % (snapshot2.pk) in resp
assert resp.text.count('diff_sub') == 0
assert resp.text.count('diff_add') == 0
assert resp.text.count('diff_chg') == 2
def test_unavailability_calendar_history_as_manager(app, manager_user):
unavailability_calendar = UnavailabilityCalendar.objects.create(slug='foo', label='Foo')
snapshot1 = unavailability_calendar.take_snapshot()
snapshot2 = unavailability_calendar.take_snapshot()
app = login(app, username='manager', password='manager')
unavailability_calendar.view_role = manager_user.groups.all()[0]
unavailability_calendar.save()
app.get('/manage/unavailability-calendar/%s/history/' % unavailability_calendar.pk, status=403)
app.get(
'/manage/unavailability-calendar/%s/history/compare/?version1=%s&version2=%s'
% (unavailability_calendar.pk, snapshot2.pk, snapshot1.pk),
status=403,
)
unavailability_calendar.edit_role = manager_user.groups.all()[0]
unavailability_calendar.save()
app.get('/manage/unavailability-calendar/%s/history/' % unavailability_calendar.pk, status=200)
app.get(
'/manage/unavailability-calendar/%s/history/compare/?version1=%s&version2=%s'
% (unavailability_calendar.pk, snapshot2.pk, snapshot1.pk),
status=200,
)

View File

@ -3,6 +3,8 @@ import os
from unittest import mock
import pytest
from django.db import connection
from django.test.utils import CaptureQueriesContext
from webtest import Upload
from chrono.agendas.models import (
@ -709,3 +711,32 @@ def test_unavailability_calendar_delete_unavailability_permissions(app, manager_
unavailability_calendar.edit_role = group
unavailability_calendar.save()
app.get(url)
def test_inspect_unavailability_calendar(app, admin_user):
unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar 1')
TimePeriodException.objects.create(
unavailability_calendar=unavailability_calendar,
start_datetime=now() - datetime.timedelta(days=2),
end_datetime=now() - datetime.timedelta(days=1),
)
app = login(app)
resp = app.get('/manage/unavailability-calendar/%s/settings' % unavailability_calendar.pk)
with CaptureQueriesContext(connection) as ctx:
resp = resp.click('Inspect')
assert len(ctx.captured_queries) == 4
def test_inspect_unavailability_calendar_as_manager(app, manager_user):
unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar 1')
app = login(app, username='manager', password='manager')
unavailability_calendar.view_role = manager_user.groups.all()[0]
unavailability_calendar.save()
app.get('/manage/unavailability-calendar/%s/inspect/' % unavailability_calendar.pk, status=403)
unavailability_calendar.edit_role = manager_user.groups.all()[0]
unavailability_calendar.save()
app.get('/manage/unavailability-calendar/%s/inspect/' % unavailability_calendar.pk, status=200)

View File

@ -186,6 +186,7 @@ def test_import_export_events_agenda_options(app):
label='Foo Bar',
kind='events',
default_view='open_events',
anonymize_delay=42,
booking_form_url='{{ eservices_url }}backoffice/submission/inscription-aux-activites/',
minimal_booking_delay_in_working_days=True,
booking_user_block_template='foo bar',
@ -210,6 +211,7 @@ def test_import_export_events_agenda_options(app):
assert Agenda.objects.count() == 1
agenda = Agenda.objects.first()
assert agenda.default_view == 'open_events'
assert agenda.anonymize_delay == 42
assert agenda.booking_form_url == '{{ eservices_url }}backoffice/submission/inscription-aux-activites/'
assert agenda.minimal_booking_delay_in_working_days is True
assert agenda.booking_user_block_template == 'foo bar'
@ -897,6 +899,11 @@ def test_import_export_time_period_exception_source_ics_file(mocked_get):
assert TimePeriodExceptionSource.objects.count() == 1
assert TimePeriodException.objects.count() == 2
payload['agendas'][0]['desks'][0]['exception_sources'][0]['ics_file'] = 'garbage'
with pytest.raises(AgendaImportError) as excinfo:
import_site(payload)
assert '%s' % excinfo.value == 'Bad ics file'
@override_settings(
EXCEPTIONS_SOURCES={

View File

@ -36,6 +36,7 @@ def test_get_weekday_index():
CHECK_TYPES_DATA = [
{'id': 'bar-reason', 'kind': 'presence', 'text': 'Bar reason'},
{'id': 'baz-reason', 'kind': 'presence', 'text': 'Baz reason', 'unexpected_presence': True},
{'id': 'foo-reason', 'kind': 'absence', 'text': 'Foo reason'},
]
@ -92,6 +93,7 @@ def test_get_agenda_check_types():
requests_get.return_value = MockedRequestResponse(content=json.dumps(data))
assert get_agenda_check_types(agenda) == [
CheckType(slug='bar-reason', label='Bar reason', kind='presence'),
CheckType(slug='baz-reason', label='Baz reason', kind='presence', unexpected_presence=True),
CheckType(slug='foo-reason', label='Foo reason', kind='absence'),
]

View File

@ -44,7 +44,7 @@ allowlist_externals =
commands =
./getlasso3.sh
python3 setup.py compile_translations
py.test {env:COVERAGE:} {posargs:tests/}
py.test -v --dist loadfile {env:COVERAGE:} {posargs:tests/}
codestyle: pre-commit run --all-files --show-diff-on-failure
[testenv:pylint]
@ -67,6 +67,7 @@ deps =
psycopg2-binary<2.9
git+https://git.entrouvert.org/publik-django-templatetags.git
responses
lxml
commands =
./getlasso3.sh
pylint: ./pylint.sh chrono/ tests/