diff --git a/.gitmodules b/.gitmodules index 4ae5678366d5787af63d81dd7973038fcd320f13..473e630164971d3e1620a9d67eb6810d839c441e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,7 @@ path = apps/official/AlekSIS-App-Exlibris url = https://edugit.org/AlekSIS/AlekSIS-App-Exlibris ignore = untracked +[submodule "apps/official/AlekSIS-App-DashboardFeeds"] + path = apps/official/AlekSIS-App-DashboardFeeds + url = https://edugit.org/AlekSIS/AlekSIS-App-DashboardFeeds.git + ignore = untracked diff --git a/aleksis/core/migrations/0009_dashboard_widget.py b/aleksis/core/migrations/0009_dashboard_widget.py new file mode 100644 index 0000000000000000000000000000000000000000..b57c272b4cc8501e41b5ffaf9f2ec3d796795021 --- /dev/null +++ b/aleksis/core/migrations/0009_dashboard_widget.py @@ -0,0 +1,28 @@ +# Generated by Django 3.0.2 on 2020-01-29 16:45 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('core', '0008_rename_fields_notification_activity'), + ] + + operations = [ + migrations.CreateModel( + name='DashboardWidget', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=150, verbose_name='Widget Title')), + ('active', models.BooleanField(blank=True, verbose_name='Activate Widget')), + ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_core.dashboardwidget_set+', to='contenttypes.ContentType')), + ], + options={ + 'verbose_name': 'Dashboard Widget', + 'verbose_name_plural': 'Dashboard Widgets', + }, + ), + ] diff --git a/aleksis/core/models.py b/aleksis/core/models.py index ecf66c8ed4e0d15ff46cd9c0ee239bfb84bc4737..8995d6d578095fe60713fe1aa2a0a5bb2caaed64 100644 --- a/aleksis/core/models.py +++ b/aleksis/core/models.py @@ -6,6 +6,7 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from image_cropping import ImageCropField, ImageRatioField from phonenumber_field.modelfields import PhoneNumberField +from polymorphic.models import PolymorphicModel from .mixins import ExtensibleModel from .util.notifications import send_notification @@ -227,3 +228,46 @@ class Notification(models.Model): class Meta: verbose_name = _("Notification") verbose_name_plural = _("Notifications") + + +class DashboardWidget(PolymorphicModel): + """ Base class for dashboard widgets on the index page + + To implement a widget, add a model that subclasses DashboardWidget, sets the template + and implements the get_context method to return a dictionary to be passed as context + to the template. + + If your widget does not add any database fields, you should mark it as a proxy model. + + Example:: + + from aleksis.core.models import DashboardWidget + + class MyWidget(DhasboardWIdget): + template = "myapp/widget.html" + + def get_context(self): + context = {"some_content": "foo"} + return context + + class Meta: + proxy = True + """ + + template = None + + title = models.CharField(max_length=150, verbose_name=_("Widget Title")) + active = models.BooleanField(blank=True, verbose_name=_("Activate Widget")) + + def get_context(self): + raise NotImplementedError("A widget subclass needs to implement the get_context method.") + + def get_template(self): + return self.template + + def __str__(self): + return self.title + + class Meta: + verbose_name = _("Dashboard Widget") + verbose_name_plural = _("Dashboard Widgets") diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py index 46b070449c3bc6ff46e98ace323953b7f6252ac1..c314e3a7305a2c1655de57df9d4ac05220b4238a 100644 --- a/aleksis/core/settings.py +++ b/aleksis/core/settings.py @@ -51,6 +51,7 @@ INSTALLED_APPS = [ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "polymorphic", "django_global_request", "settings_context_processor", "sass_processor", diff --git a/aleksis/core/static/js/include_ajax_live.js b/aleksis/core/static/js/include_ajax_live.js new file mode 100644 index 0000000000000000000000000000000000000000..3a4794bad9881ebe502ea7786b8c0d2a740e65f0 --- /dev/null +++ b/aleksis/core/static/js/include_ajax_live.js @@ -0,0 +1,35 @@ +const asyncIntervals = []; + +const runAsyncInterval = async (cb, interval, intervalIndex) => { + await cb(); + if (asyncIntervals[intervalIndex]) { + setTimeout(() => runAsyncInterval(cb, interval, intervalIndex), interval); + } +}; + +const setAsyncInterval = (cb, interval) => { + if (cb && typeof cb === "function") { + const intervalIndex = asyncIntervals.length; + asyncIntervals.push(true); + runAsyncInterval(cb, interval, intervalIndex); + return intervalIndex; + } else { + throw new Error('Callback must be a function'); + } +}; + +const clearAsyncInterval = (intervalIndex) => { + if (asyncIntervals[intervalIndex]) { + asyncIntervals[intervalIndex] = false; + } +}; + +let live_load_interval = setAsyncInterval(async () => { + console.log('fetching new data'); + const promise = new Promise((resolve) => { + $('#live_load').load(window.location.pathname + " #live_load"); + resolve(1); + }); + await promise; + console.log('data fetched successfully'); +}, 15000); diff --git a/aleksis/core/templates/core/index.html b/aleksis/core/templates/core/index.html index 05875979203262f7f8818db06400da609679c891..22d0d4013929f402276f9a960450c4f2f99725e8 100644 --- a/aleksis/core/templates/core/index.html +++ b/aleksis/core/templates/core/index.html @@ -1,5 +1,5 @@ {% extends 'core/base.html' %} -{% load i18n %} +{% load i18n static dashboard %} {% block browser_title %}{% blocktrans %}Home{% endblocktrans %}{% endblock %} @@ -24,6 +24,14 @@ </div> {% endfor %} + <div class="row" id="live_load"> + {% for widget in widgets %} + <div class="col s12 m12 l6 xl4"> + {% include_widget widget %} + </div> + {% endfor %} + </div> + <div class="row"> <div class="col s12 m6"> <h5>{% blocktrans %}Last activities{% endblocktrans %}</h5> @@ -77,4 +85,6 @@ </div> </div> {% endif %} + + <script type="text/javascript" src="{% static "js/include_ajax_live.js" %}"></script> {% endblock %} diff --git a/aleksis/core/templatetags/dashboard.py b/aleksis/core/templatetags/dashboard.py new file mode 100644 index 0000000000000000000000000000000000000000..b051084f88008a1b30758d46f1d91c9695a408e1 --- /dev/null +++ b/aleksis/core/templatetags/dashboard.py @@ -0,0 +1,13 @@ +from django.template import Library, loader + +register = Library() + + +@register.simple_tag +def include_widget(widget) -> dict: + """ Render a template with context from a defined widget """ + + template = loader.get_template(widget.get_template()) + context = widget.get_context() + + return template.render(context) diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py index 63929088ba38406d0e560fcaee218404ad1462bf..6f1d4275e0fd15f2b1cd315353beb56d219342fd 100644 --- a/aleksis/core/urls.py +++ b/aleksis/core/urls.py @@ -66,4 +66,9 @@ for app_config in apps.app_configs.values(): if not app_config.name.startswith("aleksis.apps."): continue - urlpatterns.append(path("app/%s/" % app_config.label, include("%s.urls" % app_config.name))) + try: + urlpatterns.append(path("app/%s/" % app_config.label, include("%s.urls" % app_config.name))) + except ModuleNotFoundError: + # Ignore exception as app just has no URLs + pass # noqa + diff --git a/aleksis/core/views.py b/aleksis/core/views.py index ff11482f68268d5f5d090502039d1c4be12e99a2..c610ec8abf640246355fd04e56130e15f103f483 100644 --- a/aleksis/core/views.py +++ b/aleksis/core/views.py @@ -16,7 +16,7 @@ from .forms import ( EditTermForm, PersonsAccountsFormSet, ) -from .models import Activity, Group, Notification, Person, School +from .models import Activity, Group, Notification, Person, School, DashboardWidget from .tables import GroupsTable, PersonsTable from .util import messages @@ -33,6 +33,8 @@ def index(request: HttpRequest) -> HttpResponse: context["notifications"] = notifications context["unread_notifications"] = unread_notifications + context["widgets"] = DashboardWidget.objects.filter(active=True) + return render(request, "core/index.html", context) diff --git a/apps/official/AlekSIS-App-DashboardFeeds b/apps/official/AlekSIS-App-DashboardFeeds new file mode 160000 index 0000000000000000000000000000000000000000..8207d487baa1767cff5ee43cc1706ab025bf644e --- /dev/null +++ b/apps/official/AlekSIS-App-DashboardFeeds @@ -0,0 +1 @@ +Subproject commit 8207d487baa1767cff5ee43cc1706ab025bf644e diff --git a/pyproject.toml b/pyproject.toml index 1c3bdb77b59e270f075470b6b0dd82525d126073..99d24b842d98224ee165ce0ee0191a50a6523b01 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,7 @@ django-celery-results = {version="^1.1.2", optional=true} django-celery-beat = {version="^1.5.0", optional=true} django-celery-email = {version="^3.0.0", optional=true} django-jsonstore = "^0.4.1" +django-polymorphic = "^2.1.2" [tool.poetry.extras] ldap = ["django-auth-ldap"]