Compare commits

..

4 Commits

Author SHA1 Message Date
Valentin Deniaud 0a25fc4d9a add text info corresponding to color
gitea/chrono/pipeline/head This commit looks good Details
2024-03-13 11:30:15 +01:00
Valentin Deniaud 337f0a682f fix today 2024-03-13 10:48:06 +01:00
Valentin Deniaud c6c942dedd fix tests 2024-03-13 10:36:04 +01:00
Thomas Jund df02ae1d1e manager: improve html & CSS of partial booking month view (#79863)
gitea/chrono/pipeline/head There was a failure building this commit Details
2024-03-12 17:03:52 +01:00
103 changed files with 390 additions and 5459 deletions

2
Jenkinsfile vendored
View File

@ -6,7 +6,7 @@ pipeline {
stages {
stage('Unit Tests') {
steps {
sh 'NUMPROCESSES=3 tox -rv'
sh 'tox -rv -- --numprocesses 3'
}
post {
always {

View File

@ -9,7 +9,6 @@ recursive-include chrono/api/templates *.html
recursive-include chrono/agendas/templates *.html *.txt
recursive-include chrono/manager/templates *.html *.txt
recursive-include chrono/apps/ants_hub/templates *.html
recursive-include chrono/apps/journal/templates *.html
# sql (migrations)
recursive-include chrono/agendas/sql *.sql

View File

@ -17,6 +17,6 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='event',
name='url',
field=models.URLField(blank=True, null=True, verbose_name='URL'),
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='URL'),
),
]

View File

@ -10,6 +10,6 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='agenda',
name='desk_simple_management',
field=models.BooleanField(default=False, verbose_name='Global desk management'),
field=models.BooleanField(default=False),
),
]

View File

@ -27,9 +27,8 @@ import sys
import uuid
from contextlib import contextmanager
import icalendar
import recurring_ical_events
import requests
import vobject
from dateutil.relativedelta import SU, relativedelta
from dateutil.rrule import DAILY, WEEKLY, rrule, rruleset
from django.conf import settings
@ -63,14 +62,13 @@ from django.template import (
VariableDoesNotExist,
engines,
)
from django.template.defaultfilters import yesno
from django.urls import reverse
from django.utils import functional, timezone
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, linebreaks
from django.utils.html import escape
from django.utils.module_loading import import_string
from django.utils.safestring import mark_safe
from django.utils.text import slugify
@ -79,7 +77,6 @@ from django.utils.translation import gettext_lazy as _
from django.utils.translation import ngettext, pgettext_lazy
from chrono.apps.export_import.models import WithApplicationMixin
from chrono.apps.journal.utils import audit
from chrono.apps.snapshot.models import (
AgendaSnapshot,
CategorySnapshot,
@ -180,35 +177,12 @@ 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']
)
# pylint: disable=too-many-public-methods
class Agenda(WithSnapshotMixin, WithApplicationMixin, WithInspectMixin, models.Model):
class Agenda(WithSnapshotMixin, WithApplicationMixin, models.Model):
# mark temporarily restored snapshots
snapshot = models.ForeignKey(
AgendaSnapshot, on_delete=models.CASCADE, null=True, related_name='temporary_instance'
@ -276,7 +250,7 @@ class Agenda(WithSnapshotMixin, WithApplicationMixin, WithInspectMixin, models.M
booking_form_url = models.CharField(
_('Booking form URL'), max_length=200, blank=True, validators=[django_template_validator]
)
desk_simple_management = models.BooleanField(_('Global desk management'), default=False)
desk_simple_management = models.BooleanField(default=False)
mark_event_checked_auto = models.BooleanField(
_('Automatically mark event as checked when all bookings have been checked'), default=False
)
@ -516,6 +490,7 @@ class Agenda(WithSnapshotMixin, WithApplicationMixin, WithInspectMixin, models.M
'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'):
@ -543,7 +518,6 @@ class Agenda(WithSnapshotMixin, WithApplicationMixin, WithInspectMixin, models.M
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()]
@ -567,8 +541,6 @@ class Agenda(WithSnapshotMixin, WithApplicationMixin, WithInspectMixin, models.M
for permission in ('view', 'edit'):
if permissions.get(permission):
data[permission + '_role'] = Group.objects.get(name=permissions[permission])
if permissions.get('admin'):
data['edit_role'] = Group.objects.get(name=permissions['admin'])
resources_slug = data.pop('resources', [])
resources_by_slug = {r.slug: r for r in Resource.objects.filter(slug__in=resources_slug)}
for resource_slug in resources_slug:
@ -589,8 +561,7 @@ class Agenda(WithSnapshotMixin, WithApplicationMixin, WithInspectMixin, models.M
slug = data.pop('slug')
qs_kwargs = {}
if snapshot:
qs_kwargs = {'snapshot': snapshot} # don't take slug from snapshot: it has to be unique !
data['slug'] = str(uuid.uuid4()) # random slug
qs_kwargs = {'snapshot': snapshot}
else:
qs_kwargs = {'slug': slug}
agenda, created = cls.objects.update_or_create(defaults=data, **qs_kwargs)
@ -605,7 +576,7 @@ class Agenda(WithSnapshotMixin, WithApplicationMixin, WithInspectMixin, models.M
AgendaNotificationsSettings.objects.filter(agenda=agenda).delete()
for event_data in events:
event_data['agenda'] = agenda
Event.import_json(event_data, snapshot=snapshot)
Event.import_json(event_data)
if notifications_settings:
notifications_settings['agenda'] = agenda
AgendaNotificationsSettings.import_json(notifications_settings)
@ -654,62 +625,6 @@ class Agenda(WithSnapshotMixin, WithApplicationMixin, WithInspectMixin, models.M
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)
@ -1175,9 +1090,6 @@ class Agenda(WithSnapshotMixin, WithApplicationMixin, WithInspectMixin, models.M
event_queryset = Agenda.filter_for_guardian(
event_queryset, guardian_external_id, user_external_id
)
event_queryset = event_queryset.prefetch_related(
Prefetch('primary_event', queryset=Event.objects.all().order_by())
)
return qs.filter(kind='events').prefetch_related(
Prefetch(
@ -1849,7 +1761,7 @@ WEEK_CHOICES = [
]
class TimePeriod(WithInspectMixin, models.Model):
class TimePeriod(models.Model):
weekday = models.IntegerField(_('Week day'), choices=WEEKDAYS_LIST, null=True)
weekday_indexes = ArrayField(
models.IntegerField(choices=WEEK_CHOICES),
@ -1916,15 +1828,6 @@ class TimePeriod(WithInspectMixin, 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)
@ -2132,7 +2035,7 @@ class SharedTimePeriod:
start_datetime += datetime.timedelta(days=7)
class MeetingType(WithInspectMixin, models.Model):
class MeetingType(models.Model):
agenda = models.ForeignKey(Agenda, on_delete=models.CASCADE)
label = models.CharField(_('Label'), max_length=150)
slug = models.SlugField(_('Identifier'), max_length=160)
@ -2169,13 +2072,6 @@ class MeetingType(WithInspectMixin, 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
@ -2197,7 +2093,7 @@ class MeetingType(WithInspectMixin, models.Model):
)
class Event(WithInspectMixin, models.Model):
class Event(models.Model):
id = models.BigAutoField(primary_key=True)
INTERVAL_CHOICES = [
(1, _('Every week')),
@ -2238,7 +2134,7 @@ class Event(WithInspectMixin, models.Model):
_('Description'), null=True, blank=True, help_text=_('Optional event description.')
)
pricing = models.CharField(_('Pricing'), max_length=150, null=True, blank=True)
url = models.URLField(_('URL'), max_length=200, null=True, blank=True)
url = models.CharField(_('URL'), max_length=200, null=True, blank=True)
booked_places = models.PositiveSmallIntegerField(default=0)
booked_waiting_list_places = models.PositiveSmallIntegerField(default=0)
almost_full = models.BooleanField(default=False)
@ -2280,12 +2176,6 @@ class Event(WithInspectMixin, models.Model):
return self.label
return date_format(localtime(self.start_datetime), format='DATETIME_FORMAT')
def get_journal_label(self):
date_str = date_format(localtime(self.start_datetime), format='SHORT_DATETIME_FORMAT')
if self.label:
return f'{self.label} ({date_str})'
return date_str
@functional.cached_property
def cancellation_status(self):
if self.cancelled:
@ -2505,15 +2395,13 @@ class Event(WithInspectMixin, models.Model):
'booking',
filter=Q(
booking__cancellation_datetime__isnull=True,
booking__in_waiting_list=False,
)
& ~Q(booking__lease__lock_code=lock_code),
),
unlocked_booked_waiting_list_places=Count(
'booking',
filter=Q(
booking__cancellation_datetime__isnull=True,
booking__in_waiting_list=True,
booking__cancellation_datetime__isnull=False,
)
& ~Q(booking__lease__lock_code=lock_code),
),
@ -2705,7 +2593,7 @@ class Event(WithInspectMixin, models.Model):
return
@classmethod
def import_json(cls, data, snapshot=None):
def import_json(cls, data):
try:
data['start_datetime'] = make_aware(
datetime.datetime.strptime(data['start_datetime'], '%Y-%m-%d %H:%M:%S')
@ -2713,12 +2601,6 @@ class Event(WithInspectMixin, models.Model):
except ValueError:
raise AgendaImportError(_('Bad datetime format "%s"') % data['start_datetime'])
if data.get('end_time'):
try:
data['end_time'] = datetime.datetime.strptime(data['end_time'], '%H:%M').time()
except ValueError:
raise AgendaImportError(_('Bad time format "%s"') % data['end_time'])
if data.get('recurrence_days'):
# keep stable weekday numbering after switch to ISO in db
data['recurrence_days'] = [i + 1 for i in data['recurrence_days']]
@ -2731,14 +2613,13 @@ class Event(WithInspectMixin, models.Model):
else:
event = cls(**data)
event.save()
if snapshot is None and event.recurrence_days:
if event.recurrence_days:
event.refresh_from_db()
if event.recurrence_end_date:
event.recurrences.filter(start_datetime__gt=event.recurrence_end_date).delete()
update_fields = {
field: getattr(event, field)
for field in [
'end_time',
'label',
'duration',
'publication_datetime',
@ -2747,7 +2628,6 @@ class Event(WithInspectMixin, models.Model):
'description',
'pricing',
'url',
'custom_fields',
]
}
event.recurrences.update(**update_fields)
@ -2759,7 +2639,6 @@ class Event(WithInspectMixin, models.Model):
)
return {
'start_datetime': make_naive(self.start_datetime).strftime('%Y-%m-%d %H:%M:%S'),
'end_time': self.end_time.strftime('%H:%M') if self.end_time else None,
'publication_datetime': make_naive(self.publication_datetime).strftime('%Y-%m-%d %H:%M:%S')
if self.publication_datetime
else None,
@ -2780,26 +2659,8 @@ class Event(WithInspectMixin, models.Model):
'url': self.url,
'pricing': self.pricing,
'duration': self.duration,
'custom_fields': self.get_custom_fields(),
}
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
@ -2984,7 +2845,7 @@ class Event(WithInspectMixin, models.Model):
return custom_fields
class EventsType(WithSnapshotMixin, WithApplicationMixin, WithInspectMixin, models.Model):
class EventsType(WithSnapshotMixin, WithApplicationMixin, models.Model):
# mark temporarily restored snapshots
snapshot = models.ForeignKey(
EventsTypeSnapshot, on_delete=models.CASCADE, null=True, related_name='temporary_instance'
@ -3044,8 +2905,7 @@ class EventsType(WithSnapshotMixin, WithApplicationMixin, WithInspectMixin, mode
slug = data.pop('slug')
qs_kwargs = {}
if snapshot:
qs_kwargs = {'snapshot': snapshot} # don't take slug from snapshot: it has to be unique !
data['slug'] = str(uuid.uuid4()) # random slug
qs_kwargs = {'snapshot': snapshot}
else:
qs_kwargs = {'slug': slug}
events_type, created = cls.objects.update_or_create(defaults=data, **qs_kwargs)
@ -3058,9 +2918,6 @@ class EventsType(WithSnapshotMixin, WithApplicationMixin, WithInspectMixin, mode
'custom_fields': self.custom_fields,
}
def get_inspect_keys(self):
return ['label', 'slug']
class BookingColor(models.Model):
COLOR_COUNT = 8
@ -3124,34 +2981,9 @@ class Booking(models.Model):
start_time = models.TimeField(null=True)
end_time = models.TimeField(null=True)
def get_journal_label(self):
parts = [_('ID: %s') % self.id]
if self.user_name:
parts.append(_('user: %s') % self.user_name)
if self.in_waiting_list:
parts.append(_('in waiting list'))
if self.cancellation_datetime:
parts.append(
_('cancelled at %s')
% date_format(localtime(self.cancellation_datetime), format='SHORT_DATETIME_FORMAT')
)
if self.start_time and self.end_time:
parts.append(
'%s%s'
% (
date_format(self.start_time, 'TIME_FORMAT'),
date_format(self.end_time, 'TIME_FORMAT'),
)
)
elif self.start_time:
parts.append('%s → ?' % date_format(self.start_time, 'TIME_FORMAT'))
elif self.end_time:
parts.append('? → %s' % date_format(self.end_time, 'TIME_FORMAT'))
return ' / '.join([str(x) for x in parts])
@property
def user_name(self):
return ('%s %s' % (self.user_first_name or '', self.user_last_name or '')).strip()
return ('%s %s' % (self.user_first_name, self.user_last_name)).strip()
@cached_property
def user_check(self): # pylint: disable=method-hidden
@ -3181,15 +3013,9 @@ class Booking(models.Model):
del self.user_check
return super().refresh_from_db(*args, **kwargs)
def cancel(self, trigger_callback=False, request=None):
def cancel(self, trigger_callback=False):
timestamp = now()
with transaction.atomic():
audit(
'booking:cancel',
request=request,
agenda=self.event.agenda,
extra_data={'booking': self, 'event': self.event},
)
self.secondary_booking_set.update(cancellation_datetime=timestamp)
self.cancellation_datetime = timestamp
self.save()
@ -3209,26 +3035,15 @@ class Booking(models.Model):
self.secondary_booking_set.update(in_waiting_list=True)
self.save()
def reset_user_was_present(self, request=None):
def reset_user_was_present(self):
with transaction.atomic():
audit(
'check:reset',
request=request,
agenda=self.event.agenda,
extra_data={
'event': self.event,
'user_name': self.user_name,
},
)
if self.user_check:
self.user_check.delete()
self.user_check = None
self.event.checked = False
self.event.save(update_fields=['checked'])
def mark_user_absence(
self, check_type_slug=None, check_type_label=None, start_time=None, end_time=None, request=None
):
def mark_user_absence(self, check_type_slug=None, check_type_label=None, start_time=None, end_time=None):
if not self.user_check:
self.user_check = BookingCheck(booking=self)
self.user_check.presence = False
@ -3239,23 +3054,12 @@ class Booking(models.Model):
self.cancellation_datetime = None
with transaction.atomic():
audit(
'check:absence',
request=request,
agenda=self.event.agenda,
extra_data={
'event': self.event,
'user_name': self.user_name,
},
)
self.user_check.save()
self.secondary_booking_set.update(cancellation_datetime=None)
self.save()
self.event.set_is_checked()
def mark_user_presence(
self, check_type_slug=None, check_type_label=None, start_time=None, end_time=None, request=None
):
def mark_user_presence(self, check_type_slug=None, check_type_label=None, start_time=None, end_time=None):
if not self.user_check:
self.user_check = BookingCheck(booking=self)
self.user_check.presence = True
@ -3266,15 +3070,6 @@ class Booking(models.Model):
self.cancellation_datetime = None
with transaction.atomic():
audit(
'check:presence',
request=request,
agenda=self.event.agenda,
extra_data={
'event': self.event,
'user_name': self.user_name,
},
)
self.user_check.save()
self.secondary_booking_set.update(cancellation_datetime=None)
self.save()
@ -3351,44 +3146,41 @@ class Booking(models.Model):
)
def get_vevent_ics(self, request=None):
event = icalendar.Event()
event.add(
'uid',
'%s-%s-%s'
% (
self.event.start_datetime.isoformat(),
self.event.agenda.pk,
self.pk,
),
vevent = vobject.newFromBehavior('vevent')
vevent.add('uid').value = '%s-%s-%s' % (
self.event.start_datetime.isoformat(),
self.event.agenda.pk,
self.pk,
)
event.add('summary', self.user_display_label or self.label)
event.add('dtstart', self.event.start_datetime)
vevent.add('summary').value = self.user_display_label or self.label
vevent.add('dtstart').value = self.event.start_datetime
if self.user_name:
event.add('attendee', self.user_name)
vevent.add('attendee').value = self.user_name
if request is None or request.GET.get('organizer') != 'no':
organizer_name = getattr(settings, 'TEMPLATE_VARS', {}).get('global_title', 'chrono')
organizer_email = getattr(settings, 'TEMPLATE_VARS', {}).get(
'default_from_email', 'chrono@example.net'
)
organizer = icalendar.vCalAddress(f'mailto:{organizer_email}')
organizer.params['cn'] = organizer_name
event.add('organizer', organizer)
organizer = vevent.add('organizer')
organizer.value = f'mailto:{organizer_email}'
organizer.cn_param = organizer_name
if self.event.end_datetime:
event.add('dtend', self.event.end_datetime)
vevent.add('dtend').value = self.event.end_datetime
for field in ('description', 'location', 'comment', 'url'):
field_value = request and request.GET.get(field) or (self.extra_data or {}).get(field)
if field_value:
event.add(field, field_value)
return event
vevent.add(field).value = field_value
return vevent
def get_ics(self, request=None):
cal = icalendar.Calendar()
cal.add('propid', '-//Entr\'ouvert//NON SGML Publik')
cal.add_component(self.get_vevent_ics(request))
return cal.to_ical().decode('utf-8')
ics = vobject.iCalendar()
ics.add('prodid').value = '-//Entr\'ouvert//NON SGML Publik'
vevent = self.get_vevent_ics(request)
ics.add(vevent)
return ics.serialize()
def clone(self, primary_booking=None, save=True):
new_booking = copy.deepcopy(self)
@ -3520,7 +3312,7 @@ class BookingCheck(models.Model):
OpeningHour = collections.namedtuple('OpeningHour', ['begin', 'end'])
class Desk(WithInspectMixin, models.Model):
class Desk(models.Model):
agenda = models.ForeignKey(Agenda, on_delete=models.CASCADE)
label = models.CharField(_('Label'), max_length=150)
slug = models.SlugField(_('Identifier'), max_length=160)
@ -3583,12 +3375,6 @@ class Desk(WithInspectMixin, 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)
@ -3690,7 +3476,7 @@ class Desk(WithInspectMixin, models.Model):
).delete() # source was not in settings anymore
class Resource(WithSnapshotMixin, WithApplicationMixin, WithInspectMixin, models.Model):
class Resource(WithSnapshotMixin, WithApplicationMixin, models.Model):
# mark temporarily restored snapshots
snapshot = models.ForeignKey(
ResourceSnapshot, on_delete=models.CASCADE, null=True, related_name='temporary_instance'
@ -3740,8 +3526,7 @@ class Resource(WithSnapshotMixin, WithApplicationMixin, WithInspectMixin, models
slug = data.pop('slug')
qs_kwargs = {}
if snapshot:
qs_kwargs = {'snapshot': snapshot} # don't take slug from snapshot: it has to be unique !
data['slug'] = str(uuid.uuid4()) # random slug
qs_kwargs = {'snapshot': snapshot}
else:
qs_kwargs = {'slug': slug}
resource, created = cls.objects.update_or_create(defaults=data, **qs_kwargs)
@ -3754,11 +3539,8 @@ class Resource(WithSnapshotMixin, WithApplicationMixin, WithInspectMixin, models
'description': self.description,
}
def get_inspect_keys(self):
return ['label', 'slug', 'description']
class Category(WithSnapshotMixin, WithApplicationMixin, WithInspectMixin, models.Model):
class Category(WithSnapshotMixin, WithApplicationMixin, models.Model):
# mark temporarily restored snapshots
snapshot = models.ForeignKey(
CategorySnapshot, on_delete=models.CASCADE, null=True, related_name='temporary_instance'
@ -3801,8 +3583,7 @@ class Category(WithSnapshotMixin, WithApplicationMixin, WithInspectMixin, models
slug = data.pop('slug')
qs_kwargs = {}
if snapshot:
qs_kwargs = {'snapshot': snapshot} # don't take slug from snapshot: it has to be unique !
data['slug'] = str(uuid.uuid4()) # random slug
qs_kwargs = {'snapshot': snapshot}
else:
qs_kwargs = {'slug': slug}
category, created = cls.objects.update_or_create(defaults=data, **qs_kwargs)
@ -3814,15 +3595,12 @@ class Category(WithSnapshotMixin, WithApplicationMixin, WithInspectMixin, models
'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(WithInspectMixin, models.Model):
class TimePeriodExceptionSource(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)
@ -3937,30 +3715,25 @@ class TimePeriodExceptionSource(WithInspectMixin, models.Model):
data = force_str(self.ics_file.read())
try:
cal = icalendar.Calendar.from_ical(data)
except ValueError:
parsed = vobject.readOne(data)
except vobject.base.ParseError:
raise ICSError(_('File format is invalid.'))
vevents = list(cal.walk('vevent'))
if len(vevents) == 0:
if not parsed.contents.get('vevent'):
raise ICSError(_('The file doesn\'t contain any events.'))
for vevent in vevents:
for vevent in parsed.contents.get('vevent', []):
summary = self._get_summary_from_vevent(vevent)
if 'dtstart' not in vevent:
try:
vevent.dtstart.value
except AttributeError:
raise ICSError(_('Event "%s" has no start date.') % summary)
# with icalendar date parse error lead to None properties
# and then raises an Attribute error when trying to decode it
if vevent['dtstart'] is None:
raise ICSError(_('File format is invalid.'))
if 'dtend' in vevent and vevent['dtend'] is None:
raise ICSError(_('File format is invalid.'))
return cal
return parsed
def _get_summary_from_vevent(self, vevent):
if 'summary' in vevent:
return vevent.decoded('summary').decode('utf-8')
if 'summary' in vevent.contents:
return force_str(vevent.contents['summary'][0].value)
return _('Exception')
def refresh_timeperiod_exceptions(self, data=None):
@ -3989,36 +3762,31 @@ class TimePeriodExceptionSource(WithInspectMixin, models.Model):
self.timeperiodexception_set.all().delete()
# create new exceptions
update_datetime = now()
for vevent in parsed.walk('vevent'):
for vevent in parsed.contents.get('vevent', []):
summary = self._get_summary_from_vevent(vevent)
if 'dtstart' in vevent:
start_dt = vevent.decoded('dtstart')
try:
start_dt = vevent.dtstart.value
if not isinstance(start_dt, datetime.datetime):
start_dt = datetime.datetime.combine(start_dt, datetime.datetime.min.time())
if not is_aware(start_dt):
start_dt = make_aware(start_dt)
else:
# Enforce local timezone to calculate the end of the day
# when no DTEND and no duration in the local tz
start_dt = start_dt.astimezone(timezone.get_current_timezone())
else:
except AttributeError:
raise ICSError(_('Event "%s" has no start date.') % summary)
if 'dtend' in vevent:
end_dt = vevent.decoded('dtend')
try:
end_dt = vevent.dtend.value
if not isinstance(end_dt, datetime.datetime):
end_dt = datetime.datetime.combine(end_dt, datetime.datetime.min.time())
if not is_aware(end_dt):
end_dt = make_aware(end_dt)
else:
duration = end_dt - start_dt
except AttributeError:
try:
duration = vevent.decoded('duration')
duration = vevent.duration.value
end_dt = start_dt + duration
except (KeyError, AttributeError):
# events without end date and with no/invalid duration are
# considered as ending the same day leading in "strange"
# ics files with a DTEND set at 23:59:59.999999 meaning
# that the event ends at 23:59:59.999998 (DTEND is excluded)
except AttributeError:
# events without end date are considered as ending the same day
end_dt = make_aware(datetime.datetime.combine(start_dt, datetime.datetime.max.time()))
duration = end_dt - start_dt
event = {
'start_datetime': start_dt,
@ -4030,34 +3798,29 @@ class TimePeriodExceptionSource(WithInspectMixin, models.Model):
'recurrence_id': 0,
}
if 'categories' in vevent and len(vevent['categories'].cats) > 0:
category = str(vevent['categories'].cats[0])
if 'categories' in vevent.contents and len(vevent.categories.value) > 0:
category = vevent.categories.value[0]
else:
category = None
# Updating vevent to match calculated start & end so the
# recurrence matches what we calculated
vevent.pop('dtstart')
vevent.add('dtstart', start_dt)
if 'duration' in vevent:
vevent.pop('duration')
if 'dtend' in vevent:
vevent.pop('dtend')
vevent.add('dtend', end_dt)
rrule = recurring_ical_events.of(vevent)
if 'rrule' not in vevent:
if not vevent.rruleset:
# classical event
exception = TimePeriodException.objects.create(**event)
if category:
categories[category].append(exception)
elif len(rrule.repetitions) > 0:
elif vevent.rruleset.count():
# recurring event until recurring_days in the future
from_dt = start_dt
until_dt = update_datetime + datetime.timedelta(days=recurring_days)
for i, revent in enumerate(rrule.between(from_dt, until_dt)):
start_dt = revent.decoded('dtstart')
end_dt = revent.decoded('dtend')
if not is_aware(vevent.rruleset[0]):
from_dt = make_naive(from_dt)
until_dt = make_naive(until_dt)
i = -1
for i, start_dt in enumerate(vevent.rruleset.between(from_dt, until_dt, inc=True)):
# recompute start_dt and end_dt from occurrences and duration
if not is_aware(start_dt):
start_dt = make_aware(start_dt)
end_dt = start_dt + duration
event['recurrence_id'] = i
event['start_datetime'] = start_dt
event['end_datetime'] = end_dt
@ -4080,21 +3843,15 @@ class TimePeriodExceptionSource(WithInspectMixin, models.Model):
data = clean_import_data(cls, data)
if data.get('ics_file'):
try:
data['ics_file'] = ContentFile(base64.b64decode(data['ics_file']), name=data['ics_filename'])
except base64.binascii.Error:
raise AgendaImportError(_('Bad ics file'))
elif data.get('ics_filename'):
# filename but no file content, skip this source
return
data['ics_file'] = ContentFile(base64.b64decode(data['ics_file']), name=data['ics_filename'])
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()
@ -4114,18 +3871,8 @@ class TimePeriodExceptionSource(WithInspectMixin, 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, WithInspectMixin, models.Model):
class UnavailabilityCalendar(WithSnapshotMixin, WithApplicationMixin, models.Model):
# mark temporarily restored snapshots
snapshot = models.ForeignKey(
UnavailabilityCalendarSnapshot, on_delete=models.CASCADE, null=True, related_name='temporary_instance'
@ -4221,8 +3968,7 @@ class UnavailabilityCalendar(WithSnapshotMixin, WithApplicationMixin, WithInspec
slug = data.pop('slug')
qs_kwargs = {}
if snapshot:
qs_kwargs = {'snapshot': snapshot} # don't take slug from snapshot: it has to be unique !
data['slug'] = str(uuid.uuid4()) # random slug
qs_kwargs = {'snapshot': snapshot}
else:
qs_kwargs = {'slug': slug}
unavailability_calendar, created = cls.objects.update_or_create(defaults=data, **qs_kwargs)
@ -4234,12 +3980,6 @@ class UnavailabilityCalendar(WithSnapshotMixin, WithApplicationMixin, WithInspec
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)
@ -4254,7 +3994,7 @@ class TimePeriodExceptionGroup(models.Model):
return self.label
class TimePeriodException(WithInspectMixin, models.Model):
class TimePeriodException(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)
@ -4368,13 +4108,6 @@ class TimePeriodException(WithInspectMixin, 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)
@ -4466,7 +4199,7 @@ class NotificationType:
return self.settings._meta.get_field(self.name).verbose_name
class AgendaNotificationsSettings(WithInspectMixin, models.Model):
class AgendaNotificationsSettings(models.Model):
EMAIL_FIELD = 'use-email-field'
VIEW_ROLE = 'view-role'
EDIT_ROLE = 'edit-role'
@ -4530,13 +4263,6 @@ class AgendaNotificationsSettings(WithInspectMixin, 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
@ -4545,7 +4271,7 @@ class AgendaNotificationsSettings(WithInspectMixin, models.Model):
return new_settings
class AgendaReminderSettings(WithInspectMixin, models.Model):
class AgendaReminderSettings(models.Model):
ONE_DAY_BEFORE = 1
TWO_DAYS_BEFORE = 2
THREE_DAYS_BEFORE = 3
@ -4634,14 +4360,6 @@ class AgendaReminderSettings(WithInspectMixin, 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

@ -467,7 +467,6 @@ class DateRangeSerializer(DateRangeMixin, serializers.Serializer):
class DatetimesSerializer(DateRangeSerializer):
min_places = serializers.IntegerField(min_value=1, default=1)
max_places = serializers.IntegerField(min_value=1, default=None)
user_external_id = serializers.CharField(required=False, max_length=250, allow_blank=True)
exclude_user_external_id = serializers.CharField(required=False, max_length=250, allow_blank=True)
events = serializers.CharField(required=False, max_length=32, allow_blank=True)

View File

@ -156,5 +156,4 @@ urlpatterns = [
path('statistics/', views.statistics_list, name='api-statistics-list'),
path('statistics/bookings/', views.bookings_statistics, name='api-statistics-bookings'),
path('ants/', include('chrono.apps.ants_hub.api_urls')),
path('user-preferences/', include('chrono.apps.user_preferences.api_urls')),
]

View File

@ -20,7 +20,7 @@ import datetime
import json
import uuid
import icalendar
import vobject
from django.conf import settings
from django.db import IntegrityError, transaction
from django.db.models import BooleanField, Case, Count, ExpressionWrapper, F, Func, Prefetch, Q, When
@ -57,7 +57,6 @@ from chrono.agendas.models import (
)
from chrono.api import serializers
from chrono.api.utils import APIError, APIErrorBadRequest, Response
from chrono.apps.journal.utils import audit
from chrono.utils.publik_urls import translate_to_publik_url
from chrono.utils.timezone import localtime, make_aware, now
@ -150,7 +149,6 @@ def get_event_places(event):
def is_event_disabled(
event,
min_places=1,
max_places=None,
disable_booked=True,
bookable_events=None,
bypass_delays=False,
@ -172,9 +170,7 @@ def is_event_disabled(
):
# event is out of minimal delay and we don't want to bypass delays
return True
if max_places and max_places <= event.remaining_places:
return True
elif event.remaining_places < min_places and event.remaining_waiting_list_places < min_places:
if event.remaining_places < min_places and event.remaining_waiting_list_places < min_places:
if enable_full_when_booked and getattr(event, 'user_places_count', 0) > 0:
return False
return True
@ -214,7 +210,6 @@ def get_event_detail(
booking=None,
agenda=None,
min_places=1,
max_places=None,
booked_user_external_id=None,
bookable_events=None,
multiple_agendas=False,
@ -269,7 +264,6 @@ def get_event_detail(
'disabled': is_event_disabled(
event,
min_places=min_places,
max_places=max_places,
disable_booked=disable_booked,
bookable_events=bookable_events,
bypass_delays=bypass_delays,
@ -337,11 +331,9 @@ def get_short_event_detail(
details = {
'id': '%s@%s' % (agenda.slug, event.slug) if multiple_agendas else event.slug,
'slug': event.slug, # kept for compatibility
'primary_event': None,
'text': get_event_text(event, agenda),
'label': event.label or '',
'agenda_label': agenda.label,
'agenda_slug': agenda.slug,
'date': format_response_date(event.start_datetime),
'datetime': format_response_datetime(event.start_datetime),
'end_datetime': format_response_datetime(event.end_datetime) if event.end_datetime else '',
@ -353,12 +345,6 @@ def get_short_event_detail(
'check_locked': event.check_locked,
'invoiced': event.invoiced,
}
if event.primary_event:
details['primary_event'] = (
'%s@%s' % (agenda.slug, event.primary_event.slug)
if multiple_agendas
else event.primary_event.slug
)
for key, value in event.get_custom_fields().items():
details['custom_field_%s' % key] = value
return details
@ -369,7 +355,6 @@ def get_events_meta_detail(
events,
agenda=None,
min_places=1,
max_places=0,
bookable_events=None,
multiple_agendas=False,
bypass_delays=False,
@ -380,11 +365,7 @@ def get_events_meta_detail(
for event in events:
bookable_datetimes_number_total += 1
if not is_event_disabled(
event,
min_places=min_places,
max_places=max_places,
bookable_events=bookable_events,
bypass_delays=bypass_delays,
event, min_places=min_places, bookable_events=bookable_events, bypass_delays=bypass_delays
):
bookable_datetimes_number_available += 1
if not first_bookable_slot:
@ -393,7 +374,6 @@ def get_events_meta_detail(
event,
agenda=agenda,
min_places=min_places,
max_places=max_places,
bookable_events=bookable_events,
multiple_agendas=multiple_agendas,
bypass_delays=bypass_delays,
@ -683,7 +663,6 @@ class Datetimes(APIView):
entries = Event.annotate_queryset_for_user(entries, user_external_id)
if lock_code:
entries = Event.annotate_queryset_for_lock_code(entries, lock_code=lock_code)
entries = entries.prefetch_related(Prefetch('primary_event', queryset=Event.objects.all().order_by()))
entries = entries.order_by('start_datetime', 'duration', 'label')
if payload['hide_disabled']:
@ -692,8 +671,7 @@ class Datetimes(APIView):
for e in entries
if not is_event_disabled(
e,
min_places=payload['min_places'],
max_places=payload['max_places'],
payload['min_places'],
disable_booked=disable_booked,
bookable_events=bookable_events,
bypass_delays=payload.get('bypass_delays'),
@ -707,7 +685,6 @@ class Datetimes(APIView):
x,
agenda=agenda,
min_places=payload['min_places'],
max_places=payload['max_places'],
booked_user_external_id=payload.get('user_external_id'),
bookable_events=bookable_events_raw,
disable_booked=disable_booked,
@ -720,7 +697,6 @@ class Datetimes(APIView):
entries,
agenda=agenda,
min_places=payload['min_places'],
max_places=payload['max_places'],
bookable_events=bookable_events_raw,
),
}
@ -767,9 +743,6 @@ class MultipleAgendasDatetimes(APIView):
entries = Event.annotate_queryset_for_user(entries, user_external_id, with_status=with_status)
if lock_code:
Event.annotate_queryset_for_lock_code(entries, lock_code)
entries = entries.prefetch_related(
Prefetch('primary_event', queryset=Event.objects.all().order_by())
)
if check_overlaps:
entries = Event.annotate_queryset_with_overlaps(entries)
@ -818,7 +791,6 @@ class MultipleAgendasDatetimes(APIView):
request,
x,
min_places=payload['min_places'],
max_places=payload['max_places'],
booked_user_external_id=payload.get('user_external_id'),
multiple_agendas=True,
disable_booked=disable_booked,
@ -829,11 +801,7 @@ class MultipleAgendasDatetimes(APIView):
for x in entries
],
'meta': get_events_meta_detail(
request,
entries,
min_places=payload['min_places'],
max_places=payload['max_places'],
multiple_agendas=True,
request, entries, min_places=payload['min_places'], multiple_agendas=True
),
}
return Response(response)
@ -1338,7 +1306,7 @@ class EventsAgendaFillslot(APIView):
if to_cancel_booking:
cancelled_booking_id = to_cancel_booking.pk
to_cancel_booking.cancel(request=request)
to_cancel_booking.cancel()
# now we have a list of events, book them.
primary_booking = None
@ -1351,17 +1319,6 @@ class EventsAgendaFillslot(APIView):
in_waiting_list=in_waiting_list,
)
new_booking.save()
audit(
'booking:create',
request=request,
agenda=event.agenda,
extra_data={
'booking': new_booking,
'event': event,
'primary_booking_id': primary_booking.id if primary_booking else None,
},
)
if lock_code and not confirm_after_lock:
Lease.objects.create(
booking=new_booking,
@ -1586,7 +1543,7 @@ class MeetingsAgendaFillslot(APIView):
).delete()
if to_cancel_booking:
cancelled_booking_id = to_cancel_booking.pk
to_cancel_booking.cancel(request=request)
to_cancel_booking.cancel()
# book event
event.save()
@ -1599,16 +1556,6 @@ class MeetingsAgendaFillslot(APIView):
color=color,
)
booking.save()
audit(
'booking:create',
request=request,
agenda=event.agenda,
extra_data={
'booking': booking,
'event': event,
},
)
if lock_code and not confirm_after_lock:
Lease.objects.create(
booking=booking,
@ -1898,7 +1845,6 @@ class RecurringFillslots(APIView):
min_start=start_datetime,
max_start=end_datetime,
)
events = events.prefetch_related(Prefetch('primary_event', queryset=Event.objects.all().order_by()))
return events
@ -2057,7 +2003,6 @@ class EventsFillslots(APIView):
output_field=BooleanField(),
)
)
events = events.prefetch_related(Prefetch('primary_event', queryset=Event.objects.all().order_by()))
waiting_list_event_ids = [event.pk for event in events if event.in_waiting_list]
extra_data = get_extra_data(request, payload)
@ -2282,13 +2227,9 @@ class MultipleAgendasEventsFillslotsRevert(APIView):
if booking.previous_state == 'cancelled':
bookings_to_cancel.append(booking)
events = (
Event.objects.filter(
pk__in=[b.event_id for b in bookings_to_cancel + bookings_to_book + bookings_to_delete]
)
.prefetch_related('agenda')
.prefetch_related(Prefetch('primary_event', queryset=Event.objects.all().order_by()))
)
events = Event.objects.filter(
pk__in=[b.event_id for b in bookings_to_cancel + bookings_to_book + bookings_to_delete]
).prefetch_related('agenda')
events_by_id = {x.id: x for x in events}
with transaction.atomic():
cancellation_datetime = now()
@ -2507,16 +2448,6 @@ class MultipleAgendasEventsCheckLock(APIView):
for event in events:
event.async_refresh_booking_computed_times()
for event in events:
audit(
'check:lock' if check_locked else 'check:unlock',
request=request,
agenda=event.agenda,
extra_data={
'event': event,
},
)
return Response({'err': 0})
@ -2546,16 +2477,6 @@ class MultipleAgendasEventsInvoiced(APIView):
)
events.update(invoiced=invoiced)
for event in events:
audit(
'invoice:mark' if invoiced else 'invoice:unmark',
request=request,
agenda=event.agenda,
extra_data={
'event': event,
},
)
return Response({'err': 0})
@ -2832,12 +2753,10 @@ class BookingsAPI(ListAPIView):
return Response({'err': 0, 'data': data})
def get_queryset(self):
event_queryset = Event.objects.all().prefetch_related(
'agenda', 'desk', Prefetch('primary_event', queryset=Event.objects.all().order_by())
)
return (
Booking.objects.filter(primary_booking__isnull=True, cancellation_datetime__isnull=True)
.prefetch_related('user_checks', Prefetch('event', queryset=event_queryset))
.select_related('event', 'event__agenda', 'event__desk')
.prefetch_related('user_checks')
.order_by('event__start_datetime', 'event__slug', 'event__agenda__slug', 'pk')
)
@ -2855,14 +2774,14 @@ class BookingsICS(BookingsAPI):
except ValidationError as e:
raise APIErrorBadRequest(N_('invalid payload'), errors=e.detail)
cal = icalendar.Calendar()
cal['propid'] = '-//Entr\'ouvert//NON SGML Publik'
ics = vobject.iCalendar()
ics.add('prodid').value = '-//Entr\'ouvert//NON SGML Publik'
for booking in bookings:
vevent = booking.get_vevent_ics()
cal.add_component(vevent)
ics.add(vevent)
return HttpResponse(cal.to_ical(), content_type='text/calendar')
return HttpResponse(ics.serialize(), content_type='text/calendar')
bookings_ics = BookingsICS.as_view()
@ -2993,7 +2912,7 @@ class BookingAPI(APIView):
if self.booking.primary_booking is not None:
raise APIError(N_('secondary booking'), err=2)
self.booking.cancel(request=request)
self.booking.cancel()
response = {'err': 0, 'booking_id': self.booking.pk}
return Response(response)
@ -3017,7 +2936,7 @@ class CancelBooking(APIView):
raise APIError(N_('already cancelled'))
if booking.primary_booking is not None:
raise APIError(N_('secondary booking'), err=2)
booking.cancel(request=request)
booking.cancel()
response = {'err': 0, 'booking_id': booking.id}
return Response(response)
@ -3046,16 +2965,6 @@ class AcceptBooking(APIView):
raise APIError(N_('booking is not in waiting list'), err=3)
booking.accept()
event = booking.event
audit(
'booking:accept',
request=request,
agenda=event.agenda,
extra_data={
'booking_id': booking.id,
'event': event,
},
)
response = {
'err': 0,
'booking_id': booking.pk,
@ -3087,17 +2996,6 @@ class SuspendBooking(APIView):
if booking.in_waiting_list:
raise APIError(N_('booking is already in waiting list'), err=3)
booking.suspend()
event = booking.event
audit(
'booking:suspend',
request=request,
agenda=event.agenda,
extra_data={
'booking': booking,
'event': event,
},
)
response = {'err': 0, 'booking_id': booking.pk}
return Response(response)
@ -3436,14 +3334,6 @@ class EventCheck(APIView):
if not event.checked:
event.checked = True
event.save(update_fields=['checked'])
audit(
'check:mark',
request=request,
agenda=event.agenda,
extra_data={
'event': event,
},
)
event.async_notify_checked()
response = {
'err': 0,

View File

@ -208,7 +208,6 @@ class Place(models.Model):
'annulation_url': self.cancel_url,
'plages': list(self.iter_open_dates()),
'rdvs': list(self.iter_predemandes()),
'logo_url': self.logo_url,
}
return payload

View File

@ -14,11 +14,11 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import io
import json
import tarfile
from django.contrib.auth.models import Group
from django.contrib.contenttypes.models import ContentType
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
@ -28,7 +28,6 @@ from rest_framework.generics import GenericAPIView
from rest_framework.response import Response
from chrono.agendas.models import Agenda, Category, EventsType, Resource, UnavailabilityCalendar
from chrono.api.utils import APIErrorBadRequest
from chrono.apps.export_import.models import Application, ApplicationElement
from chrono.manager.utils import import_site
@ -42,24 +41,9 @@ klasses_translation = {
}
klasses_translation_reverse = {v: k for k, v in klasses_translation.items()}
compare_urls = {
'agendas': 'chrono-manager-agenda-history-compare',
'categories': 'chrono-manager-category-history-compare',
'events_types': 'chrono-manager-events-type-history-compare',
'resources': 'chrono-manager-resource-history-compare',
'unavailability_calendars': 'chrono-manager-unavailability-calendar-history-compare',
}
def get_klass_from_component_type(component_type):
try:
return klasses[component_type]
except KeyError:
raise Http404
class Index(GenericAPIView):
permission_classes = (permissions.IsAdminUser,)
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def get(self, request, *args, **kwargs):
data = []
@ -153,10 +137,10 @@ def get_component_bundle_entry(request, component):
class ListComponents(GenericAPIView):
permission_classes = (permissions.IsAdminUser,)
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def get(self, request, *args, **kwargs):
klass = get_klass_from_component_type(kwargs['component_type'])
klass = klasses[kwargs['component_type']]
order_by = 'slug'
if klass == Group:
order_by = 'name'
@ -168,11 +152,11 @@ list_components = ListComponents.as_view()
class ExportComponent(GenericAPIView):
permission_classes = (permissions.IsAdminUser,)
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def get(self, request, slug, *args, **kwargs):
klass = get_klass_from_component_type(kwargs['component_type'])
serialisation = get_object_or_404(klass, slug=slug).export_json()
klass = klasses[kwargs['component_type']]
serialisation = klass.objects.get(slug=slug).export_json()
return Response({'data': serialisation})
@ -180,11 +164,11 @@ export_component = ExportComponent.as_view()
class ComponentDependencies(GenericAPIView):
permission_classes = (permissions.IsAdminUser,)
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def get(self, request, slug, *args, **kwargs):
klass = get_klass_from_component_type(kwargs['component_type'])
component = get_object_or_404(klass, slug=slug)
klass = klasses[kwargs['component_type']]
component = klass.objects.get(slug=slug)
def dependency_dict(element):
return get_component_bundle_entry(request, element)
@ -197,29 +181,8 @@ component_dependencies = ComponentDependencies.as_view()
def component_redirect(request, component_type, slug):
klass = get_klass_from_component_type(component_type)
klass = klasses[component_type]
component = get_object_or_404(klass, slug=slug)
if component_type not in klasses or component_type == 'roles':
raise Http404
if (
'compare' in request.GET
and request.GET.get('application')
and request.GET.get('version1')
and request.GET.get('version2')
):
component_type = klasses_translation.get(component_type, component_type)
return redirect(
'%s?version1=%s&version2=%s&application=%s'
% (
reverse(compare_urls[component_type], args=[component.pk]),
request.GET['version1'],
request.GET['version2'],
request.GET['application'],
)
)
if klass == Agenda:
return redirect(reverse('chrono-manager-agenda-view', kwargs={'pk': component.pk}))
if klass == Category:
@ -234,163 +197,41 @@ def component_redirect(request, component_type, slug):
class BundleCheck(GenericAPIView):
permission_classes = (permissions.IsAdminUser,)
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def post(self, request, *args, **kwargs):
bundle = request.FILES['bundle']
try:
with tarfile.open(fileobj=bundle) as tar:
try:
manifest = json.loads(tar.extractfile('manifest.json').read().decode())
except KeyError:
raise APIErrorBadRequest(_('Invalid tar file, missing manifest'))
application_slug = manifest.get('slug')
application_version = manifest.get('version_number')
if not application_slug or not application_version:
return Response({'data': {}})
differences = []
unknown_elements = []
no_history_elements = []
legacy_elements = []
content_types = ContentType.objects.get_for_models(
*[v for k, v in klasses.items() if k != 'roles']
)
for element in manifest.get('elements'):
component_type = element['type']
if component_type not in klasses or component_type == 'roles':
continue
klass = klasses[component_type]
component_type = klasses_translation.get(component_type, component_type)
try:
component = klass.objects.get(slug=element['slug'])
except klass.DoesNotExist:
unknown_elements.append(
{
'type': component_type,
'slug': element['slug'],
}
)
continue
elements_qs = ApplicationElement.objects.filter(
application__slug=application_slug,
content_type=content_types[klass],
object_id=component.pk,
)
if not elements_qs.exists():
# object exists, but not linked to the application
legacy_elements.append(
{
'type': component.application_component_type,
'slug': str(component.slug),
# information needed here, Relation objects may not exist yet in hobo
'text': component.label,
'url': reverse(
'api-export-import-component-redirect',
kwargs={
'slug': str(component.slug),
'component_type': component.application_component_type,
},
),
}
)
continue
snapshot_for_app = (
klass.get_snapshot_model()
.objects.filter(
instance=component,
application_slug=application_slug,
application_version=application_version,
)
.order_by('timestamp')
.last()
)
if not snapshot_for_app:
# no snapshot for this bundle
no_history_elements.append(
{
'type': element['type'],
'slug': element['slug'],
}
)
continue
last_snapshot = (
klass.get_snapshot_model().objects.filter(instance=component).latest('timestamp')
)
if snapshot_for_app.pk != last_snapshot.pk:
differences.append(
{
'type': element['type'],
'slug': element['slug'],
'url': '%s?version1=%s&version2=%s'
% (
request.build_absolute_uri(
reverse(compare_urls[component_type], args=[component.pk])
),
snapshot_for_app.pk,
last_snapshot.pk,
),
}
)
except tarfile.TarError:
raise APIErrorBadRequest(_('Invalid tar file'))
return Response(
{
'data': {
'differences': differences,
'unknown_elements': unknown_elements,
'no_history_elements': no_history_elements,
'legacy_elements': legacy_elements,
}
}
)
def put(self, request, *args, **kwargs):
return Response({'err': 0, 'data': {}})
bundle_check = BundleCheck.as_view()
class BundleImport(GenericAPIView):
permission_classes = (permissions.IsAdminUser,)
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
install = True
def post(self, request, *args, **kwargs):
bundle = request.FILES['bundle']
def put(self, request, *args, **kwargs):
tar_io = io.BytesIO(request.read())
components = {}
try:
with tarfile.open(fileobj=bundle) as tar:
try:
manifest = json.loads(tar.extractfile('manifest.json').read().decode())
except KeyError:
raise APIErrorBadRequest(_('Invalid tar file, missing manifest'))
self.application = Application.update_or_create_from_manifest(
manifest,
tar,
editable=not self.install,
with tarfile.open(fileobj=tar_io) as tar:
manifest = json.loads(tar.extractfile('manifest.json').read().decode())
self.application = Application.update_or_create_from_manifest(
manifest,
tar,
editable=not self.install,
)
for element in manifest.get('elements'):
component_type = element['type']
if component_type not in klasses or element['type'] == 'roles':
continue
component_type = klasses_translation.get(component_type, component_type)
if component_type not in components:
components[component_type] = []
component_content = (
tar.extractfile('%s/%s' % (element['type'], element['slug'])).read().decode()
)
for element in manifest.get('elements'):
component_type = element['type']
if component_type not in klasses or element['type'] == 'roles':
continue
component_type = klasses_translation.get(component_type, component_type)
if component_type not in components:
components[component_type] = []
try:
component_content = (
tar.extractfile('%s/%s' % (element['type'], element['slug'])).read().decode()
)
except KeyError:
raise APIErrorBadRequest(
_(
'Invalid tar file, missing component %s/%s'
% (element['type'], element['slug'])
)
)
components[component_type].append(json.loads(component_content).get('data'))
except tarfile.TarError:
raise APIErrorBadRequest(_('Invalid tar file'))
components[component_type].append(json.loads(component_content).get('data'))
# init cache of application elements, from manifest
self.application_elements = set()
# import agendas
@ -419,11 +260,6 @@ class BundleImport(GenericAPIView):
self.application, existing_component
)
self.application_elements.add(element.content_object)
if self.install is True:
existing_component.take_snapshot(
comment=_('Application (%s)') % self.application,
application=self.application,
)
def unlink_obsolete_objects(self):
known_elements = ApplicationElement.objects.filter(application=self.application)
@ -447,7 +283,7 @@ bundle_declare = BundleDeclare.as_view()
class BundleUnlink(GenericAPIView):
permission_classes = (permissions.IsAdminUser,)
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def post(self, request, *args, **kwargs):
if request.POST.get('application'):

View File

@ -55,10 +55,10 @@ class Application(models.Model):
slug=manifest.get('slug'), defaults={'editable': editable}
)
application.name = manifest.get('application')
application.description = manifest.get('description') or ''
application.documentation_url = manifest.get('documentation_url') or ''
application.description = manifest.get('description')
application.documentation_url = manifest.get('documentation_url')
application.version_number = manifest.get('version_number') or 'unknown'
application.version_notes = manifest.get('version_notes') or ''
application.version_notes = manifest.get('version_notes')
if not editable:
application.editable = editable
application.visible = manifest.get('visible', True)

View File

@ -1,56 +0,0 @@
# chrono - agendas system
# Copyright (C) 2016-2024 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import datetime
import django_filters
from django.forms.widgets import DateInput
from django.utils.translation import gettext_lazy as _
from chrono.agendas.models import Agenda
from .models import AuditEntry
class DateWidget(DateInput):
input_type = 'date'
def __init__(self, *args, **kwargs):
kwargs['format'] = '%Y-%m-%d'
super().__init__(*args, **kwargs)
class DayFilter(django_filters.DateFilter):
def filter(self, qs, value):
if value:
qs = qs.filter(timestamp__gte=value, timestamp__lt=value + datetime.timedelta(days=1))
return qs
class JournalFilterSet(django_filters.FilterSet):
timestamp = DayFilter(widget=DateWidget())
agenda = django_filters.ModelChoiceFilter(queryset=Agenda.objects.all())
action_type = django_filters.ChoiceFilter(
choices=(
('booking', _('Booking')),
('check', _('Checking')),
('invoice', _('Invoicing')),
)
)
class Meta:
model = AuditEntry
fields = []

View File

@ -1,6 +0,0 @@
from django.db import migrations
class Migration(migrations.Migration):
dependencies = []
operations = []

View File

@ -1,52 +0,0 @@
# Generated by Django 3.2.16 on 2024-04-23 11:49
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('agendas', '0171_snapshot_models'),
('journal', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='AuditEntry',
fields=[
(
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('timestamp', models.DateTimeField(auto_now_add=True, verbose_name='Date')),
('action_type', models.CharField(max_length=100, verbose_name='Action type')),
('action_code', models.CharField(max_length=100, verbose_name='Action code')),
('extra_data', models.JSONField(blank=True, default=dict)),
(
'agenda',
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='audit_entries',
to='agendas.agenda',
),
),
(
'user',
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
verbose_name='User',
),
),
],
options={
'ordering': ('-timestamp',),
},
),
]

View File

@ -1,57 +0,0 @@
# chrono - agendas system
# Copyright (C) 2016-2024 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.conf import settings
from django.db import models
from django.utils.translation import gettext_lazy as _
MESSAGES = {
'booking:accept': _('acceptation of booking (%(booking_id)s) in event "%(event)s"'),
'booking:cancel': _('cancellation of booking (%(booking_id)s) in event "%(event)s"'),
'booking:create': _('created booking (%(booking_id)s) for event %(event)s'),
'booking:suspend': _('suspension of booking (%(booking_id)s) in event "%(event)s"'),
'check:mark': _('marked event %(event)s as checked'),
'check:mark-unchecked-absent': _('marked unchecked users as absent in %(event)s'),
'check:reset': _('reset check of %(user_name)s in %(event)s'),
'check:lock': _('marked event %(event)s as locked for checks'),
'check:unlock': _('unmarked event %(event)s as locked for checks'),
'check:absence': _('marked absence of %(user_name)s in %(event)s'),
'check:presence': _('marked presence of %(user_name)s in %(event)s'),
'invoice:mark': _('marked event %(event)s as invoiced'),
'invoice:unmark': _('unmarked event %(event)s as invoiced'),
}
class AuditEntry(models.Model):
timestamp = models.DateTimeField(verbose_name=_('Date'), auto_now_add=True)
user = models.ForeignKey(
settings.AUTH_USER_MODEL, verbose_name=_('User'), on_delete=models.SET_NULL, null=True
)
action_type = models.CharField(verbose_name=_('Action type'), max_length=100)
action_code = models.CharField(verbose_name=_('Action code'), max_length=100)
agenda = models.ForeignKey(
'agendas.Agenda', on_delete=models.SET_NULL, null=True, related_name='audit_entries'
)
extra_data = models.JSONField(blank=True, default=dict)
class Meta:
ordering = ('-timestamp',)
def get_action_text(self):
try:
return MESSAGES[f'{self.action_type}:{self.action_code}'] % self.extra_data
except KeyError:
return _('Unknown entry (%s:%s)') % (self.action_type, self.action_code)

View File

@ -1,49 +0,0 @@
{% extends "chrono/manager_base.html" %}
{% load gadjo i18n %}
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'chrono-manager-audit-journal' %}">{% trans "Audit journal" %}</a>
{% endblock %}
{% block appbar %}
<h2>{% trans "Audit journal" %}</h2>
{% endblock %}
{% block content %}
<table class="main">
<thead>
<tr>
<th>{% trans "Date" %}</th>
<th>{% trans "User" %}</th>
<th>{% trans "Agenda" %}</th>
<th>{% trans "Action" %}</th>
</tr>
</thead>
<tbody>
{% for line in object_list %}
<tr>
<td>{{ line.timestamp }}</td>
<td>{{ line.user.get_full_name }}</td>
<td>{{ line.agenda }}</td>
<td>{{ line.get_action_text }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% include "gadjo/pagination.html" %}
{% endblock %}
{% block sidebar %}
<aside id="sidebar">
<h3>{% trans "Search" %}</h3>
<form action=".">
{{ filter.form|with_template }}
<div class="buttons">
<button>{% trans "Search" %}</button>
</div>
</form>
</aside>
{% endblock %}

View File

@ -1,24 +0,0 @@
# chrono - agendas system
# Copyright (C) 2016-2024 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.urls import path
from . import views
urlpatterns = [
path('', views.journal_home, name='chrono-manager-audit-journal'),
]

View File

@ -1,39 +0,0 @@
# chrono - agendas system
# Copyright (C) 2016-2024 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.contrib.auth import get_user_model
from .models import AuditEntry
User = get_user_model()
def audit(action, request=None, user=None, agenda=None, extra_data=None):
action_type, action_code = action.split(':', 1)
extra_data = extra_data or {}
if 'event' in extra_data:
extra_data['event_id'] = extra_data['event'].id
extra_data['event'] = extra_data['event'].get_journal_label()
if 'booking' in extra_data:
extra_data['booking_id'] = extra_data['booking'].id
extra_data['booking'] = extra_data['booking'].get_journal_label()
return AuditEntry.objects.create(
user=request.user if request and isinstance(request.user, User) else user,
action_type=action_type,
action_code=action_code,
agenda=agenda,
extra_data=extra_data,
)

View File

@ -1,42 +0,0 @@
# chrono - agendas system
# Copyright (C) 2016-2024 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.core.exceptions import PermissionDenied
from django.views.generic import ListView
from .forms import JournalFilterSet
class JournalHomeView(ListView):
template_name = 'chrono/journal/home.html'
paginate_by = 10
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
self.filterset = JournalFilterSet(self.request.GET)
return self.filterset.qs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['filter'] = self.filterset
return context
journal_home = JournalHomeView.as_view()

View File

@ -38,7 +38,7 @@ class Migration(migrations.Migration):
(
'user',
models.ForeignKey(
null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL
null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
],
@ -72,7 +72,7 @@ class Migration(migrations.Migration):
(
'user',
models.ForeignKey(
null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL
null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
],
@ -106,7 +106,7 @@ class Migration(migrations.Migration):
(
'user',
models.ForeignKey(
null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL
null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
],
@ -140,7 +140,7 @@ class Migration(migrations.Migration):
(
'user',
models.ForeignKey(
null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL
null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
],
@ -174,7 +174,7 @@ class Migration(migrations.Migration):
(
'user',
models.ForeignKey(
null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL
null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
],

View File

@ -40,12 +40,12 @@ class WithSnapshotMixin:
return cls._meta.get_field('snapshot').related_model
def take_snapshot(self, *args, **kwargs):
return self.get_snapshot_model().take(self, *args, **kwargs)
self.get_snapshot_model().take(self, *args, **kwargs)
class AbstractSnapshot(models.Model):
timestamp = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True)
comment = models.TextField(blank=True, null=True)
serialization = models.JSONField(blank=True, default=dict)
label = models.CharField(_('Label'), max_length=150, blank=True)
@ -74,68 +74,17 @@ 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
instance = self.get_instance_model().snapshots.get(snapshot=self)
return self.get_instance_model().snapshots.get(snapshot=self)
except self.get_instance_model().DoesNotExist:
instance = self.load_instance(self.serialization, snapshot=self)
instance.slug = self.serialization['slug'] # restore slug
return instance
return self.load_instance(self.serialization, snapshot=self)
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

@ -1,213 +0,0 @@
# chrono - agendas system
# Copyright (C) 2016-2024 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import difflib
import json
import re
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect
from django.template import loader
from django.urls import reverse
from django.utils.formats import date_format
from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, ListView
from lxml.html.diff import htmldiff
from pyquery import PyQuery as pq
from chrono.utils.timezone import localtime
class InstanceWithSnapshotHistoryView(ListView):
def get_queryset(self):
self.instance = get_object_or_404(self.model.get_instance_model(), pk=self.kwargs['pk'])
return self.instance.instance_snapshots.all().select_related('user')
def get_context_data(self, **kwargs):
kwargs[self.instance_context_key] = self.instance
kwargs['object'] = self.instance
current_date = None
context = super().get_context_data(**kwargs)
day_snapshot = None
for snapshot in context['object_list']:
if snapshot.timestamp.date() != current_date:
current_date = snapshot.timestamp.date()
snapshot.new_day = True
snapshot.day_other_count = 0
day_snapshot = snapshot
else:
day_snapshot.day_other_count += 1
return context
class InstanceWithSnapshotHistoryCompareView(DetailView):
def get_snapshots_from_application(self):
version1 = self.request.GET.get('version1')
version2 = self.request.GET.get('version2')
if not version1 or not version2:
raise Http404
snapshot_for_app1 = (
self.model.get_snapshot_model()
.objects.filter(
instance=self.object,
application_slug=self.request.GET['application'],
application_version=self.request.GET['version1'],
)
.order_by('timestamp')
.last()
)
snapshot_for_app2 = (
self.model.get_snapshot_model()
.objects.filter(
instance=self.object,
application_slug=self.request.GET['application'],
application_version=self.request.GET['version2'],
)
.order_by('timestamp')
.last()
)
return snapshot_for_app1, snapshot_for_app2
def get_snapshots(self):
if 'application' in self.request.GET:
return self.get_snapshots_from_application()
id1 = self.request.GET.get('version1')
id2 = self.request.GET.get('version2')
if not id1 or not id2:
raise Http404
snapshot1 = get_object_or_404(self.model.get_snapshot_model(), pk=id1, instance=self.object)
snapshot2 = get_object_or_404(self.model.get_snapshot_model(), pk=id2, instance=self.object)
return snapshot1, snapshot2
def get_context_data(self, **kwargs):
kwargs[self.instance_context_key] = self.object
mode = self.request.GET.get('mode') or 'json'
if mode not in ['json', 'inspect']:
raise Http404
snapshot1, snapshot2 = self.get_snapshots()
if not snapshot1 or not snapshot2:
return redirect(reverse(self.history_view, args=[self.object.pk]))
if snapshot1.timestamp > snapshot2.timestamp:
snapshot1, snapshot2 = snapshot2, snapshot1
kwargs['mode'] = mode
kwargs['snapshot1'] = snapshot1
kwargs['snapshot2'] = snapshot2
kwargs['fromdesc'] = self.get_snapshot_desc(snapshot1)
kwargs['todesc'] = self.get_snapshot_desc(snapshot2)
kwargs.update(getattr(self, 'get_compare_%s_context' % mode)(snapshot1, snapshot2))
return super().get_context_data(**kwargs)
def get(self, request, *args, **kwargs):
self.object = self.get_object()
context = self.get_context_data(object=self.object)
if isinstance(context, HttpResponseRedirect):
return context
return self.render_to_response(context)
def get_compare_inspect_context(self, snapshot1, snapshot2):
instance1 = snapshot1.get_instance()
instance2 = snapshot2.get_instance()
def get_context(instance):
return {
'object': instance,
}
def fix_result(panel_diff):
if not panel_diff:
return panel_diff
panel = pq(panel_diff)
# remove "Link" added by htmldiff
for link in panel.find('a'):
d = pq(link)
text = d.html()
new_text = re.sub(r' Link: .*$', '', text)
d.html(new_text)
# remove empty ins and del tags
for elem in panel.find('ins, del'):
d = pq(elem)
if not (d.html() or '').strip():
d.remove()
# prevent auto-closing behaviour of pyquery .html() method
for elem in panel.find('span, ul, div'):
d = pq(elem)
if not d.html():
d.html(' ')
return panel.html()
inspect1 = loader.render_to_string(self.inspect_template_name, get_context(instance1), self.request)
d1 = pq(str(inspect1))
inspect2 = loader.render_to_string(self.inspect_template_name, get_context(instance2), self.request)
d2 = pq(str(inspect2))
panels_attrs = [tab.attrib for tab in d1('[role="tabpanel"]')]
panels1 = list(d1('[role="tabpanel"]'))
panels2 = list(d2('[role="tabpanel"]'))
# build tab list (merge version 1 and version2)
tabs1 = d1.find('[role="tab"]')
tabs2 = d2.find('[role="tab"]')
tabs_order = [t.get('id') for t in panels_attrs]
tabs = {}
for tab in tabs1 + tabs2:
tab_id = pq(tab).attr('aria-controls')
tabs[tab_id] = pq(tab).outer_html()
tabs = [tabs[k] for k in tabs_order if k in tabs]
# build diff of each panel
panels_diff = list(map(htmldiff, panels1, panels2))
panels_diff = [fix_result(t) for t in panels_diff]
return {
'tabs': tabs,
'panels': zip(panels_attrs, panels_diff),
'tab_class_names': d1('.pk-tabs').attr('class'),
}
def get_compare_json_context(self, snapshot1, snapshot2):
s1 = json.dumps(snapshot1.serialization, sort_keys=True, indent=2)
s2 = json.dumps(snapshot2.serialization, sort_keys=True, indent=2)
diff_serialization = difflib.HtmlDiff(wrapcolumn=160).make_table(
fromlines=s1.splitlines(True),
tolines=s2.splitlines(True),
)
return {
'diff_serialization': diff_serialization,
}
def get_snapshot_desc(self, snapshot):
label_or_comment = ''
if snapshot.label:
label_or_comment = snapshot.label
elif snapshot.comment:
label_or_comment = snapshot.comment
if snapshot.application_version:
label_or_comment += ' (%s)' % _('Version %s') % snapshot.application_version
return '{name} ({pk}) - {label_or_comment} ({user}{timestamp})'.format(
name=_('Snapshot'),
pk=snapshot.id,
label_or_comment=label_or_comment,
user='%s ' % snapshot.user if snapshot.user_id else '',
timestamp=date_format(localtime(snapshot.timestamp), format='DATETIME_FORMAT'),
)

View File

@ -1,23 +0,0 @@
# chrono - agendas system
# Copyright (C) 2024 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.urls import path
from . import api_views
urlpatterns = [
path('save', api_views.save_preference, name='api-user-preferences'),
]

View File

@ -1,43 +0,0 @@
# chrono - agendas system
# Copyright (C) 2024 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import json
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse, HttpResponseBadRequest
from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_exempt
from . import models
@csrf_exempt
@login_required
def save_preference(request):
user_pref, dummy = models.UserPreferences.objects.get_or_create(user=request.user)
if len(request.body) > 1000:
return HttpResponseBadRequest(_('Payload is too large'))
try:
prefs = json.loads(request.body)
except json.JSONDecodeError:
return HttpResponseBadRequest(_('Bad format'))
if not isinstance(prefs, dict) or len(prefs) != 1:
return HttpResponseBadRequest(_('Bad format'))
user_pref.preferences.update(prefs)
user_pref.save()
return HttpResponse('', status=204)

View File

@ -1,32 +0,0 @@
# Generated by Django 3.2.18 on 2024-04-11 15:30
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='UserPreferences',
fields=[
(
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('preferences', models.JSONField(default=dict, verbose_name='Preferences')),
(
'user',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
],
),
]

View File

@ -1,24 +0,0 @@
# chrono - agendas system
# Copyright (C) 2024 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.conf import settings
from django.db import models
from django.utils.translation import gettext_lazy as _
class UserPreferences(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
preferences = models.JSONField(_('Preferences'), default=dict)

View File

@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: chrono 0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-29 14:07+0000\n"
"PO-Revision-Date: 2024-04-29 16:07+0200\n"
"POT-Creation-Date: 2024-02-27 16:39+0100\n"
"PO-Revision-Date: 2024-02-01 09:50+0100\n"
"Last-Translator: Frederic Peters <fpeters@entrouvert.com>\n"
"Language: French\n"
"MIME-Version: 1.0\n"
@ -41,7 +41,6 @@ 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
@ -143,7 +142,6 @@ msgid "Label"
msgstr "Libellé"
#: agendas/models.py
#: manager/templates/chrono/includes/snapshot_history_fragment.html
msgid "Identifier"
msgstr "Identifiant"
@ -194,10 +192,6 @@ 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 ""
@ -273,7 +267,7 @@ msgstr ""
"réservation seront ceux qui commencent après lheure actuelle, en prenant en "
"compte les délais de réservation minimal et maximal."
#: agendas/models.py apps/journal/forms.py
#: agendas/models.py
msgid "Invoicing"
msgstr "Facturation"
@ -298,7 +292,6 @@ msgid "Tolerance"
msgstr "Tolérance"
#: agendas/models.py api/views.py apps/ants_hub/models.py
#: apps/journal/templates/chrono/journal/home.html
msgid "Agenda"
msgstr "Agenda"
@ -386,9 +379,7 @@ msgstr "Jour de la semaine"
msgid "Repeat"
msgstr "Répéter"
#: agendas/models.py apps/journal/models.py
#: apps/journal/templates/chrono/journal/home.html manager/forms.py
#: manager/templates/chrono/includes/snapshot_history_fragment.html
#: agendas/models.py manager/forms.py
msgid "Date"
msgstr "Date"
@ -470,7 +461,6 @@ 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"
@ -502,11 +492,6 @@ msgstr "Annulation en cours"
msgid "Bad datetime format \"%s\""
msgstr "Mauvais format pour la date/heure « %s »"
#: agendas/models.py
#, python-format
msgid "Bad time format \"%s\""
msgstr "Mauvais format pour lheure « %s »"
#: agendas/models.py
msgid "Daily"
msgstr "Tous les jours"
@ -559,25 +544,6 @@ msgstr "Types dévènements"
msgid "Label displayed to user"
msgstr "Libellé affiché à lusager"
#: agendas/models.py
#, python-format
msgid "ID: %s"
msgstr "ID : %s"
#: agendas/models.py
#, python-format
msgid "user: %s"
msgstr "utilisateur : %s"
#: agendas/models.py
msgid "in waiting list"
msgstr "sur liste dattente"
#: agendas/models.py
#, python-format
msgid "cancelled at %s"
msgstr "annulé le %s"
#: agendas/models.py
msgid "Arrival"
msgstr "Arrivée"
@ -601,7 +567,6 @@ 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
@ -647,16 +612,11 @@ 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"
@ -937,7 +897,7 @@ msgstr "Rôle de gestion"
msgid "Holidays calendar"
msgstr "Calendrier des vacances"
#: agendas/models.py apps/journal/forms.py manager/forms.py
#: agendas/models.py manager/forms.py
msgid "Booking"
msgstr "Réservation"
@ -1584,12 +1544,10 @@ msgid "Booked"
msgstr "Réservé"
#: api/views.py manager/forms.py
#: manager/templates/chrono/manager_partial_bookings_month_view.html
msgid "Present"
msgstr "Présent"
#: api/views.py manager/forms.py
#: manager/templates/chrono/manager_partial_bookings_month_view.html
msgid "Absent"
msgstr "Absent"
@ -1976,158 +1934,10 @@ msgstr "Rôles"
msgid "Role"
msgstr "Rôle"
#: apps/export_import/api_views.py
msgid "Invalid tar file, missing manifest"
msgstr "Mauvais format de fichier tar, manifest manquant"
#: apps/export_import/api_views.py
msgid "Invalid tar file"
msgstr "Mauvais format de fichier tar"
#: apps/export_import/api_views.py
#, python-format
msgid "Invalid tar file, missing component %s/%s"
msgstr "Mauvais format de fichier tar, composant %s/%s manquant"
#: apps/export_import/api_views.py
#, python-format
msgid "Application (%s)"
msgstr "Application (%s)"
#: apps/journal/forms.py
msgid "Checking"
msgstr "Pointage"
#: apps/journal/models.py
#, python-format
msgid "acceptation of booking (%(booking_id)s) in event \"%(event)s\""
msgstr ""
"acceptation de la réservation (%(booking_id)s) sur lévènement « %(event)s »"
#: apps/journal/models.py
#, python-format
msgid "cancellation of booking (%(booking_id)s) in event \"%(event)s\""
msgstr ""
"annulation la réservation (%(booking_id)s) sur lévènement « %(event)s »"
#: apps/journal/models.py
#, python-format
msgid "created booking (%(booking_id)s) for event %(event)s"
msgstr ""
"création de la réservation (%(booking_id)s) sur lévènement « %(event)s »"
#: apps/journal/models.py
#, python-format
msgid "suspension of booking (%(booking_id)s) in event \"%(event)s\""
msgstr ""
"suspension de la réservation (%(booking_id)s) sur lévènement « %(event)s »"
#: apps/journal/models.py
#, python-format
msgid "marked event %(event)s as checked"
msgstr "marquage de lévènement %(event)s comme pointé"
#: apps/journal/models.py
#, python-format
msgid "marked unchecked users as absent in %(event)s"
msgstr ""
"marquage des usagers non pointés comme étant absents sur lévènement "
"« %(event)s »"
#: apps/journal/models.py
#, python-format
msgid "reset check of %(user_name)s in %(event)s"
msgstr "retrait du pointage de %(user_name)s sur lévènement « %(event)s »"
#: apps/journal/models.py
#, python-format
msgid "marked event %(event)s as locked for checks"
msgstr ""
"marquage de lévènement %(event)s comme étant verrouillé pour le pointage"
#: apps/journal/models.py
#, python-format
msgid "unmarked event %(event)s as locked for checks"
msgstr ""
"retrait du marquage de lévènement %(event)s comme étant verrouillé pour le "
"pointage"
#: apps/journal/models.py
#, python-format
msgid "marked absence of %(user_name)s in %(event)s"
msgstr "pointage de labssence de %(user_name) sur lévènement « %(event)s »"
#: apps/journal/models.py
#, python-format
msgid "marked presence of %(user_name)s in %(event)s"
msgstr "pointage de la présnce de %(user_name) sur lévènement « %(event)s »"
#: apps/journal/models.py
#, python-format
msgid "marked event %(event)s as invoiced"
msgstr "marquage de lévènement %(event)s comme étant facturé"
#: apps/journal/models.py
#, python-format
msgid "unmarked event %(event)s as invoiced"
msgstr "retrait du marquage de lévènement %(event)s comme étant facturé"
#: apps/journal/models.py apps/journal/templates/chrono/journal/home.html
#: manager/templates/chrono/includes/snapshot_history_fragment.html
msgid "User"
msgstr "Usager"
#: apps/journal/models.py
msgid "Action type"
msgstr "Type daction"
#: apps/journal/models.py
msgid "Action code"
msgstr "Code de laction"
#: apps/journal/models.py
#, python-format
msgid "Unknown entry (%s:%s)"
msgstr "Type daction inconnu (%s:%s)"
#: apps/journal/templates/chrono/journal/home.html
#: manager/templates/chrono/manager_home.html
msgid "Audit journal"
msgstr "Journal daudit"
#: apps/journal/templates/chrono/journal/home.html
msgid "Action"
msgstr "Action"
#: apps/journal/templates/chrono/journal/home.html
msgid "Search"
msgstr "Rechercher"
#: apps/snapshot/models.py
msgid "deletion"
msgstr "suppression"
#: apps/snapshot/views.py
#, python-format
msgid "Version %s"
msgstr "Version %s"
#: apps/snapshot/views.py
msgid "Snapshot"
msgstr "Sauvegarde"
#: apps/user_preferences/api_views.py
msgid "Payload is too large"
msgstr "Le contenu de requête est trop grand"
#: apps/user_preferences/api_views.py
msgid "Bad format"
msgstr "Mauvais format"
#: apps/user_preferences/models.py
msgid "Preferences"
msgstr "Préférences"
#: manager/forms.py
msgid "Desk 1"
msgstr "Guichet 1"
@ -2201,17 +2011,14 @@ 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"
@ -2245,7 +2052,6 @@ msgid "Without booking"
msgstr "Sans réservation"
#: manager/forms.py
#: manager/templates/chrono/manager_partial_bookings_month_view.html
msgid "Not checked"
msgstr "Non pointé"
@ -2635,45 +2441,12 @@ 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
#: 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
@ -2805,173 +2578,6 @@ 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 "Custom fields:"
msgstr "Champs personnalisés :"
#: 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"
@ -3056,6 +2662,18 @@ 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."
@ -3117,19 +2735,23 @@ 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"
@ -3173,10 +2795,6 @@ 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"
@ -3498,11 +3116,37 @@ 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 "
@ -3685,7 +3329,6 @@ 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"
@ -3693,22 +3336,6 @@ 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"
@ -3803,6 +3430,19 @@ 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"
@ -3828,6 +3468,11 @@ 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"
@ -3957,10 +3602,6 @@ 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"
@ -4145,10 +3786,6 @@ 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"
@ -4200,6 +3837,14 @@ 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,17 +589,12 @@ 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

@ -878,13 +878,6 @@ div#main-content.partial-booking-dayview {
& col.we {
background-color: var(--zebra-color);
}
& col.today {
background-image: linear-gradient(
135deg,
hsl(65, 65%, 94%) 20%,
hsl(65, 55%, 92%) 70%,
hsl(65, 50%, 90%) 90%);
}
&--day {
padding: .33em;
a {
@ -892,9 +885,6 @@ div#main-content.partial-booking-dayview {
font-weight: normal;
text-decoration: none;
}
&.today a {
font-weight: bold;
}
}
& .registrant {
&--name {
@ -911,11 +901,10 @@ div#main-content.partial-booking-dayview {
text-align: center;
vertical-align: middle;
padding: .33em;
line-height: 0;
& .booking {
display: inline-block;
width: Min(100%, 1.75em);
height: 1.75em;
width: Min(100%, 1.5em);
height: 1.5em;
--booking-color: #1066bc;
background-color: var(--booking-color);
&.present {
@ -1009,7 +998,3 @@ a.button.button-paragraph {
.application-logo, .application-icon {
vertical-align: middle;
}
.snapshots-list .collapsed {
display: none;
}

View File

@ -1,23 +1,4 @@
$(function() {
const foldableClassObserver = new MutationObserver((mutations) => {
mutations.forEach(mu => {
const old_folded = (mu.oldValue.indexOf('folded') != -1);
const new_folded = mu.target.classList.contains('folded')
if (old_folded == new_folded) { return; }
var pref_message = Object();
pref_message[mu.target.dataset.sectionFoldedPrefName] = new_folded;
fetch('/api/user-preferences/save', {
method: 'POST',
credentials: 'include',
headers: {'Accept': 'application/json', 'Content-Type': 'application/json'},
body: JSON.stringify(pref_message)
});
});
});
document.querySelectorAll('[data-section-folded-pref-name]').forEach(
elt => foldableClassObserver.observe(elt, {attributes: true, attributeFilter: ['class'], attributeOldValue: true})
);
$('[data-total]').each(function() {
var total = $(this).data('total');
var booked = $(this).data('booked');

View File

@ -1,20 +0,0 @@
<p class="snapshot-description">{{ fromdesc|safe }} ➔ {{ todesc|safe }}</p>
<div class="snapshot-diff">
{% if mode == 'json' %}
{{ diff_serialization|safe }}
{% else %}
<div class="{{ tab_class_names }}">
<div class="pk-tabs--tab-list" role="tablist">
{% for tab in tabs %}{{ tab|safe }}{% endfor %}
{{ tab_list|safe }}
</div>
<div class="pk-tabs--container">
{% for attrs, panel in panels %}
<div{% for k, v in attrs.items %} {{ k }}="{{ v }}"{% endfor %}>
{{ panel|safe }}
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>

View File

@ -1,63 +0,0 @@
{% 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

@ -1,19 +0,0 @@
{% 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

@ -1,29 +0,0 @@
{% extends "chrono/manager_agenda_history.html" %}
{% load gadjo static i18n %}
{% block css %}
{{ block.super }}
<link rel="stylesheet" type="text/css" media="all" href="{% static "css/gadjo.snapshotdiff.css" %}?{% start_timestamp %}"/>
{% endblock %}
{% block extrascripts %}
{{ block.super }}
<script src="{% static "js/gadjo.snapshotdiff.js" %}?{% start_timestamp %}"></script>
{% endblock %}
{% block appbar %}
<h2>{% trans 'Compare snapshots' %} ({% if mode == 'json' %}{% trans "JSON" %}{% else %}{% trans "Inspect" %}{% endif %})</h2>
<span class="actions">
<a href="?version1={{ snapshot1.pk }}&version2={{ snapshot2.pk }}&mode=inspect">{% trans "Compare inspect" %}</a>
<a href="?version1={{ snapshot1.pk }}&version2={{ snapshot2.pk }}&mode=json">{% trans "Compare JSON" %}</a>
</span>
{% endblock %}
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'chrono-manager-agenda-history-compare' pk=agenda.pk %}">{% trans "Compare snapshots" %}</a>
{% endblock %}
{% block content %}
{% include 'chrono/includes/snapshot_compare_fragment.html' %}
{% endblock %}

View File

@ -1,19 +0,0 @@
{% 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

@ -1,322 +0,0 @@
{% load i18n %}
<div class="pk-tabs">
<div class="pk-tabs--tab-list" role="tablist">
<button aria-controls="panel-information" aria-selected="true" id="tab-information" role="tab" tabindex="0">{% trans "Information" %}</button>
<button aria-controls="panel-settings" aria-selected="false" id="tab-settings" role="tab" tabindex="-1">{% trans "Settings" %}</button>
<button aria-controls="panel-permissions" aria-selected="false" id="tab-permissions" role="tab" tabindex="-1">{% trans "Permissions" %}</button>
{% if object.kind == 'events' %}
<button aria-controls="panel-events" aria-selected="false" id="tab-events" role="tab" tabindex="-1">{% trans "Events" %}</button>
<button aria-controls="panel-exceptions" aria-selected="false" id="tab-exceptions" role="tab" tabindex="-1">{% trans "Recurrence exceptions" %}</button>
{% elif object.kind == 'meetings' %}
<button aria-controls="panel-meeting-types" aria-selected="false" id="tab-meeting-types" role="tab" tabindex="-1">{% trans "Meeting Types" %}</button>
<button aria-controls="panel-desks" aria-selected="false" id="tab-desks" role="tab" tabindex="-1">{% trans "Desks" %}</button>
<button aria-controls="panel-resources" aria-selected="false" id="tab-resources" role="tab" tabindex="-1">{% trans "Resources" %}</button>
{% elif object.kind == 'virtual' %}
<button aria-controls="panel-agendas" aria-selected="false" id="tab-agendas" role="tab" tabindex="-1">{% trans "Included Agendas" %}</button>
<button aria-controls="panel-time-periods" aria-selected="false" id="tab-time-periods" role="tab" tabindex="-1">{% trans "Excluded Periods" %}</button>
{% endif %}
</div>
<div class="pk-tabs--container">
<div aria-labelledby="tab-information" id="panel-information" role="tabpanel" tabindex="0">
<div class="section">
<ul>
{% for label, value in object.get_inspect_fields %}
<li class="parameter-{{ label|slugify }}">
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
{{ value }}
</li>
{% endfor %}
</ul>
</div>
</div>
<div aria-labelledby="tab-settings" hidden id="panel-settings" role="tabpanel" tabindex="0">
<div class="section">
{% if object.kind != 'virtual' %}
<h4>{% trans "Display options" %}</h4>
<ul>
{% for label, value in object.get_display_inspect_fields %}
<li class="parameter-{{ label|slugify }}">
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
{{ value }}
</li>
{% endfor %}
</ul>
{% endif %}
{% if object.kind == 'events' %}
<h4>{% trans "Booking check options" %}</h4>
<ul>
{% for label, value in object.get_booking_check_inspect_fields %}
<li class="parameter-{{ label|slugify }}">
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
{{ value }}
</li>
{% endfor %}
</ul>
{% endif %}
{% if object.kind == 'events' %}
{% if agenda.partial_bookings %}
<h4>{% trans "Invoicing options" %}</h4>
<ul>
{% for label, value in object.get_invoicing_inspect_fields %}
<li class="parameter-{{ label|slugify }}">
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
{{ value }}
</li>
{% endfor %}
</ul>
{% else %}
<h4>{% trans "Management notifications" %}</h4>
<ul>
{% for label, value in object.get_notifications_inspect_fields %}
<li class="parameter-{{ label|slugify }}">
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
{{ value }}
</li>
{% endfor %}
</ul>
{% endif %}
{% endif %}
{% if object.kind != 'virtual' and not object.partial_bookings %}
<h4>{% trans "Booking reminders" %}</h4>
<ul>
{% for label, value in object.get_reminder_inspect_fields %}
<li class="parameter-{{ label|slugify }}">
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
{{ value }}
</li>
{% endfor %}
</ul>
{% endif %}
<h4>{% trans "Booking Delays" %}</h4>
<ul>
{% for label, value in object.get_booking_delays_inspect_fields %}
<li class="parameter-{{ label|slugify }}">
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
{{ value }}
</li>
{% endfor %}
</ul>
</div>
</div>
<div aria-labelledby="tab-permissions" hidden id="panel-permissions" role="tabpanel" tabindex="0">
<div class="section">
<ul>
{% for label, value in object.get_permissions_inspect_fields %}
<li class="parameter-{{ label|slugify }}">
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
{{ value }}
</li>
{% endfor %}
</ul>
</div>
</div>
{% if object.kind == 'events' %}
<div aria-labelledby="tab-events" hidden id="panel-events" role="tabpanel" tabindex="0">
<div class="section">
{% for event in object.event_set.all %}
<h4>{{ event }}</h4>
<ul>
{% for label, value in event.get_inspect_fields %}
<li class="parameter-{{ label|slugify }}">
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
{{ value }}
</li>
{% endfor %}
{% if object.events_type %}
<li class="parameter-custom-fields">
<span class="parameter">{% trans "Custom fields:" %}</span>
<ul>
{% for value in object.events_type.get_custom_fields %}
<li class="parameter-custom-field-{{ value.varname }}">
<span class="parameter">{% blocktrans with label=value.label %}{{ label }}:{% endblocktrans %}</span>
{{ event.get_custom_fields|get:value.varname|default:"" }}
</li>
{% endfor %}
</ul>
</li>
{% endif %}
</ul>
{% endfor %}
</div>
</div>
<div aria-labelledby="tab-exceptions" hidden id="panel-exceptions" role="tabpanel" tabindex="0">
<div class="section">
{% for desk in object.desk_set.all %}{% if desk.slug == '_exceptions_holder' %}
<h4>{% trans "Unavailability calendars" %}</h4>
<ul>
{% for unavailability_calendar in desk.unavailability_calendars.all %}
<li class="parameter-unavailability-calendar }}">
{{ unavailability_calendar }}
</li>
{% endfor %}
</ul>
<h4>{% trans "Exception sources" %}</h4>
{% for source in desk.timeperiodexceptionsource_set.all %}
<h5>{{ source }}</h5>
<ul>
{% for label, value in source.get_inspect_fields %}
<li class="parameter-{{ label|slugify }}">
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
{{ value }}
</li>
{% endfor %}
</ul>
{% endfor %}
<h4>{% trans "Exceptions" %}</h4>
{% for exception in desk.timeperiodexception_set.all %}
<h5>{{ exception }}</h5>
<ul>
{% for label, value in exception.get_inspect_fields %}
<li class="parameter-{{ label|slugify }}">
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
{{ value }}
</li>
{% endfor %}
</ul>
{% endfor %}
{% endif %}{% endfor %}
</div>
</div>
{% elif object.kind == 'meetings' %}
<div aria-labelledby="tab-meeting-types" hidden id="panel-meeting-types" role="tabpanel" tabindex="0">
<div class="section">
{% for meeting_type in object.meetingtype_set.all %}
<h4>{{ meeting_type }}</h4>
<ul>
{% for label, value in meeting_type.get_inspect_fields %}
<li class="parameter-{{ label|slugify }}">
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
{{ value }}
</li>
{% endfor %}
</ul>
{% endfor %}
</div>
</div>
<div aria-labelledby="tab-desks" hidden id="panel-desks" role="tabpanel" tabindex="0">
<div class="section">
{% for desk in object.desk_set.all %}
<h4>{{ desk }}</h4>
<ul>
{% for label, value in desk.get_inspect_fields %}
<li class="parameter-{{ label|slugify }}">
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
{{ value }}
</li>
{% endfor %}
</ul>
<h5>{% trans "Opening hours" %}</h5>
{% for time_period in desk.timeperiod_set.all %}
<h6>{{ time_period }}</h6>
<ul>
{% for label, value in time_period.get_inspect_fields %}
<li class="parameter-{{ label|slugify }}">
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
{{ value }}
</li>
{% endfor %}
</ul>
{% endfor %}
<h5>{% trans "Unavailability calendars" %}</h5>
<ul>
{% for unavailability_calendar in desk.unavailability_calendars.all %}
<li class="parameter-unavailability-calendar }}">
{{ unavailability_calendar }}
</li>
{% endfor %}
</ul>
<h5>{% trans "Exception sources" %}</h5>
{% for source in desk.timeperiodexceptionsource_set.all %}
<h6>{{ source }}</h6>
<ul>
{% for label, value in source.get_inspect_fields %}
<li class="parameter-{{ label|slugify }}">
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
{{ value }}
</li>
{% endfor %}
</ul>
{% endfor %}
<h5>{% trans "Exceptions" %}</h5>
{% for exception in desk.timeperiodexception_set.all %}
<h6>{{ exception }}</h6>
<ul>
{% for label, value in exception.get_inspect_fields %}
<li class="parameter-{{ label|slugify }}">
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
{{ value }}
</li>
{% endfor %}
</ul>
{% endfor %}
{% endfor %}
</div>
</div>
<div aria-labelledby="tab-resources" hidden id="panel-resources" role="tabpanel" tabindex="0">
<div class="section">
<ul>
{% for resource in object.resources.all %}
<li class="parameter-resource }}">
{{ resource }}
</li>
{% endfor %}
</ul>
</div>
</div>
{% elif object.kind == "virtual" %}
<div aria-labelledby="tab-agendas" hidden id="panel-agendas" role="tabpanel" tabindex="0">
<div class="section">
<ul>
{% for agenda in object.real_agendas.all %}
<li class="parameter-agenda }}">
{{ agenda }}
</li>
{% endfor %}
</ul>
</div>
</div>
<div aria-labelledby="tab-time-periods" hidden id="panel-time-periods" role="tabpanel" tabindex="0">
<div class="section">
{% for time_period in object.excluded_timeperiods.all %}
<h4>{{ time_period }}</h4>
<ul>
{% for label, value in time_period.get_inspect_fields %}
<li class="parameter-{{ label|slugify }}">
<span class="parameter">{% blocktrans %}{{ label }}:{% endblocktrans %}</span>
{{ value }}
</li>
{% endfor %}
</ul>
{% endfor %}
</div>
</div>
{% endif %}
</div>
</div>

View File

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

@ -1,19 +0,0 @@
{% 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

@ -1,29 +0,0 @@
{% extends "chrono/manager_category_history.html" %}
{% load gadjo static i18n %}
{% block css %}
{{ block.super }}
<link rel="stylesheet" type="text/css" media="all" href="{% static "css/gadjo.snapshotdiff.css" %}?{% start_timestamp %}"/>
{% endblock %}
{% block extrascripts %}
{{ block.super }}
<script src="{% static "js/gadjo.snapshotdiff.js" %}?{% start_timestamp %}"></script>
{% endblock %}
{% block appbar %}
<h2>{% trans 'Compare snapshots' %} ({% if mode == 'json' %}{% trans "JSON" %}{% else %}{% trans "Inspect" %}{% endif %})</h2>
<span class="actions">
<a href="?version1={{ snapshot1.pk }}&version2={{ snapshot2.pk }}&mode=inspect">{% trans "Compare inspect" %}</a>
<a href="?version1={{ snapshot1.pk }}&version2={{ snapshot2.pk }}&mode=json">{% trans "Compare JSON" %}</a>
</span>
{% endblock %}
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'chrono-manager-category-history-compare' pk=category.pk %}">{% trans "Compare snapshots" %}</a>
{% endblock %}
{% block content %}
{% include 'chrono/includes/snapshot_compare_fragment.html' %}
{% endblock %}

View File

@ -1,18 +0,0 @@
{% 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

@ -1,21 +0,0 @@
{% 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,6 +8,7 @@
{% 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,14 +20,6 @@
{% 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

@ -1,19 +0,0 @@
{% 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

@ -1,29 +0,0 @@
{% extends "chrono/manager_events_type_history.html" %}
{% load gadjo static i18n %}
{% block css %}
{{ block.super }}
<link rel="stylesheet" type="text/css" media="all" href="{% static "css/gadjo.snapshotdiff.css" %}?{% start_timestamp %}"/>
{% endblock %}
{% block extrascripts %}
{{ block.super }}
<script src="{% static "js/gadjo.snapshotdiff.js" %}?{% start_timestamp %}"></script>
{% endblock %}
{% block appbar %}
<h2>{% trans 'Compare snapshots' %} ({% if mode == 'json' %}{% trans "JSON" %}{% else %}{% trans "Inspect" %}{% endif %})</h2>
<span class="actions">
<a href="?version1={{ snapshot1.pk }}&version2={{ snapshot2.pk }}&mode=inspect">{% trans "Compare inspect" %}</a>
<a href="?version1={{ snapshot1.pk }}&version2={{ snapshot2.pk }}&mode=json">{% trans "Compare JSON" %}</a>
</span>
{% endblock %}
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'chrono-manager-events-type-history-compare' pk=events_type.pk %}">{% trans "Compare snapshots" %}</a>
{% endblock %}
{% block content %}
{% include 'chrono/includes/snapshot_compare_fragment.html' %}
{% endblock %}

View File

@ -1,18 +0,0 @@
{% 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

@ -1,47 +0,0 @@
{% 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

@ -1,5 +1,5 @@
{% extends "chrono/manager_base.html" %}
{% load i18n thumbnail chrono %}
{% load i18n thumbnail %}
{% block appbar %}
{% include 'chrono/includes/application_appbar_fragment.html' with title_no_application=_('Agendas outside applications') title_object_list=_('Agendas') %}
@ -16,23 +16,19 @@
{% if object_list %}
{% regroup object_list by category as agenda_groups %}
{% for group in agenda_groups %}
{% with i=group.grouper.id|stringformat:"s" %}
{% with foldname='foldable-manager-category-group-'|add:i %}
<div class="section foldable {% if user|get_preference:foldname %}folded{% endif %}" data-section-folded-pref-name="{{foldname}}">
{% endwith %}
{% endwith %}
{% if group.grouper %}<h3>{{ group.grouper }}</h3>{% elif not forloop.first %}<h3>{% trans "Misc" %}</h3>{% endif %}
<ul class="objects-list single-links">
{% for object in group.list %}
<li>
<a href="{% url 'chrono-manager-agenda-view' pk=object.id %}">
<span class="badge">{{ object.get_real_kind_display }}</span>
{% include 'chrono/includes/application_icon_fragment.html' %}
{{ object.label }}{% if user.is_staff %} <span class="identifier">[{% trans "identifier:" %} {{ object.slug }}]{% endif %}</span>
</a>
</li>
{% endfor %}
</ul>
<div class="section">
{% if group.grouper %}<h3>{{ group.grouper }}</h3>{% elif not forloop.first %}<h3>{% trans "Misc" %}</h3>{% endif %}
<ul class="objects-list single-links">
{% for object in group.list %}
<li>
<a href="{% url 'chrono-manager-agenda-view' pk=object.id %}">
<span class="badge">{{ object.get_real_kind_display }}</span>
{% include 'chrono/includes/application_icon_fragment.html' %}
{{ object.label }}{% if user.is_staff %} <span class="identifier">[{% trans "identifier:" %} {{ object.slug }}]{% endif %}</span>
</a>
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
{% elif not no_application %}
@ -75,9 +71,6 @@
<a class="button button-paragraph" href="{% url 'chrono-manager-ants-hub' %}">{% trans 'ANTS Hub' %}</a>
{% endif %}
{% endif %}
{% if user.is_staff and audit_journal_enabled %}
<a class="button button-paragraph" href="{% url 'chrono-manager-audit-journal' %}">{% trans 'Audit journal' %}</a>
{% endif %}
{% endif %}
{% include 'chrono/includes/application_list_fragment.html' with title_no_application=_('Agendas outside applications') %}

View File

@ -8,9 +8,11 @@
<colgroup>
<col class="name" />
{% for day in days %}
<col class="{% if day|date:"w" == "0" or day|date:"w" == "6" %}we{% endif %}
{% if today == day.date %}today{% endif %}
" />
{% if day|date:"w" == "0" or day|date:"w" == "6" %}
<col class="we" />
{% else %}
<col />
{% endif %}
{% endfor %}
</colgroup>
@ -18,7 +20,7 @@
<tr class="partial-booking-month--day-list">
<td></td>
{% for day in days %}
<th scope="col" class="partial-booking-month--day{% if today == day.date %} today{% endif %}">
<th scope="col" class="partial-booking-month--day {% if view.date.date == day.date %}today{% endif %}">
<a href="{% url 'chrono-manager-agenda-day-view' pk=agenda.pk year=day.date|date:"Y" month=day.date|date:"m" day=day.date|date:"d" %}">
<time datetime="{{ day|date:"Y-m-d" }}">{{ day|date:"d" }}</time>
</a>
@ -34,15 +36,14 @@
{% for booking in booking_info.bookings %}
<td class="registrant--day-cell">
{% if booking %}
{% if booking.check_css_class == 'present' %}
{% trans "Present" as booking_status %}
{% elif booking.check_css_class == 'absent' %}
{% trans "Absent" as booking_status %}
{% else %}
{% trans "Not checked" as booking_status %}
{% endif %}
<span title="{{ booking_status }}" class="booking {{ booking.check_css_class }}">
<span class="sr-only">{{ booking_status }}</span>
<span class="booking {{ booking.check_css_class }}">
{% if booking.check_css_class == 'present' %}
{% trans "Present" %}
{% elif booking.check_css_class == 'absent' %}
{% trans "Absent" %}
{% else %}
{% trans "Not checked" %}
{% endif %}
</span>
{% endif %}
</td>

View File

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

@ -1,19 +0,0 @@
{% 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

@ -1,29 +0,0 @@
{% extends "chrono/manager_resource_history.html" %}
{% load gadjo static i18n %}
{% block css %}
{{ block.super }}
<link rel="stylesheet" type="text/css" media="all" href="{% static "css/gadjo.snapshotdiff.css" %}?{% start_timestamp %}"/>
{% endblock %}
{% block extrascripts %}
{{ block.super }}
<script src="{% static "js/gadjo.snapshotdiff.js" %}?{% start_timestamp %}"></script>
{% endblock %}
{% block appbar %}
<h2>{% trans 'Compare snapshots' %} ({% if mode == 'json' %}{% trans "JSON" %}{% else %}{% trans "Inspect" %}{% endif %})</h2>
<span class="actions">
<a href="?version1={{ snapshot1.pk }}&version2={{ snapshot2.pk }}&mode=inspect">{% trans "Compare inspect" %}</a>
<a href="?version1={{ snapshot1.pk }}&version2={{ snapshot2.pk }}&mode=json">{% trans "Compare JSON" %}</a>
</span>
{% endblock %}
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'chrono-manager-resource-history-compare' pk=resource.pk %}">{% trans "Compare snapshots" %}</a>
{% endblock %}
{% block content %}
{% include 'chrono/includes/snapshot_compare_fragment.html' %}
{% endblock %}

View File

@ -1,18 +0,0 @@
{% 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

@ -1,22 +0,0 @@
{% 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

@ -1,19 +0,0 @@
{% 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

@ -1,29 +0,0 @@
{% extends "chrono/manager_unavailability_calendar_history.html" %}
{% load gadjo static i18n %}
{% block css %}
{{ block.super }}
<link rel="stylesheet" type="text/css" media="all" href="{% static "css/gadjo.snapshotdiff.css" %}?{% start_timestamp %}"/>
{% endblock %}
{% block extrascripts %}
{{ block.super }}
<script src="{% static "js/gadjo.snapshotdiff.js" %}?{% start_timestamp %}"></script>
{% endblock %}
{% block appbar %}
<h2>{% trans 'Compare snapshots' %} ({% if mode == 'json' %}{% trans "JSON" %}{% else %}{% trans "Inspect" %}{% endif %})</h2>
<span class="actions">
<a href="?version1={{ snapshot1.pk }}&version2={{ snapshot2.pk }}&mode=inspect">{% trans "Compare inspect" %}</a>
<a href="?version1={{ snapshot1.pk }}&version2={{ snapshot2.pk }}&mode=json">{% trans "Compare JSON" %}</a>
</span>
{% endblock %}
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'chrono-manager-unavailability-calendar-history-compare' pk=unavailability_calendar.pk %}">{% trans "Compare snapshots" %}</a>
{% endblock %}
{% block content %}
{% include 'chrono/includes/snapshot_compare_fragment.html' %}
{% endblock %}

View File

@ -1,18 +0,0 @@
{% 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

@ -1,53 +0,0 @@
{% 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,12 +54,6 @@
<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

@ -17,8 +17,6 @@
from django import template
from django.utils.formats import date_format
from chrono.apps.user_preferences.models import UserPreferences
register = template.Library()
@ -33,9 +31,3 @@ def human_date_range(date_start, date_end):
date_start_format = 'd'
return '%s %s' % (date_format(date_start, date_start_format), date_format(date_end, date_end_format))
@register.filter
def get_preference(user, pref_name):
user_preferences, dummy = UserPreferences.objects.get_or_create(user=user)
return user_preferences.preferences.get(pref_name) or False

View File

@ -65,21 +65,6 @@ 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'),
@ -105,24 +90,10 @@ 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'),
@ -131,17 +102,6 @@ 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'),
@ -489,13 +449,6 @@ 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,
@ -568,5 +521,4 @@ urlpatterns = [
),
re_path(r'^menu.json$', views.menu_json),
path('ants/', include('chrono.apps.ants_hub.urls')),
path('journal/', include('chrono.apps.journal.urls')),
]

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, Prefetch, Q, Value
from django.db.models import BooleanField, Count, ExpressionWrapper, F, Func, Max, Min, 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,15 +92,6 @@ from chrono.agendas.models import (
VirtualMember,
)
from chrono.apps.export_import.models import Application
from chrono.apps.journal.utils import audit
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
@ -222,7 +213,6 @@ class HomepageView(WithApplicationsMixin, ListView):
context['has_access_to_unavailability_calendars'] = self.has_access_to_unavailability_calendars()
context['shared_custody_enabled'] = settings.SHARED_CUSTODY_ENABLED
context['ants_hub_enabled'] = bool(settings.CHRONO_ANTS_HUB_URL)
context['audit_journal_enabled'] = settings.AUDIT_JOURNAL_ENABLED
context['with_sidebar'] = True
return self.with_applications_context_data(context)
@ -298,7 +288,6 @@ 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
@ -802,49 +791,6 @@ 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
@ -906,10 +852,6 @@ 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()
@ -934,49 +876,6 @@ 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
@ -1035,7 +934,6 @@ 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
@ -1122,50 +1020,6 @@ 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
@ -1398,7 +1252,6 @@ 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
@ -1761,7 +1614,6 @@ class EventChecksMixin:
)
subscription.presence_form = BookingCheckPresenceForm(
agenda=self.agenda,
subscription=True,
)
# sort results
if (
@ -2270,7 +2122,6 @@ class AgendaWeekMonthMixin:
self.first_day + datetime.timedelta(days=i)
for i in range((first_day_next_month - self.first_day).days)
]
context['today'] = localtime().date()
booking_info_by_user = {}
bookings = Booking.objects.filter(event__in=self.events).prefetch_related('user_checks')
@ -2579,7 +2430,6 @@ 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':
@ -3336,19 +3186,9 @@ class EventPresenceView(EventCheckMixin, ViewableAgendaMixin, FormView):
booking_check = BookingCheck(booking=booking, presence=True, **qs_kwargs)
booking_checks_to_create.append(booking_check)
with transaction.atomic():
audit(
'check:mark-unchecked-present',
request=request,
agenda=self.event.agenda,
extra_data={
'event': self.event,
'check_type_slug': qs_kwargs['type_slug'],
},
)
BookingCheck.objects.filter(booking__in=bookings).update(presence=True, **qs_kwargs)
BookingCheck.objects.bulk_create(booking_checks_to_create)
self.event.set_is_checked()
BookingCheck.objects.filter(booking__in=bookings).update(presence=True, **qs_kwargs)
BookingCheck.objects.bulk_create(booking_checks_to_create)
self.event.set_is_checked()
return self.response(request)
@ -3377,20 +3217,9 @@ class EventAbsenceView(EventCheckMixin, ViewableAgendaMixin, FormView):
booking_check = BookingCheck(booking=booking, presence=False, **qs_kwargs)
booking_checks_to_create.append(booking_check)
with transaction.atomic():
audit(
'check:mark-unchecked-absent',
request=request,
agenda=self.event.agenda,
extra_data={
'event': self.event,
'check_type_slug': qs_kwargs['type_slug'],
},
)
BookingCheck.objects.filter(booking__in=bookings).update(presence=False, **qs_kwargs)
BookingCheck.objects.bulk_create(booking_checks_to_create)
self.event.set_is_checked()
BookingCheck.objects.filter(booking__in=bookings).update(presence=False, **qs_kwargs)
BookingCheck.objects.bulk_create(booking_checks_to_create)
self.event.set_is_checked()
return self.response(request)
@ -3402,14 +3231,6 @@ class EventCheckedView(EventCheckMixin, ViewableAgendaMixin, View):
if not self.event.checked:
self.event.checked = True
self.event.save(update_fields=['checked'])
audit(
'check:mark',
request=request,
agenda=self.agenda,
extra_data={
'event': self.event,
},
)
self.event.async_notify_checked()
return self.response(request)
@ -4231,60 +4052,6 @@ 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
@ -4302,7 +4069,7 @@ class BookingCancelView(ViewableAgendaMixin, UpdateView):
def form_valid(self, form):
trigger_callback = not form.cleaned_data['disable_trigger']
try:
self.booking.cancel(trigger_callback, request=self.request)
self.booking.cancel(trigger_callback)
except requests.RequestException:
form.add_error(None, _('There has been an error sending cancellation notification to form.'))
form.add_error(None, _('Check this box if you are sure you want to proceed anyway.'))
@ -4387,7 +4154,6 @@ class PresenceViewMixin:
booking.mark_user_presence(
check_type_slug=check_type.slug if check_type else None,
check_type_label=check_type.label if check_type else None,
request=request,
)
return self.response(request, booking)
@ -4404,7 +4170,6 @@ class AbsenceViewMixin:
booking.mark_user_absence(
check_type_slug=check_type.slug if check_type else None,
check_type_label=check_type.label if check_type else None,
request=request,
)
return self.response(request, booking)
@ -4426,7 +4191,7 @@ booking_absence = BookingAbsenceView.as_view()
class BookingResetView(ViewableAgendaMixin, BookingCheckMixin, FormView):
def post(self, request, *args, **kwargs):
booking = self.get_booking(**kwargs)
booking.reset_user_was_present(request=request)
booking.reset_user_was_present()
return self.response(request, booking)
@ -4795,7 +4560,6 @@ 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
@ -4905,45 +4669,6 @@ 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

@ -62,9 +62,7 @@ INSTALLED_APPS = (
'chrono.manager',
'chrono.apps.ants_hub',
'chrono.apps.export_import',
'chrono.apps.journal',
'chrono.apps.snapshot',
'chrono.apps.user_preferences',
)
MIDDLEWARE = (
@ -208,8 +206,6 @@ REST_FRAMEWORK = {'EXCEPTION_HANDLER': 'chrono.api.utils.exception_handler'}
SHARED_CUSTODY_ENABLED = False
PARTIAL_BOOKINGS_ENABLED = False
SNAPSHOTS_ENABLED = False
AUDIT_JOURNAL_ENABLED = False
CHRONO_ANTS_HUB_URL = None

View File

@ -62,7 +62,6 @@ class CheckType:
slug: str
label: str
kind: str
unexpected_presence: bool = False
def get_agenda_check_types(agenda):
@ -74,12 +73,5 @@ 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'],
unexpected_presence=ct.get('unexpected_presence') or False,
)
)
check_types.append(CheckType(slug=ct['id'], label=ct['text'], kind=ct['kind']))
return check_types

4
debian/control vendored
View File

@ -14,9 +14,7 @@ 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},
@ -35,9 +33,9 @@ Depends: libcairo-gobject2,
python3-django-mellon,
python3-django-tenant-schemas,
python3-hobo (>= 1.34),
python3-icalendar,
python3-psycopg2,
python3-sorl-thumbnail,
python3-vobject,
uwsgi,
uwsgi-plugin-python3,
weasyprint,

View File

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

View File

@ -163,15 +163,12 @@ setup(
'gadjo',
'djangorestframework>=3.4,<3.15',
'django-filter<23.2',
'vobject',
'python-dateutil',
'icalendar<=4.0.3',
'recurring-ical-events<=2.0.1',
'pyquery',
'requests',
'workalendar',
'weasyprint',
'sorl-thumbnail',
'lxml',
],
zip_safe=False,
cmdclass={

View File

@ -58,7 +58,6 @@ def test_sync_ants_hub(db, hub, place_agenda, freezer):
'rdvs': [],
'url': '',
'ville': 'Newcity',
'logo_url': '',
}
assert len(payload['collectivites'][0]['lieux'][0]['plages']) == 39
assert payload['collectivites'][0]['lieux'][0]['plages'][0] == {

View File

@ -58,7 +58,6 @@ def ants_setup(db, freezer):
address='2 rue du four',
zipcode='99999',
city_name='Saint-Didier',
logo_url='https://saint-didier.fr/logo.png',
)
annexe = Place.objects.create(
id=2,
@ -526,7 +525,6 @@ def test_export_to_push(ants_setup):
'types_rdv': ['CNI', 'PASSPORT'],
},
],
'logo_url': 'https://saint-didier.fr/logo.png',
},
{
'full': True,
@ -620,7 +618,6 @@ def test_export_to_push(ants_setup):
'types_rdv': ['CNI', 'PASSPORT'],
},
],
'logo_url': '',
},
],
}

View File

@ -28,7 +28,7 @@ def admin_user():
return user
@pytest.fixture(params=['Europe/Brussels', 'Asia/Kolkata', 'America/Sao_Paulo'])
@pytest.fixture(params=['Europe/Brussels', 'Asia/Kolkata', 'Brazil/East'])
def time_zone(request, settings):
settings.TIME_ZONE = request.param

View File

@ -129,7 +129,6 @@ def test_datetime_api_label(app):
agenda=agenda,
)
resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug)
assert resp.json['data'][0]['primary_event'] is None
assert resp.json['data'][0]['text'] == 'Hello world'
assert resp.json['data'][0]['label'] == 'Hello world'
@ -198,7 +197,7 @@ def test_datetime_api_backoffice_url(app, admin_user):
assert event.label in app.get(url).text
def test_datetimes_api_min_max_places(app):
def test_datetimes_api_min_places(app):
agenda = Agenda.objects.create(label='Foo bar', kind='events')
event = Event.objects.create(start_datetime=now() + datetime.timedelta(days=7), places=5, agenda=agenda)
@ -212,30 +211,12 @@ def test_datetimes_api_min_max_places(app):
resp = app.get('/api/agenda/%s/datetimes/?min_places=5' % agenda.slug)
assert resp.json['data'][0]['disabled']
resp = app.get('/api/agenda/%s/datetimes/?max_places=4' % agenda.slug)
assert resp.json['data'][0]['disabled']
resp = app.get('/api/agenda/%s/datetimes/?max_places=5' % agenda.slug)
assert not resp.json['data'][0]['disabled']
resp = app.get('/api/agenda/%s/datetimes/?max_places=10&min_places=2' % agenda.slug)
assert not resp.json['data'][0]['disabled']
resp = app.get('/api/agenda/%s/datetimes/?min_places=' % agenda.slug)
assert not resp.json['data'][0]['disabled']
resp = app.get('/api/agenda/%s/datetimes/?max_places=' % agenda.slug)
assert not resp.json['data'][0]['disabled']
resp = app.get('/api/agenda/%s/datetimes/?max_places=&min_places=' % agenda.slug)
assert not resp.json['data'][0]['disabled']
resp = app.get('/api/agenda/%s/datetimes/?min_places=wrong' % agenda.slug, status=400)
assert resp.json['err'] == 1
resp = app.get('/api/agenda/%s/datetimes/?max_places=wrong' % agenda.slug, status=400)
assert resp.json['err'] == 1
def test_datetimes_api_(app):
events_type = EventsType.objects.create(
@ -652,15 +633,6 @@ def test_datetimes_api_meta(app, freezer):
'first_bookable_slot': resp.json['data'][1],
}
resp = app.get(api_url + '?max_places=15')
assert len(resp.json['data']) == 3
assert resp.json['meta'] == {
'no_bookable_datetimes': False,
'bookable_datetimes_number_total': 3,
'bookable_datetimes_number_available': 1,
'first_bookable_slot': resp.json['data'][0],
}
simulate_booking(events[0], 10)
resp = app.get(api_url)
assert len(resp.json['data']) == 3
@ -720,8 +692,6 @@ def test_recurring_events_api(app, user, freezer):
assert data[0]['id'] == 'abc--2021-01-19-1305'
assert data[0]['datetime'] == '2021-01-19 13:05:00'
assert data[0]['text'] == "Rock'n roll (Jan. 19, 2021, 1:05 p.m.)"
assert data[0]['label'] == "Rock'n roll"
assert data[0]['primary_event'] == 'abc'
assert data[3]['id'] == 'abc--2021-02-09-1305'
assert Event.objects.count() == 6
@ -743,7 +713,7 @@ def test_recurring_events_api(app, user, freezer):
# check querysets
with CaptureQueriesContext(connection) as ctx:
app.get('/api/agenda/%s/datetimes/' % agenda.slug)
assert len(ctx.captured_queries) == 4
assert len(ctx.captured_queries) == 3
# events follow agenda display template
agenda.event_display_template = '{{ event.label }} - {{ event.start_datetime }}'
@ -1188,43 +1158,30 @@ def test_past_datetimes_places(app, user):
assert resp.json['meta']['first_bookable_slot']['id'] == 'today-before-now'
def test_past_datetimes_min_max_places(app, user):
def test_past_datetimes_min_places(app, user):
agenda = Agenda.objects.create(
label='Foo bar', kind='events', minimal_booking_delay=0, maximal_booking_delay=30
)
Event.objects.create(
label='Today before now',
start_datetime=localtime(now() - datetime.timedelta(hours=1)),
places=10,
places=1,
agenda=agenda,
)
Event.objects.create(
label='Today after now',
start_datetime=localtime(now() + datetime.timedelta(hours=1)),
places=10,
places=1,
agenda=agenda,
)
resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug, params={'events': 'future', 'min_places': 20})
resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug, params={'events': 'future', 'min_places': 2})
data = resp.json['data']
assert len(data) == 1
assert data[0]['id'] == 'today-after-now'
assert data[0]['disabled'] is True
resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug, params={'events': 'past', 'min_places': 20})
data = resp.json['data']
assert len(data) == 1
assert data[0]['id'] == 'today-before-now'
assert data[0]['disabled'] is False # always available if past
assert resp.json['meta']['first_bookable_slot']['id'] == 'today-before-now'
resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug, params={'events': 'future', 'max_places': 5})
data = resp.json['data']
assert len(data) == 1
assert data[0]['id'] == 'today-after-now'
assert data[0]['disabled'] is True
resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug, params={'events': 'past', 'max_places': 5})
resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug, params={'events': 'past', 'min_places': 2})
data = resp.json['data']
assert len(data) == 1
assert data[0]['id'] == 'today-before-now'

View File

@ -34,14 +34,12 @@ def test_datetimes_multiple_agendas(app):
Desk.objects.create(agenda=first_agenda, slug='_exceptions_holder')
Event.objects.create(
slug='event',
label='Event',
start_datetime=now() + datetime.timedelta(days=5),
places=5,
agenda=first_agenda,
)
event = Event.objects.create( # base recurring event not visible in datetimes api
slug='recurring',
label='Recurring',
start_datetime=now() + datetime.timedelta(hours=1),
recurrence_days=[localtime().isoweekday()],
recurrence_end_date=now() + datetime.timedelta(days=15),
@ -62,18 +60,11 @@ def test_datetimes_multiple_agendas(app):
Booking.objects.create(event=event)
agenda_slugs = '%s,%s' % (first_agenda.slug, second_agenda.slug)
with CaptureQueriesContext(connection) as ctx:
resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda_slugs})
assert len(ctx.captured_queries) == 3
resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda_slugs})
assert len(resp.json['data']) == 5
assert resp.json['data'][0]['id'] == 'first-agenda@recurring--2021-05-06-1700'
assert resp.json['data'][0]['text'] == 'Recurring (May 6, 2021, 5 p.m.)'
assert resp.json['data'][0]['label'] == 'Recurring'
assert resp.json['data'][0]['primary_event'] == 'first-agenda@recurring'
assert resp.json['data'][1]['id'] == 'first-agenda@event'
assert resp.json['data'][1]['text'] == 'Event'
assert resp.json['data'][1]['label'] == 'Event'
assert resp.json['data'][1]['primary_event'] is None
assert resp.json['data'][1]['text'] == 'May 11, 2021, 4 p.m.'
assert resp.json['data'][1]['places']['available'] == 5
assert resp.json['data'][2]['id'] == 'second-agenda@event'
@ -104,27 +95,13 @@ def test_datetimes_multiple_agendas(app):
assert 'booked_for_external_user' not in resp.json['data'][2]
assert resp.json['data'][2]['disabled'] is True
# check min_places & max_places
# check min_places
resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda_slugs, 'min_places': 4})
assert resp.json['data'][1]['disabled'] is False
assert resp.json['data'][2]['disabled'] is True
resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda_slugs, 'max_places': 3})
assert resp.json['data'][1]['disabled'] is True
assert resp.json['data'][2]['disabled'] is True
resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda_slugs, 'max_places': 4})
assert resp.json['data'][1]['disabled'] is True
assert resp.json['data'][2]['disabled'] is False
resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda_slugs, 'max_places': 10})
assert resp.json['data'][1]['disabled'] is False
assert resp.json['data'][2]['disabled'] is False
# check meta
resp = app.get(
'/api/agendas/datetimes/', params={'agendas': agenda_slugs, 'min_places': 4, 'max_places': 10}
)
resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda_slugs, 'min_places': 4})
assert resp.json['meta']['bookable_datetimes_number_total'] == 5
assert resp.json['meta']['bookable_datetimes_number_available'] == 4
assert resp.json['meta']['first_bookable_slot'] == resp.json['data'][0]

View File

@ -434,7 +434,7 @@ def test_recurring_events_api_list_multiple_agendas_queries(app):
'/api/agendas/recurring-events/?subscribed=category-a&user_external_id=xxx&guardian_external_id=father_id'
)
assert len(resp.json['data']) == 40
assert len(ctx.captured_queries) == 6
assert len(ctx.captured_queries) == 5
@pytest.mark.freeze_time('2021-09-06 12:00')

View File

@ -1248,7 +1248,7 @@ def test_agenda_meeting_api_multiple_desk(app, user):
with CaptureQueriesContext(connection) as ctx:
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.pk, event_id))
assert len(ctx.captured_queries) == 19
assert len(ctx.captured_queries) == 18
assert resp_booking.json['datetime'] == localtime(Booking.objects.last().event.start_datetime).strftime(
'%Y-%m-%d %H:%M:%S'
@ -1283,7 +1283,7 @@ def test_agenda_meeting_api_multiple_desk(app, user):
booking_url = event_data['api']['fillslot_url']
with CaptureQueriesContext(connection) as ctx:
app.post(booking_url)
assert len(ctx.captured_queries) == 18
assert len(ctx.captured_queries) == 17
with CaptureQueriesContext(connection) as ctx:
app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
@ -1678,24 +1678,24 @@ def test_duration_on_booking_api_fillslot_response(app, user):
assert resp.json['end_datetime'] is None
assert 'ics_url' in resp.json['api']
ics = app.get(resp.json['api']['ics_url']).text
assert 'DTSTART;VALUE=DATE-TIME:20170519T231200Z' in ics
assert 'DTEND' not in ics
assert 'DTSTART:20170519T231200Z' in ics
assert 'DTEND:' not in ics
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, evt[1].id))
assert resp.json['datetime'] == '2017-05-21 01:12:00'
assert resp.json['end_datetime'] == resp.json['datetime']
assert 'ics_url' in resp.json['api']
ics = app.get(resp.json['api']['ics_url']).text
assert 'DTSTART;VALUE=DATE-TIME:20170520T231200Z' in ics
assert 'DTEND;VALUE=DATE-TIME:20170520T231200Z' in ics
assert 'DTSTART:20170520T231200Z' in ics
assert 'DTEND:20170520T231200Z' in ics
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, evt[2].id))
assert resp.json['datetime'] == '2017-05-22 01:12:00'
assert resp.json['end_datetime'] == '2017-05-22 01:57:00'
assert 'ics_url' in resp.json['api']
ics = app.get(resp.json['api']['ics_url']).text
assert 'DTSTART;VALUE=DATE-TIME:20170521T231200Z' in ics
assert 'DTEND;VALUE=DATE-TIME:20170521T235700Z' in ics
assert 'DTSTART:20170521T231200Z' in ics
assert 'DTEND:20170521T235700Z' in ics
def test_fillslot_past_event(app, user):

View File

@ -711,7 +711,7 @@ def test_api_events_fillslots_with_lock_code(app, user, freezer):
'waiting_list_places': 1,
},
'Event 2': {
'start_datetime': now() + datetime.timedelta(days=2),
'start_datetime': now() + datetime.timedelta(days=1),
'places': 2,
'waiting_list_places': 1,
},
@ -807,7 +807,7 @@ def test_api_events_fillslots_with_lock_code_expiration(app, user, freezer):
'waiting_list_places': 1,
},
'Event 2': {
'start_datetime': now() + datetime.timedelta(days=2),
'start_datetime': now() + datetime.timedelta(days=1),
'places': 2,
'waiting_list_places': 1,
},
@ -859,82 +859,3 @@ def test_api_events_fillslots_with_lock_code_expiration(app, user, freezer):
assert response.json['data'][0]['places']['reserved'] == 0
assert response.json['data'][1]['places']['available'] == 2
assert response.json['data'][1]['places']['reserved'] == 0
def test_waitin_list_places_using_lock_code(app, user, freezer):
agenda = build_event_agenda(
events={
'Event 1': {
'start_datetime': now() + datetime.timedelta(days=1),
'places': 2,
'waiting_list_places': 3,
}
}
)
# setup authorization
app.authorization = ('Basic', ('john.doe', 'password'))
# list events
resp = app.get(agenda.get_datetimes_url())
slot = resp.json['data'][0]
assert slot['places']['available'] == 2
assert slot['places']['full'] is False
# book first one
fillslot_url = slot['api']['fillslot_url']
datas = [app.post_json(fillslot_url, params={'lock_code': f'MYLOCK{i}'}).json for i in range(4)]
assert all(data['err'] == 0 for data in datas), 'Not all responses are ok'
# cancel second booking (in main list)
resp = app.post_json(datas[1]['api']['cancel_url'])
assert resp.json['err'] == 0
# cancel fourth booking (in waiting list)
resp = app.post_json(datas[3]['api']['cancel_url'])
assert resp.json['err'] == 0
# list events without lock code
resp = app.get(agenda.get_datetimes_url())
places = resp.json['data'][0]['places']
assert places == {
'total': 2,
'reserved': 1,
'available': 1,
'full': False,
'has_waiting_list': True,
'waiting_list_total': 3,
'waiting_list_reserved': 1,
'waiting_list_available': 2,
'waiting_list_activated': True,
}
# list events with lock code of first booking (in main list)
resp = app.get(agenda.get_datetimes_url(), params={'lock_code': 'MYLOCK0', 'hide_disabled': 'true'})
places = resp.json['data'][0]['places']
assert places == {
'total': 2,
'reserved': 0,
'available': 2,
'full': False,
'has_waiting_list': True,
'waiting_list_total': 3,
'waiting_list_reserved': 1,
'waiting_list_available': 2,
'waiting_list_activated': True,
}
# list events with lock code of third booking (in waiting list)
resp = app.get(agenda.get_datetimes_url(), params={'lock_code': 'MYLOCK2', 'hide_disabled': 'true'})
places = resp.json['data'][0]['places']
assert places == {
'total': 2,
'reserved': 1,
'available': 1,
'full': False,
'has_waiting_list': True,
'waiting_list_total': 3,
'waiting_list_reserved': 0,
'waiting_list_available': 3,
'waiting_list_activated': False,
}

View File

@ -936,7 +936,6 @@ def test_api_events_fillslots_multiple_agendas_revert(app, user):
'cancelled_events': [
{
'agenda_label': 'Foo bar',
'agenda_slug': 'foo-bar',
'check_locked': False,
'checked': False,
'date': '2021-02-28',
@ -951,7 +950,6 @@ def test_api_events_fillslots_multiple_agendas_revert(app, user):
'slug': 'event',
'text': 'Event',
'url': None,
'primary_event': None,
}
],
'deleted_booking_count': 0,
@ -974,7 +972,6 @@ def test_api_events_fillslots_multiple_agendas_revert(app, user):
'cancelled_events': [
{
'agenda_label': 'Foo bar',
'agenda_slug': 'foo-bar',
'check_locked': False,
'checked': False,
'date': '2021-02-28',
@ -989,7 +986,6 @@ def test_api_events_fillslots_multiple_agendas_revert(app, user):
'slug': 'event',
'text': 'Event',
'url': None,
'primary_event': None,
}
],
'deleted_booking_count': 0,
@ -1018,7 +1014,6 @@ def test_api_events_fillslots_multiple_agendas_revert(app, user):
'booked_events': [
{
'agenda_label': 'Foo bar',
'agenda_slug': 'foo-bar',
'check_locked': False,
'checked': False,
'date': '2021-02-28',
@ -1033,7 +1028,6 @@ def test_api_events_fillslots_multiple_agendas_revert(app, user):
'slug': 'event',
'text': 'Event',
'url': None,
'primary_event': None,
}
],
}
@ -1056,7 +1050,6 @@ def test_api_events_fillslots_multiple_agendas_revert(app, user):
'booked_events': [
{
'agenda_label': 'Foo bar',
'agenda_slug': 'foo-bar',
'check_locked': False,
'checked': False,
'date': '2021-02-28',
@ -1071,7 +1064,6 @@ def test_api_events_fillslots_multiple_agendas_revert(app, user):
'slug': 'event',
'text': 'Event',
'url': None,
'primary_event': None,
}
],
}
@ -1094,7 +1086,6 @@ def test_api_events_fillslots_multiple_agendas_revert(app, user):
'deleted_events': [
{
'agenda_label': 'Foo bar',
'agenda_slug': 'foo-bar',
'check_locked': False,
'checked': False,
'date': '2021-02-28',
@ -1109,7 +1100,6 @@ def test_api_events_fillslots_multiple_agendas_revert(app, user):
'slug': 'event',
'text': 'Event',
'url': None,
'primary_event': None,
}
],
'booked_booking_count': 0,
@ -1134,7 +1124,6 @@ def test_api_events_fillslots_multiple_agendas_revert(app, user):
'deleted_events': [
{
'agenda_label': 'Foo bar',
'agenda_slug': 'foo-bar',
'check_locked': False,
'checked': False,
'date': '2021-02-28',
@ -1149,7 +1138,6 @@ def test_api_events_fillslots_multiple_agendas_revert(app, user):
'slug': 'event',
'text': 'Event',
'url': None,
'primary_event': None,
}
],
'booked_booking_count': 0,
@ -1181,14 +1169,10 @@ def test_api_events_fillslots_multiple_agendas_revert(app, user):
duration=120,
places=1,
agenda=agenda,
recurrence_days=[7],
recurrence_end_date=now() + datetime.timedelta(days=14), # 2 weeks
)
event.create_all_recurrences()
event = event.recurrences.first()
Booking.objects.create(
event=event, request_uuid=request_uuid, previous_state='unbooked', cancellation_datetime=now()
)
with CaptureQueriesContext(connection) as ctx:
resp = app.post(revert_url)
assert len(ctx.captured_queries) == 15
assert len(ctx.captured_queries) == 14

View File

@ -107,7 +107,7 @@ def test_recurring_events_api_fillslots(app, user, freezer, action):
params['user_external_id'] = 'user_id_3'
with CaptureQueriesContext(connection) as ctx:
resp = app.post_json(fillslots_url, params=params)
assert len(ctx.captured_queries) in [15, 16]
assert len(ctx.captured_queries) in [12, 13]
# everything goes in waiting list
assert events.filter(booked_waiting_list_places=1).count() == 6
# but an event was full
@ -1368,7 +1368,7 @@ def test_recurring_events_api_fillslots_multiple_agendas_queries(app, user):
)
assert resp.json['booking_count'] == 180
assert resp.json['cancelled_booking_count'] == 0
assert len(ctx.captured_queries) == 17
assert len(ctx.captured_queries) == 15
with CaptureQueriesContext(connection) as ctx:
resp = app.post_json(
@ -1382,7 +1382,7 @@ def test_recurring_events_api_fillslots_multiple_agendas_queries(app, user):
)
assert resp.json['booking_count'] == 0
assert resp.json['cancelled_booking_count'] == 5
assert len(ctx.captured_queries) == 18
assert len(ctx.captured_queries) == 17
father = Person.objects.create(user_external_id='father_id', first_name='John', last_name='Doe')
mother = Person.objects.create(user_external_id='mother_id', first_name='Jane', last_name='Doe')
@ -1401,7 +1401,7 @@ def test_recurring_events_api_fillslots_multiple_agendas_queries(app, user):
params={'slots': events_to_book, 'user_external_id': 'xxx'},
)
assert resp.json['booking_count'] == 100
assert len(ctx.captured_queries) == 16
assert len(ctx.captured_queries) == 14
@pytest.mark.freeze_time('2022-03-07 14:00') # Monday of 10th week

View File

@ -320,7 +320,7 @@ def test_agendas_api(settings, app):
with CaptureQueriesContext(connection) as ctx:
resp = app.get('/api/agenda/', params={'with_open_events': '1'})
assert len(ctx.captured_queries) == 4
assert len(ctx.captured_queries) == 3
def test_agenda_detail_api(app):

View File

@ -9,16 +9,12 @@ from django.contrib.contenttypes.models import ContentType
from chrono.agendas.models import Agenda, Category, Desk, EventsType, Resource, UnavailabilityCalendar
from chrono.apps.export_import.models import Application, ApplicationElement
from chrono.apps.snapshot.models import AgendaSnapshot
pytestmark = pytest.mark.django_db
def test_object_types(app, user, admin_user):
def test_object_types(app, 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': [
@ -67,8 +63,8 @@ def test_object_types(app, user, admin_user):
}
def test_list(app, admin_user):
app.authorization = ('Basic', ('admin', 'admin'))
def test_list(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
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')
@ -166,12 +162,9 @@ def test_list(app, admin_user):
'data': [{'id': group.pk, 'text': 'group1', 'type': 'roles', 'urls': {}, 'uuid': None}]
}
# unknown component type
app.get('/api/export-import/unknown/', status=404)
def test_export_agenda(app, admin_user):
app.authorization = ('Basic', ('admin', 'admin'))
def test_export_agenda(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
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)
@ -180,8 +173,8 @@ def test_export_agenda(app, admin_user):
assert resp.json['data']['permissions'] == {'view': 'group2', 'edit': 'group1'}
def test_export_minor_components(app, admin_user):
app.authorization = ('Basic', ('admin', 'admin'))
def test_export_minor_components(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
Category.objects.create(slug='cat', label='Category')
Resource.objects.create(slug='foo', label='Foo')
EventsType.objects.create(slug='foo', label='Foo')
@ -196,15 +189,9 @@ def test_export_minor_components(app, admin_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)
# unknown component type
app.get('/api/export-import/unknown/foo/', status=404)
def test_agenda_dependencies_category(app, admin_user):
app.authorization = ('Basic', ('admin', 'admin'))
def test_agenda_dependencies_category(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
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/')
@ -225,8 +212,8 @@ def test_agenda_dependencies_category(app, admin_user):
}
def test_agenda_dependencies_resources(app, admin_user):
app.authorization = ('Basic', ('admin', 'admin'))
def test_agenda_dependencies_resources(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
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/')
@ -247,8 +234,8 @@ def test_agenda_dependencies_resources(app, admin_user):
}
def test_agenda_dependencies_unavailability_calendars(app, admin_user):
app.authorization = ('Basic', ('admin', 'admin'))
def test_agenda_dependencies_unavailability_calendars(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
meetings_agenda = Agenda.objects.create(label='Rdv', slug='rdv', kind='meetings')
desk = Desk.objects.create(slug='foo', label='Foo', agenda=meetings_agenda)
unavailability_calendar = UnavailabilityCalendar.objects.create(slug='foo', label='Foo')
@ -290,8 +277,8 @@ def test_agenda_dependencies_unavailability_calendars(app, admin_user):
}
def test_agenda_dependencies_groups(app, admin_user):
app.authorization = ('Basic', ('admin', 'admin'))
def test_agenda_dependencies_groups(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
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)
@ -307,8 +294,8 @@ def test_agenda_dependencies_groups(app, admin_user):
}
def test_agenda_dependencies_virtual_agendas(app, admin_user):
app.authorization = ('Basic', ('admin', 'admin'))
def test_agenda_dependencies_virtual_agendas(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
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')
@ -342,8 +329,8 @@ def test_agenda_dependencies_virtual_agendas(app, admin_user):
}
def test_agenda_dependencies_events_type(app, admin_user):
app.authorization = ('Basic', ('admin', 'admin'))
def test_agenda_dependencies_events_type(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
events_type = EventsType.objects.create(slug='foo', label='Foo')
events_agenda = Agenda.objects.create(label='Evt', slug='evt', kind='events', events_type=events_type)
Desk.objects.create(agenda=events_agenda, slug='_exceptions_holder')
@ -365,17 +352,8 @@ def test_agenda_dependencies_events_type(app, admin_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_unknown_compoment_type_dependencies(app, admin_user):
app.authorization = ('Basic', ('admin', 'admin'))
app.get('/api/export-import/unknown/foo/dependencies/', status=404)
def test_redirect(app):
def test_redirect(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
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')
@ -385,64 +363,26 @@ def test_redirect(app):
redirect_url = f'/api/export-import/agendas/{agenda.slug}/redirect/'
resp = app.get(redirect_url, status=302)
assert resp.location == f'/manage/agendas/{agenda.pk}/'
resp = app.get(redirect_url + '?compare', status=302)
assert resp.location == f'/manage/agendas/{agenda.pk}/'
resp = app.get(redirect_url + '?compare&version1=bar&version2=bar&application=foo', status=302)
assert (
resp.location
== f'/manage/agendas/{agenda.pk}/history/compare/?version1=bar&version2=bar&application=foo'
)
redirect_url = f'/api/export-import/agendas_categories/{category.slug}/redirect/'
resp = app.get(redirect_url, status=302)
assert resp.location == '/manage/categories/'
resp = app.get(redirect_url + '?compare', status=302)
assert resp.location == '/manage/categories/'
resp = app.get(redirect_url + '?compare&version1=bar&version2=bar&application=foo', status=302)
assert (
resp.location
== f'/manage/category/{category.pk}/history/compare/?version1=bar&version2=bar&application=foo'
)
redirect_url = f'/api/export-import/resources/{resource.slug}/redirect/'
resp = app.get(redirect_url, status=302)
assert resp.location == f'/manage/resource/{resource.pk}/'
resp = app.get(redirect_url + '?compare', status=302)
assert resp.location == f'/manage/resource/{resource.pk}/'
resp = app.get(redirect_url + '?compare&version1=bar&version2=bar&application=foo', status=302)
assert (
resp.location
== f'/manage/resource/{resource.pk}/history/compare/?version1=bar&version2=bar&application=foo'
)
redirect_url = f'/api/export-import/events_types/{events_type.slug}/redirect/'
resp = app.get(redirect_url, status=302)
assert resp.location == '/manage/events-types/'
resp = app.get(redirect_url + '?compare', status=302)
assert resp.location == '/manage/events-types/'
resp = app.get(redirect_url + '?compare&version1=bar&version2=bar&application=foo', status=302)
assert (
resp.location
== f'/manage/events-type/{events_type.pk}/history/compare/?version1=bar&version2=bar&application=foo'
)
redirect_url = f'/api/export-import/unavailability_calendars/{unavailability_calendar.slug}/redirect/'
resp = app.get(redirect_url, status=302)
assert resp.location == f'/manage/unavailability-calendar/{unavailability_calendar.pk}/'
resp = app.get(redirect_url + '?compare', status=302)
assert resp.location == f'/manage/unavailability-calendar/{unavailability_calendar.pk}/'
resp = app.get(redirect_url + '?compare&version1=bar&version2=bar&application=foo', status=302)
assert (
resp.location
== f'/manage/unavailability-calendar/{unavailability_calendar.pk}/history/compare/?version1=bar&version2=bar&application=foo'
)
# unknown component type
app.get('/api/export-import/unknown/foo/redirect/', status=404)
def create_bundle(app, admin_user, visible=True, version_number='42.0'):
app.authorization = ('Basic', ('admin', 'admin'))
def create_bundle(app, user, visible=True, version_number='42.0'):
app.authorization = ('Basic', ('john.doe', 'password'))
group, _ = Group.objects.get_or_create(name='plop')
category, _ = Category.objects.get_or_create(slug='foo', label='Foo')
@ -527,12 +467,12 @@ def bundle(app, user):
return create_bundle(app, user)
def test_bundle_import(app, admin_user):
app.authorization = ('Basic', ('admin', 'admin'))
def test_bundle_import(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
bundles = []
for version_number in ['42.0', '42.1']:
bundles.append(create_bundle(app, admin_user, version_number=version_number))
bundles.append(create_bundle(app, user, version_number=version_number))
Agenda.objects.all().delete()
Category.objects.all().delete()
@ -540,7 +480,7 @@ def test_bundle_import(app, admin_user):
EventsType.objects.all().delete()
UnavailabilityCalendar.objects.all().delete()
resp = app.post('/api/export-import/bundle-import/', upload_files=[('bundle', 'bundle.tar', bundles[0])])
resp = app.put('/api/export-import/bundle-import/', bundles[0])
assert Agenda.objects.all().count() == 4
assert resp.json['err'] == 0
assert Application.objects.count() == 1
@ -555,12 +495,6 @@ def test_bundle_import(app, admin_user):
assert application.editable is False
assert application.visible is True
assert ApplicationElement.objects.count() == 8
for model in [Agenda, Category, EventsType, Resource, UnavailabilityCalendar]:
for instance in model.objects.all():
last_snapshot = model.get_snapshot_model().objects.filter(instance=instance).latest('pk')
assert last_snapshot.comment == 'Application (Test)'
assert last_snapshot.application_slug == 'test'
assert last_snapshot.application_version == '42.0'
# check editable flag is kept on install
application.editable = True
@ -575,7 +509,7 @@ def test_bundle_import(app, admin_user):
)
# check update
resp = app.post('/api/export-import/bundle-import/', upload_files=[('bundle', 'bundle.tar', bundles[1])])
resp = app.put('/api/export-import/bundle-import/', bundles[1])
assert Agenda.objects.all().count() == 4
assert resp.json['err'] == 0
assert Application.objects.count() == 1
@ -590,61 +524,13 @@ def test_bundle_import(app, admin_user):
).exists()
is False
)
for model in [Agenda, Category, EventsType, Resource, UnavailabilityCalendar]:
for instance in model.objects.all():
last_snapshot = model.get_snapshot_model().objects.filter(instance=instance).latest('pk')
assert last_snapshot.comment == 'Application (Test)'
assert last_snapshot.application_slug == 'test'
assert last_snapshot.application_version == '42.1'
# bad file format
resp = app.post(
'/api/export-import/bundle-import/', upload_files=[('bundle', 'bundle.tar', b'garbage')], status=400
)
assert resp.json['err']
assert resp.json['err_desc'] == 'Invalid tar file'
# missing manifest
tar_io = io.BytesIO()
with tarfile.open(mode='w', fileobj=tar_io) as tar:
foo_fd = io.BytesIO(json.dumps({'foo': 'bar'}, indent=2).encode())
tarinfo = tarfile.TarInfo('foo.json')
tarinfo.size = len(foo_fd.getvalue())
tar.addfile(tarinfo, fileobj=foo_fd)
resp = app.post(
'/api/export-import/bundle-import/',
upload_files=[('bundle', 'bundle.tar', tar_io.getvalue())],
status=400,
)
assert resp.json['err']
assert resp.json['err_desc'] == 'Invalid tar file, missing manifest'
# missing component
tar_io = io.BytesIO()
with tarfile.open(mode='w', fileobj=tar_io) as tar:
manifest_json = {
'application': 'Test',
'slug': 'test',
'elements': [{'type': 'agendas', 'slug': 'foo', 'name': 'foo'}],
}
manifest_fd = io.BytesIO(json.dumps(manifest_json, indent=2).encode())
tarinfo = tarfile.TarInfo('manifest.json')
tarinfo.size = len(manifest_fd.getvalue())
tar.addfile(tarinfo, fileobj=manifest_fd)
resp = app.post(
'/api/export-import/bundle-import/',
upload_files=[('bundle', 'bundle.tar', tar_io.getvalue())],
status=400,
)
assert resp.json['err']
assert resp.json['err_desc'] == 'Invalid tar file, missing component agendas/foo'
def test_bundle_declare(app, admin_user):
app.authorization = ('Basic', ('admin', 'admin'))
def test_bundle_declare(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
bundle = create_bundle(app, admin_user, visible=False)
resp = app.post('/api/export-import/bundle-declare/', upload_files=[('bundle', 'bundle.tar', bundle)])
bundle = create_bundle(app, user, visible=False)
resp = app.put('/api/export-import/bundle-declare/', bundle)
assert Agenda.objects.all().count() == 4
assert resp.json['err'] == 0
assert Application.objects.count() == 1
@ -660,7 +546,7 @@ def test_bundle_declare(app, admin_user):
assert application.visible is False
assert ApplicationElement.objects.count() == 8
bundle = create_bundle(app, admin_user, visible=True)
bundle = create_bundle(app, user, visible=True)
# create link to element not present in manifest: it should be unlinked
last_page = Agenda.objects.latest('pk')
ApplicationElement.objects.create(
@ -668,60 +554,18 @@ def test_bundle_declare(app, admin_user):
content_type=ContentType.objects.get_for_model(Agenda),
object_id=last_page.pk + 1,
)
# and remove agendas to have unknown references in manifest
# and remove agendas to have unkown references in manifest
Agenda.objects.all().delete()
resp = app.post('/api/export-import/bundle-declare/', upload_files=[('bundle', 'bundle.tar', bundle)])
resp = app.put('/api/export-import/bundle-declare/', bundle)
assert Application.objects.count() == 1
application = Application.objects.latest('pk')
assert application.visible is True
assert ApplicationElement.objects.count() == 4 # category, events_type, unavailability_calendar, resource
# bad file format
resp = app.post(
'/api/export-import/bundle-declare/', upload_files=[('bundle', 'bundle.tar', b'garbage')], status=400
)
assert resp.json['err']
assert resp.json['err_desc'] == 'Invalid tar file'
# missing manifest
tar_io = io.BytesIO()
with tarfile.open(mode='w', fileobj=tar_io) as tar:
foo_fd = io.BytesIO(json.dumps({'foo': 'bar'}, indent=2).encode())
tarinfo = tarfile.TarInfo('foo.json')
tarinfo.size = len(foo_fd.getvalue())
tar.addfile(tarinfo, fileobj=foo_fd)
resp = app.post(
'/api/export-import/bundle-declare/',
upload_files=[('bundle', 'bundle.tar', tar_io.getvalue())],
status=400,
)
assert resp.json['err']
assert resp.json['err_desc'] == 'Invalid tar file, missing manifest'
# missing component
tar_io = io.BytesIO()
with tarfile.open(mode='w', fileobj=tar_io) as tar:
manifest_json = {
'application': 'Test',
'slug': 'test',
'elements': [{'type': 'agendas', 'slug': 'foo', 'name': 'foo'}],
}
manifest_fd = io.BytesIO(json.dumps(manifest_json, indent=2).encode())
tarinfo = tarfile.TarInfo('manifest.json')
tarinfo.size = len(manifest_fd.getvalue())
tar.addfile(tarinfo, fileobj=manifest_fd)
resp = app.post(
'/api/export-import/bundle-declare/',
upload_files=[('bundle', 'bundle.tar', tar_io.getvalue())],
status=400,
)
assert resp.json['err']
assert resp.json['err_desc'] == 'Invalid tar file, missing component agendas/foo'
def test_bundle_unlink(app, admin_user, bundle):
app.authorization = ('Basic', ('admin', 'admin'))
def test_bundle_unlink(app, user, bundle):
app.authorization = ('Basic', ('john.doe', 'password'))
application = Application.objects.create(
name='Test',
@ -775,295 +619,6 @@ def test_bundle_unlink(app, admin_user, bundle):
assert ApplicationElement.objects.count() == 2
def test_bundle_check(app, admin_user):
app.authorization = ('Basic', ('admin', 'admin'))
bundles = []
for version_number in ['42.0', '42.1']:
bundles.append(create_bundle(app, admin_user, version_number=version_number))
Agenda.objects.all().delete()
Category.objects.all().delete()
Resource.objects.all().delete()
EventsType.objects.all().delete()
UnavailabilityCalendar.objects.all().delete()
incomplete_bundles = []
for manifest_json in [{'slug': 'test'}, {'version_number': '1.0'}]:
tar_io = io.BytesIO()
with tarfile.open(mode='w', fileobj=tar_io) as tar:
manifest_fd = io.BytesIO(json.dumps(manifest_json, indent=2).encode())
tarinfo = tarfile.TarInfo('manifest.json')
tarinfo.size = len(manifest_fd.getvalue())
tar.addfile(tarinfo, fileobj=manifest_fd)
incomplete_bundles.append(tar_io.getvalue())
# incorrect bundles, missing information
resp = app.post(
'/api/export-import/bundle-check/', upload_files=[('bundle', 'bundle.tar', incomplete_bundles[0])]
)
assert resp.json == {'data': {}}
resp = app.post(
'/api/export-import/bundle-check/', upload_files=[('bundle', 'bundle.tar', incomplete_bundles[1])]
)
assert resp.json == {'data': {}}
# not yet imported
resp = app.post('/api/export-import/bundle-check/', upload_files=[('bundle', 'bundle.tar', bundles[0])])
assert resp.json == {
'data': {
'differences': [],
'no_history_elements': [],
'unknown_elements': [
{'slug': 'rdv', 'type': 'agendas'},
{'slug': 'foo', 'type': 'categories'},
{'slug': 'foo', 'type': 'resources'},
{'slug': 'foo', 'type': 'unavailability_calendars'},
{'slug': 'evt', 'type': 'agendas'},
{'slug': 'foo', 'type': 'events_types'},
{'slug': 'virt', 'type': 'agendas'},
{'slug': 'sub', 'type': 'agendas'},
],
'legacy_elements': [],
}
}
# import bundle
resp = app.post('/api/export-import/bundle-import/', upload_files=[('bundle', 'bundle.tar', bundles[0])])
assert Application.objects.count() == 1
assert ApplicationElement.objects.count() == 8
# remove application links
Application.objects.all().delete()
resp = app.post('/api/export-import/bundle-check/', upload_files=[('bundle', 'bundle.tar', bundles[0])])
assert resp.json == {
'data': {
'differences': [],
'no_history_elements': [],
'unknown_elements': [],
'legacy_elements': [
{
'slug': 'rdv',
'text': 'Rdv',
'type': 'agendas',
'url': '/api/export-import/agendas/rdv/redirect/',
},
{
'slug': 'foo',
'text': 'Foo',
'type': 'agendas_categories',
'url': '/api/export-import/agendas_categories/foo/redirect/',
},
{
'slug': 'foo',
'text': 'Foo',
'type': 'resources',
'url': '/api/export-import/resources/foo/redirect/',
},
{
'slug': 'foo',
'text': 'Foo',
'type': 'unavailability_calendars',
'url': '/api/export-import/unavailability_calendars/foo/redirect/',
},
{
'slug': 'evt',
'text': 'Evt',
'type': 'agendas',
'url': '/api/export-import/agendas/evt/redirect/',
},
{
'slug': 'foo',
'text': 'Foo',
'type': 'events_types',
'url': '/api/export-import/events_types/foo/redirect/',
},
{
'slug': 'virt',
'text': 'Virt',
'type': 'agendas',
'url': '/api/export-import/agendas/virt/redirect/',
},
{
'slug': 'sub',
'text': 'Sub',
'type': 'agendas',
'url': '/api/export-import/agendas/sub/redirect/',
},
],
}
}
# import bundle again, recreate links
resp = app.post('/api/export-import/bundle-import/', upload_files=[('bundle', 'bundle.tar', bundles[0])])
assert Application.objects.count() == 1
assert ApplicationElement.objects.count() == 8
# no changes since last import
resp = app.post('/api/export-import/bundle-check/', upload_files=[('bundle', 'bundle.tar', bundles[0])])
assert resp.json == {
'data': {
'differences': [],
'unknown_elements': [],
'no_history_elements': [],
'legacy_elements': [],
}
}
# add local changes
snapshots = {}
for model in [Agenda, Category, EventsType, Resource, UnavailabilityCalendar]:
for instance in model.objects.all():
old_snapshot = model.get_snapshot_model().objects.filter(instance=instance).latest('pk')
instance.take_snapshot(comment='local changes')
new_snapshot = model.get_snapshot_model().objects.filter(instance=instance).latest('pk')
assert new_snapshot.pk > old_snapshot.pk
snapshots[f'{instance.application_component_type}:{instance.slug}'] = (
instance.pk,
old_snapshot.pk,
new_snapshot.pk,
)
# and check
resp = app.post('/api/export-import/bundle-check/', upload_files=[('bundle', 'bundle.tar', bundles[0])])
assert resp.json == {
'data': {
'differences': [
{
'slug': 'rdv',
'type': 'agendas',
'url': 'http://testserver/manage/agendas/%s/history/compare/?version1=%s&version2=%s'
% (
snapshots['agendas:rdv'][0],
snapshots['agendas:rdv'][1],
snapshots['agendas:rdv'][2],
),
},
{
'slug': 'foo',
'type': 'agendas_categories',
'url': 'http://testserver/manage/category/%s/history/compare/?version1=%s&version2=%s'
% (
snapshots['agendas_categories:foo'][0],
snapshots['agendas_categories:foo'][1],
snapshots['agendas_categories:foo'][2],
),
},
{
'slug': 'foo',
'type': 'resources',
'url': 'http://testserver/manage/resource/%s/history/compare/?version1=%s&version2=%s'
% (
snapshots['resources:foo'][0],
snapshots['resources:foo'][1],
snapshots['resources:foo'][2],
),
},
{
'slug': 'foo',
'type': 'unavailability_calendars',
'url': 'http://testserver/manage/unavailability-calendar/%s/history/compare/?version1=%s&version2=%s'
% (
snapshots['unavailability_calendars:foo'][0],
snapshots['unavailability_calendars:foo'][1],
snapshots['unavailability_calendars:foo'][2],
),
},
{
'slug': 'evt',
'type': 'agendas',
'url': 'http://testserver/manage/agendas/%s/history/compare/?version1=%s&version2=%s'
% (
snapshots['agendas:evt'][0],
snapshots['agendas:evt'][1],
snapshots['agendas:evt'][2],
),
},
{
'slug': 'foo',
'type': 'events_types',
'url': 'http://testserver/manage/events-type/%s/history/compare/?version1=%s&version2=%s'
% (
snapshots['events_types:foo'][0],
snapshots['events_types:foo'][1],
snapshots['events_types:foo'][2],
),
},
{
'slug': 'virt',
'type': 'agendas',
'url': 'http://testserver/manage/agendas/%s/history/compare/?version1=%s&version2=%s'
% (
snapshots['agendas:virt'][0],
snapshots['agendas:virt'][1],
snapshots['agendas:virt'][2],
),
},
{
'slug': 'sub',
'type': 'agendas',
'url': 'http://testserver/manage/agendas/%s/history/compare/?version1=%s&version2=%s'
% (
snapshots['agendas:sub'][0],
snapshots['agendas:sub'][1],
snapshots['agendas:sub'][2],
),
},
],
'unknown_elements': [],
'no_history_elements': [],
'legacy_elements': [],
}
}
# update bundle
resp = app.post('/api/export-import/bundle-import/', upload_files=[('bundle', 'bundle.tar', bundles[1])])
# and check
resp = app.post('/api/export-import/bundle-check/', upload_files=[('bundle', 'bundle.tar', bundles[1])])
assert resp.json == {
'data': {
'differences': [],
'unknown_elements': [],
'no_history_elements': [],
'legacy_elements': [],
}
}
# snapshots without application info
AgendaSnapshot.objects.update(application_slug=None, application_version=None)
resp = app.post('/api/export-import/bundle-check/', upload_files=[('bundle', 'bundle.tar', bundles[1])])
assert resp.json == {
'data': {
'differences': [],
'unknown_elements': [],
'no_history_elements': [
{'slug': 'rdv', 'type': 'agendas'},
{'slug': 'evt', 'type': 'agendas'},
{'slug': 'virt', 'type': 'agendas'},
{'slug': 'sub', 'type': 'agendas'},
],
'legacy_elements': [],
}
}
# bad file format
resp = app.post(
'/api/export-import/bundle-check/', upload_files=[('bundle', 'bundle.tar', b'garbage')], status=400
)
assert resp.json['err']
assert resp.json['err_desc'] == 'Invalid tar file'
# missing manifest
tar_io = io.BytesIO()
with tarfile.open(mode='w', fileobj=tar_io) as tar:
foo_fd = io.BytesIO(json.dumps({'foo': 'bar'}, indent=2).encode())
tarinfo = tarfile.TarInfo('foo.json')
tarinfo.size = len(foo_fd.getvalue())
tar.addfile(tarinfo, fileobj=foo_fd)
resp = app.post(
'/api/export-import/bundle-check/',
upload_files=[('bundle', 'bundle.tar', tar_io.getvalue())],
status=400,
)
assert resp.json['err']
assert resp.json['err_desc'] == 'Invalid tar file, missing manifest'
def test_bundle_check(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
assert app.put('/api/export-import/bundle-check/').json == {'err': 0, 'data': {}}

View File

@ -32,8 +32,8 @@ def test_booking_ics(app, some_data, meetings_agenda, user, settings, rf):
booking_ics = booking.get_ics()
assert 'UID:%s-%s-%s\r\n' % (event.start_datetime.isoformat(), agenda.pk, booking.pk) in booking_ics
assert 'SUMMARY:\r\n' in booking_ics
assert 'DTSTART;VALUE=DATE-TIME:%sZ\r\n' % formatted_start_date in booking_ics
assert 'DTEND' not in booking_ics
assert 'DTSTART:%sZ\r\n' % formatted_start_date in booking_ics
assert 'DTEND:' not in booking_ics
assert 'ORGANIZER;CN=chrono:mailto:chrono@example.net\r\n' in booking_ics
booking_ics = booking.get_ics(rf.get('/?organizer=no'))
@ -53,7 +53,7 @@ def test_booking_ics(app, some_data, meetings_agenda, user, settings, rf):
assert 'SUMMARY:foo\r\n' in booking_ics
assert 'ATTENDEE:bar\r\n' in booking_ics
assert 'URL:http://example.com/booking\r\n' in booking_ics
assert 'ORGANIZER;CN="meeting server":mailto:donotanswer@meeting-server.com\r\n' in booking_ics
assert 'ORGANIZER;CN=meeting server:mailto:donotanswer@meeting-server.com\r\n' in booking_ics
# test with user_label in additionnal data
booking.user_first_name = 'foo'
@ -110,8 +110,8 @@ def test_booking_ics(app, some_data, meetings_agenda, user, settings, rf):
end = (
booking.event.start_datetime + datetime.timedelta(minutes=booking.event.meeting_type.duration)
).strftime('%Y%m%dT%H%M%S')
assert 'DTSTART;VALUE=DATE-TIME:%sZ\r\n' % start in booking_ics
assert 'DTEND;VALUE=DATE-TIME:%sZ\r\n' % end in booking_ics
assert 'DTSTART:%sZ\r\n' % start in booking_ics
assert 'DTEND:%sZ\r\n' % end in booking_ics
@pytest.mark.freeze_time('2023-09-18 14:00')
@ -135,13 +135,13 @@ def test_bookings_ics(app, user):
resp = app.get('/api/bookings/ics/', params={'user_external_id': 'enfant-1234'})
assert 'BEGIN:VCALENDAR' in resp.text
assert resp.text.count('UID') == 2
assert 'DTSTART;VALUE=DATE-TIME:20230921T140000Z\r\n' in resp.text
assert 'DTSTART;VALUE=DATE-TIME:20230922T140000Z\r\n' in resp.text
assert 'DTSTART:20230921' in resp.text
assert 'DTSTART:20230922' in resp.text
resp = app.get('/api/bookings/ics/', params={'user_external_id': 'enfant-1234', 'agenda': 'foo-bar'})
assert resp.text.count('UID') == 1
assert 'DTSTART;VALUE=DATE-TIME:20230921T140000Z\r\n' in resp.text
assert 'DTSTART;VALUE=DATE-TIME:20230922T140000Z\r\n' not in resp.text
assert 'DTSTART:20230921' in resp.text
assert 'DTSTART:20230922' not in resp.text
resp = app.get('/api/bookings/ics/', params={'user_external_id': 'enfant-1234', 'agenda': 'xxx'})
assert 'BEGIN:VCALENDAR' in resp.text
@ -188,7 +188,7 @@ def test_bookings_api(app, user):
with CaptureQueriesContext(connection) as ctx:
resp = app.get('/api/bookings/', params={'user_external_id': 'enfant-1234'})
assert len(ctx.captured_queries) == 6
assert len(ctx.captured_queries) == 3
assert resp.json['err'] == 0
assert resp.json['data'] == [

View File

@ -44,11 +44,9 @@ def test_status(app, user):
'err': 0,
'id': 'event-slug',
'slug': 'event-slug',
'primary_event': None,
'text': str(event),
'label': '',
'agenda_label': 'Foo bar',
'agenda_slug': 'foo-bar',
'date': localtime(event.start_datetime).strftime('%Y-%m-%d'),
'datetime': localtime(event.start_datetime).strftime('%Y-%m-%d %H:%M:%S'),
'end_datetime': '',
@ -86,11 +84,9 @@ def test_status(app, user):
'err': 0,
'id': 'event-slug',
'slug': 'event-slug',
'primary_event': None,
'text': str(event),
'label': '',
'agenda_label': 'Foo bar',
'agenda_slug': 'foo-bar',
'date': localtime(event.start_datetime).strftime('%Y-%m-%d'),
'datetime': localtime(event.start_datetime).strftime('%Y-%m-%d %H:%M:%S'),
'end_datetime': '',

View File

@ -1,71 +0,0 @@
import json
import pytest
from django.urls import reverse
from chrono.apps.user_preferences.models import UserPreferences
from tests.utils import login
pytestmark = pytest.mark.django_db
def test_user_preferences_api_ok(app, admin_user):
login(app)
fake_id = 'fake-id-1'
url = reverse('api-user-preferences')
app.post_json(url, params={fake_id: True}, status=204)
user_pref = UserPreferences.objects.get(user=admin_user)
assert user_pref.preferences[fake_id] is True
app.post_json(url, params={fake_id: False}, status=204)
user_pref = UserPreferences.objects.get(user=admin_user)
assert user_pref.preferences[fake_id] is False
fake_id2 = 'fake-id-2'
app.post_json(url, params={fake_id2: False}, status=204)
user_pref = UserPreferences.objects.get(user=admin_user)
assert user_pref.preferences[fake_id] is False
assert user_pref.preferences[fake_id2] is False
app.post_json(url, params={fake_id2: False}, status=204)
user_pref = UserPreferences.objects.get(user=admin_user)
assert user_pref.preferences[fake_id] is False
assert user_pref.preferences[fake_id2] is False
app.post_json(url, params={fake_id2: True}, status=204)
user_pref = UserPreferences.objects.get(user=admin_user)
assert user_pref.preferences[fake_id] is False
assert user_pref.preferences[fake_id2] is True
@pytest.mark.parametrize(
'bad_body',
(
json.dumps({'fake-id-1': True, 'fake-id-2': False}),
'"not a dict"',
'[1,2,3]',
'{\'fake-id-1\': true',
),
)
def test_user_preferences_api_invalid(app, admin_user, bad_body):
login(app)
url = reverse('api-user-preferences')
app.post(url, params=bad_body, status=400)
def test_user_preferences_api_large_payload(app, admin_user):
login(app)
url = reverse('api-user-preferences')
app.post(url, params='a' * 1024, status=400)
app.post_json(url, params={'b' * 1024: True}, status=400)
def test_user_preferences_api_unauthorized(app):
url = reverse('api-user-preferences')
app.post(url, params={'toto': True}, status=302)

View File

@ -1,9 +1,5 @@
import django_webtest
import pytest
from django.core.signals import setting_changed
from django.dispatch import receiver
from chrono.utils.timezone import get_default_timezone
@pytest.fixture
@ -27,10 +23,3 @@ def nocache(settings):
'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
}
}
@receiver(setting_changed)
def update_connections_time_zone(**kwargs):
if kwargs['setting'] == 'TIME_ZONE':
# Reset local time zone lru cache
get_default_timezone.cache_clear()

View File

@ -13,17 +13,14 @@ 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,
)
@ -481,15 +478,13 @@ 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_BOOKINGS_ENABLED = True
settings.PARTIAL_BOOKING_ENABLED = True
group = Group.objects.create(name='testgroup')
agenda = Agenda.objects.create(label='Foobar')
@ -803,78 +798,6 @@ 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,20 +1,12 @@
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
from chrono.apps.user_preferences.models import UserPreferences
from tests.utils import login
pytestmark = pytest.mark.django_db
def update_preference(user_preference, name, value):
user_preference.preferences.update({name: value})
user_preference.save()
def test_list_categories_as_manager(app, manager_user):
agenda = Agenda(label='Foo Bar')
agenda.view_role = manager_user.groups.all()[0]
@ -92,91 +84,3 @@ 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
def test_category_fold_preferences(app, admin_user):
category1 = Category.objects.create(label='Foo bar')
category2 = Category.objects.create(label='Toto')
pref_name1 = f'foldable-manager-category-group-{category1.id}'
pref_name2 = f'foldable-manager-category-group-{category2.id}'
Agenda.objects.create(label='Foo bar', category=category1)
agenda2 = Agenda.objects.create(label='Titi', category=category2)
app = login(app)
resp = app.get('/manage/')
elt = resp.pyquery.find(f'div[data-section-folded-pref-name={pref_name1}]')
assert len(elt) == 1
assert 'foldable' in elt[0].classes
assert 'folded' not in elt[0].classes
elt = resp.pyquery.find(f'div[data-section-folded-pref-name={pref_name2}]')
assert len(elt) == 1
assert 'foldable' in elt[0].classes
assert 'folded' not in elt[0].classes
user_prefs = UserPreferences.objects.get(user=admin_user)
update_preference(user_prefs, pref_name1, True)
resp = app.get('/manage/')
elt = resp.pyquery.find(f'div[data-section-folded-pref-name={pref_name1}]')
assert len(elt) == 1
assert 'foldable' in elt[0].classes
assert 'folded' in elt[0].classes
elt = resp.pyquery.find(f'div[data-section-folded-pref-name={pref_name2}]')
assert len(elt) == 1
assert 'foldable' in elt[0].classes
assert 'folded' not in elt[0].classes
# Order is preserved when adding a new category : preferences are preserved
category_temp = Category.objects.create(label='Tata0')
category3 = Category.objects.create(label='Tata')
pref_name3 = f'foldable-manager-category-group-{category3.id}'
category_temp.delete()
Agenda.objects.create(label='Titi', category=category3)
update_preference(user_prefs, pref_name1, False)
update_preference(user_prefs, pref_name2, True)
resp = app.get('/manage/')
elt = resp.pyquery.find(f'div[data-section-folded-pref-name={pref_name1}]')
assert len(elt) == 1
assert 'foldable' in elt[0].classes
assert 'folded' not in elt[0].classes
elt = resp.pyquery.find(f'div[data-section-folded-pref-name={pref_name2}]')
assert len(elt) == 1
assert 'foldable' in elt[0].classes
assert 'folded' in elt[0].classes
elt = resp.pyquery.find(f'div[data-section-folded-pref-name={pref_name3}]')
assert len(elt) == 1
assert 'foldable' in elt[0].classes
assert 'folded' not in elt[0].classes
# Preferences are not "shifted" when a category is deleted
agenda2.delete()
resp = app.get('/manage/')
elt = resp.pyquery.find(f'div[data-section-folded-pref-name={pref_name1}]')
assert len(elt) == 1
assert 'foldable' in elt[0].classes
assert 'folded' not in elt[0].classes
elt = resp.pyquery.find(f'div[data-section-folded-pref-name={pref_name3}]')
assert len(elt) == 1
assert 'foldable' in elt[0].classes
assert 'folded' not in elt[0].classes

View File

@ -764,7 +764,7 @@ def test_export_events(app, admin_user):
resp = app.get('/manage/agendas/%s/import-events' % agenda.id)
resp.form['events_csv_file'] = Upload(
't.csv',
b'2016-09-16,23:30,10,5,label,slug,"description\nfoobar",pricing,https://example.net/event,2016-10-16 00:00,90',
b'2016-09-16,23:30,10,5,label,slug,"description\nfoobar",pricing,url,2016-10-16 00:00,90',
'text/csv',
)
resp.form.submit(status=302)
@ -774,7 +774,7 @@ def test_export_events(app, admin_user):
csv_export
== 'date,time,number of places,number of places in waiting list,label,identifier,description,pricing,URL,publication date/time,duration\r\n'
'2016-09-16,00:30,10,0,,foo-bar-event,,,,,\r\n'
'2016-09-16,23:30,10,5,label,slug,"description\nfoobar",pricing,https://example.net/event,2016-10-16 00:00,90\r\n'
'2016-09-16,23:30,10,5,label,slug,"description\nfoobar",pricing,url,2016-10-16 00:00,90\r\n'
)
@ -967,7 +967,7 @@ def test_import_events(app, admin_user):
resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
resp.form['events_csv_file'] = Upload(
't.csv',
b'2016-09-16,18:00,10,5,label,slug,"description\nfoobar",pricing,https://example.net/event,2016-10-16,90',
b'2016-09-16,18:00,10,5,label,slug,"description\nfoobar",pricing,url,2016-10-16,90',
'text/csv',
)
resp = resp.form.submit(status=302)
@ -975,7 +975,7 @@ def test_import_events(app, admin_user):
event = Event.objects.get()
assert event.description == 'description\nfoobar'
assert event.pricing == 'pricing'
assert event.url == 'https://example.net/event'
assert event.url == 'url'
assert str(event.publication_datetime) == '2016-10-15 22:00:00+00:00'
assert str(event.publication_datetime.tzinfo) == 'UTC'
assert event.duration == 90
@ -983,7 +983,7 @@ def test_import_events(app, admin_user):
resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
resp.form['events_csv_file'] = Upload(
't.csv',
b'2016-09-16,18:00,10,5,label,slug,"description\nfoobar",pricing,https://example.net/event,2016-10-16 10:00,90',
b'2016-09-16,18:00,10,5,label,slug,"description\nfoobar",pricing,url,2016-10-16 10:00,90',
'text/csv',
)
resp = resp.form.submit(status=302)
@ -991,7 +991,7 @@ def test_import_events(app, admin_user):
event = Event.objects.get()
assert event.description == 'description\nfoobar'
assert event.pricing == 'pricing'
assert event.url == 'https://example.net/event'
assert event.url == 'url'
assert str(event.publication_datetime) == '2016-10-16 08:00:00+00:00'
assert str(event.publication_datetime.tzinfo) == 'UTC'
assert event.duration == 90
@ -999,9 +999,7 @@ def test_import_events(app, admin_user):
# publication date/time bad format
resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
resp.form['events_csv_file'] = Upload(
't.csv',
b'2016-09-16,18:00,10,5,label,slug,description,pricing,https://example.net/event,foobar',
'text/csv',
't.csv', b'2016-09-16,18:00,10,5,label,slug,description,pricing,url,foobar', 'text/csv'
)
resp = resp.form.submit(status=200)
assert 'Wrong publication date/time format. (1st event)' in resp.text
@ -1009,9 +1007,7 @@ def test_import_events(app, admin_user):
# duration bad format
resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
resp.form['events_csv_file'] = Upload(
't.csv',
b'2016-09-16,18:00,10,5,label,slug,description,pricing,https://example.net/event,2016-09-16,foobar',
'text/csv',
't.csv', b'2016-09-16,18:00,10,5,label,slug,description,pricing,url,2016-09-16,foobar', 'text/csv'
)
resp = resp.form.submit(status=200)
assert 'Duration must be an integer. (1st event)' in resp.text
@ -1030,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) == 31
assert len(ctx.captured_queries) == 32
assert Event.objects.count() == 5
assert set(Event.objects.values_list('slug', flat=True)) == {
'labelb',
@ -1206,9 +1202,7 @@ def test_import_events_partial_bookings(app, admin_user):
# no end time
resp = app.get('/manage/agendas/%s/import-events' % agenda.pk)
resp.form['events_csv_file'] = Upload(
't.csv',
b'2016-09-16,18:00,10,5,label,slug,description,pricing,https://example.net/event,2016-09-16',
'text/csv',
't.csv', b'2016-09-16,18:00,10,5,label,slug,description,pricing,url,2016-09-16', 'text/csv'
)
resp = resp.form.submit(status=200)
assert 'Missing end_time.' in resp.text
@ -1216,9 +1210,7 @@ def test_import_events_partial_bookings(app, admin_user):
# invalid end time
resp = app.get('/manage/agendas/%s/import-events' % agenda.pk)
resp.form['events_csv_file'] = Upload(
't.csv',
b'2016-09-16,18:00,10,5,label,slug,description,pricing,https://example.net/event,2016-09-16,xxx',
'text/csv',
't.csv', b'2016-09-16,18:00,10,5,label,slug,description,pricing,url,2016-09-16,xxx', 'text/csv'
)
resp = resp.form.submit(status=200)
assert '“xxx” value has an invalid format' in resp.text
@ -2490,12 +2482,11 @@ 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', unexpected_presence=True),
CheckType(slug='bar-reason', label='Bar reason', kind='presence'),
]
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()
@ -2842,14 +2833,6 @@ 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,6 +1,4 @@
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
@ -214,14 +212,3 @@ 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

@ -652,34 +652,8 @@ END:VCALENDAR"""
assert AgendaSnapshot.objects.count() == 1
# Testing with a DTEND and with a DURATION
@pytest.mark.parametrize(
'recurrent_ics',
(
b"""BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//foo.bar//EN
BEGIN:VEVENT
DTSTART:20180101
DTEND:20180102
SUMMARY:New Year's Eve
RRULE:FREQ=YEARLY
END:VEVENT
END:VCALENDAR""",
b"""BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//foo.bar//EN
BEGIN:VEVENT
DTSTART:20180101
DURATION:P1D
SUMMARY:New Year's Eve
RRULE:FREQ=YEARLY
END:VEVENT
END:VCALENDAR""",
),
)
@pytest.mark.freeze_time('2017-12-01')
def test_agenda_import_time_period_exception_from_ics_recurrent(app, admin_user, recurrent_ics):
def test_agenda_import_time_period_exception_from_ics_recurrent(app, admin_user):
agenda = Agenda.objects.create(label='Example', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Test Desk')
MeetingType(agenda=agenda, label='Foo').save()
@ -689,54 +663,19 @@ def test_agenda_import_time_period_exception_from_ics_recurrent(app, admin_user,
login(app)
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
resp = resp.click('manage exceptions')
resp.form['ics_file'] = Upload('exceptions.ics', recurrent_ics, 'text/calendar')
resp = resp.form.submit(status=302).follow()
assert TimePeriodException.objects.filter(desk=desk).count() == 2
expt_start = '2018-01-01T00:00:00+0100', '2019-01-01T00:00:00T+0100'
expt_end = '2018-01-02T00:00:00+0100', '2019-01-02T00:00:00T+0100'
assert set(TimePeriodException.objects.values_list('start_datetime', flat=True)) == {
datetime.datetime.fromisoformat(dt) for dt in expt_start
}
assert set(TimePeriodException.objects.values_list('end_datetime', flat=True)) == {
datetime.datetime.fromisoformat(dt) for dt in expt_end
}
@pytest.mark.freeze_time('2017-12-01')
def test_agenda_import_time_period_exception_from_ics_recurrent_invalid_duration(app, admin_user):
# Specific test for invalid/missing duration : in this case
# we set the DTEND to 23:59:59.999999 the same day.
agenda = Agenda.objects.create(label='Example', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Test Desk')
MeetingType(agenda=agenda, label='Foo').save()
TimePeriod.objects.create(
weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)
)
login(app)
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
resp = resp.click('manage exceptions')
recurrent_ics = b"""BEGIN:VCALENDAR
ics_with_recurrent_exceptions = b"""BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//foo.bar//EN
BEGIN:VEVENT
DTSTART:20180101
DURATION:invalid duration as 1 day - 1us
DTEND:20180101
SUMMARY:New Year's Eve
RRULE:FREQ=YEARLY
END:VEVENT
END:VCALENDAR"""
resp.form['ics_file'] = Upload('exceptions.ics', recurrent_ics, 'text/calendar')
resp.form['ics_file'] = Upload('exceptions.ics', ics_with_recurrent_exceptions, 'text/calendar')
resp = resp.form.submit(status=302).follow()
assert TimePeriodException.objects.filter(desk=desk).count() == 2
expt_start = '2018-01-01T00:00:00+0100', '2019-01-01T00:00:00T+0100'
expt_end = '2018-01-01T23:59:59.999999+0100', '2019-01-01T23:59:59.999999T+0100'
assert set(TimePeriodException.objects.values_list('start_datetime', flat=True)) == {
datetime.datetime.fromisoformat(dt) for dt in expt_start
}
assert set(TimePeriodException.objects.values_list('end_datetime', flat=True)) == {
datetime.datetime.fromisoformat(dt) for dt in expt_end
}
@mock.patch('chrono.agendas.models.requests.get')

View File

@ -1,394 +0,0 @@
import datetime
import pytest
from pyquery import PyQuery
from chrono.agendas.models import Agenda, Booking, Event
from chrono.apps.journal.models import AuditEntry
from chrono.apps.journal.utils import audit
from chrono.utils.timezone import make_aware
from tests.utils import login
pytestmark = pytest.mark.django_db
def test_journal_permission(app, admin_user, manager_user):
app = login(app, username='manager', password='manager')
app.get('/manage/journal/', status=403)
app = login(app)
app.get('/manage/journal/', status=200)
def test_journal_feature_flag(app, admin_user, settings):
app = login(app)
assert settings.AUDIT_JOURNAL_ENABLED is False
resp = app.get('/manage/')
assert 'Audit journal' not in resp.text
settings.AUDIT_JOURNAL_ENABLED = True
resp = app.get('/manage/')
assert 'Audit journal' in resp.text
def test_journal_browse(app, admin_user, manager_user, settings):
settings.AUDIT_JOURNAL_ENABLED = True
admin_user.first_name = 'Admin'
admin_user.save()
manager_user.first_name = 'Manager'
manager_user.save()
# some audit events
agendas = [
Agenda.objects.create(label='Foo', kind='events'),
Agenda.objects.create(label='Bar', kind='events'),
Agenda.objects.create(label='Baz', kind='events'),
]
event = Event.objects.create(
start_datetime=make_aware(datetime.datetime(2024, 1, 2, 3, 4)), places=20, agenda=agendas[0]
)
booking = Booking.objects.create(event=event)
event2 = Event.objects.create(
start_datetime=make_aware(datetime.datetime(2024, 1, 2, 3, 4)),
places=20,
label='foobar',
agenda=agendas[0],
)
for i in range(20):
user = admin_user if i % 3 else manager_user
agenda = agendas[i % 3]
entry = audit(
'booking:cancel',
user=user,
agenda=agenda,
extra_data={'booking': booking, 'event': event},
)
entry.timestamp = make_aware(
datetime.datetime(2024, 1, 1) + datetime.timedelta(days=i, hours=i, minutes=i)
)
entry.save()
entry = audit(
'check:absence', user=user, agenda=agenda, extra_data={'user_name': 'User', 'event': event2}
)
entry.timestamp = make_aware(
datetime.datetime(2024, 1, 2) + datetime.timedelta(days=i, hours=i, minutes=i)
)
entry.save()
app = login(app)
resp = app.get('/manage/journal/')
assert [[x.text for x in PyQuery(x).find('td')] for x in resp.pyquery('tbody tr')] == [
[
'Jan. 21, 2024, 7:19 p.m.',
'Admin',
'Bar',
'marked absence of User in foobar (01/02/2024 3:04 a.m.)',
],
[
'Jan. 20, 2024, 7:19 p.m.',
'Admin',
'Bar',
f'cancellation of booking ({booking.id}) in event "01/02/2024 3:04 a.m."',
],
[
'Jan. 20, 2024, 6:18 p.m.',
'Manager',
'Foo',
'marked absence of User in foobar (01/02/2024 3:04 a.m.)',
],
[
'Jan. 19, 2024, 6:18 p.m.',
'Manager',
'Foo',
f'cancellation of booking ({booking.id}) in event "01/02/2024 3:04 a.m."',
],
[
'Jan. 19, 2024, 5:17 p.m.',
'Admin',
'Baz',
'marked absence of User in foobar (01/02/2024 3:04 a.m.)',
],
[
'Jan. 18, 2024, 5:17 p.m.',
'Admin',
'Baz',
f'cancellation of booking ({booking.id}) in event "01/02/2024 3:04 a.m."',
],
[
'Jan. 18, 2024, 4:16 p.m.',
'Admin',
'Bar',
'marked absence of User in foobar (01/02/2024 3:04 a.m.)',
],
[
'Jan. 17, 2024, 4:16 p.m.',
'Admin',
'Bar',
f'cancellation of booking ({booking.id}) in event "01/02/2024 3:04 a.m."',
],
[
'Jan. 17, 2024, 3:15 p.m.',
'Manager',
'Foo',
'marked absence of User in foobar (01/02/2024 3:04 a.m.)',
],
[
'Jan. 16, 2024, 3:15 p.m.',
'Manager',
'Foo',
f'cancellation of booking ({booking.id}) in event "01/02/2024 3:04 a.m."',
],
]
resp = resp.click('2') # pagination
assert [[x.text for x in PyQuery(x).find('td')] for x in resp.pyquery('tbody tr')] == [
[
'Jan. 16, 2024, 2:14 p.m.',
'Admin',
'Baz',
'marked absence of User in foobar (01/02/2024 3:04 a.m.)',
],
[
'Jan. 15, 2024, 2:14 p.m.',
'Admin',
'Baz',
f'cancellation of booking ({booking.id}) in event "01/02/2024 3:04 a.m."',
],
[
'Jan. 15, 2024, 1:13 p.m.',
'Admin',
'Bar',
'marked absence of User in foobar (01/02/2024 3:04 a.m.)',
],
[
'Jan. 14, 2024, 1:13 p.m.',
'Admin',
'Bar',
f'cancellation of booking ({booking.id}) in event "01/02/2024 3:04 a.m."',
],
[
'Jan. 14, 2024, 12:12 p.m.',
'Manager',
'Foo',
'marked absence of User in foobar (01/02/2024 3:04 a.m.)',
],
[
'Jan. 13, 2024, 12:12 p.m.',
'Manager',
'Foo',
f'cancellation of booking ({booking.id}) in event "01/02/2024 3:04 a.m."',
],
[
'Jan. 13, 2024, 11:11 a.m.',
'Admin',
'Baz',
'marked absence of User in foobar (01/02/2024 3:04 a.m.)',
],
[
'Jan. 12, 2024, 11:11 a.m.',
'Admin',
'Baz',
f'cancellation of booking ({booking.id}) in event "01/02/2024 3:04 a.m."',
],
[
'Jan. 12, 2024, 10:10 a.m.',
'Admin',
'Bar',
'marked absence of User in foobar (01/02/2024 3:04 a.m.)',
],
[
'Jan. 11, 2024, 10:10 a.m.',
'Admin',
'Bar',
f'cancellation of booking ({booking.id}) in event "01/02/2024 3:04 a.m."',
],
]
# filters
assert resp.form['timestamp'].attrs == {'type': 'date'}
resp.form['timestamp'].value = '2024-01-19'
resp = resp.form.submit()
assert [[x.text for x in PyQuery(x).find('td')] for x in resp.pyquery('tbody tr')] == [
[
'Jan. 19, 2024, 6:18 p.m.',
'Manager',
'Foo',
f'cancellation of booking ({booking.id}) in event "01/02/2024 3:04 a.m."',
],
[
'Jan. 19, 2024, 5:17 p.m.',
'Admin',
'Baz',
'marked absence of User in foobar (01/02/2024 3:04 a.m.)',
],
]
assert resp.form['timestamp'].value == '2024-01-19'
resp.form['agenda'].value = agendas[0].id
resp = resp.form.submit()
assert [[x.text for x in PyQuery(x).find('td')] for x in resp.pyquery('tbody tr')] == [
[
'Jan. 19, 2024, 6:18 p.m.',
'Manager',
'Foo',
f'cancellation of booking ({booking.id}) in event "01/02/2024 3:04 a.m."',
]
]
resp.form['agenda'].value = agendas[1].id
resp = resp.form.submit()
assert [[x.text for x in PyQuery(x).find('td')] for x in resp.pyquery('tbody tr')] == []
resp.form['timestamp'].value = ''
resp.form['action_type'].value = 'booking'
resp = resp.form.submit()
assert [[x.text for x in PyQuery(x).find('td')] for x in resp.pyquery('tbody tr')] == [
[
'Jan. 20, 2024, 7:19 p.m.',
'Admin',
'Bar',
f'cancellation of booking ({booking.id}) in event "01/02/2024 3:04 a.m."',
],
[
'Jan. 17, 2024, 4:16 p.m.',
'Admin',
'Bar',
f'cancellation of booking ({booking.id}) in event "01/02/2024 3:04 a.m."',
],
[
'Jan. 14, 2024, 1:13 p.m.',
'Admin',
'Bar',
f'cancellation of booking ({booking.id}) in event "01/02/2024 3:04 a.m."',
],
[
'Jan. 11, 2024, 10:10 a.m.',
'Admin',
'Bar',
f'cancellation of booking ({booking.id}) in event "01/02/2024 3:04 a.m."',
],
[
'Jan. 8, 2024, 7:07 a.m.',
'Admin',
'Bar',
f'cancellation of booking ({booking.id}) in event "01/02/2024 3:04 a.m."',
],
[
'Jan. 5, 2024, 4:04 a.m.',
'Admin',
'Bar',
f'cancellation of booking ({booking.id}) in event "01/02/2024 3:04 a.m."',
],
[
'Jan. 2, 2024, 1:01 a.m.',
'Admin',
'Bar',
f'cancellation of booking ({booking.id}) in event "01/02/2024 3:04 a.m."',
],
]
def test_journal_audit_booking():
agenda = Agenda.objects.create(label='Bar', kind='events')
event = Event.objects.create(
start_datetime=make_aware(datetime.datetime(2024, 1, 2, 3, 4)), places=20, agenda=agenda
)
booking = Booking.objects.create(
event=event,
in_waiting_list=True,
cancellation_datetime=make_aware(datetime.datetime(2024, 1, 2, 3, 4)),
)
entry = audit(
'booking:cancel',
agenda=agenda,
extra_data={'booking': booking, 'event': event},
)
assert (
entry.extra_data['booking']
== f'ID: {booking.id} / in waiting list / cancelled at 01/02/2024 3:04 a.m.'
)
booking.user_first_name = 'first'
booking.user_last_name = 'last'
booking.in_waiting_list = False
booking.save()
entry = audit(
'booking:cancel',
agenda=agenda,
extra_data={'booking': booking, 'event': event},
)
assert (
entry.extra_data['booking']
== f'ID: {booking.id} / user: first last / cancelled at 01/02/2024 3:04 a.m.'
)
booking.cancellation_datetime = None
booking.start_time = datetime.time(10, 0)
booking.save()
entry = audit(
'booking:cancel',
agenda=agenda,
extra_data={'booking': booking, 'event': event},
)
assert entry.extra_data['booking'] == f'ID: {booking.id} / user: first last / 10 a.m. → ?'
booking.end_time = datetime.time(11, 0)
booking.save()
entry = audit(
'booking:cancel',
agenda=agenda,
extra_data={'booking': booking, 'event': event},
)
assert entry.extra_data['booking'] == f'ID: {booking.id} / user: first last / 10 a.m. → 11 a.m.'
booking.start_time = None
booking.save()
entry = audit(
'booking:cancel',
agenda=agenda,
extra_data={'booking': booking, 'event': event},
)
assert entry.extra_data['booking'] == f'ID: {booking.id} / user: first last / ? → 11 a.m.'
def test_journal_browse_invalid_or_unknown_event(app, admin_user, settings):
settings.AUDIT_JOURNAL_ENABLED = True
admin_user.first_name = 'Admin'
admin_user.save()
AuditEntry.objects.all().delete()
agenda = Agenda.objects.create(label='Foo', kind='events')
event = Event.objects.create(
start_datetime=make_aware(datetime.datetime(2024, 1, 2, 3, 4)), places=20, agenda=agenda
)
entry = audit(
'booking:cancel',
user=admin_user,
agenda=agenda,
extra_data={'event': event}, # missing booking_id
)
entry.timestamp = make_aware(datetime.datetime(2024, 1, 1))
entry.save()
resp = login(app).get('/manage/journal/')
assert [[x.text for x in PyQuery(x).find('td')] for x in resp.pyquery('tbody tr')] == [
['Jan. 1, 2024, midnight', 'Admin', 'Foo', 'Unknown entry (booking:cancel)']
]
AuditEntry.objects.all().delete()
entry = audit(
'foo:bar',
user=admin_user,
agenda=agenda,
)
entry.timestamp = make_aware(datetime.datetime(2024, 1, 1))
entry.save()
resp = login(app).get('/manage/journal/')
assert [[x.text for x in PyQuery(x).find('td')] for x in resp.pyquery('tbody tr')] == [
['Jan. 1, 2024, midnight', 'Admin', 'Foo', 'Unknown entry (foo:bar)']
]

View File

@ -1151,13 +1151,13 @@ def test_manager_partial_bookings_month_view(app, admin_user, freezer):
user_absent_row = resp.pyquery('tbody tr')[0]
assert len(resp.pyquery(user_absent_row)('td')) == 31
assert len(resp.pyquery(user_absent_row)('td span.booking')) == 1
assert len(resp.pyquery(user_absent_row)('td span')) == 1
assert len(resp.pyquery(user_absent_row)('td span.booking.absent')) == 1
assert resp.pyquery(user_absent_row)('td span.booking.absent').text() == 'Absent'
subscription_not_booked_row = resp.pyquery('tbody tr')[1]
assert len(resp.pyquery(subscription_not_booked_row)('td')) == 31
assert len(resp.pyquery(subscription_not_booked_row)('td span.booking')) == 0
assert len(resp.pyquery(subscription_not_booked_row)('td span')) == 0
user_not_checked_row = resp.pyquery('tbody tr')[2]
assert len(resp.pyquery(user_not_checked_row)('td')) == 31
@ -1166,7 +1166,7 @@ def test_manager_partial_bookings_month_view(app, admin_user, freezer):
user_present_row = resp.pyquery('tbody tr')[3]
assert len(resp.pyquery(user_present_row)('td')) == 31
assert len(resp.pyquery(user_present_row)('td span.booking')) == 1
assert len(resp.pyquery(user_present_row)('td span')) == 1
assert len(resp.pyquery(user_present_row)('td span.booking.present')) == 1
assert resp.pyquery(user_present_row)('td span.booking.present').text() == 'Present'
@ -1185,16 +1185,6 @@ def test_manager_partial_bookings_month_view(app, admin_user, freezer):
assert [x.text for x in resp.pyquery('tbody tr th')] == ['Subscription Next Month']
assert len(resp.pyquery('tbody tr td')) == 30
freezer.move_to('2023-05-10 14:00')
resp = app.get(resp.request.url)
assert len(resp.pyquery('th.today')) == 0
assert len(resp.pyquery('col.today')) == 0
freezer.move_to('2023-06-10 14:00')
resp = app.get(resp.request.url)
assert resp.pyquery('th.today').text() == '10'
assert len(resp.pyquery('col.today')) == 1
def test_manager_partial_bookings_occupation_rates(app, admin_user):
agenda = Agenda.objects.create(label='Foo bar', kind='events', partial_bookings=True)

View File

@ -1043,14 +1043,3 @@ 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

@ -1,476 +0,0 @@
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_slug = 'foobar'
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') == 18
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') == 18
assert resp.text.count('diff_chg') == 0
# check compare on application version number
snapshot1.application_slug = 'foobar'
snapshot1.application_version = '41.0'
snapshot1.save()
# application not found
resp = app.get(
'/manage/agendas/%s/history/compare/?application=foobaz&version1=41.0&version2=42.0' % agenda.pk
)
assert resp.location == '/manage/agendas/%s/history/' % agenda.pk
# version1 not found
resp = app.get(
'/manage/agendas/%s/history/compare/?application=foobar&version1=40.0&version2=42.0' % agenda.pk
)
assert resp.location == '/manage/agendas/%s/history/' % agenda.pk
# version2 not found
resp = app.get(
'/manage/agendas/%s/history/compare/?application=foobar&version1=41.0&version2=43.0' % agenda.pk
)
assert resp.location == '/manage/agendas/%s/history/' % agenda.pk
# ok
resp = app.get(
'/manage/agendas/%s/history/compare/?application=foobar&version1=41.0&version2=42.0' % agenda.pk
)
assert 'Snapshot (%s) - (Version 41.0)' % snapshot1.pk in resp
assert 'Snapshot (%s) - (Version 42.0)' % snapshot2.pk in resp
assert AgendaSnapshot.objects.update(user=admin_user)
admin_user.delete()
assert AgendaSnapshot.objects.count() == 2
assert AgendaSnapshot.objects.filter(user__isnull=True).count() == 2
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_slug = 'foobar'
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
# check compare on application version number
snapshot1.application_slug = 'foobar'
snapshot1.application_version = '41.0'
snapshot1.save()
# application not found
resp = app.get(
'/manage/category/%s/history/compare/?application=foobaz&version1=41.0&version2=42.0' % category.pk
)
assert resp.location == '/manage/category/%s/history/' % category.pk
# version1 not found
resp = app.get(
'/manage/category/%s/history/compare/?application=foobar&version1=40.0&version2=42.0' % category.pk
)
assert resp.location == '/manage/category/%s/history/' % category.pk
# version2 not found
resp = app.get(
'/manage/category/%s/history/compare/?application=foobar&version1=41.0&version2=43.0' % category.pk
)
assert resp.location == '/manage/category/%s/history/' % category.pk
# ok
resp = app.get(
'/manage/category/%s/history/compare/?application=foobar&version1=41.0&version2=42.0' % category.pk
)
assert 'Snapshot (%s) - (Version 41.0)' % snapshot1.pk in resp
assert 'Snapshot (%s) - (Version 42.0)' % snapshot2.pk in resp
assert CategorySnapshot.objects.update(user=admin_user)
admin_user.delete()
assert CategorySnapshot.objects.count() == 2
assert CategorySnapshot.objects.filter(user__isnull=True).count() == 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_slug = 'foobar'
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
# check compare on application version number
snapshot1.application_slug = 'foobar'
snapshot1.application_version = '41.0'
snapshot1.save()
# application not found
resp = app.get(
'/manage/events-type/%s/history/compare/?application=foobaz&version1=41.0&version2=42.0'
% events_type.pk
)
assert resp.location == '/manage/events-type/%s/history/' % events_type.pk
# version1 not found
resp = app.get(
'/manage/events-type/%s/history/compare/?application=foobar&version1=40.0&version2=42.0'
% events_type.pk
)
assert resp.location == '/manage/events-type/%s/history/' % events_type.pk
# version2 not found
resp = app.get(
'/manage/events-type/%s/history/compare/?application=foobar&version1=41.0&version2=43.0'
% events_type.pk
)
assert resp.location == '/manage/events-type/%s/history/' % events_type.pk
# ok
resp = app.get(
'/manage/events-type/%s/history/compare/?application=foobar&version1=41.0&version2=42.0'
% events_type.pk
)
assert 'Snapshot (%s) - (Version 41.0)' % snapshot1.pk in resp
assert 'Snapshot (%s) - (Version 42.0)' % snapshot2.pk in resp
assert EventsTypeSnapshot.objects.update(user=admin_user)
admin_user.delete()
assert EventsTypeSnapshot.objects.count() == 2
assert EventsTypeSnapshot.objects.filter(user__isnull=True).count() == 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_slug = 'foobar'
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
# check compare on application version number
snapshot1.application_slug = 'foobar'
snapshot1.application_version = '41.0'
snapshot1.save()
# application not found
resp = app.get(
'/manage/resource/%s/history/compare/?application=foobaz&version1=41.0&version2=42.0' % resource.pk
)
assert resp.location == '/manage/resource/%s/history/' % resource.pk
# version1 not found
resp = app.get(
'/manage/resource/%s/history/compare/?application=foobar&version1=40.0&version2=42.0' % resource.pk
)
assert resp.location == '/manage/resource/%s/history/' % resource.pk
# version2 not found
resp = app.get(
'/manage/resource/%s/history/compare/?application=foobar&version1=41.0&version2=43.0' % resource.pk
)
assert resp.location == '/manage/resource/%s/history/' % resource.pk
# ok
resp = app.get(
'/manage/resource/%s/history/compare/?application=foobar&version1=41.0&version2=42.0' % resource.pk
)
assert 'Snapshot (%s) - (Version 41.0)' % snapshot1.pk in resp
assert 'Snapshot (%s) - (Version 42.0)' % snapshot2.pk in resp
assert ResourceSnapshot.objects.update(user=admin_user)
admin_user.delete()
assert ResourceSnapshot.objects.count() == 2
assert ResourceSnapshot.objects.filter(user__isnull=True).count() == 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_slug = 'foobar'
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
# check compare on application version number
snapshot1.application_slug = 'foobar'
snapshot1.application_version = '41.0'
snapshot1.save()
# application not found
resp = app.get(
'/manage/unavailability-calendar/%s/history/compare/?application=foobaz&version1=41.0&version2=42.0'
% unavailability_calendar.pk
)
assert resp.location == '/manage/unavailability-calendar/%s/history/' % unavailability_calendar.pk
# version1 not found
resp = app.get(
'/manage/unavailability-calendar/%s/history/compare/?application=foobar&version1=40.0&version2=42.0'
% unavailability_calendar.pk
)
assert resp.location == '/manage/unavailability-calendar/%s/history/' % unavailability_calendar.pk
# version2 not found
resp = app.get(
'/manage/unavailability-calendar/%s/history/compare/?application=foobar&version1=41.0&version2=43.0'
% unavailability_calendar.pk
)
assert resp.location == '/manage/unavailability-calendar/%s/history/' % unavailability_calendar.pk
# ok
resp = app.get(
'/manage/unavailability-calendar/%s/history/compare/?application=foobar&version1=41.0&version2=42.0'
% unavailability_calendar.pk
)
assert 'Snapshot (%s) - (Version 41.0)' % snapshot1.pk in resp
assert 'Snapshot (%s) - (Version 42.0)' % snapshot2.pk in resp
assert UnavailabilityCalendarSnapshot.objects.update(user=admin_user)
admin_user.delete()
assert UnavailabilityCalendarSnapshot.objects.count() == 2
assert UnavailabilityCalendarSnapshot.objects.filter(user__isnull=True).count() == 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,8 +3,6 @@ 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 (
@ -711,32 +709,3 @@ 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

@ -63,64 +63,6 @@ SEQUENCE:2
END:VEVENT
END:VCALENDAR"""
ICS_SAMPLE_WITH_TIMEZONES = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//foo.bar//EN
BEGIN:VTIMEZONE
TZID:(UTC+01:00) Bruxelles, Copenhague, Madrid, Paris
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19700329T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19701025T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
END:VTIMEZONE
BEGIN:VTIMEZONE
TZID:CUSTOM TZ
BEGIN:DAYLIGHT
TZOFFSETFROM:+0300
TZOFFSETTO:+0400
TZNAME:WTF0
DTSTART:19700101T020000
RRULE:FREQ=MONTHLY;BYMONTH=1,3,5,7,9,11
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0400
TZOFFSETTO:+0300
TZNAME:WTF1
DTSTART:19700201T030000
RRULE:FREQ=MONTHLY;BYMONTH=2,4,6,8,10,12
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;TZID="(UTC+01:00) Bruxelles, Copenhague, Madrid, Paris":20171213T120100
DTEND;TZID="(UTC+01:00) Bruxelles, Copenhague, Madrid, Paris":20171213T120200
END:VEVENT
BEGIN:VEVENT
DTSTART;TZID="CUSTOM TZ":20180101T112233
DTEND;TZID="CUSTOM TZ":20180202T112233
END:VEVENT
BEGIN:VEVENT
DTSTART:20190102T030405Z
DTEND:20190504T030201Z
END:VEVENT
END:VCALENDAR
"""
ICS_SAMPLE_WITH_DURATION = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//foo.bar//EN
@ -134,7 +76,7 @@ END:VEVENT
BEGIN:VEVENT
DTSTAMP:20170824T092855Z
DTSTART:20170830T180800Z
DURATION:P1DT4H26M
DURATION:P1D4H26M
SEQUENCE:2
SUMMARY:Event 2
END:VEVENT
@ -1136,83 +1078,6 @@ def test_timeperiodexception_creation_from_ics_with_duration():
}
def test_timeperiodexception_creation_from_ics_with_timezone():
agenda = Agenda.objects.create(label='Test 1 agenda')
desk = Desk.objects.create(label='Test 1 desk', agenda=agenda)
source = desk.timeperiodexceptionsource_set.create(
ics_filename='sample.ics', ics_file=ContentFile(ICS_SAMPLE_WITH_TIMEZONES, name='sample.ics')
)
source.refresh_timeperiod_exceptions_from_ics()
assert TimePeriodException.objects.filter(desk=desk).count() == 3
expt_start = '2017-12-13T12:01:00+0100', '2018-01-01T11:22:33+0400', '2019-01-02T03:04:05Z'
expt_end = '2017-12-13T12:02:00+0100', '2018-02-02T11:22:33+0300', '2019-05-04T03:02:01Z'
assert set(TimePeriodException.objects.values_list('start_datetime', flat=True)) == {
datetime.datetime.fromisoformat(dt) for dt in expt_start
}
assert set(TimePeriodException.objects.values_list('end_datetime', flat=True)) == {
datetime.datetime.fromisoformat(dt) for dt in expt_end
}
@pytest.mark.parametrize(
'bad_ics_content',
[
pytest.param(
"""BBEGIN:nothing
VERSION:2.0
PRODID:-//foo.bar//EN
BEGIN:VEVENT
DTSTAMP:20170824T082855Z
DTSTART:20170831T170800Z
DTEND:20170831T203400Z
SEQUENCE:1
SUMMARY:Évènement 1
END:VEVENT
END:VCALENDAR""",
marks=pytest.mark.comment('Missing BEGIN:VCALENDAR'),
),
pytest.param(
"""BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//foo.bar//EN
BEGIN:VEVENT
DTSTAMP:20170824T082855Z
DTSTART:2017-08-24T13:37:00
DTEND:20170831T203400Z
SEQUENCE:1
SUMMARY:Évènement 1
END:VEVENT
END:VCALENDAR""",
marks=pytest.mark.comment('Bad DTSTART format'),
),
pytest.param(
"""BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//foo.bar//EN
BEGIN:VEVENT
DTSTAMP:20170824T082855Z
DTSTART:20170830T203400Z
DTEND:something
SEQUENCE:1
SUMMARY:Évènement 1
END:VEVENT
END:VCALENDAR""",
marks=pytest.mark.comment('Bad DTEND format'),
),
],
)
def test_timeperiodexception_creation_from_bad_ics(bad_ics_content):
agenda = Agenda.objects.create(label='Test 1 agenda')
desk = Desk.objects.create(label='Test 1 desk', agenda=agenda)
source = desk.timeperiodexceptionsource_set.create(
ics_filename='sample.ics', ics_file=ContentFile(bad_ics_content, name='sample.ics')
)
with pytest.raises(ICSError):
source.refresh_timeperiod_exceptions_from_ics()
assert TimePeriodException.objects.filter(desk=desk).count() == 0
@pytest.mark.freeze_time('2017-12-01')
def test_timeperiodexception_creation_from_ics_with_recurrences_in_the_past():
# test that recurrent events before today are not created

View File

@ -181,25 +181,6 @@ def test_import_export_bad_date_format(app):
assert '%s' % excinfo.value == 'Bad datetime format "17-05-22 08:00:00"'
def test_import_export_bad_end_time_format(app):
agenda_events = Agenda.objects.create(label='Events Agenda', kind='events')
Desk.objects.create(agenda=agenda_events, slug='_exceptions_holder')
Event.objects.create(
agenda=agenda_events,
start_datetime=make_aware(datetime.datetime(2020, 7, 21, 16, 42, 35)),
places=10,
end_time=datetime.time(20, 00),
)
output = get_output_of_command('export_site')
payload = json.loads(output)
assert len(payload['agendas']) == 1
payload['agendas'][0]['events'][0]['end_time'] = 'xxx20:00'
with pytest.raises(AgendaImportError) as excinfo:
import_site(payload)
assert '%s' % excinfo.value == 'Bad time format "xxx20:00"'
def test_import_export_events_agenda_options(app):
agenda = Agenda.objects.create(
label='Foo Bar',
@ -264,15 +245,7 @@ def test_import_export_events_agenda_options(app):
def test_import_export_event_details(app):
events_type = EventsType.objects.create(
label='Foo',
custom_fields=[
{'varname': 'text', 'label': 'Text', 'field_type': 'text'},
{'varname': 'textarea', 'label': 'TextArea', 'field_type': 'textarea'},
{'varname': 'bool', 'label': 'Bool', 'field_type': 'bool'},
],
)
agenda = Agenda.objects.create(label='Foo Bar', kind='events', events_type=events_type)
agenda = Agenda.objects.create(label='Foo Bar', kind='events')
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
Event.objects.create(
slug='event',
@ -283,13 +256,7 @@ def test_import_export_event_details(app):
publication_datetime=make_aware(datetime.datetime(2020, 5, 11)),
places=42,
start_datetime=now(),
end_time=datetime.time(20, 00),
duration=30,
custom_fields={
'text': 'foo',
'textarea': 'foo bar',
'bool': True,
},
)
# check event (agenda, slug) unicity
agenda2 = Agenda.objects.create(label='Foo Bar 2', kind='events')
@ -320,40 +287,20 @@ def test_import_export_event_details(app):
assert str(first_imported_event.publication_datetime) == '2020-05-10 22:00:00+00:00'
assert str(first_imported_event.publication_datetime.tzinfo) == 'UTC'
assert first_imported_event.duration == 30
assert first_imported_event.end_time == datetime.time(20, 00)
assert first_imported_event.custom_fields == {
'text': 'foo',
'textarea': 'foo bar',
'bool': True,
}
assert Agenda.objects.get(label='Foo Bar 2').event_set.first().slug == 'event'
def test_import_export_recurring_event(app, freezer):
freezer.move_to('2021-01-12 12:10')
events_type = EventsType.objects.create(
label='Foo',
custom_fields=[
{'varname': 'text', 'label': 'Text', 'field_type': 'text'},
{'varname': 'textarea', 'label': 'TextArea', 'field_type': 'textarea'},
{'varname': 'bool', 'label': 'Bool', 'field_type': 'bool'},
],
)
agenda = Agenda.objects.create(label='Foo Bar', kind='events', events_type=events_type)
agenda = Agenda.objects.create(label='Foo Bar', kind='events')
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
event = Event.objects.create(
agenda=agenda,
start_datetime=now(),
end_time=datetime.time(20, 00),
recurrence_days=[now().isoweekday()],
recurrence_week_interval=2,
places=10,
slug='test',
custom_fields={
'text': 'foo',
'textarea': 'foo bar',
'bool': True,
},
)
assert Event.objects.count() == 1
@ -386,14 +333,6 @@ def test_import_export_recurring_event(app, freezer):
event = Event.objects.get(slug='test')
assert Event.objects.filter(primary_event=event).count() == 1
first_event = event.recurrences.first()
assert first_event.custom_fields == {
'text': 'foo',
'textarea': 'foo bar',
'bool': True,
}
first_event.custom_fields = {}
first_event.save()
# import again
with tempfile.NamedTemporaryFile() as f:
@ -403,13 +342,6 @@ def test_import_export_recurring_event(app, freezer):
event = Event.objects.get(slug='test')
assert Event.objects.filter(primary_event=event).count() == 1
first_event2 = event.recurrences.first()
assert first_event.pk == first_event2.pk
assert first_event2.custom_fields == {
'text': 'foo',
'textarea': 'foo bar',
'bool': True,
}
# import again but change places
payload = json.loads(output)
@ -421,7 +353,6 @@ def test_import_export_recurring_event(app, freezer):
event = Event.objects.get(slug='test')
assert event.places == 42
assert event.end_time == datetime.time(20, 00)
assert Event.objects.filter(primary_event=event, places=42).count() == 1
@ -485,43 +416,6 @@ def test_import_export_permissions(app):
assert agenda.edit_role == group2
def test_import_export_permissions_admin_role(app):
meetings_agenda = Agenda.objects.create(label='Foo Bar', kind='meetings')
group1 = Group.objects.create(name='gé1')
group2 = Group.objects.create(name='gé2')
group3 = Group.objects.create(name='gé3')
meetings_agenda.view_role = group1
meetings_agenda.edit_role = group2
meetings_agenda.save()
output = json.loads(get_output_of_command('export_site'))
# simulate newest export format
output['agendas'][0]['permissions'] = {
'admin': 'gé1', # new admin role permissions corresponds to current edit role permissions
'edit': 'gé2', # new edit role has no equivalent and should be ignored
'view': 'gé3', # view role corresponds to current view role
}
import_site(data={}, clean=True)
assert Agenda.objects.count() == 0
Group.objects.all().delete()
group1 = Group.objects.create(name='gé1')
group2 = Group.objects.create(name='gé2')
group3 = Group.objects.create(name='gé3')
import_site(output, overwrite=True)
agenda = Agenda.objects.get(slug=meetings_agenda.slug)
assert agenda.edit_role == group1
assert agenda.view_role == group3
group1.delete()
with pytest.raises(AgendaImportError) as excinfo:
import_site(output, overwrite=True)
assert '%s' % excinfo.value == 'Missing roles: "gé1"'
def test_import_export_agenda_with_resources(app):
meetings_agenda = Agenda.objects.create(label='Foo Bar', kind='meetings')
resource = Resource.objects.create(label='foo')
@ -1005,17 +899,6 @@ 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'
Agenda.objects.all().delete()
payload['agendas'][0]['desks'][0]['exception_sources'][0]['ics_file'] = None
import_site(payload)
assert TimePeriodExceptionSource.objects.count() == 0
assert TimePeriodException.objects.count() == 0
@override_settings(
EXCEPTIONS_SOURCES={

Some files were not shown because too many files have changed in this diff Show More