This commit is contained in:
parent
7c91b91d89
commit
22eae6fb48
|
@ -9,6 +9,7 @@ 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
|
||||
|
|
|
@ -78,6 +78,7 @@ 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,
|
||||
|
@ -3142,9 +3143,16 @@ class Booking(models.Model):
|
|||
del self.user_check
|
||||
return super().refresh_from_db(*args, **kwargs)
|
||||
|
||||
def cancel(self, trigger_callback=False):
|
||||
def cancel(self, trigger_callback=False, request=None):
|
||||
timestamp = now()
|
||||
with transaction.atomic():
|
||||
audit(
|
||||
'booking',
|
||||
_('cancellation of booking (%(booking_id)s) in event "%(event)s"')
|
||||
% {'booking_id': self.id, 'event': self.event},
|
||||
request=request,
|
||||
agenda=self.event.agenda,
|
||||
)
|
||||
self.secondary_booking_set.update(cancellation_datetime=timestamp)
|
||||
self.cancellation_datetime = timestamp
|
||||
self.save()
|
||||
|
@ -3164,15 +3172,23 @@ class Booking(models.Model):
|
|||
self.secondary_booking_set.update(in_waiting_list=True)
|
||||
self.save()
|
||||
|
||||
def reset_user_was_present(self):
|
||||
def reset_user_was_present(self, request=None):
|
||||
with transaction.atomic():
|
||||
audit(
|
||||
'check',
|
||||
_('reset check of %(user)s in %(event)s') % {'user': self.user_name, 'event': self.event},
|
||||
request=request,
|
||||
agenda=self.event.agenda,
|
||||
)
|
||||
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):
|
||||
def mark_user_absence(
|
||||
self, check_type_slug=None, check_type_label=None, start_time=None, end_time=None, request=None
|
||||
):
|
||||
if not self.user_check:
|
||||
self.user_check = BookingCheck(booking=self)
|
||||
self.user_check.presence = False
|
||||
|
@ -3183,12 +3199,20 @@ class Booking(models.Model):
|
|||
|
||||
self.cancellation_datetime = None
|
||||
with transaction.atomic():
|
||||
audit(
|
||||
'check',
|
||||
_('marked absence of %(user)s in %(event)s') % {'user': self.user_name, 'event': self.event},
|
||||
request=request,
|
||||
agenda=self.event.agenda,
|
||||
)
|
||||
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):
|
||||
def mark_user_presence(
|
||||
self, check_type_slug=None, check_type_label=None, start_time=None, end_time=None, request=None
|
||||
):
|
||||
if not self.user_check:
|
||||
self.user_check = BookingCheck(booking=self)
|
||||
self.user_check.presence = True
|
||||
|
@ -3199,6 +3223,12 @@ class Booking(models.Model):
|
|||
|
||||
self.cancellation_datetime = None
|
||||
with transaction.atomic():
|
||||
audit(
|
||||
'check',
|
||||
_('marked presence of %(user)s in %(event)s') % {'user': self.user_name, 'event': self.event},
|
||||
request=request,
|
||||
agenda=self.event.agenda,
|
||||
)
|
||||
self.user_check.save()
|
||||
self.secondary_booking_set.update(cancellation_datetime=None)
|
||||
self.save()
|
||||
|
|
|
@ -57,6 +57,7 @@ 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
|
||||
|
||||
|
@ -1318,7 +1319,7 @@ class EventsAgendaFillslot(APIView):
|
|||
|
||||
if to_cancel_booking:
|
||||
cancelled_booking_id = to_cancel_booking.pk
|
||||
to_cancel_booking.cancel()
|
||||
to_cancel_booking.cancel(request=request)
|
||||
|
||||
# now we have a list of events, book them.
|
||||
primary_booking = None
|
||||
|
@ -1331,6 +1332,14 @@ class EventsAgendaFillslot(APIView):
|
|||
in_waiting_list=in_waiting_list,
|
||||
)
|
||||
new_booking.save()
|
||||
audit(
|
||||
'booking',
|
||||
_('created a booking (%(booking_id)s) for event %(event)s')
|
||||
% {'booking_id': new_booking.id, 'event': event},
|
||||
request=request,
|
||||
agenda=event.agenda,
|
||||
)
|
||||
|
||||
if lock_code and not confirm_after_lock:
|
||||
Lease.objects.create(
|
||||
booking=new_booking,
|
||||
|
@ -1555,7 +1564,7 @@ class MeetingsAgendaFillslot(APIView):
|
|||
).delete()
|
||||
if to_cancel_booking:
|
||||
cancelled_booking_id = to_cancel_booking.pk
|
||||
to_cancel_booking.cancel()
|
||||
to_cancel_booking.cancel(request=request)
|
||||
|
||||
# book event
|
||||
event.save()
|
||||
|
@ -1568,6 +1577,14 @@ class MeetingsAgendaFillslot(APIView):
|
|||
color=color,
|
||||
)
|
||||
booking.save()
|
||||
audit(
|
||||
'booking',
|
||||
_('created a booking (%(booking_id)s) for event %(event)s')
|
||||
% {'booking_id': booking.id, 'event': event},
|
||||
request=request,
|
||||
agenda=event.agenda,
|
||||
)
|
||||
|
||||
if lock_code and not confirm_after_lock:
|
||||
Lease.objects.create(
|
||||
booking=booking,
|
||||
|
@ -2466,6 +2483,19 @@ class MultipleAgendasEventsCheckLock(APIView):
|
|||
for event in events:
|
||||
event.async_refresh_booking_computed_times()
|
||||
|
||||
message = (
|
||||
_('marked event %(event)s as locked for checks')
|
||||
if check_locked
|
||||
else _('unmarked event %(event)s as locked for checks')
|
||||
)
|
||||
for event in events:
|
||||
audit(
|
||||
'check',
|
||||
message % {'event': event},
|
||||
request=request,
|
||||
agenda=event.agenda,
|
||||
)
|
||||
|
||||
return Response({'err': 0})
|
||||
|
||||
|
||||
|
@ -2495,6 +2525,17 @@ class MultipleAgendasEventsInvoiced(APIView):
|
|||
)
|
||||
events.update(invoiced=invoiced)
|
||||
|
||||
message = (
|
||||
_('marked event %(event)s as invoiced') if invoiced else _('unmarked event %(event)s as invoiced')
|
||||
)
|
||||
for event in events:
|
||||
audit(
|
||||
'invoice',
|
||||
message % {'event': event},
|
||||
request=request,
|
||||
agenda=event.agenda,
|
||||
)
|
||||
|
||||
return Response({'err': 0})
|
||||
|
||||
|
||||
|
@ -2932,7 +2973,7 @@ class BookingAPI(APIView):
|
|||
if self.booking.primary_booking is not None:
|
||||
raise APIError(N_('secondary booking'), err=2)
|
||||
|
||||
self.booking.cancel()
|
||||
self.booking.cancel(request=request)
|
||||
response = {'err': 0, 'booking_id': self.booking.pk}
|
||||
return Response(response)
|
||||
|
||||
|
@ -2956,7 +2997,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()
|
||||
booking.cancel(request=request)
|
||||
response = {'err': 0, 'booking_id': booking.id}
|
||||
return Response(response)
|
||||
|
||||
|
@ -2985,6 +3026,14 @@ class AcceptBooking(APIView):
|
|||
raise APIError(N_('booking is not in waiting list'), err=3)
|
||||
booking.accept()
|
||||
event = booking.event
|
||||
audit(
|
||||
'booking',
|
||||
_('acceptation of booking (%(booking_id)s) in event "%(event)s"')
|
||||
% {'booking_id': booking.id, 'event': event},
|
||||
request=request,
|
||||
agenda=event.agenda,
|
||||
)
|
||||
|
||||
response = {
|
||||
'err': 0,
|
||||
'booking_id': booking.pk,
|
||||
|
@ -3016,6 +3065,15 @@ 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',
|
||||
_('suspension of booking (%(booking_id)s) in event "%(event)s"')
|
||||
% {'booking_id': booking.id, 'event': event},
|
||||
request=request,
|
||||
agenda=event.agenda,
|
||||
)
|
||||
|
||||
response = {'err': 0, 'booking_id': booking.pk}
|
||||
return Response(response)
|
||||
|
||||
|
@ -3354,6 +3412,12 @@ class EventCheck(APIView):
|
|||
if not event.checked:
|
||||
event.checked = True
|
||||
event.save(update_fields=['checked'])
|
||||
audit(
|
||||
'check',
|
||||
_('marked event %(event)s as checked') % {'event': event},
|
||||
request=request,
|
||||
agenda=event.agenda,
|
||||
)
|
||||
event.async_notify_checked()
|
||||
response = {
|
||||
'err': 0,
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
# 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 = []
|
|
@ -0,0 +1,6 @@
|
|||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = []
|
||||
operations = []
|
|
@ -0,0 +1,51 @@
|
|||
# Generated by Django 3.2.16 on 2024-04-15 13:59
|
||||
|
||||
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_text', models.TextField()),
|
||||
(
|
||||
'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',),
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,34 @@
|
|||
# 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 _
|
||||
|
||||
|
||||
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_text = models.TextField()
|
||||
agenda = models.ForeignKey(
|
||||
'agendas.Agenda', on_delete=models.SET_NULL, null=True, related_name='audit_entries'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ('-timestamp',)
|
|
@ -0,0 +1,49 @@
|
|||
{% 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.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 %}
|
|
@ -0,0 +1,24 @@
|
|||
# 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'),
|
||||
]
|
|
@ -0,0 +1,26 @@
|
|||
# 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 .models import AuditEntry
|
||||
|
||||
|
||||
def audit(action_type, action_text, request=None, user=None, agenda=None):
|
||||
return AuditEntry.objects.create(
|
||||
user=request.user if request else user,
|
||||
action_type=action_type,
|
||||
action_text=action_text,
|
||||
agenda=agenda,
|
||||
)
|
|
@ -0,0 +1,42 @@
|
|||
# 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()
|
|
@ -71,6 +71,9 @@
|
|||
<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') %}
|
||||
|
|
|
@ -568,4 +568,5 @@ urlpatterns = [
|
|||
),
|
||||
re_path(r'^menu.json$', views.menu_json),
|
||||
path('ants/', include('chrono.apps.ants_hub.urls')),
|
||||
path('journal/', include('chrono.apps.journal.urls')),
|
||||
]
|
||||
|
|
|
@ -92,6 +92,7 @@ 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,
|
||||
|
@ -221,6 +222,7 @@ 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)
|
||||
|
||||
|
@ -3334,9 +3336,16 @@ class EventPresenceView(EventCheckMixin, ViewableAgendaMixin, FormView):
|
|||
booking_check = BookingCheck(booking=booking, presence=True, **qs_kwargs)
|
||||
booking_checks_to_create.append(booking_check)
|
||||
|
||||
BookingCheck.objects.filter(booking__in=bookings).update(presence=True, **qs_kwargs)
|
||||
BookingCheck.objects.bulk_create(booking_checks_to_create)
|
||||
self.event.set_is_checked()
|
||||
with transaction.atomic():
|
||||
audit(
|
||||
'check',
|
||||
_('marked unchecked users as present in %(event)s') % {'event': self.event},
|
||||
request=request,
|
||||
agenda=self.event.agenda,
|
||||
)
|
||||
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)
|
||||
|
||||
|
||||
|
@ -3365,9 +3374,17 @@ class EventAbsenceView(EventCheckMixin, ViewableAgendaMixin, FormView):
|
|||
booking_check = BookingCheck(booking=booking, presence=False, **qs_kwargs)
|
||||
booking_checks_to_create.append(booking_check)
|
||||
|
||||
BookingCheck.objects.filter(booking__in=bookings).update(presence=False, **qs_kwargs)
|
||||
BookingCheck.objects.bulk_create(booking_checks_to_create)
|
||||
self.event.set_is_checked()
|
||||
with transaction.atomic():
|
||||
audit(
|
||||
'check',
|
||||
_('marked unchecked users as absent in %(event)s') % {'event': self.event},
|
||||
request=request,
|
||||
agenda=self.event.agenda,
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
|
@ -3379,6 +3396,12 @@ class EventCheckedView(EventCheckMixin, ViewableAgendaMixin, View):
|
|||
if not self.event.checked:
|
||||
self.event.checked = True
|
||||
self.event.save(update_fields=['checked'])
|
||||
audit(
|
||||
'check',
|
||||
_('marked %(event)s as checked') % {'event': self.event},
|
||||
request=request,
|
||||
agenda=self.agenda,
|
||||
)
|
||||
self.event.async_notify_checked()
|
||||
return self.response(request)
|
||||
|
||||
|
@ -4271,7 +4294,7 @@ class BookingCancelView(ViewableAgendaMixin, UpdateView):
|
|||
def form_valid(self, form):
|
||||
trigger_callback = not form.cleaned_data['disable_trigger']
|
||||
try:
|
||||
self.booking.cancel(trigger_callback)
|
||||
self.booking.cancel(trigger_callback, request=self.request)
|
||||
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.'))
|
||||
|
@ -4356,6 +4379,7 @@ 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)
|
||||
|
||||
|
@ -4372,6 +4396,7 @@ 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)
|
||||
|
||||
|
@ -4393,7 +4418,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()
|
||||
booking.reset_user_was_present(request=request)
|
||||
return self.response(request, booking)
|
||||
|
||||
|
||||
|
|
|
@ -62,6 +62,7 @@ INSTALLED_APPS = (
|
|||
'chrono.manager',
|
||||
'chrono.apps.ants_hub',
|
||||
'chrono.apps.export_import',
|
||||
'chrono.apps.journal',
|
||||
'chrono.apps.snapshot',
|
||||
)
|
||||
|
||||
|
@ -207,6 +208,7 @@ 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
|
||||
|
||||
|
|
|
@ -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) == 18
|
||||
assert len(ctx.captured_queries) == 19
|
||||
|
||||
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) == 17
|
||||
assert len(ctx.captured_queries) == 18
|
||||
|
||||
with CaptureQueriesContext(connection) as ctx:
|
||||
app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
|
||||
|
|
|
@ -0,0 +1,138 @@
|
|||
import datetime
|
||||
|
||||
import pytest
|
||||
from pyquery import PyQuery
|
||||
|
||||
from chrono.agendas.models import Agenda
|
||||
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'),
|
||||
]
|
||||
|
||||
for i in range(20):
|
||||
user = admin_user if i % 3 else manager_user
|
||||
agenda = agendas[i % 3]
|
||||
|
||||
entry = audit('booking', 'cancellation of booking (%s)' % i, user=user, agenda=agenda)
|
||||
entry.timestamp = make_aware(
|
||||
datetime.datetime(
|
||||
2024,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
+ datetime.timedelta(days=i, hours=i, minutes=i)
|
||||
)
|
||||
entry.save()
|
||||
|
||||
entry = audit('check', 'marked absence (%s)' % i, user=user, agenda=agenda)
|
||||
entry.timestamp = make_aware(
|
||||
datetime.datetime(
|
||||
2024,
|
||||
1,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
+ 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 (19)'],
|
||||
['Jan. 20, 2024, 7:19 p.m.', 'Admin', 'Bar', 'cancellation of booking (19)'],
|
||||
['Jan. 20, 2024, 6:18 p.m.', 'Manager', 'Foo', 'marked absence (18)'],
|
||||
['Jan. 19, 2024, 6:18 p.m.', 'Manager', 'Foo', 'cancellation of booking (18)'],
|
||||
['Jan. 19, 2024, 5:17 p.m.', 'Admin', 'Baz', 'marked absence (17)'],
|
||||
['Jan. 18, 2024, 5:17 p.m.', 'Admin', 'Baz', 'cancellation of booking (17)'],
|
||||
['Jan. 18, 2024, 4:16 p.m.', 'Admin', 'Bar', 'marked absence (16)'],
|
||||
['Jan. 17, 2024, 4:16 p.m.', 'Admin', 'Bar', 'cancellation of booking (16)'],
|
||||
['Jan. 17, 2024, 3:15 p.m.', 'Manager', 'Foo', 'marked absence (15)'],
|
||||
['Jan. 16, 2024, 3:15 p.m.', 'Manager', 'Foo', 'cancellation of booking (15)'],
|
||||
]
|
||||
|
||||
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 (14)'],
|
||||
['Jan. 15, 2024, 2:14 p.m.', 'Admin', 'Baz', 'cancellation of booking (14)'],
|
||||
['Jan. 15, 2024, 1:13 p.m.', 'Admin', 'Bar', 'marked absence (13)'],
|
||||
['Jan. 14, 2024, 1:13 p.m.', 'Admin', 'Bar', 'cancellation of booking (13)'],
|
||||
['Jan. 14, 2024, 12:12 p.m.', 'Manager', 'Foo', 'marked absence (12)'],
|
||||
['Jan. 13, 2024, 12:12 p.m.', 'Manager', 'Foo', 'cancellation of booking (12)'],
|
||||
['Jan. 13, 2024, 11:11 a.m.', 'Admin', 'Baz', 'marked absence (11)'],
|
||||
['Jan. 12, 2024, 11:11 a.m.', 'Admin', 'Baz', 'cancellation of booking (11)'],
|
||||
['Jan. 12, 2024, 10:10 a.m.', 'Admin', 'Bar', 'marked absence (10)'],
|
||||
['Jan. 11, 2024, 10:10 a.m.', 'Admin', 'Bar', 'cancellation of booking (10)'],
|
||||
]
|
||||
|
||||
# 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', 'cancellation of booking (18)'],
|
||||
['Jan. 19, 2024, 5:17 p.m.', 'Admin', 'Baz', 'marked absence (17)'],
|
||||
]
|
||||
|
||||
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', 'cancellation of booking (18)']
|
||||
]
|
||||
|
||||
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', 'cancellation of booking (19)'],
|
||||
['Jan. 17, 2024, 4:16 p.m.', 'Admin', 'Bar', 'cancellation of booking (16)'],
|
||||
['Jan. 14, 2024, 1:13 p.m.', 'Admin', 'Bar', 'cancellation of booking (13)'],
|
||||
['Jan. 11, 2024, 10:10 a.m.', 'Admin', 'Bar', 'cancellation of booking (10)'],
|
||||
['Jan. 8, 2024, 7:07 a.m.', 'Admin', 'Bar', 'cancellation of booking (7)'],
|
||||
['Jan. 5, 2024, 4:04 a.m.', 'Admin', 'Bar', 'cancellation of booking (4)'],
|
||||
['Jan. 2, 2024, 1:01 a.m.', 'Admin', 'Bar', 'cancellation of booking (1)'],
|
||||
]
|
Loading…
Reference in New Issue