manager: add view of events agenda for restricted users (#20279)

This commit is contained in:
Frédéric Péters 2019-12-22 14:27:13 +01:00
parent 14670bc709
commit f1f7d8a7d7
5 changed files with 231 additions and 132 deletions

View File

@ -26,56 +26,6 @@
<a href="{% url 'chrono-manager-agenda-settings' pk=agenda.id %}">{% trans 'Settings' %}</a>
{% endif %}
<a href="" onclick="window.print()">{% trans 'Print' %}</a>
<a href="{% url 'chrono-manager-agenda-day-view' pk=agenda.id year=view.date|date:"Y" month=view.date|date:"m" day=view.date|date:"d" %}">{% trans 'Day view' %}</a>
{% endblock %}
{% block extra-actions %}{% endblock %}
</span>
{% block content %}
{% for week_days in view.get_timetable_infos %}
{% if forloop.first %}
<table class="agenda-table month-view">
<tbody>
{% endif %}
<tr>
<th></th>
{% for day in week_days.days %}
<th class="weekday {% if day.today %}today{% endif %}">{% if not day.other_month %}<a href="{% url 'chrono-manager-agenda-day-view' pk=agenda.id year=day.date|date:"Y" month=day.date|date:"m" day=day.date|date:"d" %}">{{ day.date|date:"l j" }}</a>{% endif %}</th>
{% endfor %}
</tr>
{% for hour in week_days.periods %}
<tr class="{% cycle 'odd' 'even' %}">
<th class="hour">{{ hour|date:"TIME_FORMAT" }}</th>
{% for day in week_days.days %}
<td class="{% if day.other_month %}other-month{% endif %} {% if day.today %}today{% endif %}">
{% if forloop.parentloop.first %}
{% for slot in day.infos.opening_hours %}
<div class="opening-hours" style="height:{{ slot.css_height|stringformat:".1f" }}%;top:{{ slot.css_top|stringformat:".1f" }}%;width:{{ slot.css_width|stringformat:".1f" }}%;left:{{ slot.css_left|stringformat:".1f" }}%;"></div>
{% endfor %}
{% for slot in day.infos.booked_slots %}
<div class="booking" style="left:{{ slot.css_left|stringformat:".1f" }}%;height:{{ slot.css_height|stringformat:".1f" }}%;top:{{ slot.css_top|stringformat:".1f" }}%;width:{{ slot.css_width|stringformat:".1f" }}%">
<span class="start-time">{{slot.booking.event.start_datetime|date:"TIME_FORMAT"}}</span>
<a {% if slot.booking.backoffice_url %}href="{{slot.booking.backoffice_url}}"{% endif %}
>{% if slot.booking.label or slot.booking.user_name %}
{{slot.booking.label}}{% if slot.booking.label and slot.booking.user_name %} - {% endif %} {{slot.booking.user_name}}
{% else %}{% trans "booked" %}{% endif %}</a>
<span class="desk">{{ slot.desk }}</span>
</div>
{% endfor %}
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
{% resetcycle %}
{% if forloop.last %}
</tbody>
</table>
{% endif %}
{% empty %}
<div class="closed-for-the-day">
<p>{% trans "No opening hours this month." %}</p>
</div>
{% endfor %}
{% endblock %}

View File

@ -0,0 +1,53 @@
{% extends "chrono/manager_agenda_month_view.html" %}
{% load i18n %}
{% block content %}
<div class="section">
<h3>{% trans "Events" %}</h3>
<div>
{% if object_list %}
<ul class="objects-list single-links">
{% for event in object_list %}
<li class="{% if event.booked_places > event.places %}overbooking{% endif %}
{% if event.full %}full{% endif %}
{% if not event.in_bookable_period %}not-{% endif %}bookable"
{% if event.places %}
data-total="{{event.places}}" data-booked="{{event.booked_places}}"
{% elif event.waiting_list_places %}
data-total="{{event.waiting_list_places}}" data-booked="{{event.waiting_list}}"
{% endif %}
><a>
{% if event.label %}{{event.label}} / {% endif %}
{{ event.start_datetime }}
{% if event.full %}/ <span class="full">{% trans "full" %}</span>{% endif %}
(
{% if event.places %}
{% blocktrans with places=event.places booked_places=event.booked_places %}{{ places }} places, {{ booked_places }} booked places{% endblocktrans %}
{% endif %}
{% if event.places and event.waiting_list_places %} / {% endif %}
{% if event.waiting_list_places %}
{% blocktrans with places=event.waiting_list_places waiting_places=event.waiting_list %}
{{waiting_places}} on {{ places }} in waiting list
{% endblocktrans %}
{% endif %}
)
{% if not event.in_bookable_period %}
({% trans "out of bookable period" %})
{% endif %}
</a>
<span class="occupation-bar"></span>
</li>
{% endfor %}
</ul>
{% include "gadjo/pagination.html" %}
{% else %}
<div class="big-msg-info">
{% blocktrans %}
This month doesn't have any event configured.
{% endblocktrans %}
</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,56 @@
{% extends "chrono/manager_agenda_month_view.html" %}
{% load i18n %}
{% block extra-actions %}
<a href="{% url 'chrono-manager-agenda-day-view' pk=agenda.id year=view.date|date:"Y" month=view.date|date:"m" day=view.date|date:"d" %}">{% trans 'Day view' %}</a>
{% endblock %}
{% block content %}
{% for week_days in view.get_timetable_infos %}
{% if forloop.first %}
<table class="agenda-table month-view">
<tbody>
{% endif %}
<tr>
<th></th>
{% for day in week_days.days %}
<th class="weekday {% if day.today %}today{% endif %}">{% if not day.other_month %}<a href="{% url 'chrono-manager-agenda-day-view' pk=agenda.id year=day.date|date:"Y" month=day.date|date:"m" day=day.date|date:"d" %}">{{ day.date|date:"l j" }}</a>{% endif %}</th>
{% endfor %}
</tr>
{% for hour in week_days.periods %}
<tr class="{% cycle 'odd' 'even' %}">
<th class="hour">{{ hour|date:"TIME_FORMAT" }}</th>
{% for day in week_days.days %}
<td class="{% if day.other_month %}other-month{% endif %} {% if day.today %}today{% endif %}">
{% if forloop.parentloop.first %}
{% for slot in day.infos.opening_hours %}
<div class="opening-hours" style="height:{{ slot.css_height|stringformat:".1f" }}%;top:{{ slot.css_top|stringformat:".1f" }}%;width:{{ slot.css_width|stringformat:".1f" }}%;left:{{ slot.css_left|stringformat:".1f" }}%;"></div>
{% endfor %}
{% for slot in day.infos.booked_slots %}
<div class="booking" style="left:{{ slot.css_left|stringformat:".1f" }}%;height:{{ slot.css_height|stringformat:".1f" }}%;top:{{ slot.css_top|stringformat:".1f" }}%;width:{{ slot.css_width|stringformat:".1f" }}%">
<span class="start-time">{{slot.booking.event.start_datetime|date:"TIME_FORMAT"}}</span>
<a {% if slot.booking.backoffice_url %}href="{{slot.booking.backoffice_url}}"{% endif %}
>{% if slot.booking.label or slot.booking.user_name %}
{{slot.booking.label}}{% if slot.booking.label and slot.booking.user_name %} - {% endif %} {{slot.booking.user_name}}
{% else %}{% trans "booked" %}{% endif %}</a>
<span class="desk">{{ slot.desk }}</span>
</div>
{% endfor %}
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
{% resetcycle %}
{% if forloop.last %}
</tbody>
</table>
{% endif %}
{% empty %}
<div class="closed-for-the-day">
<p>{% trans "No opening hours this month." %}</p>
</div>
{% endfor %}
{% endblock %}

View File

@ -39,6 +39,7 @@ from django.views.generic import (
TemplateView,
DayArchiveView,
MonthArchiveView,
View,
)
from chrono.agendas.models import (
@ -160,17 +161,42 @@ class AgendasImportView(FormView):
agendas_import = AgendasImportView.as_view()
class AgendaEditView(UpdateView):
class ViewableAgendaMixin(object):
agenda = None
def dispatch(self, request, *args, **kwargs):
self.agenda = get_object_or_404(Agenda, id=kwargs.get('pk'))
if not self.check_permissions(request.user):
raise PermissionDenied()
return super(ViewableAgendaMixin, self).dispatch(request, *args, **kwargs)
def check_permissions(self, user):
return self.agenda.can_be_viewed(user)
def get_context_data(self, **kwargs):
context = super(ViewableAgendaMixin, self).get_context_data(**kwargs)
context['agenda'] = self.agenda
return context
class ManagedAgendaMixin(ViewableAgendaMixin):
def check_permissions(self, user):
return self.agenda.can_be_managed(user)
def get_initial(self):
initial = super(ManagedAgendaMixin, self).get_initial()
initial['agenda'] = self.agenda.id
return initial
def get_success_url(self):
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.agenda.id})
class AgendaEditView(ManagedAgendaMixin, UpdateView):
template_name = 'chrono/manager_agenda_form.html'
model = Agenda
form_class = AgendaEditForm
def get_object(self, queryset=None):
obj = super(AgendaEditView, self).get_object(queryset=queryset)
if not obj.can_be_managed(self.request.user):
raise PermissionDenied()
return obj
def get_success_url(self):
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.object.id})
@ -208,35 +234,44 @@ class AgendaDeleteView(DeleteView):
agenda_delete = AgendaDeleteView.as_view()
class AgendaView(DetailView):
model = Agenda
class AgendaView(ViewableAgendaMixin, View):
def get(self, request, *args, **kwargs):
try:
agenda = Agenda.objects.get(id=kwargs.get('pk'))
except Agenda.DoesNotExist:
raise Http404()
if not agenda.can_be_viewed(self.request.user):
raise PermissionDenied()
if agenda.kind == 'meetings':
today = datetime.date.today()
if self.agenda.kind == 'meetings':
# redirect to today view
today = datetime.date.today()
return HttpResponseRedirect(
reverse(
'chrono-manager-agenda-day-view',
kwargs={'pk': agenda.id, 'year': today.year, 'month': today.month, 'day': today.day},
kwargs={'pk': self.agenda.id, 'year': today.year, 'month': today.month, 'day': today.day},
)
)
# redirect to settings
return HttpResponseRedirect(reverse('chrono-manager-agenda-settings', kwargs={'pk': agenda.id}))
if self.agenda.kind == 'events':
# redirect to monthly view, to first month where there are events,
# otherwise to latest month with events, otherwise to this month.
event = self.agenda.event_set.filter(
start_datetime__gte=datetime.date(today.year, today.month, 1)
).first()
if not event:
event = self.agenda.event_set.filter(
start_datetime__lte=datetime.date(today.year, today.month, 1)
).last()
if event:
day = event.start_datetime
else:
day = today
return HttpResponseRedirect(
reverse(
'chrono-manager-agenda-month-view',
kwargs={'pk': self.agenda.id, 'year': day.year, 'month': day.month},
)
)
agenda_view = AgendaView.as_view()
class AgendaDateView(object):
class AgendaDateView(ViewableAgendaMixin):
model = Event
month_format = '%m'
date_field = 'start_datetime'
@ -244,12 +279,6 @@ class AgendaDateView(object):
allow_future = True
def dispatch(self, request, *args, **kwargs):
self.agenda = get_object_or_404(Agenda, id=kwargs.get('pk'))
if self.agenda.kind != 'meetings':
raise Http404()
if not self.agenda.can_be_viewed(request.user):
raise PermissionDenied()
# specify 6am time to get the expected timezone on daylight saving time
# days.
try:
@ -267,7 +296,7 @@ class AgendaDateView(object):
return HttpResponseRedirect(
reverse(
'chrono-manager-agenda-day-view',
kwargs={'pk': self.agenda.id, 'year': date.year, 'month': date.month, 'day': date.day},
kwargs={'pk': kwargs['pk'], 'year': date.year, 'month': date.month, 'day': date.day},
)
)
return super(AgendaDateView, self).dispatch(request, *args, **kwargs)
@ -275,10 +304,11 @@ class AgendaDateView(object):
def get_context_data(self, **kwargs):
context = super(AgendaDateView, self).get_context_data(**kwargs)
context['agenda'] = self.agenda
try:
context['hour_span'] = max(60 // self.agenda.get_base_meeting_duration(), 1)
except ValueError: # no meeting types defined
context['hour_span'] = 1
if self.agenda.kind == 'meetings':
try:
context['hour_span'] = max(60 // self.agenda.get_base_meeting_duration(), 1)
except ValueError: # no meeting types defined
context['hour_span'] = 1
context['user_can_manage'] = self.agenda.can_be_managed(self.request.user)
return context
@ -301,6 +331,11 @@ class AgendaDateView(object):
class AgendaDayView(AgendaDateView, DayArchiveView):
template_name = 'chrono/manager_agenda_day_view.html'
def dispatch(self, request, *args, **kwargs):
# day view should only exist for meetings kind.
get_object_or_404(Agenda, id=kwargs.get('pk'), kind='meetings')
return super(AgendaDayView, self).dispatch(request, *args, **kwargs)
def get_previous_day_url(self):
previous_day = self.date.date() - datetime.timedelta(days=1)
return reverse(
@ -385,7 +420,8 @@ agenda_day_view = AgendaDayView.as_view()
class AgendaMonthView(AgendaDateView, MonthArchiveView):
template_name = 'chrono/manager_agenda_month_view.html'
def get_template_names(self):
return ['chrono/manager_%s_agenda_month_view.html' % self.agenda.kind]
def get_previous_month_url(self):
previous_month = self.get_previous_month(self.date.date())
@ -518,32 +554,6 @@ class AgendaMonthView(AgendaDateView, MonthArchiveView):
agenda_monthly_view = AgendaMonthView.as_view()
class ManagedAgendaMixin(object):
agenda = None
def dispatch(self, request, *args, **kwargs):
try:
self.agenda = Agenda.objects.get(id=kwargs.get('pk'))
except Agenda.DoesNotExist:
raise Http404()
if not self.agenda.can_be_managed(request.user):
raise PermissionDenied()
return super(ManagedAgendaMixin, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super(ManagedAgendaMixin, self).get_context_data(**kwargs)
context['agenda'] = self.agenda
return context
def get_initial(self):
initial = super(ManagedAgendaMixin, self).get_initial()
initial['agenda'] = self.agenda.id
return initial
def get_success_url(self):
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.agenda.id})
class ManagedAgendaSubobjectMixin(object):
agenda = None
@ -611,22 +621,9 @@ class ManagedDeskSubobjectMixin(object):
class AgendaSettings(ManagedAgendaMixin, DetailView):
model = Agenda
def dispatch(self, request, *args, **kwargs):
try:
self.agenda = Agenda.objects.get(id=kwargs.get('pk'))
except Agenda.DoesNotExist:
raise Http404()
if not self.agenda.can_be_managed(request.user):
# "events" agendas settings page can be access by user with the
# view permission as there are no other "view" page for this type
# of agenda.
if self.agenda.kind != 'events' or not self.agenda.can_be_viewed(request.user):
raise PermissionDenied()
return super(DetailView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super(AgendaSettings, self).get_context_data(**kwargs)
context['user_can_manage'] = self.get_object().can_be_managed(self.request.user)
context['user_can_manage'] = True
return context
def get_template_names(self):

View File

@ -159,11 +159,8 @@ def test_view_agendas_as_manager(app, manager_user):
# check user doesn't have access
app.get('/manage/agendas/%s/' % agenda2.id, status=403)
# check view gives access to the settings page for "events" agenda
resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200)
# but there's no links to actions
assert not '>New Event<' in resp.text
assert not '>Options<' in resp.text
# check there's no access to the settings page for "events" agenda
resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=403)
app.get('/manage/agendas/%s/add-event' % agenda.id, status=403)
app.get('/manage/agendas/%s/edit' % agenda.id, status=403)
@ -207,6 +204,7 @@ def test_options_agenda(app, admin_user):
app = login(app)
resp = app.get('/manage/', status=200)
resp = resp.click('Foo bar').follow()
resp = resp.click('Settings')
resp = resp.click('Options')
assert resp.form['label'].value == 'Foo bar'
resp.form['label'] = 'Foo baz'
@ -225,7 +223,7 @@ def test_options_agenda_as_manager(app, manager_user):
resp = app.get('/manage/', status=200)
resp = resp.click('Foo bar')
assert not 'Settings' in resp.text
resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200) # ok for "events" agendas
resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=403)
resp = app.get('/manage/agendas/%s/edit' % agenda.id, status=403)
agenda.kind = 'meetings'
agenda.save()
@ -241,6 +239,7 @@ def test_options_agenda_as_manager(app, manager_user):
resp = app.get('/manage/', status=200)
resp = resp.click('Foo bar').follow()
resp = resp.click('Settings')
resp = resp.click('Options')
assert resp.form['label'].value == 'Foo bar'
resp.form['label'] = 'Foo baz'
@ -257,6 +256,7 @@ def test_delete_agenda(app, admin_user):
app = login(app)
resp = app.get('/manage/', status=200)
resp = resp.click('Foo bar').follow()
resp = resp.click('Settings')
resp = resp.click('Delete')
resp = resp.form.submit()
assert resp.location.endswith('/manage/')
@ -273,6 +273,7 @@ def test_delete_busy_agenda(app, admin_user):
app = login(app)
resp = app.get('/manage/', status=200)
resp = resp.click('Foo bar').follow()
resp = resp.click('Settings')
resp = resp.click('Delete')
assert 'Are you sure you want to delete this?' in resp.text
@ -280,6 +281,7 @@ def test_delete_busy_agenda(app, admin_user):
booking.save()
resp = app.get('/manage/', status=200)
resp = resp.click('Foo bar').follow()
resp = resp.click('Settings')
resp = resp.click('Delete')
assert 'This cannot be removed' in resp.text
@ -287,6 +289,7 @@ def test_delete_busy_agenda(app, admin_user):
booking.save()
resp = app.get('/manage/', status=200)
resp = resp.click('Foo bar').follow()
resp = resp.click('Settings')
resp = resp.click('Delete')
assert 'Are you sure you want to delete this?' in resp.text
@ -304,6 +307,7 @@ def test_delete_agenda_as_manager(app, manager_user):
app = login(app, username='manager', password='manager')
resp = app.get('/manage/', status=200)
resp = resp.click('Foo bar').follow()
resp = resp.click('Settings')
assert 'Options' in resp.text
assert 'Delete' not in resp.text
resp = app.get('/manage/agendas/%s/delete' % agenda.id, status=403)
@ -391,6 +395,7 @@ def test_add_event_as_manager(app, manager_user):
agenda.save()
resp = app.get('/manage/agendas/%s/' % agenda.id).follow()
resp = resp.click('Settings')
assert '<h2>Settings' in resp.text
resp = resp.click('New Event')
resp.form['start_datetime'] = '2016-02-15 17:00'
@ -1685,6 +1690,44 @@ def test_agenda_invalid_day_view(app, admin_user, manager_user, api_user):
assert resp.location.endswith('2018/11/30/')
def test_agenda_events_month_view(app, admin_user, manager_user, api_user):
agenda = Agenda.objects.create(label='Events', kind='events')
login(app)
resp = app.get('/manage/agendas/%s/' % agenda.id, status=302)
resp = resp.follow()
assert "This month doesn't have any event configured." in resp.text
today = datetime.date.today()
assert resp.request.url.endswith('%s/%s/' % (today.year, today.month))
# add event in a future month
event = Event(label='xyz', start_datetime=now() + datetime.timedelta(days=40), places=10, agenda=agenda)
event.save()
resp = app.get('/manage/agendas/%s/' % agenda.id).follow()
assert 'xyz' in resp.text
day = event.start_datetime
assert resp.request.url.endswith('%s/%s/' % (day.year, day.month))
# current month still doesn't have events
resp = app.get('/manage/agendas/%s/%s/%s/' % (agenda.id, today.year, today.month))
assert "This month doesn't have any event configured." in resp.text
# add event in the past
event2 = Event(label='zyx', start_datetime=now() - datetime.timedelta(days=40), places=10, agenda=agenda)
event2.save()
resp = app.get('/manage/agendas/%s/' % agenda.id).follow()
assert 'xyz' in resp.text
day = event.start_datetime # still the future event
assert resp.request.url.endswith('%s/%s/' % (day.year, day.month))
# remove future event
event.delete()
resp = app.get('/manage/agendas/%s/' % agenda.id).follow()
assert 'zyx' in resp.text
day = event2.start_datetime # now the past event
assert resp.request.url.endswith('%s/%s/' % (day.year, day.month))
def test_agenda_month_view(app, admin_user, manager_user, api_user):
agenda = Agenda.objects.create(label='Passeports', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Desk A')