Avoir des pages d'inspect (#87751) #220

Merged
lguerin merged 7 commits from wip/87751-inspect into main 2024-03-15 08:32:47 +01:00
39 changed files with 1254 additions and 107 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
)
@ -490,7 +514,6 @@ class Agenda(WithSnapshotMixin, WithApplicationMixin, models.Model):
'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'):
@ -518,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()]
@ -561,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)
@ -625,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)
@ -1761,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),
@ -1828,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)
@ -2035,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)
@ -2072,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
@ -2093,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')),
@ -2661,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
@ -2845,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'
@ -2905,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)
@ -2918,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
@ -3312,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)
@ -3375,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)
@ -3476,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'
@ -3526,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)
@ -3539,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'
@ -3583,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)
@ -3595,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)
@ -3871,8 +4003,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'
@ -3968,7 +4110,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)
@ -3980,6 +4123,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)
@ -3994,7 +4143,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)
@ -4108,6 +4257,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)
@ -4199,7 +4355,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'
@ -4263,6 +4419,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
@ -4271,7 +4434,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
@ -4360,6 +4523,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

@ -79,9 +79,11 @@ class AbstractSnapshot(models.Model):
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]

View File

@ -16,13 +16,17 @@
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
@ -65,7 +69,7 @@ class InstanceWithSnapshotHistoryCompareView(DetailView):
kwargs[self.instance_context_key] = self.object
mode = self.request.GET.get('mode') or 'json'
if mode not in ['json']:
if mode not in ['json', 'inspect']:
raise Http404
snapshot1, snapshot2 = self.get_snapshots()
@ -90,6 +94,65 @@ class InstanceWithSnapshotHistoryCompareView(DetailView):
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)

View File

@ -966,6 +966,33 @@ p.snapshot-description {
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;

View File

@ -3,6 +3,10 @@
{% 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 %}

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 "Exception 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,10 +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 %}
<h3>{% trans "Navigation" %}</h3>
<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,9 +20,12 @@
{% else %}
<h2>{% trans "New Category" %}</h2>
{% endif %}
{% if show_history and category %}
{% if category %}
<span class="actions">
<a href="{% url 'chrono-manager-category-history' pk=category.pk %}">{% trans 'History' %}</a>
<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 %}

View File

@ -3,6 +3,10 @@
{% 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 %}

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

View File

@ -20,9 +20,12 @@
{% else %}
<h2>{% trans "New events type" %}</h2>
{% endif %}
{% if show_history and object.pk %}
{% if object.pk %}
<span class="actions">
<a href="{% url 'chrono-manager-events-type-history' pk=object.pk %}">{% trans 'History' %}</a>
<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 %}

View File

@ -3,6 +3,10 @@
{% 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 %}

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,8 +63,9 @@
<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 %}
<h3>{% trans "Navigation" %}</h3>
<a class="button button-paragraph" href="{% url 'chrono-manager-resource-history' pk=resource.pk %}">{% trans 'History' %}</a>
{% endif %}

View File

@ -3,6 +3,10 @@
{% 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 %}

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

@ -3,6 +3,10 @@
{% 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 %}

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,10 +54,11 @@
<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 %}
<h3>{% trans "Navigation" %}</h3>
<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' %}

View File

@ -65,6 +65,11 @@ 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,
@ -100,6 +105,7 @@ 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/',
@ -110,6 +116,7 @@ urlpatterns = [
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/',
@ -124,6 +131,9 @@ 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'
),
@ -479,6 +489,7 @@ 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/',

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
@ -800,6 +800,19 @@ 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
@ -816,6 +829,7 @@ 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'
@ -918,6 +932,19 @@ 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
@ -934,6 +961,7 @@ 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'
@ -1092,6 +1120,20 @@ 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
@ -1108,6 +1150,7 @@ 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'
@ -1353,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
@ -4154,31 +4198,56 @@ class TimePeriodExceptionSourceRefreshView(ManagedTimePeriodExceptionMixin, Deta
time_period_exception_source_refresh = TimePeriodExceptionSourceRefreshView.as_view()
class AgendaHistoryView(InstanceWithSnapshotHistoryView):
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'
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
agenda_history = AgendaHistoryView.as_view()
class AgendaHistoryCompareView(InstanceWithSnapshotHistoryCompareView):
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'
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
agenda_history_compare = AgendaHistoryCompareView.as_view()
@ -4801,31 +4870,41 @@ class UnavailabilityCalendarImportUnavailabilitiesView(ManagedUnavailabilityCale
unavailability_calendar_import_unavailabilities = UnavailabilityCalendarImportUnavailabilitiesView.as_view()
class UnavailabilityCalendarHistoryView(InstanceWithSnapshotHistoryView):
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'
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
unavailability_calendar_history = UnavailabilityCalendarHistoryView.as_view()
class UnavailabilityCalendarHistoryCompareView(InstanceWithSnapshotHistoryCompareView):
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'
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
unavailability_calendar_history_compare = UnavailabilityCalendarHistoryCompareView.as_view()

1
debian/control vendored
View File

@ -14,6 +14,7 @@ Package: python3-chrono
Architecture: all
Depends: python3-django (>= 2:3.2),
python3-gadjo,
python3-lxml,
python3-publik-django-templatetags,
python3-requests,
python3-uwsgidecorators,

View File

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

View File

@ -169,6 +169,7 @@ setup(
'workalendar',
'weasyprint',
'sorl-thumbnail',
'lxml',
],
zip_safe=False,
cmdclass={

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,6 +481,8 @@ 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
@ -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',

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

@ -44,15 +44,20 @@ def test_agenda_history(settings, app, admin_user):
]
assert '(Version 42.0)' in resp.pyquery('tr:nth-child(1)').text()
resp = app.get(
'/manage/agendas/%s/history/compare/?version1=%s&version2=%s'
% (agenda.pk, snapshot1.pk, snapshot2.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
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)
@ -64,6 +69,32 @@ def test_agenda_history(settings, app, admin_user):
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()
@ -86,15 +117,20 @@ def test_category_history(settings, app, admin_user):
]
assert '(Version 42.0)' in resp.pyquery('tr:nth-child(1)').text()
resp = app.get(
'/manage/category/%s/history/compare/?version1=%s&version2=%s'
% (category.pk, snapshot1.pk, snapshot2.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
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)
@ -128,15 +164,20 @@ def test_events_type_history(settings, app, admin_user):
]
assert '(Version 42.0)' in resp.pyquery('tr:nth-child(1)').text()
resp = app.get(
'/manage/events-type/%s/history/compare/?version1=%s&version2=%s'
% (events_type.pk, snapshot1.pk, snapshot2.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
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)
@ -170,15 +211,20 @@ def test_resource_history(settings, app, admin_user):
]
assert '(Version 42.0)' in resp.pyquery('tr:nth-child(1)').text()
resp = app.get(
'/manage/resource/%s/history/compare/?version1=%s&version2=%s'
% (resource.pk, snapshot1.pk, snapshot2.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
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)
@ -212,15 +258,20 @@ def test_unavailability_calendar_history(settings, app, admin_user):
]
assert '(Version 42.0)' in resp.pyquery('tr:nth-child(1)').text()
resp = app.get(
'/manage/unavailability-calendar/%s/history/compare/?version1=%s&version2=%s'
% (unavailability_calendar.pk, snapshot1.pk, snapshot2.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
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)
@ -230,3 +281,28 @@ def test_unavailability_calendar_history(settings, app, admin_user):
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

@ -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/}
Review

Pour mémoire ça a été vu par jabber; sans ça il y a un micmac de timezone en exécution multiprocess, avec une fixture modifiant la timezone dans un process qui influence la timezone dans un autre.

Pour mémoire ça a été vu par jabber; sans ça il y a un micmac de timezone en exécution multiprocess, avec une fixture modifiant la timezone dans un process qui influence la timezone dans un autre.
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/