Compare commits

..

16 Commits

Author SHA1 Message Date
Yann Weber 758ef73ae8 manager: make agenda's groups foldable (#85616)
gitea/chrono/pipeline/head This commit looks good Details
2024-04-03 11:23:01 +02:00
Valentin Deniaud 3576928b2c agendas: import/export end time event field (#88615)
gitea/chrono/pipeline/head This commit looks good Details
2024-04-03 11:05:31 +02:00
Frédéric Péters ae55827939 api: add agenda slug to event details (#88764)
gitea/chrono/pipeline/head This commit looks good Details
2024-03-29 08:28:58 +01:00
Lauréline Guérin d733e91135 api: add primary_event in event details (#88559)
gitea/chrono/pipeline/head This commit looks good Details
2024-03-29 08:21:04 +01:00
Lauréline Guérin 4b8c3412e4
snapshot: do not delete snapshots on user deletion (#88623)
gitea/chrono/pipeline/head This commit looks good Details
2024-03-26 13:32:51 +01:00
Valentin Deniaud be975cfa29 ci: do not run tests in parallel by default (#88626)
gitea/chrono/pipeline/head This commit looks good Details
2024-03-25 14:13:08 +01:00
Lauréline Guérin f7e224ba9b
misc: fix failing test due to dst change (#88568)
gitea/chrono/pipeline/head This commit looks good Details
2024-03-23 10:30:24 +01:00
Thomas Jund 41cadbcfa9 manager: improve html & CSS of partial booking month view (#79863)
gitea/chrono/pipeline/head There was a failure building this commit Details
2024-03-22 09:40:49 +01:00
Lauréline Guérin a34d55879e
translation update
gitea/chrono/pipeline/head This commit looks good Details
2024-03-21 13:41:06 +01:00
Lauréline Guérin 43c42c507c
export_import: missing component in bundle (#88068)
gitea/chrono/pipeline/head This commit looks good Details
2024-03-21 13:35:50 +01:00
Lauréline Guérin 886afb206e
export_import: unknown component_type in urls (#88068) 2024-03-21 13:35:50 +01:00
Lauréline Guérin 2c30eec6ac
export_import: invalid bundle (#88068) 2024-03-21 13:35:49 +01:00
Lauréline Guérin 1896c33f29
manager: get snapshots to compare from application version (#87653)
gitea/chrono/pipeline/head This commit looks good Details
2024-03-21 11:49:48 +01:00
Lauréline Guérin 1f23f85b3d
export_import: redirect to compare view if compare in GET params (#87653) 2024-03-21 11:49:48 +01:00
Lauréline Guérin 393a20b87b
export_import: bundle-check endpoint (#87653) 2024-03-21 11:49:48 +01:00
Lauréline Guérin df0e356e75
export_import: snapshots on application import (#87653) 2024-03-21 10:10:48 +01:00
26 changed files with 1059 additions and 97 deletions

2
Jenkinsfile vendored
View File

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

View File

@ -1171,6 +1171,9 @@ 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(
@ -2698,6 +2701,12 @@ 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']]
@ -2717,6 +2726,7 @@ class Event(WithInspectMixin, models.Model):
update_fields = {
field: getattr(event, field)
for field in [
'end_time',
'label',
'duration',
'publication_datetime',
@ -2736,6 +2746,7 @@ 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,

View File

@ -331,9 +331,11 @@ def get_short_event_detail(
details = {
'id': '%s@%s' % (agenda.slug, event.slug) if multiple_agendas else event.slug,
'slug': event.slug, # kept for compatibility
'primary_event': None,
'text': get_event_text(event, agenda),
'label': event.label or '',
'agenda_label': agenda.label,
'agenda_slug': agenda.slug,
'date': format_response_date(event.start_datetime),
'datetime': format_response_datetime(event.start_datetime),
'end_datetime': format_response_datetime(event.end_datetime) if event.end_datetime else '',
@ -345,6 +347,12 @@ def get_short_event_detail(
'check_locked': event.check_locked,
'invoiced': event.invoiced,
}
if event.primary_event:
details['primary_event'] = (
'%s@%s' % (agenda.slug, event.primary_event.slug)
if multiple_agendas
else event.primary_event.slug
)
for key, value in event.get_custom_fields().items():
details['custom_field_%s' % key] = value
return details
@ -663,6 +671,7 @@ class Datetimes(APIView):
entries = Event.annotate_queryset_for_user(entries, user_external_id)
if lock_code:
entries = Event.annotate_queryset_for_lock_code(entries, lock_code=lock_code)
entries = entries.prefetch_related(Prefetch('primary_event', queryset=Event.objects.all().order_by()))
entries = entries.order_by('start_datetime', 'duration', 'label')
if payload['hide_disabled']:
@ -743,6 +752,9 @@ class MultipleAgendasDatetimes(APIView):
entries = Event.annotate_queryset_for_user(entries, user_external_id, with_status=with_status)
if lock_code:
Event.annotate_queryset_for_lock_code(entries, lock_code)
entries = entries.prefetch_related(
Prefetch('primary_event', queryset=Event.objects.all().order_by())
)
if check_overlaps:
entries = Event.annotate_queryset_with_overlaps(entries)
@ -1845,6 +1857,7 @@ class RecurringFillslots(APIView):
min_start=start_datetime,
max_start=end_datetime,
)
events = events.prefetch_related(Prefetch('primary_event', queryset=Event.objects.all().order_by()))
return events
@ -2003,6 +2016,7 @@ class EventsFillslots(APIView):
output_field=BooleanField(),
)
)
events = events.prefetch_related(Prefetch('primary_event', queryset=Event.objects.all().order_by()))
waiting_list_event_ids = [event.pk for event in events if event.in_waiting_list]
extra_data = get_extra_data(request, payload)
@ -2227,9 +2241,13 @@ class MultipleAgendasEventsFillslotsRevert(APIView):
if booking.previous_state == 'cancelled':
bookings_to_cancel.append(booking)
events = Event.objects.filter(
pk__in=[b.event_id for b in bookings_to_cancel + bookings_to_book + bookings_to_delete]
).prefetch_related('agenda')
events = (
Event.objects.filter(
pk__in=[b.event_id for b in bookings_to_cancel + bookings_to_book + bookings_to_delete]
)
.prefetch_related('agenda')
.prefetch_related(Prefetch('primary_event', queryset=Event.objects.all().order_by()))
)
events_by_id = {x.id: x for x in events}
with transaction.atomic():
cancellation_datetime = now()
@ -2753,10 +2771,12 @@ class BookingsAPI(ListAPIView):
return Response({'err': 0, 'data': data})
def get_queryset(self):
event_queryset = Event.objects.all().prefetch_related(
'agenda', 'desk', Prefetch('primary_event', queryset=Event.objects.all().order_by())
)
return (
Booking.objects.filter(primary_booking__isnull=True, cancellation_datetime__isnull=True)
.select_related('event', 'event__agenda', 'event__desk')
.prefetch_related('user_checks')
.prefetch_related('user_checks', Prefetch('event', queryset=event_queryset))
.order_by('event__start_datetime', 'event__slug', 'event__agenda__slug', 'pk')
)

View File

@ -19,6 +19,7 @@ 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,6 +29,7 @@ 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 APIError
from chrono.apps.export_import.models import Application, ApplicationElement
from chrono.manager.utils import import_site
@ -41,6 +43,21 @@ 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,)
@ -140,7 +157,7 @@ class ListComponents(GenericAPIView):
permission_classes = (permissions.IsAdminUser,)
def get(self, request, *args, **kwargs):
klass = klasses[kwargs['component_type']]
klass = get_klass_from_component_type(kwargs['component_type'])
order_by = 'slug'
if klass == Group:
order_by = 'name'
@ -155,7 +172,7 @@ class ExportComponent(GenericAPIView):
permission_classes = (permissions.IsAdminUser,)
def get(self, request, slug, *args, **kwargs):
klass = klasses[kwargs['component_type']]
klass = get_klass_from_component_type(kwargs['component_type'])
serialisation = get_object_or_404(klass, slug=slug).export_json()
return Response({'data': serialisation})
@ -167,7 +184,7 @@ class ComponentDependencies(GenericAPIView):
permission_classes = (permissions.IsAdminUser,)
def get(self, request, slug, *args, **kwargs):
klass = klasses[kwargs['component_type']]
klass = get_klass_from_component_type(kwargs['component_type'])
component = get_object_or_404(klass, slug=slug)
def dependency_dict(element):
@ -181,8 +198,29 @@ component_dependencies = ComponentDependencies.as_view()
def component_redirect(request, component_type, slug):
klass = klasses[component_type]
klass = get_klass_from_component_type(component_type)
component = get_object_or_404(klass, slug=slug)
if component_type not in klasses or component_type == 'roles':
raise Http404
if (
'compare' in request.GET
and request.GET.get('application')
and request.GET.get('version1')
and request.GET.get('version2')
):
component_type = klasses_translation.get(component_type, component_type)
return redirect(
'%s?version1=%s&version2=%s&application=%s'
% (
reverse(compare_urls[component_type], args=[component.pk]),
request.GET['version1'],
request.GET['version2'],
request.GET['application'],
)
)
if klass == Agenda:
return redirect(reverse('chrono-manager-agenda-view', kwargs={'pk': component.pk}))
if klass == Category:
@ -200,7 +238,114 @@ class BundleCheck(GenericAPIView):
permission_classes = (permissions.IsAdminUser,)
def put(self, request, *args, **kwargs):
return Response({'err': 0, 'data': {}})
tar_io = io.BytesIO(request.read())
try:
with tarfile.open(fileobj=tar_io) as tar:
try:
manifest = json.loads(tar.extractfile('manifest.json').read().decode())
except KeyError:
raise APIError(_('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 APIError(_('Invalid tar file'))
return Response(
{
'data': {
'differences': differences,
'unknown_elements': unknown_elements,
'no_history_elements': no_history_elements,
'legacy_elements': legacy_elements,
}
}
)
bundle_check = BundleCheck.as_view()
@ -213,25 +358,40 @@ class BundleImport(GenericAPIView):
def put(self, request, *args, **kwargs):
tar_io = io.BytesIO(request.read())
components = {}
with tarfile.open(fileobj=tar_io) as tar:
manifest = json.loads(tar.extractfile('manifest.json').read().decode())
self.application = Application.update_or_create_from_manifest(
manifest,
tar,
editable=not self.install,
)
for element in manifest.get('elements'):
component_type = element['type']
if component_type not in klasses or element['type'] == 'roles':
continue
component_type = klasses_translation.get(component_type, component_type)
if component_type not in components:
components[component_type] = []
component_content = (
tar.extractfile('%s/%s' % (element['type'], element['slug'])).read().decode()
try:
with tarfile.open(fileobj=tar_io) as tar:
try:
manifest = json.loads(tar.extractfile('manifest.json').read().decode())
except KeyError:
raise APIError(_('Invalid tar file, missing manifest'))
self.application = Application.update_or_create_from_manifest(
manifest,
tar,
editable=not self.install,
)
components[component_type].append(json.loads(component_content).get('data'))
for element in manifest.get('elements'):
component_type = element['type']
if component_type not in klasses or element['type'] == 'roles':
continue
component_type = klasses_translation.get(component_type, component_type)
if component_type not in components:
components[component_type] = []
try:
component_content = (
tar.extractfile('%s/%s' % (element['type'], element['slug'])).read().decode()
)
except KeyError:
raise APIError(
_(
'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 APIError(_('Invalid tar file'))
# init cache of application elements, from manifest
self.application_elements = set()
# import agendas
@ -260,6 +420,11 @@ class BundleImport(GenericAPIView):
self.application, existing_component
)
self.application_elements.add(element.content_object)
if self.install is True:
existing_component.take_snapshot(
comment=_('Application (%s)') % self.application,
application=self.application,
)
def unlink_obsolete_objects(self):
known_elements = ApplicationElement.objects.filter(application=self.application)

View File

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

View File

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

View File

@ -45,7 +45,7 @@ class WithSnapshotMixin:
class AbstractSnapshot(models.Model):
timestamp = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True)
comment = models.TextField(blank=True, null=True)
serialization = models.JSONField(blank=True, default=dict)
label = models.CharField(_('Label'), max_length=150, blank=True)

View File

@ -54,7 +54,38 @@ class InstanceWithSnapshotHistoryView(ListView):
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:

View File

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: chrono 0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-21 08:36+0100\n"
"POT-Creation-Date: 2024-03-21 13:40+0100\n"
"PO-Revision-Date: 2024-02-01 09:50+0100\n"
"Last-Translator: Frederic Peters <fpeters@entrouvert.com>\n"
"Language: French\n"
@ -1948,6 +1948,24 @@ 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/snapshot/models.py
msgid "deletion"
msgstr "suppression"

View File

@ -870,20 +870,73 @@ div#main-content.partial-booking-dayview {
background-color: var(--red);
z-index: 3;
}
}
.agenda-table.partial-bookings .booking {
height: 70%;
width: 100%;
position: absolute;
right: 0;
top: 15%;
background: #1066bc;
&.present {
background: hsl(120, 57%, 35%);
}
&.absent {
background: hsl(355, 80%, 45%);
// Month view, table element
&-month {
width: 100%;
border-spacing: 0;
& col.we {
background-color: var(--zebra-color);
}
& col.today {
background-image: linear-gradient(
135deg,
hsl(65, 65%, 94%) 20%,
hsl(65, 55%, 92%) 70%,
hsl(65, 50%, 90%) 90%);
}
&--day {
padding: .33em;
a {
color: var(--font-color);
font-weight: normal;
text-decoration: none;
}
&.today a {
font-weight: bold;
}
}
& .registrant {
&--name {
box-sizing: border-box;
text-align: right;
padding: .66rem;
font-size: 130%;
color: #505050;
font-weight: normal;
width: var(--registrant-name-width);
}
&--day-cell {
border-left: var(--separator-size) solid var(--separator-color);
text-align: center;
vertical-align: middle;
padding: .33em;
line-height: 0;
& .booking {
display: inline-block;
width: Min(100%, 1.75em);
height: 1.75em;
--booking-color: #1066bc;
background-color: var(--booking-color);
&.present {
background: var(--green);
}
&.absent {
background: var(--red);
}
}
}
}
&--registrant:nth-child(odd) {
& th, & td {
background-color: var(--zebra-color);
}
}
&--registrant:nth-child(even) {
& th, & td {
--separator-color: var(--zebra-color);
}
}
}
}

View File

@ -3,33 +3,54 @@
{% block content %}
<table class="agenda-table partial-bookings">
<thead>
<tr>
<td></td>
<div class="pk-table-wrapper">
<table class="partial-booking partial-booking-month">
<colgroup>
<col class="name" />
{% for day in days %}
<th>
<a href="{% url 'chrono-manager-agenda-day-view' pk=agenda.pk year=day.date|date:"Y" month=day.date|date:"m" day=day.date|date:"d" %}">{{ day|date:"d" }}</a>
</th>
<col class="{% if day|date:"w" == "0" or day|date:"w" == "6" %}we{% endif %}
{% if today == day.date %}today{% endif %}
" />
{% endfor %}
</tr>
</thead>
</colgroup>
<tbody>
{% for booking_info in user_booking_info %}
<tr class="{% cycle 'odd' 'even' %}">
<th>{{ booking_info.user_name }}</th>
{% for booking in booking_info.bookings %}
<td class="day-cell">
{% if booking %}
<span class="booking {{ booking.check_css_class }}"></span>
{% endif %}
</td>
<thead>
<tr class="partial-booking-month--day-list">
<td></td>
{% for day in days %}
<th scope="col" class="partial-booking-month--day{% if today == day.date %} today{% endif %}">
<a href="{% url 'chrono-manager-agenda-day-view' pk=agenda.pk year=day.date|date:"Y" month=day.date|date:"m" day=day.date|date:"d" %}">
<time datetime="{{ day|date:"Y-m-d" }}">{{ day|date:"d" }}</time>
</a>
</th>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</thead>
</table>
<tbody class="partial-booking-month--registrant-items">
{% for booking_info in user_booking_info %}
<tr class="partial-booking-month--registrant">
<th class="registrant--name" scope="row">{{ booking_info.user_name }}</th>
{% for booking in booking_info.bookings %}
<td class="registrant--day-cell">
{% if booking %}
{% if booking.check_css_class == 'present' %}
{% trans "Present" as booking_status %}
{% elif booking.check_css_class == 'absent' %}
{% trans "Absent" as booking_status %}
{% else %}
{% trans "Not checked" as booking_status %}
{% endif %}
<span title="{{ booking_status }}" class="booking {{ booking.check_css_class }}">
<span class="sr-only">{{ booking_status }}</span>
</span>
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View File

@ -2268,6 +2268,7 @@ 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')

View File

@ -129,6 +129,7 @@ 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'
@ -692,6 +693,8 @@ 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
@ -713,7 +716,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) == 3
assert len(ctx.captured_queries) == 4
# events follow agenda display template
agenda.event_display_template = '{{ event.label }} - {{ event.start_datetime }}'

View File

@ -34,12 +34,14 @@ 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),
@ -60,11 +62,18 @@ def test_datetimes_multiple_agendas(app):
Booking.objects.create(event=event)
agenda_slugs = '%s,%s' % (first_agenda.slug, second_agenda.slug)
resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda_slugs})
with CaptureQueriesContext(connection) as ctx:
resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda_slugs})
assert len(ctx.captured_queries) == 3
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'] == 'May 11, 2021, 4 p.m.'
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]['places']['available'] == 5
assert resp.json['data'][2]['id'] == 'second-agenda@event'

View File

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

View File

@ -936,6 +936,7 @@ 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',
@ -950,6 +951,7 @@ def test_api_events_fillslots_multiple_agendas_revert(app, user):
'slug': 'event',
'text': 'Event',
'url': None,
'primary_event': None,
}
],
'deleted_booking_count': 0,
@ -972,6 +974,7 @@ 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',
@ -986,6 +989,7 @@ def test_api_events_fillslots_multiple_agendas_revert(app, user):
'slug': 'event',
'text': 'Event',
'url': None,
'primary_event': None,
}
],
'deleted_booking_count': 0,
@ -1014,6 +1018,7 @@ 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',
@ -1028,6 +1033,7 @@ def test_api_events_fillslots_multiple_agendas_revert(app, user):
'slug': 'event',
'text': 'Event',
'url': None,
'primary_event': None,
}
],
}
@ -1050,6 +1056,7 @@ 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',
@ -1064,6 +1071,7 @@ def test_api_events_fillslots_multiple_agendas_revert(app, user):
'slug': 'event',
'text': 'Event',
'url': None,
'primary_event': None,
}
],
}
@ -1086,6 +1094,7 @@ 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',
@ -1100,6 +1109,7 @@ def test_api_events_fillslots_multiple_agendas_revert(app, user):
'slug': 'event',
'text': 'Event',
'url': None,
'primary_event': None,
}
],
'booked_booking_count': 0,
@ -1124,6 +1134,7 @@ 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',
@ -1138,6 +1149,7 @@ def test_api_events_fillslots_multiple_agendas_revert(app, user):
'slug': 'event',
'text': 'Event',
'url': None,
'primary_event': None,
}
],
'booked_booking_count': 0,
@ -1169,10 +1181,14 @@ 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) == 14
assert len(ctx.captured_queries) == 15

View File

@ -107,7 +107,7 @@ def test_recurring_events_api_fillslots(app, user, freezer, action):
params['user_external_id'] = 'user_id_3'
with CaptureQueriesContext(connection) as ctx:
resp = app.post_json(fillslots_url, params=params)
assert len(ctx.captured_queries) in [12, 13]
assert len(ctx.captured_queries) in [15, 16]
# 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) == 15
assert len(ctx.captured_queries) == 17
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) == 17
assert len(ctx.captured_queries) == 18
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) == 14
assert len(ctx.captured_queries) == 16
@pytest.mark.freeze_time('2022-03-07 14:00') # Monday of 10th week

View File

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

View File

@ -9,6 +9,7 @@ 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
@ -165,6 +166,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'))
@ -195,6 +199,9 @@ def test_export_minor_components(app, admin_user):
# 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'))
@ -363,6 +370,11 @@ def test_unknown_compoment_dependencies(app, admin_user):
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, user):
app.authorization = ('Basic', ('john', 'doe'))
agenda = Agenda.objects.create(label='Rdv', slug='rdv', kind='meetings')
@ -374,22 +386,60 @@ def test_redirect(app, user):
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'):
@ -506,6 +556,12 @@ 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
@ -535,6 +591,44 @@ 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.put('/api/export-import/bundle-import/', b'garbage')
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.put('/api/export-import/bundle-import/', tar_io.getvalue())
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.put('/api/export-import/bundle-import/', tar_io.getvalue())
assert resp.json['err']
assert resp.json['err_desc'] == 'Invalid tar file, missing component agendas/foo'
def test_bundle_declare(app, admin_user):
@ -565,7 +659,7 @@ 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 unkown references in manifest
# and remove agendas to have unknown references in manifest
Agenda.objects.all().delete()
resp = app.put('/api/export-import/bundle-declare/', bundle)
@ -574,6 +668,38 @@ def test_bundle_declare(app, admin_user):
assert application.visible is True
assert ApplicationElement.objects.count() == 4 # category, events_type, unavailability_calendar, resource
# bad file format
resp = app.put('/api/export-import/bundle-declare/', b'garbage')
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.put('/api/export-import/bundle-declare/', tar_io.getvalue())
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.put('/api/export-import/bundle-declare/', tar_io.getvalue())
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'))
@ -632,4 +758,283 @@ def test_bundle_unlink(app, admin_user, bundle):
def test_bundle_check(app, admin_user):
app.authorization = ('Basic', ('admin', 'admin'))
assert app.put('/api/export-import/bundle-check/').json == {'err': 0, 'data': {}}
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.put('/api/export-import/bundle-check/', incomplete_bundles[0])
assert resp.json == {'data': {}}
resp = app.put('/api/export-import/bundle-check/', incomplete_bundles[1])
assert resp.json == {'data': {}}
# not yet imported
resp = app.put('/api/export-import/bundle-check/', 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.put('/api/export-import/bundle-import/', bundles[0])
assert Application.objects.count() == 1
assert ApplicationElement.objects.count() == 8
# remove application links
Application.objects.all().delete()
resp = app.put('/api/export-import/bundle-check/', 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.put('/api/export-import/bundle-import/', bundles[0])
assert Application.objects.count() == 1
assert ApplicationElement.objects.count() == 8
# no changes since last import
resp = app.put('/api/export-import/bundle-check/', 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.put('/api/export-import/bundle-check/', 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.put('/api/export-import/bundle-import/', bundles[1])
# and check
resp = app.put('/api/export-import/bundle-check/', 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.put('/api/export-import/bundle-check/', 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.put('/api/export-import/bundle-check/', b'garbage')
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.put('/api/export-import/bundle-check/', tar_io.getvalue())
assert resp.json['err']
assert resp.json['err_desc'] == 'Invalid tar file, missing manifest'

View File

@ -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) == 3
assert len(ctx.captured_queries) == 6
assert resp.json['err'] == 0
assert resp.json['data'] == [

View File

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

View File

@ -1138,7 +1138,7 @@ def test_manager_partial_bookings_month_view(app, admin_user, freezer):
today = start_datetime.date()
resp = app.get('/manage/agendas/%s/month/%d/%d/%d/' % (agenda.pk, today.year, today.month, today.day))
assert [int(x.text) for x in resp.pyquery('thead th a')] == list(range(1, 32))
assert [int(x.text) for x in resp.pyquery('thead th time')] == list(range(1, 32))
assert [x.text for x in resp.pyquery('tbody tr th')] == [
'User Absent',
@ -1151,21 +1151,24 @@ 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')) == 1
assert len(resp.pyquery(user_absent_row)('td span.booking')) == 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')) == 0
assert len(resp.pyquery(subscription_not_booked_row)('td span.booking')) == 0
user_not_checked_row = resp.pyquery('tbody tr')[2]
assert len(resp.pyquery(user_not_checked_row)('td')) == 31
assert len(resp.pyquery(user_not_checked_row)('td span.booking')) == 2
assert resp.pyquery(user_not_checked_row)('td span.booking').text() == 'Not checked Not checked'
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')) == 1
assert len(resp.pyquery(user_present_row)('td span.booking')) == 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'
user_present_mixed_row = resp.pyquery('tbody tr')[4]
assert len(resp.pyquery(user_present_mixed_row)('td')) == 31
@ -1178,10 +1181,20 @@ def test_manager_partial_bookings_month_view(app, admin_user, freezer):
assert len(resp.pyquery(user_present_incomplete_row)('td span.booking.present')) == 0
resp = resp.click('Next month')
assert [int(x.text) for x in resp.pyquery('thead th a')] == list(range(1, 31))
assert [int(x.text) for x in resp.pyquery('thead th time')] == list(range(1, 31))
assert [x.text for x in resp.pyquery('tbody tr th')] == ['Subscription Next Month']
assert len(resp.pyquery('tbody tr td')) == 30
freezer.move_to('2023-05-10 14:00')
resp = app.get(resp.request.url)
assert len(resp.pyquery('th.today')) == 0
assert len(resp.pyquery('col.today')) == 0
freezer.move_to('2023-06-10 14:00')
resp = app.get(resp.request.url)
assert resp.pyquery('th.today').text() == '10'
assert len(resp.pyquery('col.today')) == 1
def test_manager_partial_bookings_occupation_rates(app, admin_user):
agenda = Agenda.objects.create(label='Foo bar', kind='events', partial_bookings=True)

View File

@ -28,6 +28,7 @@ def test_agenda_history(settings, app, admin_user):
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
@ -56,7 +57,7 @@ def test_agenda_history(settings, app, admin_user):
assert resp.text.count('<del>') == 0
else:
assert resp.text.count('diff_sub') == 1
assert resp.text.count('diff_add') == 16
assert resp.text.count('diff_add') == 17
assert resp.text.count('diff_chg') == 0
resp = app.get(
'/manage/agendas/%s/history/compare/?version1=%s&version2=%s'
@ -65,9 +66,40 @@ def test_agenda_history(settings, app, admin_user):
assert 'Snapshot (%s)' % (snapshot1.pk) in resp
assert 'Snapshot (%s) - (Version 42.0)' % (snapshot2.pk) in resp
assert resp.text.count('diff_sub') == 1
assert resp.text.count('diff_add') == 16
assert resp.text.count('diff_add') == 17
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')
@ -101,6 +133,7 @@ def test_category_history(settings, app, admin_user):
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
@ -141,6 +174,37 @@ def test_category_history(settings, app, admin_user):
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')
@ -148,6 +212,7 @@ def test_events_type_history(settings, app, admin_user):
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
@ -188,6 +253,41 @@ def test_events_type_history(settings, app, admin_user):
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')
@ -195,6 +295,7 @@ def test_resource_history(settings, app, admin_user):
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
@ -235,6 +336,37 @@ def test_resource_history(settings, app, admin_user):
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')
@ -242,6 +374,7 @@ def test_unavailability_calendar_history(settings, app, admin_user):
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
@ -282,6 +415,41 @@ def test_unavailability_calendar_history(settings, app, admin_user):
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')

View File

@ -181,6 +181,25 @@ 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',
@ -256,6 +275,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,
)
# check event (agenda, slug) unicity
@ -287,6 +307,7 @@ 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 Agenda.objects.get(label='Foo Bar 2').event_set.first().slug == 'event'
@ -297,6 +318,7 @@ def test_import_export_recurring_event(app, freezer):
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,
@ -353,6 +375,7 @@ 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

View File

@ -275,7 +275,7 @@ def test_build_event_agenda(db):
start = now()
events = {
f'Event {i}': {
'start_datetime': start + datetime.timedelta(days=i),
'start_datetime': localtime(start) + datetime.timedelta(days=i),
'places': 10,
}
for i in range(10)

View File

@ -13,6 +13,7 @@ setenv =
CHRONO_SETTINGS_FILE=tests/settings.py
BRANCH_NAME={env:BRANCH_NAME:}
SETUPTOOLS_USE_DISTUTILS=stdlib
NUMPROCESSES={env:NUMPROCESSES:1}
coverage: COVERAGE=--junitxml=junit-{envname}.xml --cov-report xml --cov-report html --cov=chrono/ --cov-config .coveragerc
deps =
@ -44,7 +45,7 @@ allowlist_externals =
commands =
./getlasso3.sh
python3 setup.py compile_translations
py.test -v --dist loadfile {env:COVERAGE:} {posargs:tests/}
py.test {posargs:-v --dist loadfile {env:COVERAGE:} --numprocesses={env:NUMPROCESSES:1} tests/}
codestyle: pre-commit run --all-files --show-diff-on-failure
[testenv:pylint]