Skip to content
Snippets Groups Projects
Commit 9e5b936b authored by Nik | Klampfradler's avatar Nik | Klampfradler
Browse files

Merge branch 'school-apps' into 'master'

Merge school-apps

See merge request BiscuIT/BiscuIT-ng!86
parents 16ef28f2 98421708
No related branches found
No related tags found
1 merge request!86Merge school-apps
Pipeline #458 failed
Showing
with 626 additions and 15 deletions
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
*.py[cod]
__pycache__/
# Distribution / packaging
*.egg
*.egg-info/
.Python
.eggs/
.installed.cfg
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
pip-log.txt
# Translations
*.mo
......@@ -39,14 +39,17 @@ local_settings.py
# Environments
.env
.venv
ENV/
env/
venv/
ENV/
# Editors
*~
DEADJOE
\#*#
# IntelliJ
.idea
.idea/
# Database
......@@ -55,13 +58,17 @@ db.sqlite3
# Sphinx
docs/_build/
# TeX
*.aux
# Generated files
biscuit/static/
biscuit/node_modules/
biscuit/media/
biscuit/static/
.coverage
.mypy_cache/
.tox/
maintenance_mode_state.txt
htmlcov/
.mypy_cache/
maintenance_mode_state.txt
media/
package-lock.json
......@@ -33,14 +33,20 @@ Licence
::
Copyright © 2019 Dominik George <dominik.george@teckids.org>
Copyright © 2019, 2020 Dominik George <dominik.george@teckids.org>
Copyright © 2019 Martin Gummi <martin.gummi@teckids.org>
Copyright © 2019 Julian Leucker <leuckeju@katharineum.de>
Copyright © 2019 mirabilos <thorsten.glaser@teckids.org>
Copyright © 2019 Tom Teichler <tom.teichler@teckids.org>
Copyright © 2018, 2019 Frank Poetzsch-Heffter <p-h@katharineum.de>
Copyright © 2019, 2020 Tom Teichler <tom.teichler@teckids.org>
Copyright © 2018, 2019, 2020 Jonathan Weth <wethjo@katharineum.de>
Copyright © 2019, 2020 Hangzhi Yu <yuha@katharineum.de>
Licenced under the EUPL
Licenced under the EUPL, version 1.2 or later
Please see the LICENCE file accompanying this distribution for the
full licence text or on the `Europen Union Public Licence`_ website
https://joinup.ec.europa.eu/collection/eupl/guidelines-users-and-developers
(including all other official language versions).
.. _BiscuIT-ng: https://edugit.org/BiscuIT/BiscuIT-ng
# 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
## Caches
### Sitecache
Ein Seitencache basiert auf dem Django-Decorator `@cache_page` und cacht die HTML-Ausgabe des entsprechenden Views.
### Variablencache
Ein Variablencache nutzt die Low-Level-Cache-API von Django und speichert den Inhalt einer Variable.
### Verwaltung
Jedes gecachte Objekt (ob Sitecache oder Variablencache) benötigt ein Cache-Objekt in der DB. Bei Cacheinhalten für die nur eine Variable gespeichert werden muss oder ein View, wird die Datei `caches.py` verwendet, wo der Cache als Konstante gespeichert ist:
```
<EXAMPLE_CACHE>, _ = Cache.objects.get_or_create(id="<example_cache>",
defaults={
"site_cache": <True/False>,
"name": "<Readable name>",
"expiration_time": <10>}) # in seconds
```
#### Variablencache
Für Variablencaches kann mit der Funktion `get()` eines Cache-Objektes der aktuelle Inhalt des Caches abgefragt werden.
Bei abgelaufenen Caches wird `False` zurückgeben, dann ist der Wert neu zu berechnen und mit `update(<new_value>)` zu aktualisieren, wobei die Aktualisierungszeit automatisch zurückgesetzt wird.
#### Sitecache
Für einen Sitecache kann folgender Decorator zum entsprechenden View hinzugefügt werden:
```
@cache_page(<EXAMPLE_CACHE>.expiration_time)
```
### Literatur
- https://docs.djangoproject.com/en/2.2/topics/cache/
from django.contrib import admin
from .models import Activity, Notification, Cache
class CacheAdmin(admin.ModelAdmin):
readonly_fields = ["id", "site_cache", "last_time_updated"]
admin.site.register(Activity)
admin.site.register(Notification)
admin.site.register(Cache, CacheAdmin)
from django.apps import AppConfig
class DashboardConfig(AppConfig):
name = 'dashboard'
verbose_name = "Dashboard"
from dashboard.models import Cache
PARSED_LESSONS_CACHE, _ = Cache.objects.get_or_create(id="parsed_lessons",
defaults={"name": "Geparste Stunden (Regelplan)",
"expiration_time": 60 * 60 * 24})
DRIVE_CACHE, _ = Cache.objects.get_or_create(id="drive",
defaults={"name": "Zwischenspeicher für teachers, rooms, classses, etc.",
"expiration_time": 60})
EXPIRATION_TIME_CACHE_FOR_PLAN_CACHES, _ = Cache.objects.get_or_create(id="expiration_time_cache_for_plan_caches",
defaults={"name": "Ablaufzeit für Plan-Caches",
"expiration_time": 60})
BACKGROUND_CACHE_REFRESH, _ = Cache.objects.get_or_create(id="background_cache_refresh",
defaults={
"name": "Hintergrundaktualisierung der Variablencaches",
"expiration_time": 0})
PLAN_VIEW_CACHE, _ = Cache.objects.get_or_create(id="plan_view_cache",
defaults={
"site_cache": True,
"name": "Wochenplan (Regelplan/SMART PLAN)",
"expiration_time": 60})
MY_PLAN_VIEW_CACHE, _ = Cache.objects.get_or_create(id="my_plan_view_cache",
defaults={
"site_cache": True,
"name": "Mein Plan",
"expiration_time": 60})
SUBS_VIEW_CACHE, _ = Cache.objects.get_or_create(id="subs_view_cache",
defaults={
"site_cache": True,
"name": "Vertretungen (Tabellenansicht)",
"expiration_time": 60})
LATEST_ARTICLE_CACHE, _ = Cache.objects.get_or_create(id="latest_article_cache",
defaults={
"name": "Letzter Artikel von der Homepage",
"expiration_time": 60
})
CURRENT_EVENTS_CACHE, _ = Cache.objects.get_or_create(id="current_events_cache",
defaults={
"name": "Aktuelle Termine",
"expiration_time": 60
})
import datetime
from django.core.management import BaseCommand
from django.utils import timezone
from dashboard.caches import BACKGROUND_CACHE_REFRESH
from dashboard.models import Cache
from util.network import get_newest_article_from_news, get_current_events_with_cal
from timetable.views import get_next_weekday_with_time, get_calendar_week
from untisconnect.drive import build_drive, TYPE_TEACHER, TYPE_CLASS, TYPE_ROOM
from untisconnect.parse import parse
from untisconnect.plan import get_plan
class Command(BaseCommand):
help = 'Refresh all var caches'
def start(self, s):
self.stdout.write(s)
def finish(self):
self.stdout.write(self.style.SUCCESS(' Erledigt.'))
def handle(self, *args, **options):
self.start("Alte Caches löschen ...")
for cache in Cache.objects.filter(needed_until__isnull=False):
if not cache.is_needed():
print("Ist nicht mehr benötigt:", cache, ", benötigt bis", cache.needed_until)
cache.delete()
self.finish()
self.start("Aktualisiere Drive ... ")
drive = build_drive(force_update=True)
print(drive)
self.finish()
self.start("Aktualisiered Lessons ...")
parse(force_update=True)
self.finish()
self.start("Aktualisiere Pläne ...")
days = []
days.append(get_next_weekday_with_time(timezone.now(), timezone.now().time()))
days.append(get_next_weekday_with_time(days[0] + datetime.timedelta(days=1), datetime.time(0)))
print(days)
types = [
(TYPE_TEACHER, "teachers"),
(TYPE_CLASS, "classes"),
(TYPE_ROOM, "rooms")
]
for type_id, type_key in types:
self.start(type_key)
for id, obj in drive[type_key].items():
self.start(" " + obj.name if obj.name is not None else "")
self.start(" Regelplan")
get_plan(type_id, id, force_update=True)
for day in days:
calendar_week = day.isocalendar()[1]
if day != days[0] and days[0].isocalendar()[1] == calendar_week and days[0].year == day.year:
continue
monday_of_week = get_calendar_week(calendar_week, day.year)["first_day"]
self.start(" " + str(monday_of_week))
get_plan(type_id, id, smart=True, monday_of_week=monday_of_week, force_update=True)
self.finish()
self.start("Aktualisiere Artikel ...")
get_newest_article_from_news(force_update=True)
self.finish()
self.start("Aktualisiere Termine ...")
get_current_events_with_cal(force_update=True)
self.finish()
self.start("Aktualisierungszeitpunkt in der Datenbank speichern ...")
BACKGROUND_CACHE_REFRESH.last_time_updated = timezone.now()
BACKGROUND_CACHE_REFRESH.save()
self.finish()
# Generated by Django 2.2.1 on 2019-05-29 15:06
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Notification',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=200)),
('description', models.TextField(max_length=500)),
('link', models.URLField(blank=True)),
('app', models.CharField(max_length=100)),
('created_at', models.DateTimeField(default=django.utils.timezone.now)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications',
to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Activity',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=200)),
('description', models.TextField(max_length=500)),
('app', models.CharField(max_length=100)),
('created_at', models.DateTimeField(default=django.utils.timezone.now)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]
# Generated by Django 2.2.1 on 2019-08-25 10:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dashboard', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Cache',
fields=[
('id',
models.CharField(max_length=150, primary_key=True, serialize=False, unique=True, verbose_name='ID')),
('name', models.CharField(max_length=150, verbose_name='Name')),
('expiration_time', models.IntegerField(default=20, verbose_name='Ablaufzeit')),
],
options={
'verbose_name': 'Cacheeintrag',
'verbose_name_plural': 'Cacheeinträge',
},
),
]
# Generated by Django 2.2.1 on 2019-09-01 08:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dashboard', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='notification',
name='read',
field=models.BooleanField(default=False),
),
]
# Generated by Django 2.2.1 on 2019-08-25 11:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dashboard', '0002_cache'),
]
operations = [
migrations.AddField(
model_name='cache',
name='last_time_updated',
field=models.DateTimeField(blank=True, null=True,
verbose_name='Letzter Aktualisierungszeitpunkt des Caches'),
),
]
# Generated by Django 2.2.1 on 2019-08-25 11:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dashboard', '0003_cache_last_time_updated'),
]
operations = [
migrations.AddField(
model_name='cache',
name='site_cache',
field=models.BooleanField(default=False, verbose_name='Seitencache?'),
),
]
# Generated by Django 2.2.6 on 2019-11-08 20:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dashboard', '0004_cache_site_cache'),
]
operations = [
migrations.AddField(
model_name='cache',
name='needed_until',
field=models.DateField(default=None, null=True, verbose_name='Benötigt bis'),
),
]
# Generated by Django 2.2.6 on 2019-11-18 18:39
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('dashboard', '0005_cache_needed_until'),
('dashboard', '0002_notification_read'),
]
operations = [
]
import datetime
from django.core.cache import cache
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
from mailer import send_mail_with_template
class Activity(models.Model):
user = models.ForeignKey(to=User, on_delete=models.CASCADE)
title = models.CharField(max_length=150)
description = models.TextField(max_length=500)
app = models.CharField(max_length=100)
created_at = models.DateTimeField(default=timezone.now)
def __str__(self):
return self.title
class Notification(models.Model):
user = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name="notifications")
title = models.CharField(max_length=150)
description = models.TextField(max_length=500)
link = models.URLField(blank=True)
app = models.CharField(max_length=100)
read = models.BooleanField(default=False)
created_at = models.DateTimeField(default=timezone.now)
def __str__(self):
return self.title
def register_notification(user, title, description, app="SchoolApps", link=""):
n = Notification(user=user, title=title, description=description, app=app, link=link)
n.save()
context = {
'notification': n
}
send_mail_with_template(title, [user.email], "mail/notification.txt", "mail/notification.html", context)
class Cache(models.Model):
id = models.CharField(max_length=150, unique=True, primary_key=True, verbose_name="ID")
name = models.CharField(max_length=150, verbose_name="Name")
expiration_time = models.IntegerField(default=20, verbose_name="Ablaufzeit")
last_time_updated = models.DateTimeField(blank=True, null=True,
verbose_name="Letzter Aktualisierungszeitpunkt des Caches")
site_cache = models.BooleanField(default=False, verbose_name="Seitencache?")
needed_until = models.DateField(default=None, null=True, blank=True, verbose_name="Benötigt bis")
class Meta:
verbose_name = "Cacheeintrag"
verbose_name_plural = "Cacheeinträge"
def __str__(self):
return self.name or self.id
def update(self, new_value):
if not self.site_cache:
self.last_time_updated = timezone.now()
cache.set(self.id, new_value, self.expiration_time)
self.save()
def get(self):
if not self.site_cache:
return cache.get(self.id, False)
else:
return None
def is_expired(self) -> bool:
"""
Checks whether a cache is expired
:return: Is cache expired?
"""
# If cache never was updated it have to
if self.last_time_updated is None:
return True
# Else check if now is bigger than last time updated + expiration time
delta = datetime.timedelta(seconds=self.expiration_time)
return timezone.now() > self.last_time_updated + delta
def is_needed(self) -> bool:
"""
Checks whether a plan can be deleted
:return: Is cache needed?
"""
if self.needed_until is None:
return True
elif timezone.now().date() > self.needed_until:
return False
else:
return True
def delete(self, *args, **kwargs):
"""Overrides model function delete to delete cache entry, too"""
cache.delete(self.id)
super(Cache, self).delete(*args, **kwargs)
def decorator(self, func):
decorator_cache = self
def wrapper(*args, **kwargs):
if "force_update" in kwargs:
force_update = kwargs["force_update"]
del kwargs["force_update"]
else:
force_update = False
cached = decorator_cache.get()
if cached is not False and not force_update:
print("CACHED VALUE FOR ", func)
return cached
print("NON CACHED VALUE FOR ", func, "FORCE", force_update)
res = func(*args, **kwargs)
decorator_cache.update(res)
return res
return wrapper
import datetime
from django.utils import timezone
from dashboard.caches import EXPIRATION_TIME_CACHE_FOR_PLAN_CACHES
from untisconnect.drive import drive, TYPE_TEACHER, TYPE_CLASS, Cache
from untisconnect.api_helper import date_to_untis_date
def get_cache_for_plan(type: int, id: int, smart: bool = False, monday_of_week=None) -> Cache:
"""
Creates a Cache for a plan with given params
:param type: TYPE_TEACHER, TYPE_CLASS or TYPE_ROOM
:param id: database id of plan
:param smart: Is smart?
:param monday_of_week: Monday of needed week (if smart)
:return: Cache object
"""
# Create unique id for plan cache
cache_id = "plan_{}_{}{}".format(type, id, "_smart" if smart else "")
# Decide which type of plan
if type == TYPE_TEACHER:
idx = "teachers"
elif type == TYPE_CLASS:
idx = "classes"
else:
idx = "rooms"
# Set name for cache entry
name = "Stundenplan für {}".format(drive[idx][id])
needed_until = timezone.now().date() + datetime.timedelta(days=1)
if smart:
# Add date to cache id and name if smart plan
cache_id += "_" + date_to_untis_date(monday_of_week)
name += ", " + date_to_untis_date(monday_of_week)
# Set time after which cache will be deleted
needed_until = monday_of_week + datetime.timedelta(days=4)
# Create new cache entry
cache = Cache.objects.get_or_create(id=cache_id)[0]
# Set expiration time and name to cache entry
if cache.expiration_time != EXPIRATION_TIME_CACHE_FOR_PLAN_CACHES.expiration_time or cache.name != name:
cache.expiration_time = EXPIRATION_TIME_CACHE_FOR_PLAN_CACHES.expiration_time
cache.name = name
cache.needed_until = needed_until
cache.save()
return cache
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' %}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment