manager: make agenda's groups foldable (#85616)
gitea/chrono/pipeline/head There was a failure building this commit
Details
gitea/chrono/pipeline/head There was a failure building this commit
Details
This commit is contained in:
parent
07512150e8
commit
c5a2671569
|
@ -156,4 +156,5 @@ urlpatterns = [
|
|||
path('statistics/', views.statistics_list, name='api-statistics-list'),
|
||||
path('statistics/bookings/', views.bookings_statistics, name='api-statistics-bookings'),
|
||||
path('ants/', include('chrono.apps.ants_hub.api_urls')),
|
||||
path('user_preferences/', include('chrono.apps.user_preferences.api_urls')),
|
||||
]
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
# chrono - agendas system
|
||||
# Copyright (C) 2024 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from . import api_views
|
||||
|
||||
urlpatterns = [
|
||||
path('', api_views.preferences, name='api-user-preferences'),
|
||||
]
|
|
@ -0,0 +1,38 @@
|
|||
# chrono - agendas system
|
||||
# Copyright (C) 2024 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import json
|
||||
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import HttpResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from . import models
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def preferences(request):
|
||||
if not request.user:
|
||||
raise PermissionDenied()
|
||||
user_pref, _ = models.UserPreferences.objects.get_or_create(user=request.user)
|
||||
if len(request.body) > 1000:
|
||||
return HttpResponse(b'Payload too large', status_code=400)
|
||||
try:
|
||||
prefs = json.loads(request.body)
|
||||
except Exception:
|
||||
return HttpResponse(b'Bad format')
|
||||
user_pref.update_preferences(prefs)
|
||||
return HttpResponse('')
|
|
@ -0,0 +1,32 @@
|
|||
# Generated by Django 3.2.18 on 2024-04-02 13:32
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='UserPreferences',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
('preferences', models.JSONField(default=dict, verbose_name='Preferences')),
|
||||
(
|
||||
'user',
|
||||
models.ForeignKey(
|
||||
null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -0,0 +1,34 @@
|
|||
# chrono - agendas system
|
||||
# Copyright (C) 2024 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class UserPreferences(models.Model):
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True)
|
||||
preferences = models.JSONField(_('Preferences'), default=dict)
|
||||
|
||||
def get_preference(self, name):
|
||||
return self.preferences.get(name)
|
||||
|
||||
def get_preferences(self, prefix):
|
||||
return {k: v for k, v in self.preferences.items() if k.startswith(prefix)}
|
||||
|
||||
def update_preferences(self, preferences):
|
||||
self.preferences.update(preferences)
|
||||
self.save()
|
|
@ -1,4 +1,23 @@
|
|||
$(function() {
|
||||
const foldableClassObserver = new MutationObserver((mutations) => {
|
||||
mutations.forEach(mu => {
|
||||
const old_folded = (mu.oldValue.indexOf('folded') != -1);
|
||||
const new_folded = mu.target.classList.contains('folded')
|
||||
if (old_folded == new_folded) { return; }
|
||||
var pref_message = Object();
|
||||
pref_message[mu.target.dataset.sectionFoldedPrefName] = new_folded;
|
||||
fetch('/api/user_preferences/', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {'Accept': 'application/json', 'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(pref_message)
|
||||
});
|
||||
});
|
||||
});
|
||||
document.querySelectorAll('[data-section-folded-pref-name]').forEach(
|
||||
elt => foldableClassObserver.observe(elt, {attributes: true, attributeFilter: ['class'], attributeOldValue: true})
|
||||
);
|
||||
|
||||
$('[data-total]').each(function() {
|
||||
var total = $(this).data('total');
|
||||
var booked = $(this).data('booked');
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{% extends "chrono/manager_base.html" %}
|
||||
{% load i18n thumbnail %}
|
||||
{% load i18n thumbnail chrono %}
|
||||
|
||||
{% block appbar %}
|
||||
{% include 'chrono/includes/application_appbar_fragment.html' with title_no_application=_('Agendas outside applications') title_object_list=_('Agendas') %}
|
||||
|
@ -16,19 +16,23 @@
|
|||
{% if object_list %}
|
||||
{% regroup object_list by category as agenda_groups %}
|
||||
{% for group in agenda_groups %}
|
||||
<div class="section">
|
||||
{% if group.grouper %}<h3>{{ group.grouper }}</h3>{% elif not forloop.first %}<h3>{% trans "Misc" %}</h3>{% endif %}
|
||||
<ul class="objects-list single-links">
|
||||
{% for object in group.list %}
|
||||
<li>
|
||||
<a href="{% url 'chrono-manager-agenda-view' pk=object.id %}">
|
||||
<span class="badge">{{ object.get_real_kind_display }}</span>
|
||||
{% include 'chrono/includes/application_icon_fragment.html' %}
|
||||
{{ object.label }}{% if user.is_staff %} <span class="identifier">[{% trans "identifier:" %} {{ object.slug }}]{% endif %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% with forloop.counter|stringformat:"s" as i %}
|
||||
{% with 'folded-admin-forms-group-'|add:i as foldname %}
|
||||
<div class="section foldable {% if user|get_preference:foldname %}folded{% endif %}" data-section-folded-pref-name="{{foldname}}">
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% if group.grouper %}<h3>{{ group.grouper }}</h3>{% elif not forloop.first %}<h3>{% trans "Misc" %}</h3>{% endif %}
|
||||
<ul class="objects-list single-links">
|
||||
{% for object in group.list %}
|
||||
<li>
|
||||
<a href="{% url 'chrono-manager-agenda-view' pk=object.id %}">
|
||||
<span class="badge">{{ object.get_real_kind_display }}</span>
|
||||
{% include 'chrono/includes/application_icon_fragment.html' %}
|
||||
{{ object.label }}{% if user.is_staff %} <span class="identifier">[{% trans "identifier:" %} {{ object.slug }}]{% endif %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% elif not no_application %}
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
from django import template
|
||||
from django.utils.formats import date_format
|
||||
|
||||
from chrono.apps.user_preferences.models import UserPreferences
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
|
@ -31,3 +33,11 @@ def human_date_range(date_start, date_end):
|
|||
date_start_format = 'd'
|
||||
|
||||
return '%s − %s' % (date_format(date_start, date_start_format), date_format(date_end, date_end_format))
|
||||
|
||||
|
||||
@register.filter
|
||||
def get_preference(user, pref_name):
|
||||
if not user:
|
||||
return None
|
||||
user_preferences, _ = UserPreferences.objects.get_or_create(user=user)
|
||||
return user_preferences.get_preference(pref_name) if user_preferences else None
|
||||
|
|
|
@ -63,6 +63,7 @@ INSTALLED_APPS = (
|
|||
'chrono.apps.ants_hub',
|
||||
'chrono.apps.export_import',
|
||||
'chrono.apps.snapshot',
|
||||
'chrono.apps.user_preferences',
|
||||
)
|
||||
|
||||
MIDDLEWARE = (
|
||||
|
|
Loading…
Reference in New Issue