Skip to content
Snippets Groups Projects
Verified Commit 07b00af1 authored by Jonathan Weth's avatar Jonathan Weth :keyboard:
Browse files

Merge branch 'master' into feature/print-base-template

parents ed6fa53f 46ed4f3f
Branches
Tags
1 merge request!143Add base template for printing views
Pipeline #766 failed
Showing
with 122 additions and 455 deletions
......@@ -10,15 +10,7 @@
path = apps/official/AlekSIS-App-Exlibris
url = https://edugit.org/AlekSIS/AlekSIS-App-Exlibris
ignore = untracked
[submodule "apps/official/AlekSIS-App-SchILD-NRW"]
path = apps/official/AlekSIS-App-SchILD-NRW
url = https://edugit.org/AlekSIS/AlekSIS-App-SchILD-NRW
ignore = untracked
[submodule "apps/official/AlekSIS-App-Untis"]
path = apps/official/AlekSIS-App-Untis
url = https://edugit.org/AlekSIS/AlekSIS-App-Untis
ignore = untracked
[submodule "apps/official/AlekSIS-App-Hjelp"]
path = apps/official/AlekSIS-App-Hjelp
url = https://edugit.org/AlekSIS/AlekSIS-App-Hjelp.git
[submodule "apps/official/AlekSIS-App-DashboardFeeds"]
path = apps/official/AlekSIS-App-DashboardFeeds
url = https://edugit.org/AlekSIS/AlekSIS-App-DashboardFeeds.git
ignore = untracked
# Dashboard
Das Dashboard dient dazu, den Benutzer zu begrüßen (> Startseite)
und seine letzten Aktivitäten anzuzeigen.
Edit: Außerdem zeigt das Dashboard aktuelle Nachrichten für den Benutzer an.
## Aktivitäten
Als Aktivität gilt alles, was der Nutzer selbst macht, d.h., bewusst.
### Eine Aktivität registrieren
1. Importieren
from .apps import <Meine App>Config
from dashboard.models import Activity
2. Registrieren
act = Activity(title="<Titel der Aktion>", description="<Beschreibung der Aktion>", app=<Meine App>Config.verbose_name, user=<Benutzer Objekt>)
act.save()
## Benachrichtigungen
Als Benachrichtigung gilt eine Aktion, die den Nutzer betrifft.
### Eine Benachrichtigung verschicken
1. Importieren
from .apps import <Meine App>Config
from dashboard.models import Notification
2. Verschicken
register_notification(title="<Titel der Nachricht>",
description="<Weitere Informationen>",
app=<Meine App>Config.verbose_name, user=<Benutzer Objekt>,
link=request.build_absolute_uri(<Link für weitere Informationen>))
**Hinweis:** Der angegebene Link muss eine absolute URL sein.
Dies wird durch übergabe eines dynamischen Linkes (z. B. /aub/1) an die Methode `request.build_absolute_uri()` erreicht.
Um einen dynamischen Link durch den Namen einer Django-URL zu "errechnen", dient die Methode `reverse()`.
Literatur:
- [1] https://docs.djangoproject.com/en/2.1/ref/request-response/#django.http.HttpRequest.build_absolute_uri
- [2] https://docs.djangoproject.com/en/2.1/ref/urlresolvers/#reverse
import dbsettings
class LatestArticleSettings(dbsettings.Group):
latest_article_is_activated = dbsettings.BooleanValue("Funktion aktivieren?", default=True)
wp_domain = dbsettings.StringValue("WordPress-Domain", help_text="Ohne abschließenden Slash",
default="https://katharineum-zu-luebeck.de")
replace_vs_composer_stuff = dbsettings.BooleanValue("VisualComposer-Tags durch regulären Ausdruck entfernen?",
default=True)
class CurrentEventsSettings(dbsettings.Group):
current_events_is_activated = dbsettings.BooleanValue("Funktion aktivieren?", default=True)
calendar_url = dbsettings.StringValue("URL des Kalenders", help_text="Pfad zu einer ICS-Datei",
default="https://nimbus.katharineum.de/remote.php/dav/public-calendars"
"/owit7yysLB2CYNTq?export")
events_count = dbsettings.IntegerValue("Anzahl der Termine, die angezeigt werden sollen", default=5)
class MyStatusSettings(dbsettings.Group):
my_status_is_activated = dbsettings.BooleanValue("Funktion aktivieren?", default=True)
class CurrentExamsSettings(dbsettings.Group):
current_exams_is_activated = dbsettings.BooleanValue("Funktion aktivieren?", default=True)
latest_article_settings = LatestArticleSettings("Funktion: Letzter Artikel")
current_events_settings = CurrentEventsSettings("Funktion: Aktuelle Termine")
my_status_settings = MyStatusSettings("Funktion: Mein Status")
current_exams_settings = MyStatusSettings("Funktion: Aktuelle Klausuren")
{% load staticfiles %}
{% include 'partials/header.html' %}
<main>
<script>
var API_URL = "{% url "api_information" %}";
var MY_PLAN_URL = "{% url "timetable_my_plan" %}";
</script>
<script src="{% static "js/moment-with-locales.min.js" %}"></script>
{% include "components/react.html" %}
<script src="{% static "js/dashboard.js" %}"></script>
<div id="dashboard_container">
</div>
</main>
{% include 'partials/footer.html' %}
from django.db import ProgrammingError
from django.urls import path
from untisconnect.models import Terms, Schoolyear
try:
import dashboard.views.dashboard as views
urlpatterns = [
path('', views.index, name='dashboard'),
path('api', views.api_information, name="api_information"),
path('api/notifications/read/<int:id>', views.api_read_notification, name="api_read_notification"),
path('api/my-plan', views.api_my_plan_html, name="api_my_plan_html"),
]
except (Terms.DoesNotExist, Schoolyear.DoesNotExist, ProgrammingError):
from timetable import fallback_view
urlpatterns = [
path('', fallback_view.fallback, name='dashboard'),
path('api', fallback_view.fallback, name="api_information"),
path('api/notifications/read/<int:id>', fallback_view.fallback, name="api_read_notification"),
path('api/my-plan', fallback_view.fallback, name="api_my_plan_html"),
]
import dashboard.views.tools as tools_views
urlpatterns += [
path('offline', tools_views.offline, name='offline'),
path('about/', tools_views.about, name='about')
]
from email.utils import formatdate
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.template.loader import render_to_string
from django.utils import timezone
from martor.templatetags.martortags import safe_markdown
from dashboard.models import Activity, Notification
from dashboard.settings import latest_article_settings, current_events_settings
from timetable.hints import get_all_hints_by_class_and_time_period, get_all_hints_for_teachers_by_time_period
from timetable.views import get_next_weekday_with_time
from untisconnect.api import TYPE_TEACHER, TYPE_CLASS
from untisconnect.datetimeutils import get_name_for_next_week_day_from_today
from untisconnect.utils import get_type_and_object_of_user, get_plan_for_day
from util.network import get_newest_article_from_news, get_current_events_with_cal
@login_required
def index(request):
""" Dashboard: Show daily relevant information """
return render(request, 'dashboard/index.html')
@login_required
def api_information(request):
""" API request: Give information for dashboard in JSON """
# Load activities
activities = Activity.objects.filter(user=request.user).order_by('-created_at')[:5]
# Load notifications
notifications = request.user.notifications.all().filter(user=request.user).order_by('-created_at')[:5]
unread_notifications = request.user.notifications.all().filter(user=request.user, read=False).order_by(
'-created_at')
# Get latest article from homepage
if latest_article_settings.latest_article_is_activated:
newest_article = get_newest_article_from_news(domain=latest_article_settings.wp_domain)
else:
newest_article = None
# Get date information
date_formatted = get_name_for_next_week_day_from_today()
next_weekday = get_next_weekday_with_time(timezone.now(), timezone.now().time())
# Get user type (student, teacher, etc.)
_type, el = get_type_and_object_of_user(request.user)
# Get hints
if _type == TYPE_TEACHER:
# Get hints for teachers
hints = list(get_all_hints_for_teachers_by_time_period(next_weekday, next_weekday))
elif _type == TYPE_CLASS:
# Get hints for students
hints = list(get_all_hints_by_class_and_time_period(el, next_weekday, next_weekday))
else:
hints = []
# Serialize hints
ser = []
for hint in hints:
serialized = {
"from_date": formatdate(float(hint.from_date.strftime('%s'))),
"to_date": formatdate(float(hint.to_date.strftime('%s'))),
"html": safe_markdown(hint.text)
}
ser.append(serialized)
hints = ser
context = {
'activities': list(activities.values()),
'notifications': list(notifications.values()),
"unread_notifications": list(unread_notifications.values()),
# 'user_type': UserInformation.user_type(request.user),
# 'user_type_formatted': UserInformation.user_type_formatted(request.user),
# 'classes': UserInformation.user_classes(request.user),
# 'courses': UserInformation.user_courses(request.user),
# 'subjects': UserInformation.user_subjects(request.user),
# 'has_wifi': UserInformation.user_has_wifi(request.user),
"newest_article": newest_article,
"current_events": get_current_events_with_cal() if current_events_settings.current_events_is_activated else None,
"date_formatted": date_formatted,
"user": {
"username": request.user.username,
"full_name": request.user.first_name
}
}
# If plan is available for user give extra information
if _type is not None and request.user.has_perm("timetable.show_plan"):
context["plan"] = {
"type": _type,
"name": el.shortcode if _type == TYPE_TEACHER else el.name,
"hints": hints
}
context["has_plan"] = True
else:
context["has_plan"] = False
return JsonResponse(context)
@login_required
def api_read_notification(request, id):
""" API request: Mark notification as read """
notification = get_object_or_404(Notification, id=id, user=request.user)
notification.read = True
notification.save()
return JsonResponse({"success": True})
@login_required
def api_my_plan_html(request):
""" API request: Get rendered lessons with substitutions for dashboard """
# Get user type (student, teacher, etc.)
_type, el = get_type_and_object_of_user(request.user)
# Plan is only for teachers and students available
if (_type != TYPE_TEACHER and _type != TYPE_CLASS) or not request.user.has_perm("timetable.show_plan"):
return JsonResponse({"success": False})
# Get calendar week and monday of week
next_weekday = get_next_weekday_with_time()
# Get plan
plan, holiday = get_plan_for_day(_type, el.id, next_weekday)
# Serialize plan
lessons = []
for lesson_container, time in plan:
html = render_to_string("timetable/lesson.html", {"col": lesson_container, "type": _type}, request=request)
lessons.append({"time": time, "html": html})
# Return JSON
return JsonResponse(
{"success": True, "lessons": lessons, "holiday": holiday[0].__dict__ if len(holiday) > 0 else None})
from django.shortcuts import render
from meta import OPEN_SOURCE_COMPONENTS
def about(request):
return render(request, "common/about.html", context={"components": OPEN_SOURCE_COMPONENTS})
from django.core.mail import send_mail
from django.template.loader import render_to_string
from constance import config
mail_out = "{} <{}>".format(config.MAIL_OUT_NAME,
config.MAIL_OUT) if config.MAIL_OUT_NAME else config.MAIL_OUT
def send_mail_with_template(title, receivers, plain_template, html_template, context={}, mail_out=mail_out):
msg_plain = render_to_string(plain_template, context)
msg_html = render_to_string(html_template, context)
try:
send_mail(
title,
msg_plain,
mail_out,
receivers,
html_message=msg_html,
)
except Exception as e:
print("[EMAIL PROBLEM] ", e)
# 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',
},
),
]
......@@ -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
......@@ -231,3 +232,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")
......@@ -51,6 +51,7 @@ INSTALLED_APPS = [
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"polymorphic",
"django_global_request",
"settings_context_processor",
"sass_processor",
......
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);
{% load staticfiles %}
{% if debug %}
<script src="{% static "js/react/prop-types.development.js" %}"></script>
<script src="{% static "js/react/react.development.js" %}"></script>
<script src="{% static "js/react/react-dom.development.js" %}"></script>
{% else %}
<script src="{% static "js/react/prop-types.production.min.js" %}"></script>
<script src="{% static "js/react/react.production.min.js" %}"></script>
<script src="{% static "js/react/react-dom.production.min.js" %}"></script>
{% endif %}
{% 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 %}
{% include "mail/header.html" %}
<main>
Hallo {{ user.get_full_name }},
vielen Dank, dass Sie <b>SchoolApps</b> benutzen.
<hr>
Weitere Informationen finden Sie hier:
<ul>
<li><a href="https://forum.katharineum.de">FORUM</a></li>
</ul>
<hr>
Ihre Computer-AG
</main>
Hallo {{ user.name }},
vielen Dank, dass du SchoolApps benutzt.
----
Weitere Informationen findest du hier:
- FORUM (forum.katharineum.de)
----
Deine Computer-AG
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-beta/css/materialize.min.css">
<style>
main {
padding: 10px;
}
</style>
{% include "mail/header.html" %}
<main>
<p>Hallo {{ notification.user.get_full_name }}, <br>
wir haben eine neue Nachricht für dich:</p>
<blockquote>
<p>{{ notification.description }}</p>
{% if notification.link %}
<a href="{{ notification.link }}">Weitere Informationen →</a>
{% endif %}
</blockquote>
<p>Von {{ notification.app }} am {{ notification.created_at }}</p>
<i>Dein SchoolApps-Team</i>
</main>
Hallo {{ notification.user.name }},
wir haben eine neue Nachricht für dich:
{{ notification.description }}
{% if notification.link %}
Weitere Informationen: {{ notification.link }}
{% endif %}
Von {{ notification.app }} am {{ notification.created_at }}
Dein SchoolApps-Team
{% load static %}
<footer class="page-footer primary-color">
<div class="container">
<div class="row no-margin footer-row-large">
<div class="col l6 s12 no-pad-left height-inherit">
<p class="white-text valign-bot">
<a href="{% url "about" %}" class="blue-text text-lighten-4">Über Schoolapps · Entwickler, Copyright
und Lizenz
</a>
</p>
</div>
<div class="col xl15 l6 offset-xl01 s12 no-pad-right">
<ul class="no-margin right">
<a class="blue-text text-lighten-4 btn-flat no-pad-left" href="https://katharineum-zu-luebeck.de"><i
class="material-icons footer-icon left">home</i> Homepage
</a>
<a class="blue-text text-lighten-4 btn-flat" href="https://forum.katharineum.de"><i
class="material-icons footer-icon left">account_balance</i> Forum
</a>
<a class="blue-text text-lighten-4 btn-flat" href="https://nimbus.katharineum.de"><i
class="material-icons footer-icon left">cloud</i> Nimbus
</a>
<a class="blue-text text-lighten-4 btn-flat no-pad-right" href="https://webmail.katharineum.de"><i
class="material-icons footer-icon left">email</i> Webmail
</a>
</ul>
</div>
</div>
<div class="row no-margin footer-row-small">
<span class="white-text make-it-higher">
<a href="{% url "about" %}" class="blue-text text-lighten-4">Über Schoolapps · Entwickler, Copyright
und Lizenz
</a>
</span>
<ul class="no-margin footer-ul">
<a class="blue-text text-lighten-4 btn-flat no-pad-left" href="https://katharineum-zu-luebeck.de"><i
class="material-icons footer-icon left">home</i> Homepage
</a>
<a class="blue-text text-lighten-4 btn-flat" href="https://forum.katharineum.de"><i
class="material-icons footer-icon left">account_balance</i> Forum
</a>
<a class="blue-text text-lighten-4 btn-flat" href="https://nimbus.katharineum.de"><i
class="material-icons footer-icon left">cloud</i> Nimbus
</a>
<a class="blue-text text-lighten-4 btn-flat no-pad-right" href="https://webmail.katharineum.de"><i
class="material-icons footer-icon left">email</i> Webmail
</a>
</ul>
</div>
</div>
<div class="footer-copyright">
<div class="container">
<span class="left">Version {{ VERSION }} &middot; {{ COPYRIGHT_SHORT }}</span>
<span class="right">
<span id="doit"></span>
<a class="blue-text text-lighten-4" href="https://katharineum-zu-luebeck.de/impressum/">Impressum</a>
·
<a class="blue-text text-lighten-4" href="https://katharineum-zu-luebeck.de/datenschutzerklaerung/">
Datenschutzerklärung
</a>
</span>
</div>
</div>
</footer>
<!---------------->
<!-- JavaScript (jquery v. 3.4.1.slim)-->
<!---------------->
<script src="{% static 'common/manup.min.js' %}"></script>
<script src="{% static 'js/materialize.min.js' %}"></script>
<script src="{% static 'common/helper.js' %}"></script>
</body>
</html>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment