manager: display applications (#86148)
gitea/chrono/pipeline/head This commit looks good Details

This commit is contained in:
Lauréline Guérin 2024-01-29 14:52:38 +01:00
parent 3071fab8f8
commit 895758c70c
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
23 changed files with 705 additions and 80 deletions

View File

@ -76,6 +76,7 @@ from django.utils.translation import gettext
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.utils.date import get_weekday_index
from chrono.utils.db import ArraySubquery, SumCardinality
from chrono.utils.interval import Interval, IntervalSet
@ -172,7 +173,7 @@ TimeSlot = collections.namedtuple(
)
class Agenda(models.Model):
class Agenda(WithApplicationMixin, models.Model):
label = models.CharField(_('Label'), max_length=150)
slug = models.SlugField(_('Identifier'), max_length=160, unique=True)
kind = models.CharField(_('Kind'), max_length=20, choices=AGENDA_KINDS, default='events')
@ -2809,7 +2810,7 @@ class Event(models.Model):
return custom_fields
class EventsType(models.Model):
class EventsType(WithApplicationMixin, models.Model):
slug = models.SlugField(_('Identifier'), max_length=160, unique=True)
label = models.CharField(_('Label'), max_length=150)
custom_fields = models.JSONField(blank=True, default=list)
@ -3424,7 +3425,7 @@ class Desk(models.Model):
).delete() # source was not in settings anymore
class Resource(models.Model):
class Resource(WithApplicationMixin, models.Model):
slug = models.SlugField(_('Identifier'), max_length=160, unique=True)
label = models.CharField(_('Label'), max_length=150)
description = models.TextField(_('Description'), blank=True, help_text=_('Optional description.'))
@ -3472,7 +3473,7 @@ class Resource(models.Model):
}
class Category(models.Model):
class Category(WithApplicationMixin, models.Model):
slug = models.SlugField(_('Identifier'), max_length=160, unique=True)
label = models.CharField(_('Label'), max_length=150)
@ -3788,7 +3789,7 @@ class TimePeriodExceptionSource(models.Model):
}
class UnavailabilityCalendar(models.Model):
class UnavailabilityCalendar(WithApplicationMixin, models.Model):
label = models.CharField(_('Label'), max_length=150)
slug = models.SlugField(_('Identifier'), max_length=160, unique=True)
desks = models.ManyToManyField(Desk, related_name='unavailability_calendars')

View File

@ -21,6 +21,14 @@ from django.contrib.contenttypes.models import ContentType
from django.db import models
class WithApplicationMixin:
@property
def applications(self):
if getattr(self, '_applications', None) is None:
Application.load_for_object(self)
return self._applications
class Application(models.Model):
name = models.CharField(max_length=100)
slug = models.SlugField(max_length=100, unique=True)
@ -71,34 +79,23 @@ class Application(models.Model):
@classmethod
def populate_objects(cls, object_class, objects):
content_type = ContentType.objects.get_for_model(object_class)
elements = ApplicationElement.objects.filter(content_type=content_type)
elements = ApplicationElement.objects.filter(
content_type=content_type, application__visible=True
).prefetch_related('application')
elements_by_objects = collections.defaultdict(list)
for element in elements:
elements_by_objects[element.content_object].append(element)
applications_by_ids = {
a.pk: a for a in cls.objects.filter(pk__in=elements.values('application'), visible=True)
}
elements_by_objects[element.object_id].append(element)
for obj in objects:
applications = []
elements = elements_by_objects.get(obj) or []
for element in elements:
application = applications_by_ids.get(element.application_id)
if application:
applications.append(application)
applications = [element.application for element in elements_by_objects.get(obj.pk) or []]
obj._applications = sorted(applications, key=lambda a: a.name)
@classmethod
def load_for_object(cls, obj):
content_type = ContentType.objects.get_for_model(obj.__class__)
elements = ApplicationElement.objects.filter(content_type=content_type, object_id=obj.pk)
applications_by_ids = {
a.pk: a for a in cls.objects.filter(pk__in=elements.values('application'), visible=True)
}
applications = []
for element in elements:
application = applications_by_ids.get(element.application_id)
if application:
applications.append(application)
elements = ApplicationElement.objects.filter(
content_type=content_type, object_id=obj.pk, application__visible=True
).prefetch_related('application')
applications = [element.application for element in elements]
obj._applications = sorted(applications, key=lambda a: a.name)
def get_objects_for_object_class(self, object_class):
@ -106,6 +103,12 @@ class Application(models.Model):
elements = ApplicationElement.objects.filter(content_type=content_type, application=self)
return object_class.objects.filter(pk__in=elements.values('object_id'))
@classmethod
def get_orphan_objects_for_object_class(cls, object_class):
content_type = ContentType.objects.get_for_model(object_class)
elements = ApplicationElement.objects.filter(content_type=content_type, application__visible=True)
return object_class.objects.exclude(pk__in=elements.values('object_id'))
class ApplicationElement(models.Model):
application = models.ForeignKey(Application, on_delete=models.CASCADE)

View File

@ -952,3 +952,7 @@ a.button.button-paragraph {
padding-top: 0.8em;
padding-bottom: 0.8em;
}
.application-logo, .application-icon {
vertical-align: middle;
}

View File

@ -0,0 +1,13 @@
{% load thumbnail %}
{% if application %}
<h2>
{% thumbnail application.icon '64x64' format='PNG' as im %}
<img src="{{ im.url }}" alt="" class="application-logo" />
{% endthumbnail %}
{{ application }}
</h2>
{% elif no_application %}
<h2>{{ title_no_application }}</h2>
{% else %}
<h2>{{ title_object_list }}</h2>
{% endif %}

View File

@ -0,0 +1,5 @@
{% if application %}
<a href="{{ object_list_url }}?application={{ application.slug }}">{{ application }}</a>
{% elif no_application %}
<a href="{{ object_list_url }}?no-application">{{ title_no_application }}</a>
{% endif %}

View File

@ -0,0 +1,12 @@
{% load i18n thumbnail %}
{% if object.applications %}
<h3>{% trans "Applications" %}</h3>
{% for application in object.applications %}
<a class="button button-paragraph" href="{{ object_list_url }}?application={{ application.slug }}">
{% thumbnail application.icon '16x16' format='PNG' as im %}
<img src="{{ im.url }}" alt="" class="application-icon" width="16" />
{% endthumbnail %}
{{ application }}
</a>
{% endfor %}
{% endif %}

View File

@ -0,0 +1,8 @@
{% load thumbnail %}
{% if not application and not no_application %}
{% for application in object.applications %}
{% thumbnail application.icon '16x16' format='PNG' as im %}
<img src="{{ im.url }}" alt="" class="application-icon" width="16" />
{% endthumbnail %}
{% endfor %}
{% endif %}

View File

@ -0,0 +1,15 @@
{% load i18n thumbnail %}
{% if applications %}
<h3>{% trans "Applications" %}</h3>
{% for application in applications %}
<a class="button button-paragraph" href="?application={{ application.slug }}">
{% thumbnail application.icon '16x16' format='PNG' as im %}
<img src="{{ im.url }}" alt="" class="application-icon" width="16" />
{% endthumbnail %}
{{ application }}
</a>
{% endfor %}
<a class="button button-paragraph" href="?no-application">
{{ title_no_application }}
</a>
{% endif %}

View File

@ -121,5 +121,8 @@
{% endif %}
{% block agenda-extra-navigation-actions %}{% endblock %}
{% url 'chrono-manager-homepage' as object_list_url %}
{% include 'chrono/includes/application_detail_fragment.html' %}
</aside>
{% endblock %}

View File

@ -7,27 +7,31 @@
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'chrono-manager-category-list' %}">{% trans "Categories" %}</a>
{% url 'chrono-manager-category-list' as object_list_url %}
<a href="{{ object_list_url }}">{% trans "Categories" %}</a>
{% include 'chrono/includes/application_breadcrumb_fragment.html' with title_no_application=_('Categories outside applications') %}
{% endblock %}
{% block appbar %}
<h2>{% trans 'Categories' %}</h2>
{% include 'chrono/includes/application_appbar_fragment.html' with title_no_application=_('Categories outside applications') title_object_list=_('Categories') %}
{% endblock %}
{% block content %}
{% if object_list %}
<div>
<ul class="objects-list single-links">
{% for object in object_list %}
<li>
<a href="{% url 'chrono-manager-category-edit' pk=object.pk %}">{{ object.label }} ({{ object.slug }})</a>
<a href="{% url 'chrono-manager-category-edit' pk=object.pk %}">
{% include 'chrono/includes/application_icon_fragment.html' %}
{{ object.label }} ({{ object.slug }})
</a>
<a rel="popup" class="delete" href="{% url 'chrono-manager-category-delete' pk=object.id %}">{% trans "remove" %}</a>
</li>
{% endfor %}
</ul>
</div>
{% else %}
{% elif not no_application %}
<div class="big-msg-info">
{% blocktrans trimmed %}
This site doesn't have any category yet. Click on the "New" button in the top
@ -38,8 +42,12 @@
{% endblock %}
{% block sidebar %}
<aside id="sidebar">
<h3>{% trans "Actions" %}</h3>
<a class="button button-paragraph" rel="popup" href="{% url 'chrono-manager-category-add' %}">{% trans 'New category' %}</a>
</aside>
{% if not application and not no_application %}
<aside id="sidebar">
<h3>{% trans "Actions" %}</h3>
<a class="button button-paragraph" rel="popup" href="{% url 'chrono-manager-category-add' %}">{% trans 'New category' %}</a>
{% include 'chrono/includes/application_list_fragment.html' with title_no_application=_('Categories outside applications') %}
</aside>
{% endif %}
{% endblock %}

View File

@ -7,11 +7,13 @@
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'chrono-manager-events-type-list' %}">{% trans "Events types" %}</a>
{% url 'chrono-manager-events-type-list' as object_list_url %}
<a href="{{ object_list_url }}">{% trans "Events types" %}</a>
{% include 'chrono/includes/application_breadcrumb_fragment.html' with title_no_application=_('Events types outside applications') %}
{% endblock %}
{% block appbar %}
<h2>{% trans 'Events types' %}</h2>
{% include 'chrono/includes/application_appbar_fragment.html' with title_no_application=_('Events types outside applications') title_object_list=_('Events types') %}
{% endblock %}
{% block content %}
@ -23,13 +25,16 @@
<ul class="objects-list single-links">
{% for object in object_list %}
<li>
<a href="{% url 'chrono-manager-events-type-edit' pk=object.pk %}">{{ object.label }} ({{ object.slug }})</a>
<a href="{% url 'chrono-manager-events-type-edit' pk=object.pk %}">
{% include 'chrono/includes/application_icon_fragment.html' %}
{{ object.label }} ({{ object.slug }})
</a>
<a rel="popup" class="delete" href="{% url 'chrono-manager-events-type-delete' pk=object.pk %}">{% trans "remove" %}</a>
</li>
{% endfor %}
</ul>
</div>
{% else %}
{% elif not no_application %}
<div class="big-msg-info">
{% blocktrans trimmed %}
This site doesn't have any events type yet. Click on the "New" button in the top
@ -40,8 +45,12 @@
{% endblock %}
{% block sidebar %}
<aside id="sidebar">
<h3>{% trans "Actions" %}</h3>
<a class="button button-paragraph" rel="popup" href="{% url 'chrono-manager-events-type-add' %}">{% trans 'New events type' %}</a>
</aside>
{% if not application and not no_application %}
<aside id="sidebar">
<h3>{% trans "Actions" %}</h3>
<a class="button button-paragraph" rel="popup" href="{% url 'chrono-manager-events-type-add' %}">{% trans 'New events type' %}</a>
{% include 'chrono/includes/application_list_fragment.html' with title_no_application=_('Events types outside applications') %}
</aside>
{% endif %}
{% endblock %}

View File

@ -1,8 +1,14 @@
{% extends "chrono/manager_base.html" %}
{% load i18n %}
{% load i18n thumbnail %}
{% block appbar %}
<h2>{% trans 'Agendas' %}</h2>
{% include 'chrono/includes/application_appbar_fragment.html' with title_no_application=_('Agendas outside applications') title_object_list=_('Agendas') %}
{% endblock %}
{% block breadcrumb %}
{{ block.super }}
{% url 'chrono-manager-homepage' as object_list_url %}
{% include 'chrono/includes/application_breadcrumb_fragment.html' with title_no_application=_('Agendas outside applications') %}
{% endblock %}
{% block content %}
@ -14,12 +20,18 @@
{% if group.grouper %}<h3>{{ group.grouper }}</h3>{% elif not forloop.first %}<h3>{% trans "Misc" %}</h3>{% endif %}
<ul class="objects-list single-links">
{% for object in group.list %}
<li><a href="{% url 'chrono-manager-agenda-view' pk=object.id %}"><span class="badge">{{ object.get_real_kind_display }}</span> {{ object.label }}{% if user.is_staff %} <span class="identifier">[{% trans "identifier:" %} {{ object.slug }}]{% endif %}</span></a></li>
<li>
<a href="{% url 'chrono-manager-agenda-view' pk=object.id %}">
<span class="badge">{{ object.get_real_kind_display }}</span>
{% include 'chrono/includes/application_icon_fragment.html' %}
{{ object.label }}{% if user.is_staff %} <span class="identifier">[{% trans "identifier:" %} {{ object.slug }}]{% endif %}</span>
</a>
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
{% else %}
{% elif not no_application %}
<div class="big-msg-info">
{% blocktrans trimmed %}
This site doesn't have any agenda yet. Click on the "New" button in the top
@ -31,7 +43,7 @@
{% endblock %}
{% block sidebar %}
{% if with_sidebar %}
{% if with_sidebar and not application and not no_application %}
<aside id="sidebar">
{% if user.is_staff %}
@ -60,6 +72,8 @@
{% endif %}
{% endif %}
{% endif %}
{% include 'chrono/includes/application_list_fragment.html' with title_no_application=_('Agendas outside applications') %}
</aside>
{% endif %}
{% endblock %}

View File

@ -62,5 +62,8 @@
<h3>{% trans "Actions" %}</h3>
<a class="button button-paragraph" rel="popup" href="{% url 'chrono-manager-resource-edit' pk=resource.pk %}">{% trans 'Edit' %}</a>
{% endif %}
{% url 'chrono-manager-resource-list' as object_list_url %}
{% include 'chrono/includes/application_detail_fragment.html' %}
</aside>
{% endblock %}

View File

@ -7,11 +7,13 @@
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'chrono-manager-resource-list' %}">{% trans "Resources" %}</a>
{% url 'chrono-manager-resource-list' as object_list_url %}
<a href="{{ object_list_url }}">{% trans "Resources" %}</a>
{% include 'chrono/includes/application_breadcrumb_fragment.html' with title_no_application=_('Resources outside applications') %}
{% endblock %}
{% block appbar %}
<h2>{% trans 'Resources' %}</h2>
{% include 'chrono/includes/application_appbar_fragment.html' with title_no_application=_('Resources outside applications') title_object_list=_('Resources') %}
{% endblock %}
@ -21,12 +23,15 @@
<ul class="objects-list single-links">
{% for object in object_list %}
<li>
<a href="{% url 'chrono-manager-resource-view' pk=object.pk %}">{{ object.label }} ({{ object.slug }})</a>
<a href="{% url 'chrono-manager-resource-view' pk=object.pk %}">
{% include 'chrono/includes/application_icon_fragment.html' %}
{{ object.label }} ({{ object.slug }})
</a>
</li>
{% endfor %}
</ul>
</div>
{% else %}
{% elif not no_application %}
<div class="big-msg-info">
{% blocktrans trimmed %}
This site doesn't have any resource yet. Click on the "New" button in the top
@ -37,8 +42,12 @@
{% endblock %}
{% block sidebar %}
<aside id="sidebar">
<h3>{% trans "Actions" %}</h3>
<a class="button button-paragraph" rel="popup" href="{% url 'chrono-manager-resource-add' %}">{% trans 'New resource' %}</a>
</aside>
{% if not application and not no_application %}
<aside id="sidebar">
<h3>{% trans "Actions" %}</h3>
<a class="button button-paragraph" rel="popup" href="{% url 'chrono-manager-resource-add' %}">{% trans 'New resource' %}</a>
{% include 'chrono/includes/application_list_fragment.html' with title_no_application=_('Resources outside applications') %}
</aside>
{% endif %}
{% endblock %}

View File

@ -7,11 +7,13 @@
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'chrono-manager-unavailability-calendar-list' %}">{% trans "Unavailability Calendars" %}</a>
{% url 'chrono-manager-unavailability-calendar-list' as object_list_url %}
<a href="{{ object_list_url }}">{% trans "Unavailability Calendars" %}</a>
{% include 'chrono/includes/application_breadcrumb_fragment.html' with title_no_application=_('Unavailability Calendars outside applications') %}
{% endblock %}
{% block appbar %}
<h2>{% trans 'Unavailability Calendars' %}</h2>
{% include 'chrono/includes/application_appbar_fragment.html' with title_no_application=_('Unavailability Calendars outside applications') title_object_list=_('Unavailability Calendars') %}
{% endblock %}
@ -21,12 +23,15 @@
<ul class="objects-list single-links">
{% for object in object_list %}
<li>
<a href="{% url 'chrono-manager-unavailability-calendar-view' pk=object.pk %}">{{ object.label }} ({{ object.slug }})</a>
<a href="{% url 'chrono-manager-unavailability-calendar-view' pk=object.pk %}">
{% include 'chrono/includes/application_icon_fragment.html' %}
{{ object.label }} ({{ object.slug }})
</a>
</li>
{% endfor %}
</ul>
</div>
{% else %}
{% elif not no_application %}
<div class="big-msg-info">
{% blocktrans trimmed %}
This site doesn't have any unavailability calendar yet. Click on the "New" button in the top
@ -37,10 +42,14 @@
{% endblock %}
{% block sidebar %}
<aside id="sidebar">
{% if user.is_staff %}
<h3>{% trans "Actions" %}</h3>
<a class="button button-paragraph" rel="popup" href="{% url 'chrono-manager-unavailability-calendar-add' %}">{% trans 'New unavailability calendar' %}</a>
{% endif %}
</aside>
{% if not application and not no_application %}
<aside id="sidebar">
{% if user.is_staff %}
<h3>{% trans "Actions" %}</h3>
<a class="button button-paragraph" rel="popup" href="{% url 'chrono-manager-unavailability-calendar-add' %}">{% trans 'New unavailability calendar' %}</a>
{% endif %}
{% include 'chrono/includes/application_list_fragment.html' with title_no_application=_('Unavailability Calendars outside applications') %}
</aside>
{% endif %}
{% endblock %}

View File

@ -53,5 +53,8 @@
{% if user.is_staff %}
<a class="button button-paragraph" rel="popup" href="{% url 'chrono-manager-unavailability-calendar-delete' pk=unavailability_calendar.id %}">{% trans 'Delete' %}</a>
{% endif %}
{% url 'chrono-manager-unavailability-calendar-list' as object_list_url %}
{% include 'chrono/includes/application_detail_fragment.html' %}
</aside>
{% endblock %}

View File

@ -91,6 +91,7 @@ from chrono.agendas.models import (
UnavailabilityCalendar,
VirtualMember,
)
from chrono.apps.export_import.models import Application
from chrono.utils.date import get_weekday_index
from chrono.utils.timezone import localtime, make_aware, make_naive, now
@ -152,16 +153,48 @@ def is_ajax(request):
return request.headers.get('x-requested-with') == 'XMLHttpRequest'
class HomepageView(ListView):
class WithApplicationsMixin:
def with_applications_dispatch(self, request):
self.application = None
self.no_application = False
if 'application' in self.request.GET:
self.application = get_object_or_404(
Application, slug=self.request.GET['application'], visible=True
)
elif 'no-application' in self.request.GET:
self.no_application = True
def with_applications_context_data(self, context):
if self.application:
context['application'] = self.application
elif not self.no_application:
Application.populate_objects(self.model, self.object_list)
context['applications'] = Application.select_for_object_class(self.model)
context['no_application'] = self.no_application
return context
def with_applications_queryset(self):
if self.application:
return self.application.get_objects_for_object_class(self.model)
if self.no_application:
return Application.get_orphan_objects_for_object_class(self.model)
return super().get_queryset()
class HomepageView(WithApplicationsMixin, ListView):
template_name = 'chrono/manager_home.html'
model = Agenda
def dispatch(self, request, *args, **kwargs):
self.with_applications_dispatch(request)
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
queryset = super().get_queryset()
queryset = self.with_applications_queryset()
if not self.request.user.is_staff:
group_ids = [x.id for x in self.request.user.groups.all()]
queryset = queryset.filter(Q(view_role_id__in=group_ids) | Q(edit_role_id__in=group_ids))
return queryset.order_by('category__label', 'label')
return queryset.select_related('category').order_by('category__label', 'label')
def has_access_to_unavailability_calendars(self):
if self.request.user.is_staff:
@ -181,7 +214,7 @@ class HomepageView(ListView):
context['shared_custody_enabled'] = settings.SHARED_CUSTODY_ENABLED
context['ants_hub_enabled'] = bool(settings.CHRONO_ANTS_HUB_URL)
context['with_sidebar'] = True
return context
return self.with_applications_context_data(context)
def get(self, request, *args, **kwargs):
if not self.has_access():
@ -221,15 +254,23 @@ class AgendasExportView(FormView):
agendas_export = AgendasExportView.as_view()
class ResourceListView(ListView):
class ResourceListView(WithApplicationsMixin, ListView):
template_name = 'chrono/manager_resource_list.html'
model = Resource
def dispatch(self, request, *args, **kwargs):
self.with_applications_dispatch(request)
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
return self.with_applications_queryset()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
return self.with_applications_context_data(context)
resource_list = ResourceListView.as_view()
@ -736,15 +777,23 @@ class ResourceDeleteView(DeleteView):
resource_delete = ResourceDeleteView.as_view()
class CategoryListView(ListView):
class CategoryListView(WithApplicationsMixin, ListView):
template_name = 'chrono/manager_category_list.html'
model = Category
def dispatch(self, request, *args, **kwargs):
self.with_applications_dispatch(request)
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
return self.with_applications_queryset()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
return self.with_applications_context_data(context)
category_list = CategoryListView.as_view()
@ -799,15 +848,23 @@ class CategoryDeleteView(DeleteView):
category_delete = CategoryDeleteView.as_view()
class EventsTypeListView(ListView):
class EventsTypeListView(WithApplicationsMixin, ListView):
template_name = 'chrono/manager_events_type_list.html'
model = EventsType
def dispatch(self, request, *args, **kwargs):
self.with_applications_dispatch(request)
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
return self.with_applications_queryset()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
return self.with_applications_context_data(context)
events_type_list = EventsTypeListView.as_view()
@ -4184,12 +4241,16 @@ class ManagedUnavailabilityCalendarMixin(ViewableUnavailabilityCalendarMixin):
)
class UnavailabilityCalendarListView(ListView):
class UnavailabilityCalendarListView(WithApplicationsMixin, ListView):
template_name = 'chrono/manager_unavailability_calendar_list.html'
model = UnavailabilityCalendar
def dispatch(self, request, *args, **kwargs):
self.with_applications_dispatch(request)
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
queryset = super().get_queryset()
queryset = self.with_applications_queryset()
if not self.request.user.is_staff:
group_ids = [x.id for x in self.request.user.groups.all()]
queryset = queryset.filter(Q(view_role_id__in=group_ids) | Q(edit_role_id__in=group_ids))
@ -4197,6 +4258,10 @@ class UnavailabilityCalendarListView(ListView):
raise PermissionDenied
return queryset.order_by('label')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
return self.with_applications_context_data(context)
unavailability_calendar_list = UnavailabilityCalendarListView.as_view()
@ -4760,6 +4825,8 @@ partial_booking_check_view = PartialBookingCheckView.as_view()
def menu_json(request):
if not request.user.is_staff:
homepage_view = HomepageView(request=request)
homepage_view.application = None
homepage_view.no_application = False
if not (
homepage_view.get_queryset().exists() or homepage_view.has_access_to_unavailability_calendars()
):

View File

@ -54,13 +54,14 @@ INSTALLED_APPS = (
'django.contrib.staticfiles',
'django.contrib.humanize',
'gadjo',
'rest_framework',
'django_filters',
'sorl.thumbnail',
'chrono.agendas',
'chrono.api',
'chrono.manager',
'chrono.apps.ants_hub',
'chrono.apps.export_import',
'rest_framework',
'django_filters',
)
MIDDLEWARE = (
@ -207,6 +208,10 @@ PARTIAL_BOOKINGS_ENABLED = False
CHRONO_ANTS_HUB_URL = None
# from solr.thumbnail -- https://sorl-thumbnail.readthedocs.io/en/latest/reference/settings.html
THUMBNAIL_PRESERVE_FORMAT = True
THUMBNAIL_FORCE_OVERWRITE = False
local_settings_file = os.environ.get(
'CHRONO_SETTINGS_FILE', os.path.join(os.path.dirname(__file__), 'local_settings.py')
)

1
debian/control vendored
View File

@ -34,6 +34,7 @@ Depends: libcairo-gobject2,
python3-django-tenant-schemas,
python3-hobo (>= 1.34),
python3-psycopg2,
python3-sorl-thumbnail,
python3-vobject,
uwsgi,
uwsgi-plugin-python3,

View File

@ -168,6 +168,7 @@ setup(
'requests',
'workalendar',
'weasyprint',
'sorl-thumbnail',
],
zip_safe=False,
cmdclass={

BIN
tests/data/black.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 B

View File

@ -0,0 +1,432 @@
import os
import pytest
from django.core.files import File
from pyquery import PyQuery
from chrono.agendas.models import Agenda, Category, EventsType, Resource, UnavailabilityCalendar
from chrono.apps.export_import.models import Application, ApplicationElement
from tests.utils import login
pytestmark = pytest.mark.django_db
TESTS_DATA_DIR = os.path.join(os.path.dirname(__file__), '..', 'data')
@pytest.fixture
def application_with_icon():
application = Application.objects.create(
name='App 1',
slug='app-1',
version_number='1',
)
with open(os.path.join(TESTS_DATA_DIR, 'black.jpeg'), mode='rb') as fd:
application.icon.save('black.jpeg', File(fd), save=True)
return application
@pytest.fixture
def application_without_icon():
application = Application.objects.create(
name='App 2',
slug='app-2',
version_number='1',
)
return application
@pytest.mark.parametrize('icon', [True, False])
def test_agenda(app, admin_user, application_with_icon, application_without_icon, icon):
if icon:
application = application_with_icon
else:
application = application_without_icon
agenda1 = Agenda.objects.create(label='Agenda 1')
agenda2 = Agenda.objects.create(label='Agenda 2')
ApplicationElement.objects.create(content_object=agenda2, application=application)
agenda3 = Agenda.objects.create(label='Agenda 3')
ApplicationElement.objects.create(content_object=agenda3, application=application)
app = login(app)
# no categories
resp = app.get('/manage/')
assert len(resp.pyquery('.section')) == 1
assert len(resp.pyquery('.section h3')) == 0
assert len(resp.pyquery('.section ul.objects-list li')) == 3
assert (
resp.pyquery('.section ul.objects-list li:nth-child(1)').text()
== 'Events Agenda 1 [identifier: agenda-1]'
)
assert (
resp.pyquery('.section ul.objects-list li:nth-child(2)').text()
== 'Events Agenda 2 [identifier: agenda-2]'
)
assert (
resp.pyquery('.section ul.objects-list li:nth-child(3)').text()
== 'Events Agenda 3 [identifier: agenda-3]'
)
if icon:
assert len(resp.pyquery('.section ul.objects-list img')) == 2
assert len(resp.pyquery('.section ul.objects-list li:nth-child(1) img')) == 0
assert len(resp.pyquery('.section ul.objects-list li:nth-child(2) img.application-icon')) == 1
assert len(resp.pyquery('.section ul.objects-list li:nth-child(3) img.application-icon')) == 1
else:
assert len(resp.pyquery('.section ul.objects-list img')) == 0
assert resp.pyquery('h3:contains("Applications") + .button-paragraph').text() == application.name
if icon:
assert len(resp.pyquery('h3:contains("Applications") + .button-paragraph img.application-icon')) == 1
else:
assert len(resp.pyquery('h3:contains("Applications") + .button-paragraph img')) == 0
assert 'Agendas outside applications' in resp
# check application view
resp = resp.click(application.name)
assert resp.pyquery('h2').text() == application.name
if icon:
assert len(resp.pyquery('h2 img.application-logo')) == 1
else:
assert len(resp.pyquery('h2 img')) == 0
assert len(resp.pyquery('.section ul.objects-list li')) == 2
assert (
resp.pyquery('.section ul.objects-list li:nth-child(1)').text()
== 'Events Agenda 2 [identifier: agenda-2]'
)
assert (
resp.pyquery('.section ul.objects-list li:nth-child(2)').text()
== 'Events Agenda 3 [identifier: agenda-3]'
)
assert len(resp.pyquery('.section ul.objects-list li img')) == 0
# check elements outside applications
resp = app.get('/manage/')
resp = resp.click('Agendas outside applications')
assert resp.pyquery('h2').text() == 'Agendas outside applications'
assert len(resp.pyquery('.section ul.objects-list li')) == 1
assert (
resp.pyquery('.section ul.objects-list li:nth-child(1)').text()
== 'Events Agenda 1 [identifier: agenda-1]'
)
# with category
cat = Category.objects.create(label='Cat')
ApplicationElement.objects.create(content_object=cat, application=application)
agenda2.category = cat
agenda2.save()
resp = app.get('/manage/')
assert len(resp.pyquery('.section')) == 2
assert PyQuery(resp.pyquery('.section')[0]).find('h3').text() == 'Cat'
assert len(PyQuery(resp.pyquery('.section')[0]).find('ul.objects-list li')) == 1
assert (
PyQuery(resp.pyquery('.section')[0]).find('ul.objects-list li').text()
== 'Events Agenda 2 [identifier: agenda-2]'
)
assert PyQuery(resp.pyquery('.section')[1]).find('h3').text() == 'Misc'
assert len(PyQuery(resp.pyquery('.section')[1]).find('ul.objects-list li')) == 2
assert (
PyQuery(resp.pyquery('.section')[1]).find('ul.objects-list li:nth-child(1)').text()
== 'Events Agenda 1 [identifier: agenda-1]'
)
assert (
PyQuery(resp.pyquery('.section')[1]).find('ul.objects-list li:nth-child(2)').text()
== 'Events Agenda 3 [identifier: agenda-3]'
)
if icon:
assert len(PyQuery(resp.pyquery('.section')[0]).find('ul.objects-list img')) == 1
assert (
len(
PyQuery(resp.pyquery('.section')[0]).find(
'ul.objects-list li:nth-child(1) img.application-icon'
)
)
== 1
)
assert len(PyQuery(resp.pyquery('.section')[1]).find('ul.objects-list img')) == 1
assert len(PyQuery(resp.pyquery('.section')[1]).find('ul.objects-list li:nth-child(1) img')) == 0
assert (
len(
PyQuery(resp.pyquery('.section')[1]).find(
'ul.objects-list li:nth-child(2) img.application-icon'
)
)
== 1
)
# check application view
resp = resp.click(application.name)
assert len(resp.pyquery('.section')) == 2
assert PyQuery(resp.pyquery('.section')[0]).find('h3').text() == 'Cat'
assert len(PyQuery(resp.pyquery('.section')[0]).find('ul.objects-list li')) == 1
assert (
PyQuery(resp.pyquery('.section')[0]).find('ul.objects-list li:nth-child(1)').text()
== 'Events Agenda 2 [identifier: agenda-2]'
)
assert PyQuery(resp.pyquery('.section')[1]).find('h3').text() == 'Misc'
assert len(PyQuery(resp.pyquery('.section')[1]).find('ul.objects-list li')) == 1
assert (
PyQuery(resp.pyquery('.section')[1]).find('ul.objects-list li:nth-child(1)').text()
== 'Events Agenda 3 [identifier: agenda-3]'
)
# check categories
Category.objects.create(label='Cat2')
resp = app.get('/manage/categories/')
assert resp.pyquery('h3:contains("Applications") + .button-paragraph').text() == application.name
assert len(resp.pyquery('ul.objects-list li')) == 2
resp = resp.click(application.name)
assert len(resp.pyquery('ul.objects-list li')) == 1
# check detail page
resp = app.get('/manage/agendas/%s/settings' % agenda1.pk)
assert len(resp.pyquery('h3:contains("Applications")')) == 0
assert len(resp.pyquery('h3:contains("Applications") + .button-paragraph')) == 0
resp = app.get('/manage/agendas/%s/settings' % agenda2.pk)
assert resp.pyquery('h3:contains("Applications") + .button-paragraph').text() == application.name
if icon:
assert len(resp.pyquery('h3:contains("Applications") + .button-paragraph img.application-icon')) == 1
else:
assert len(resp.pyquery('h3:contains("Applications") + .button-paragraph img')) == 0
# check visible flag
application.visible = False
application.save()
resp = app.get('/manage/')
assert len(resp.pyquery('h3:contains("Applications")')) == 0
assert len(resp.pyquery('.section ul.objects-list img')) == 0
app.get('/manage/?application=%s' % application.slug, status=404)
resp = app.get('/manage/agendas/%s/settings' % agenda2.pk)
assert len(resp.pyquery('h3:contains("Applications")')) == 0
assert len(resp.pyquery('h3:contains("Applications") + .button-paragraph')) == 0
@pytest.mark.parametrize('icon', [True, False])
def test_events_type(app, admin_user, application_with_icon, application_without_icon, icon):
if icon:
application = application_with_icon
else:
application = application_without_icon
EventsType.objects.create(label='EventsType 1')
events_type2 = EventsType.objects.create(label='EventsType 2')
ApplicationElement.objects.create(content_object=events_type2, application=application)
events_type3 = EventsType.objects.create(label='EventsType 3')
ApplicationElement.objects.create(content_object=events_type3, application=application)
app = login(app)
resp = app.get('/manage/events-types/')
assert len(resp.pyquery('ul.objects-list li')) == 3
assert resp.pyquery('ul.objects-list li:nth-child(1)').text() == 'EventsType 1 (eventstype-1) remove'
assert resp.pyquery('ul.objects-list li:nth-child(2)').text() == 'EventsType 2 (eventstype-2) remove'
assert resp.pyquery('ul.objects-list li:nth-child(3)').text() == 'EventsType 3 (eventstype-3) remove'
if icon:
assert len(resp.pyquery('ul.objects-list img')) == 2
assert len(resp.pyquery('ul.objects-list li:nth-child(1) img')) == 0
assert len(resp.pyquery('ul.objects-list li:nth-child(2) img.application-icon')) == 1
assert len(resp.pyquery('ul.objects-list li:nth-child(3) img.application-icon')) == 1
else:
assert len(resp.pyquery('ul.objects-list img')) == 0
assert resp.pyquery('h3:contains("Applications") + .button-paragraph').text() == application.name
if icon:
assert len(resp.pyquery('h3:contains("Applications") + .button-paragraph img.application-icon')) == 1
else:
assert len(resp.pyquery('h3:contains("Applications") + .button-paragraph img')) == 0
assert 'Events types outside applications' in resp
# check application view
resp = resp.click(application.name)
assert resp.pyquery('h2').text() == application.name
if icon:
assert len(resp.pyquery('h2 img.application-logo')) == 1
else:
assert len(resp.pyquery('h2 img')) == 0
assert len(resp.pyquery('ul.objects-list li')) == 2
assert resp.pyquery('ul.objects-list li:nth-child(1)').text() == 'EventsType 2 (eventstype-2) remove'
assert resp.pyquery('ul.objects-list li:nth-child(2)').text() == 'EventsType 3 (eventstype-3) remove'
assert len(resp.pyquery('ul.objects-list li img')) == 0
# check elements outside applications
resp = app.get('/manage/events-types/')
resp = resp.click('Events types outside applications')
assert resp.pyquery('h2').text() == 'Events types outside applications'
assert len(resp.pyquery('ul.objects-list li')) == 1
assert resp.pyquery('ul.objects-list li:nth-child(1)').text() == 'EventsType 1 (eventstype-1) remove'
# check visible flag
application.visible = False
application.save()
resp = app.get('/manage/events-types/')
assert len(resp.pyquery('h3:contains("Applications")')) == 0
assert len(resp.pyquery('ul.objects-list img')) == 0
app.get('/manage/events-types/?application=%s' % application.slug, status=404)
@pytest.mark.parametrize('icon', [True, False])
def test_resource(app, admin_user, application_with_icon, application_without_icon, icon):
if icon:
application = application_with_icon
else:
application = application_without_icon
resource1 = Resource.objects.create(label='Resource 1')
resource2 = Resource.objects.create(label='Resource 2')
ApplicationElement.objects.create(content_object=resource2, application=application)
resource3 = Resource.objects.create(label='Resource 3')
ApplicationElement.objects.create(content_object=resource3, application=application)
app = login(app)
resp = app.get('/manage/resources/')
assert len(resp.pyquery('ul.objects-list li')) == 3
assert resp.pyquery('ul.objects-list li:nth-child(1)').text() == 'Resource 1 (resource-1)'
assert resp.pyquery('ul.objects-list li:nth-child(2)').text() == 'Resource 2 (resource-2)'
assert resp.pyquery('ul.objects-list li:nth-child(3)').text() == 'Resource 3 (resource-3)'
if icon:
assert len(resp.pyquery('ul.objects-list img')) == 2
assert len(resp.pyquery('ul.objects-list li:nth-child(1) img')) == 0
assert len(resp.pyquery('ul.objects-list li:nth-child(2) img.application-icon')) == 1
assert len(resp.pyquery('ul.objects-list li:nth-child(3) img.application-icon')) == 1
else:
assert len(resp.pyquery('ul.objects-list img')) == 0
assert resp.pyquery('h3:contains("Applications") + .button-paragraph').text() == application.name
if icon:
assert len(resp.pyquery('h3:contains("Applications") + .button-paragraph img.application-icon')) == 1
else:
assert len(resp.pyquery('h3:contains("Applications") + .button-paragraph img')) == 0
assert 'Resources outside applications' in resp
# check application view
resp = resp.click(application.name)
assert resp.pyquery('h2').text() == application.name
if icon:
assert len(resp.pyquery('h2 img.application-logo')) == 1
else:
assert len(resp.pyquery('h2 img')) == 0
assert len(resp.pyquery('ul.objects-list li')) == 2
assert resp.pyquery('ul.objects-list li:nth-child(1)').text() == 'Resource 2 (resource-2)'
assert resp.pyquery('ul.objects-list li:nth-child(2)').text() == 'Resource 3 (resource-3)'
assert len(resp.pyquery('ul.objects-list li img')) == 0
# check elements outside applications
resp = app.get('/manage/resources/')
resp = resp.click('Resources outside applications')
assert resp.pyquery('h2').text() == 'Resources outside applications'
assert len(resp.pyquery('ul.objects-list li')) == 1
assert resp.pyquery('ul.objects-list li:nth-child(1)').text() == 'Resource 1 (resource-1)'
# check detail page
resp = app.get('/manage/resource/%s/' % resource1.pk)
assert len(resp.pyquery('h3:contains("Applications")')) == 0
assert len(resp.pyquery('h3:contains("Applications") + .button-paragraph')) == 0
resp = app.get('/manage/resource/%s/' % resource2.pk)
assert resp.pyquery('h3:contains("Applications") + .button-paragraph').text() == application.name
if icon:
assert len(resp.pyquery('h3:contains("Applications") + .button-paragraph img.application-icon')) == 1
else:
assert len(resp.pyquery('h3:contains("Applications") + .button-paragraph img')) == 0
# check visible flag
application.visible = False
application.save()
resp = app.get('/manage/resources/')
assert len(resp.pyquery('h3:contains("Applications")')) == 0
assert len(resp.pyquery('ul.objects-list img')) == 0
app.get('/manage/resources/?application=%s' % application.slug, status=404)
resp = app.get('/manage/resource/%s/' % resource2.pk)
assert len(resp.pyquery('h3:contains("Applications")')) == 0
assert len(resp.pyquery('h3:contains("Applications") + .button-paragraph')) == 0
@pytest.mark.parametrize('icon', [True, False])
def test_unavailability_calendar(app, admin_user, application_with_icon, application_without_icon, icon):
if icon:
application = application_with_icon
else:
application = application_without_icon
unavailability_calendar1 = UnavailabilityCalendar.objects.create(label='UnavailabilityCalendar 1')
unavailability_calendar2 = UnavailabilityCalendar.objects.create(label='UnavailabilityCalendar 2')
ApplicationElement.objects.create(content_object=unavailability_calendar2, application=application)
unavailability_calendar3 = UnavailabilityCalendar.objects.create(label='UnavailabilityCalendar 3')
ApplicationElement.objects.create(content_object=unavailability_calendar3, application=application)
app = login(app)
resp = app.get('/manage/unavailability-calendars/')
assert len(resp.pyquery('ul.objects-list li')) == 3
assert (
resp.pyquery('ul.objects-list li:nth-child(1)').text()
== 'UnavailabilityCalendar 1 (unavailabilitycalendar-1)'
)
assert (
resp.pyquery('ul.objects-list li:nth-child(2)').text()
== 'UnavailabilityCalendar 2 (unavailabilitycalendar-2)'
)
assert (
resp.pyquery('ul.objects-list li:nth-child(3)').text()
== 'UnavailabilityCalendar 3 (unavailabilitycalendar-3)'
)
if icon:
assert len(resp.pyquery('ul.objects-list img')) == 2
assert len(resp.pyquery('ul.objects-list li:nth-child(1) img')) == 0
assert len(resp.pyquery('ul.objects-list li:nth-child(2) img.application-icon')) == 1
assert len(resp.pyquery('ul.objects-list li:nth-child(3) img.application-icon')) == 1
else:
assert len(resp.pyquery('ul.objects-list img')) == 0
assert resp.pyquery('h3:contains("Applications") + .button-paragraph').text() == application.name
if icon:
assert len(resp.pyquery('h3:contains("Applications") + .button-paragraph img.application-icon')) == 1
else:
assert len(resp.pyquery('h3:contains("Applications") + .button-paragraph img')) == 0
assert 'Unavailability Calendars outside applications' in resp
# check application view
resp = resp.click(application.name)
assert resp.pyquery('h2').text() == application.name
if icon:
assert len(resp.pyquery('h2 img.application-logo')) == 1
else:
assert len(resp.pyquery('h2 img')) == 0
assert len(resp.pyquery('ul.objects-list li')) == 2
assert (
resp.pyquery('ul.objects-list li:nth-child(1)').text()
== 'UnavailabilityCalendar 2 (unavailabilitycalendar-2)'
)
assert (
resp.pyquery('ul.objects-list li:nth-child(2)').text()
== 'UnavailabilityCalendar 3 (unavailabilitycalendar-3)'
)
assert len(resp.pyquery('ul.objects-list li img')) == 0
# check elements outside applications
resp = app.get('/manage/unavailability-calendars/')
resp = resp.click('Unavailability Calendars outside applications')
assert resp.pyquery('h2').text() == 'Unavailability Calendars outside applications'
assert len(resp.pyquery('ul.objects-list li')) == 1
assert (
resp.pyquery('ul.objects-list li:nth-child(1)').text()
== 'UnavailabilityCalendar 1 (unavailabilitycalendar-1)'
)
# check detail page
resp = app.get('/manage/unavailability-calendar/%s/settings' % unavailability_calendar1.pk)
assert len(resp.pyquery('h3:contains("Applications")')) == 0
assert len(resp.pyquery('h3:contains("Applications") + .button-paragraph')) == 0
resp = app.get('/manage/unavailability-calendar/%s/settings' % unavailability_calendar2.pk)
assert resp.pyquery('h3:contains("Applications") + .button-paragraph').text() == application.name
if icon:
assert len(resp.pyquery('h3:contains("Applications") + .button-paragraph img.application-icon')) == 1
else:
assert len(resp.pyquery('h3:contains("Applications") + .button-paragraph img')) == 0
# check visible flag
application.visible = False
application.save()
resp = app.get('/manage/unavailability-calendars/')
assert len(resp.pyquery('h3:contains("Applications")')) == 0
assert len(resp.pyquery('ul.objects-list img')) == 0
app.get('/manage/unavailability-calendars/?application=%s' % application.slug, status=404)
resp = app.get('/manage/unavailability-calendar/%s/settings' % unavailability_calendar2.pk)
assert len(resp.pyquery('h3:contains("Applications")')) == 0
assert len(resp.pyquery('h3:contains("Applications") + .button-paragraph')) == 0

View File

@ -60,7 +60,7 @@ def test_options_meetings_agenda_num_queries(app, admin_user, managers_group):
app = login(app)
with CaptureQueriesContext(connection) as ctx:
app.get('/manage/agendas/%s/settings' % agenda.pk)
assert len(ctx.captured_queries) == 13
assert len(ctx.captured_queries) in [14, 15]
# check with different kind of exceptions:
@ -76,7 +76,7 @@ def test_options_meetings_agenda_num_queries(app, admin_user, managers_group):
with CaptureQueriesContext(connection) as ctx:
app.get('/manage/agendas/%s/settings' % agenda.pk)
assert len(ctx.captured_queries) == 13
assert len(ctx.captured_queries) == 14
# exception starts in the past but ends in the futur
exception.delete()
@ -92,7 +92,7 @@ def test_options_meetings_agenda_num_queries(app, admin_user, managers_group):
with CaptureQueriesContext(connection) as ctx:
app.get('/manage/agendas/%s/settings' % agenda.pk)
assert len(ctx.captured_queries) == 13
assert len(ctx.captured_queries) == 14
# exception in more than 2 weeks
exception.delete()
@ -107,7 +107,7 @@ def test_options_meetings_agenda_num_queries(app, admin_user, managers_group):
with CaptureQueriesContext(connection) as ctx:
app.get('/manage/agendas/%s/settings' % agenda.pk)
assert len(ctx.captured_queries) == 13
assert len(ctx.captured_queries) == 14
def test_meetings_agenda_add_meeting_type(app, admin_user):