Compare commits
4 Commits
main
...
wip/79863-
Author | SHA1 | Date |
---|---|---|
Valentin Deniaud | 0a25fc4d9a | |
Valentin Deniaud | 337f0a682f | |
Valentin Deniaud | c6c942dedd | |
Thomas Jund | df02ae1d1e |
|
@ -6,7 +6,7 @@ pipeline {
|
|||
stages {
|
||||
stage('Unit Tests') {
|
||||
steps {
|
||||
sh 'NUMPROCESSES=3 tox -rv'
|
||||
sh 'tox -rv -- --numprocesses 3'
|
||||
}
|
||||
post {
|
||||
always {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')),
|
||||
]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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'):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 = []
|
|
@ -1,6 +0,0 @@
|
|||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = []
|
||||
operations = []
|
|
@ -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',),
|
||||
},
|
||||
),
|
||||
]
|
|
@ -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)
|
|
@ -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 %}
|
|
@ -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'),
|
||||
]
|
|
@ -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,
|
||||
)
|
|
@ -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()
|
|
@ -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
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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'),
|
||||
)
|
|
@ -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'),
|
||||
]
|
|
@ -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)
|
|
@ -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
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -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)
|
|
@ -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 l’heure 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 l’heure « %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é à l’usager"
|
||||
|
||||
#: 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 d’attente"
|
||||
|
||||
#: 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 » n’a 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 d’indisponibilité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 d’indisponibilité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 l’abssence 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 d’action"
|
||||
|
||||
#: apps/journal/models.py
|
||||
msgid "Action code"
|
||||
msgstr "Code de l’action"
|
||||
|
||||
#: apps/journal/models.py
|
||||
#, python-format
|
||||
msgid "Unknown entry (%s:%s)"
|
||||
msgstr "Type d’action inconnu (%s:%s)"
|
||||
|
||||
#: apps/journal/templates/chrono/journal/home.html
|
||||
#: manager/templates/chrono/manager_home.html
|
||||
msgid "Audit journal"
|
||||
msgstr "Journal d’audit"
|
||||
|
||||
#: 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 d’inscription"
|
|||
msgid "Booking form"
|
||||
msgstr "Démarche de réservation"
|
||||
|
||||
#: manager/templates/chrono/manager_agenda_history.html
|
||||
msgid "Agenda history"
|
||||
msgstr "Historique de l’agenda"
|
||||
|
||||
#: 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 d’affichage"
|
||||
|
||||
#: 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 d’administration"
|
||||
|
||||
#: 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 d’exceptions"
|
||||
|
||||
#: 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 d’indisponibilité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 n’a 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 d’affichage"
|
||||
|
||||
#: 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 d’administration"
|
||||
|
||||
#: 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 d’indisponibilités"
|
|||
msgid "New Unavailability Calendar"
|
||||
msgstr "Nouveau calendrier d’indisponibilités"
|
||||
|
||||
#: manager/templates/chrono/manager_unavailability_calendar_history.html
|
||||
msgid "UnavailabilityCalendarSnapshot calendar history"
|
||||
msgstr "Historique du calendrier d’indisponibilités"
|
||||
|
||||
#: manager/templates/chrono/manager_unavailability_calendar_list.html
|
||||
msgid "Unavailability Calendars"
|
||||
msgstr "Calendriers d’indisponibilités"
|
||||
|
@ -4200,6 +3837,14 @@ msgstr "Ajouter une période d’exclusion"
|
|||
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 "
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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>
|
|
@ -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 %} {% endif %}
|
||||
{% if not forloop.first %}<input type="radio" name="version2" value="{{ snapshot.pk }}" {% if forloop.counter == 2 %}checked="checked"{% endif %}/>{% else %} {% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ snapshot.timestamp }}
|
||||
{% if snapshot.new_day and snapshot.day_other_count %} — <a class="reveal" href="#day-{{ snapshot.timestamp|date:"Y-m-d"}}">
|
||||
{% if snapshot.day_other_count >= 50 %}<strong>{% endif %}
|
||||
{% blocktrans trimmed count counter=snapshot.day_other_count %}
|
||||
1 other this day
|
||||
{% plural %}
|
||||
{{ counter }} others
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if snapshot.label %}
|
||||
<strong>{{ snapshot.label }}</strong>
|
||||
{% elif snapshot.comment %}
|
||||
{{ snapshot.comment }}
|
||||
{% endif %}
|
||||
{% if snapshot.application_version %}({% blocktrans with version=snapshot.application_version %}Version {{ version }}{% endblocktrans %}){% endif %}
|
||||
</td>
|
||||
<td>{% if snapshot.user %} {{ snapshot.user.get_full_name }}{% endif %}</td>
|
||||
<td>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
$('tr.new-day a.reveal').on('click', function() {
|
||||
var day = $(this).parents('tr.new-day').data('day');
|
||||
$('.snapshots-list tr[data-day="' + day + '"]:not(.new-day)').toggleClass('collapsed');
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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>
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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>
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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>
|
|
@ -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') %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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>
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')),
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
[MASTER]
|
||||
persistent=yes
|
||||
ignore=vendor,Bouncers,ezt.py
|
||||
extension-pkg-allow-list=lxml
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
disable=
|
||||
|
|
5
setup.py
5
setup.py
|
@ -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={
|
||||
|
|
|
@ -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] == {
|
||||
|
|
|
@ -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': '',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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': {}}
|
||||
|
|
|
@ -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'] == [
|
||||
|
|
|
@ -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': '',
|
||||
|
|
|
@ -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)
|
|
@ -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()
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)']
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
)
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue