diff --git a/aleksis/core/forms.py b/aleksis/core/forms.py index 9297980c7cc8bbd494f6124ee914f6d542b1863f..f4599d4d0e13ac02d01fda147331a3997d0f8dd0 100644 --- a/aleksis/core/forms.py +++ b/aleksis/core/forms.py @@ -10,7 +10,15 @@ from dynamic_preferences.forms import PreferenceForm from material import Fieldset, Layout, Row from .mixins import ExtensibleForm, SchoolTermRelatedExtensibleForm -from .models import AdditionalField, Announcement, Group, GroupType, Person, SchoolTerm +from .models import ( + AdditionalField, + Announcement, + DashboardWidget, + Group, + GroupType, + Person, + SchoolTerm, +) from .registries import ( group_preferences_registry, person_preferences_registry, @@ -345,3 +353,20 @@ class SchoolTermForm(ExtensibleForm): class Meta: model = SchoolTerm exclude = [] + + +class DashboardWidgetOrderForm(ExtensibleForm): + pk = forms.ModelChoiceField( + queryset=DashboardWidget.objects.all(), + widget=forms.HiddenInput(attrs={"class": "pk-input"}), + ) + order = forms.IntegerField(initial=0, widget=forms.HiddenInput(attrs={"class": "order-input"})) + + class Meta: + model = DashboardWidget + fields = [] + + +DashboardWidgetOrderFormSet = forms.formset_factory( + form=DashboardWidgetOrderForm, max_num=0, extra=0 +) diff --git a/aleksis/core/migrations/0007_dashboard_widget_order.py b/aleksis/core/migrations/0007_dashboard_widget_order.py new file mode 100644 index 0000000000000000000000000000000000000000..9387f4ca7c0dbb3307d2a3681f091e4bf631f847 --- /dev/null +++ b/aleksis/core/migrations/0007_dashboard_widget_order.py @@ -0,0 +1,35 @@ +# Generated by Django 3.1.4 on 2020-12-21 13:38 + +import django.contrib.sites.managers +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('sites', '0002_alter_domain_unique'), + ('core', '0006_dashboard_widget_size'), + ] + + operations = [ + migrations.CreateModel( + name='DashboardWidgetOrder', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('extended_data', models.JSONField(default=dict, editable=False)), + ('order', models.PositiveIntegerField(verbose_name='Order')), + ('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.person', verbose_name='Person')), + ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.site')), + ('widget', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.dashboardwidget', verbose_name='Dashboard widget')), + ], + options={ + 'verbose_name': 'Dashboard widget order', + 'verbose_name_plural': 'Dashboard widget orders', + }, + managers=[ + ('objects', django.contrib.sites.managers.CurrentSiteManager()), + ], + ), + ] diff --git a/aleksis/core/models.py b/aleksis/core/models.py index c9d818daf98a7b34d1e124b9bf463b7d283d79b1..0f7a80d416cfe1dec30b57f2bf1e070e664a8966 100644 --- a/aleksis/core/models.py +++ b/aleksis/core/models.py @@ -234,6 +234,12 @@ class Person(ExtensibleModel): years -= 1 return years + @property + def dashboard_widgets(self): + return [ + w.widget for w in DashboardWidgetOrder.objects.filter(person=self).order_by("order") + ] + def save(self, *args, **kwargs): super().save(*args, **kwargs) @@ -727,6 +733,18 @@ class DashboardWidget(PolymorphicModel, PureDjangoModel): verbose_name_plural = _("Dashboard Widgets") +class DashboardWidgetOrder(ExtensibleModel): + widget = models.ForeignKey( + DashboardWidget, on_delete=models.CASCADE, verbose_name=_("Dashboard widget") + ) + person = models.ForeignKey(Person, on_delete=models.CASCADE, verbose_name=_("Person")) + order = models.PositiveIntegerField(verbose_name=_("Order")) + + class Meta: + verbose_name = _("Dashboard widget order") + verbose_name_plural = _("Dashboard widget orders") + + class CustomMenu(ExtensibleModel): """A custom menu to display in the footer.""" diff --git a/aleksis/core/static/js/edit_dashboard.js b/aleksis/core/static/js/edit_dashboard.js new file mode 100644 index 0000000000000000000000000000000000000000..18ddaf45775bc5ff059ca2b59ddefc46ff7afd92 --- /dev/null +++ b/aleksis/core/static/js/edit_dashboard.js @@ -0,0 +1,22 @@ +function refreshOrder() { + $(".order-input").val(0); + $("#widgets > .col").each(function (i) { + const order = (i + 1) * 10; + let pk = $(this).attr("data-pk"); + let sel = $("#order-form input[value=" + pk + "].pk-input").next(); + sel.val(order); + }) +} + +$(document).ready(function () { + $('#not-used-widgets').sortable({ + group: 'widgets', + animation: 150, + onEnd: refreshOrder + }); + $('#widgets').sortable({ + group: 'widgets', + animation: 150, + onEnd: refreshOrder + }); +}); diff --git a/aleksis/core/static/style.scss b/aleksis/core/static/style.scss index e2ea3bbd9953477469350e1c477556947bf8f0e2..d357eea2449b4b2c1510fe9ddce1c6e6f2f26bc8 100644 --- a/aleksis/core/static/style.scss +++ b/aleksis/core/static/style.scss @@ -622,3 +622,7 @@ main .alert p:first-child, main .alert div:first-child { overflow: visible; width: 100%; } + +.draggable { + cursor: grab; +} diff --git a/aleksis/core/templates/core/edit_dashboard.html b/aleksis/core/templates/core/edit_dashboard.html new file mode 100644 index 0000000000000000000000000000000000000000..a15f24bff8ebcca6a7f9b4ea081d9d8c008f3b0a --- /dev/null +++ b/aleksis/core/templates/core/edit_dashboard.html @@ -0,0 +1,41 @@ +{% extends 'core/base.html' %} +{% load i18n static dashboard any_js %} + +{% block browser_title %}{% blocktrans %}Edit dashboard{% endblocktrans %}{% endblock %} +{% block page_title %}{% blocktrans %}Edit dashboard{% endblocktrans %}{% endblock %} + +{% block content %} + <div class="alert primary"> + <p> + <i class="material-icons left">info</i> + On this page you can arrange your personal dashboard. You can drag any items from "Available widgets" to "Your + Dashboard" or change the order by moving the widgets. After you have finished, please don't forget to click on + "Save". + </p> + </div> + + <form action="" method="post" id="order-form"> + {% csrf_token %} + {{ formset.management_form }} + {% for form in formset %} + {{ form.as_p }} + {% endfor %} + {% include "core/partials/save_button.html" %} + </form> + + <h5>{% trans "Available widgets" %}</h5> + <div class="row card-panel grey lighten-3" id="not-used-widgets"> + {% for widget in not_used_widgets %} + {% include "core/partials/edit_dashboard_widget.html" %} + {% endfor %} + </div> + + <h5>{% trans "Your dashboard" %}</h5> + <div class="row card-panel grey lighten-3" id="widgets"> + {% for widget in widgets %} + {% include "core/partials/edit_dashboard_widget.html" %} + {% endfor %} + </div> + + <script src="{% static "js/edit_dashboard.js" %}"></script> +{% endblock %} diff --git a/aleksis/core/templates/core/index.html b/aleksis/core/templates/core/index.html index dc7338e83ccdce193a3974b68bfcbeb589f6ae2a..93b80ddefffaa8cf72db56a7ef503a694f0514f5 100644 --- a/aleksis/core/templates/core/index.html +++ b/aleksis/core/templates/core/index.html @@ -2,13 +2,21 @@ {% load i18n static dashboard %} {% block browser_title %}{% blocktrans %}Home{% endblocktrans %}{% endblock %} -{% block page_title %}{{ request.site.preferences.general__title }}{% endblock %} +{% block no_page_title %}{% endblock %} {% block extra_head %} {{ media }} {% endblock %} {% block content %} + <a class="btn-flat waves-effect waves-light right" href="{% url "edit_dashboard" %}"> + <i class="material-icons left">edit</i> + {% trans "Edit dashboard" %} + </a> + <h4> + {{ request.site.preferences.general__title }} + </h4> + {% if user.is_authenticated %} {% for notification in unread_notifications %} <div class="alert primary scale-transition"> @@ -34,6 +42,16 @@ <div class="col s{{ widget.size_s }} m{{ widget.size_m }} l{{ widget.size_l }} xl{{ widget.size_xl }}"> {% include_widget widget %} </div> + {% empty %} + <div class="col s12 grey-text center"> + <i class="material-icons medium ">widgets</i> + <p class="flow-text"> + {% blocktrans %} + You haven't selected any dashboard widgets. Please click on "Edit dashboard" to add widgets to your + personal dashboard. + {% endblocktrans %} + </p> + </div> {% endfor %} </div> diff --git a/aleksis/core/templates/core/partials/edit_dashboard_widget.html b/aleksis/core/templates/core/partials/edit_dashboard_widget.html new file mode 100644 index 0000000000000000000000000000000000000000..b842c0937f05c4e90a4d7337871460130fa43012 --- /dev/null +++ b/aleksis/core/templates/core/partials/edit_dashboard_widget.html @@ -0,0 +1,9 @@ +<div class="col draggable s{{ widget.size_s }} m{{ widget.size_m }} l{{ widget.size_l }} xl{{ widget.size_xl }}" + data-pk="{{ widget.pk }}"> + <div class="card placeholder"> + <div class="card-content"> + <i class="material-icons left small">drag_handle</i> + <span class="card-title">{{ widget.title }}</span> + </div> + </div> +</div> diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py index 9061962e6a65c5ae40493413c4564b5e1fedec59..0e9e20863125f0355e6bea5ba26dd5fd614aa602 100644 --- a/aleksis/core/urls.py +++ b/aleksis/core/urls.py @@ -58,6 +58,7 @@ urlpatterns = [ path("group/<int:id_>/edit", views.edit_group, name="edit_group_by_id"), path("group/<int:id_>/delete", views.delete_group, name="delete_group_by_id"), path("", views.index, name="index"), + path("dashboard/edit/", views.EditDashboardView.as_view(), name="edit_dashboard"), path( "notifications/mark-read/<int:id_>", views.notification_mark_read, diff --git a/aleksis/core/views.py b/aleksis/core/views.py index d34aa8ae81cefb821f25c2b628ed54cc1dce5563..dc3ce194debff03f231de58a803d78808cb47bfd 100644 --- a/aleksis/core/views.py +++ b/aleksis/core/views.py @@ -12,6 +12,7 @@ from django.urls import reverse_lazy from django.utils.decorators import method_decorator from django.utils.translation import gettext_lazy as _ from django.views.decorators.cache import never_cache +from django.views.generic.base import View import reversion from django_tables2 import RequestConfig, SingleTableView @@ -28,6 +29,7 @@ from .filters import GroupFilter, PersonFilter from .forms import ( AnnouncementForm, ChildGroupsForm, + DashboardWidgetOrderFormSet, EditAdditionalFieldForm, EditGroupForm, EditGroupTypeForm, @@ -43,6 +45,7 @@ from .models import ( AdditionalField, Announcement, DashboardWidget, + DashboardWidgetOrder, Group, GroupType, Notification, @@ -83,7 +86,7 @@ def index(request: HttpRequest) -> HttpResponse: announcements = Announcement.objects.at_time().for_person(request.user.person) context["announcements"] = announcements - widgets = DashboardWidget.objects.filter(active=True) + widgets = request.user.person.dashboard_widgets media = DashboardWidget.get_media(widgets) context["widgets"] = widgets @@ -774,3 +777,56 @@ class DashboardWidgetDeleteView(PermissionRequiredMixin, AdvancedDeleteView): template_name = "core/pages/delete.html" success_url = reverse_lazy("dashboard_widgets") success_message = _("The dashboard widget has been deleted.") + + +class EditDashboardView(View): + """View for editing dashboard widget order.""" + + def get(self, request): + context = {} + + widgets = request.user.person.dashboard_widgets + not_used_widgets = DashboardWidget.objects.exclude(pk__in=[w.pk for w in widgets]) + context["widgets"] = widgets + context["not_used_widgets"] = not_used_widgets + + i = 10 + initial = [] + for widget in widgets: + initial.append({"pk": widget, "order": i}) + i += 10 + for widget in not_used_widgets: + initial.append({"pk": widget, "order": 0}) + + formset = DashboardWidgetOrderFormSet( + request.POST or None, initial=initial, prefix="widget_order" + ) + + context["formset"] = formset + + if request.method == "POST" and formset.is_valid(): + added_objects = [] + for form in formset: + if not form.cleaned_data["order"]: + continue + + obj, created = DashboardWidgetOrder.objects.update_or_create( + widget=form.cleaned_data["pk"], + person=request.user.person, + defaults={"order": form.cleaned_data["order"]}, + ) + + added_objects.append(obj.pk) + + DashboardWidgetOrder.objects.filter(person=request.user.person).exclude( + pk__in=added_objects + ).delete() + + messages.success( + request, _("Your dashboard configuration has been saved successfully.") + ) + return redirect("index") + return render(request, "core/edit_dashboard.html", context=context) + + def post(self, request): + return self.get(request)