diff --git a/.gitignore b/.gitignore index ec8b50c64d9d2dccbb75b4424fbe4a02d4ce817e..9d218124e6ea053643309cf65d03fbf71c7a9096 100644 --- a/.gitignore +++ b/.gitignore @@ -1,29 +1,29 @@ # 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 diff --git a/README.rst b/README.rst index f6ef25051668258915fc5a05894a192bdaa35691..06c7091431c8465690afdb29ef8e9af1af1305d2 100644 --- a/README.rst +++ b/README.rst @@ -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 diff --git a/biscuit/core/dashboard/README.md b/biscuit/core/dashboard/README.md new file mode 100644 index 0000000000000000000000000000000000000000..b9a8f00b2082812dfb0eb882849d75574277e19e --- /dev/null +++ b/biscuit/core/dashboard/README.md @@ -0,0 +1,74 @@ +# 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/ diff --git a/biscuit/core/dashboard/__init__.py b/biscuit/core/dashboard/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/biscuit/core/dashboard/admin.py b/biscuit/core/dashboard/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..a9286d91effa36ce9fdb9e4cc020c2a3506da854 --- /dev/null +++ b/biscuit/core/dashboard/admin.py @@ -0,0 +1,11 @@ +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) diff --git a/biscuit/core/dashboard/apps.py b/biscuit/core/dashboard/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..bc6084702acf8de9b788588f5813d689a8bd82bf --- /dev/null +++ b/biscuit/core/dashboard/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class DashboardConfig(AppConfig): + name = 'dashboard' + verbose_name = "Dashboard" diff --git a/biscuit/core/dashboard/caches.py b/biscuit/core/dashboard/caches.py new file mode 100644 index 0000000000000000000000000000000000000000..663df3c5e5d474fb57c65775c9e0dc4ebe1025c3 --- /dev/null +++ b/biscuit/core/dashboard/caches.py @@ -0,0 +1,48 @@ +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 + }) diff --git a/biscuit/core/dashboard/management/commands/refresh_caches.py b/biscuit/core/dashboard/management/commands/refresh_caches.py new file mode 100644 index 0000000000000000000000000000000000000000..3ed4421d4d6f3c3cc29a16ad8876c63eedd7058d --- /dev/null +++ b/biscuit/core/dashboard/management/commands/refresh_caches.py @@ -0,0 +1,83 @@ +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() diff --git a/biscuit/core/dashboard/migrations/0001_initial.py b/biscuit/core/dashboard/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..b98c580d6c71ce0871d5260d9ae591322e824b0e --- /dev/null +++ b/biscuit/core/dashboard/migrations/0001_initial.py @@ -0,0 +1,42 @@ +# 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)), + ], + ), + ] diff --git a/biscuit/core/dashboard/migrations/0002_cache.py b/biscuit/core/dashboard/migrations/0002_cache.py new file mode 100644 index 0000000000000000000000000000000000000000..1d146b35c7961c326f2b7de0bcf33ea6bdb552a5 --- /dev/null +++ b/biscuit/core/dashboard/migrations/0002_cache.py @@ -0,0 +1,25 @@ +# 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', + }, + ), + ] diff --git a/biscuit/core/dashboard/migrations/0002_notification_read.py b/biscuit/core/dashboard/migrations/0002_notification_read.py new file mode 100644 index 0000000000000000000000000000000000000000..19cc479866c684399fe5eb3c833a61e0c9e7aa98 --- /dev/null +++ b/biscuit/core/dashboard/migrations/0002_notification_read.py @@ -0,0 +1,17 @@ +# 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), + ), + ] diff --git a/biscuit/core/dashboard/migrations/0003_cache_last_time_updated.py b/biscuit/core/dashboard/migrations/0003_cache_last_time_updated.py new file mode 100644 index 0000000000000000000000000000000000000000..2cda5789033367e5a6cac0ab4c87543b82e4a849 --- /dev/null +++ b/biscuit/core/dashboard/migrations/0003_cache_last_time_updated.py @@ -0,0 +1,18 @@ +# 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'), + ), + ] diff --git a/biscuit/core/dashboard/migrations/0004_cache_site_cache.py b/biscuit/core/dashboard/migrations/0004_cache_site_cache.py new file mode 100644 index 0000000000000000000000000000000000000000..d5ed2edb969cd34a0b45c5195ae4014c5f2a38c8 --- /dev/null +++ b/biscuit/core/dashboard/migrations/0004_cache_site_cache.py @@ -0,0 +1,17 @@ +# 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?'), + ), + ] diff --git a/biscuit/core/dashboard/migrations/0005_cache_needed_until.py b/biscuit/core/dashboard/migrations/0005_cache_needed_until.py new file mode 100644 index 0000000000000000000000000000000000000000..e1669563cba91ee60e961d442af05897e68f12e9 --- /dev/null +++ b/biscuit/core/dashboard/migrations/0005_cache_needed_until.py @@ -0,0 +1,17 @@ +# 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'), + ), + ] diff --git a/biscuit/core/dashboard/migrations/0006_merge_20191118_1939.py b/biscuit/core/dashboard/migrations/0006_merge_20191118_1939.py new file mode 100644 index 0000000000000000000000000000000000000000..057086014b0e26b629955e76c6cdea6a40428772 --- /dev/null +++ b/biscuit/core/dashboard/migrations/0006_merge_20191118_1939.py @@ -0,0 +1,13 @@ +# 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 = [ + ] diff --git a/biscuit/core/dashboard/migrations/__init__.py b/biscuit/core/dashboard/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/biscuit/core/dashboard/models.py b/biscuit/core/dashboard/models.py new file mode 100644 index 0000000000000000000000000000000000000000..8a26ce363f72ef0867614a389fc1c7b1062ee16f --- /dev/null +++ b/biscuit/core/dashboard/models.py @@ -0,0 +1,127 @@ +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 diff --git a/biscuit/core/dashboard/plan_caches.py b/biscuit/core/dashboard/plan_caches.py new file mode 100644 index 0000000000000000000000000000000000000000..9b22a3293e8467fe0842e75ec37abefbe59761ed --- /dev/null +++ b/biscuit/core/dashboard/plan_caches.py @@ -0,0 +1,51 @@ +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 diff --git a/biscuit/core/dashboard/settings.py b/biscuit/core/dashboard/settings.py new file mode 100644 index 0000000000000000000000000000000000000000..4dc09f18c3e8d8f2979aa5e8eb6dda8479f85735 --- /dev/null +++ b/biscuit/core/dashboard/settings.py @@ -0,0 +1,31 @@ +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") diff --git a/biscuit/core/dashboard/templates/dashboard/index.html b/biscuit/core/dashboard/templates/dashboard/index.html new file mode 100644 index 0000000000000000000000000000000000000000..8450678118187c74ce34ce317a27047a844b8f0e --- /dev/null +++ b/biscuit/core/dashboard/templates/dashboard/index.html @@ -0,0 +1,18 @@ +{% 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' %} diff --git a/biscuit/core/dashboard/templates/dashboard/tools.html b/biscuit/core/dashboard/templates/dashboard/tools.html new file mode 100644 index 0000000000000000000000000000000000000000..cce11ff3de4d0eccb06203b18d431f6a6b544a91 --- /dev/null +++ b/biscuit/core/dashboard/templates/dashboard/tools.html @@ -0,0 +1,76 @@ +{% include 'partials/header.html' %} +{% load msg_box %} +<main> + <h4>Tools</h4> + <div class="card"> + <div class="card-content"> + <span class="card-title"> + Cache-Management + </span> + {% msg_box "Nach Änderung der Ablaufzeit muss der entsprechende Cache (bei Variablencaches) bzw. der gesamte Cache (bei Sitecaches) geleert werden, damit die Änderung wirksam wird." "warning" "warning" %} + <table class="highlight"> + <thead> + <tr> + <th>ID</th> + <th>Name</th> + <th>Typ</th> + <th>Ablaufzeit</th> + <th>Letzter Aktualisierungszeitpunkt des Caches</th> + <th>Aktionen</th> + </tr> + </thead> + <tbody> + {% for cache in caches %} + <tr> + <td> + <pre>{{ cache.id }}</pre> + </td> + <td>{{ cache.name }}</td> + <td> + <span class="badge new">{{ cache.site_cache|yesno:"Seitencache,Variablencache" }}</span> + </td> + + <td>{{ cache.expiration_time }} s</td> + <td> + {% if cache.site_cache %} + k. A. + {% else %} + {{ cache.last_time_updated }} + {% if cache.is_expired %} + <span class="badge new red">Abgelaufen</span> + {% else %} + <span class="badge new green">Gültig</span> + {% endif %} + {% endif %} + </td> + <td> + {% if not cache.site_cache %} + <a class="btn-flat waves-effect waves-red red-text" + href="{% url "tools_clear_single_cache" cache.id %}"> + <i class="material-icons left">delete</i> Cache leeren + </a> + {% endif %} + <a class="btn-flat waves-effect waves-orange orange-text" + href="{% url "admin:dashboard_cache_change" cache.id %}" target="_blank"> + <i class="material-icons left">edit</i> + Ablaufzeit bearbeiten + </a> + + </td> + </tr> + {% endfor %} + </tbody> + </table> + {% if msg == "success_cleared_whole_cache" %} + {% msg_box "Der gesamte Cache wurde erfolgreich geleert." "success" "check_circle" %} + {% elif msg == "success_cleared_single_cache" %} + {% msg_box "Ein Cache wurde erfolgreich geleert." "success" "check_circle" %} + {% endif %} + </div> + <div class="card-action"> + <a href="{% url "tools_clear_cache" %}">Cache leeren</a> + </div> + </div> +</main> + +{% include 'partials/footer.html' %} diff --git a/biscuit/core/dashboard/urls.py b/biscuit/core/dashboard/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..b8b08c524694d3efb13566caf7a4b539195066d2 --- /dev/null +++ b/biscuit/core/dashboard/urls.py @@ -0,0 +1,34 @@ +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("tools", tools_views.tools, name="tools"), + path("tools/clear-cache", tools_views.tools_clear_cache, name="tools_clear_cache"), + path("tools/clear-cache/<str:id>", tools_views.tools_clear_cache, name="tools_clear_single_cache"), + path('about/', tools_views.about, name='about') +] diff --git a/biscuit/core/dashboard/views/dashboard.py b/biscuit/core/dashboard/views/dashboard.py new file mode 100644 index 0000000000000000000000000000000000000000..a45100ed064711904d77895318c5614e4c530266 --- /dev/null +++ b/biscuit/core/dashboard/views/dashboard.py @@ -0,0 +1,142 @@ +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}) diff --git a/biscuit/core/dashboard/views/tools.py b/biscuit/core/dashboard/views/tools.py new file mode 100644 index 0000000000000000000000000000000000000000..42d116cffcf82837c872195fdaf488d276447630 --- /dev/null +++ b/biscuit/core/dashboard/views/tools.py @@ -0,0 +1,45 @@ +from django.contrib.auth.decorators import login_required, user_passes_test +from django.core.cache import cache +from django.shortcuts import render, redirect +from django.urls import reverse + +from dashboard.models import Cache +from meta import OPEN_SOURCE_COMPONENTS + + +def offline(request): + return render(request, 'common/offline.html') + + +def about(request): + return render(request, "common/about.html", context={"components": OPEN_SOURCE_COMPONENTS}) + + +@login_required +@user_passes_test(lambda u: u.is_superuser) +def tools(request): + msg = None + if request.session.get("msg", False): + msg = request.session["msg"] + request.session["msg"] = None + + caches = Cache.objects.all() + context = { + "msg": msg, + "caches": caches + } + return render(request, "dashboard/tools.html", context) + + +@login_required +def tools_clear_cache(request, id=None): + if id is not None: + cache.delete(id) + request.session["msg"] = "success_cleared_single_cache" + print("[IMPORTANT] Single cache cleared!") + else: + cache.clear() + request.session["msg"] = "success_cleared_whole_cache" + print("[IMPORTANT] Whole cache cleared!") + + return redirect(reverse("tools")) diff --git a/biscuit/core/debug/__init__.py b/biscuit/core/debug/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/biscuit/core/debug/admin.py b/biscuit/core/debug/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..bb9534da72a30417ee61e3f5cfff774bac9275bd --- /dev/null +++ b/biscuit/core/debug/admin.py @@ -0,0 +1,16 @@ +from django.contrib import admin + +from .models import DebugLogGroup, DebugLog + + +class DebugLogAdmin(admin.ModelAdmin): + readonly_fields = ["id", "group", "return_code", "filename", "updated_at"] + + +class DebugLogGroupAdmin(admin.ModelAdmin): + readonly_fields = ["id"] + + +# Register your models here. +admin.site.register(DebugLogGroup, DebugLogGroupAdmin) +admin.site.register(DebugLog, DebugLogAdmin) diff --git a/biscuit/core/debug/apps.py b/biscuit/core/debug/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..5c1d0c0a15e96063ee78f411be2bd78e91aada77 --- /dev/null +++ b/biscuit/core/debug/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class DebugConfig(AppConfig): + name = 'debug' diff --git a/biscuit/core/debug/migrations/0001_initial.py b/biscuit/core/debug/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..e6775658862d3397caee389ea94a04ca1dc134b0 --- /dev/null +++ b/biscuit/core/debug/migrations/0001_initial.py @@ -0,0 +1,47 @@ +# Generated by Django 2.2.1 on 2019-05-23 13:59 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='DebugLogGroup', + fields=[ + ('id', models.CharField(max_length=100, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200, verbose_name='Name')), + ('desc_as_pre', + models.CharField(blank=True, max_length=250, verbose_name='Beschreibung, dargestellt als HTML-PRE')), + ], + options={ + 'verbose_name': 'Debug-Log-Gruppe', + 'verbose_name_plural': 'Debug-Log-Gruppen', + }, + ), + migrations.CreateModel( + name='DebugLog', + fields=[ + ('id', models.CharField(max_length=100, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200, verbose_name='Name')), + ('return_code', models.IntegerField(blank=True, null=True, verbose_name='UNIX-Rückgabecode')), + ('filename', models.FilePathField(match='.*.log', path='/home/wethjo/dev/school-apps/schoolapps/latex', + verbose_name='Dateiname zur Logdatei')), + ('updated_at', + models.DateTimeField(default=django.utils.timezone.now, verbose_name='Aktualisierungszeitpunkt')), + ('group', + models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, + related_name='logs', to='debug.DebugLogGroup', verbose_name='Gruppe')), + ], + options={ + 'verbose_name': 'Debug-Log', + 'verbose_name_plural': 'Debug-Logs', + }, + ), + ] diff --git a/biscuit/core/debug/migrations/0002_auto_20190523_1627.py b/biscuit/core/debug/migrations/0002_auto_20190523_1627.py new file mode 100644 index 0000000000000000000000000000000000000000..9db1d9ed1b1d80e6efdc4b16093025c0baaf44ab --- /dev/null +++ b/biscuit/core/debug/migrations/0002_auto_20190523_1627.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.1 on 2019-05-23 14:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('debug', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='debuglog', + name='log_text', + field=models.TextField(blank=True, verbose_name='Log-Text (falls nicht Datei)'), + ), + migrations.AlterField( + model_name='debuglog', + name='filename', + field=models.FilePathField(blank=True, match='.*.log', path='/home/wethjo/dev/school-apps/schoolapps/latex', + verbose_name='Dateiname zur Logdatei (falls nicht Log-Text)'), + ), + ] diff --git a/biscuit/core/debug/migrations/0003_auto_20190818_0910.py b/biscuit/core/debug/migrations/0003_auto_20190818_0910.py new file mode 100644 index 0000000000000000000000000000000000000000..a21678f276bb0834100127ef13f870aba02802da --- /dev/null +++ b/biscuit/core/debug/migrations/0003_auto_20190818_0910.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.1 on 2019-08-18 07:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('debug', '0002_auto_20190523_1627'), + ] + + operations = [ + migrations.AlterField( + model_name='debuglog', + name='filename', + field=models.FilePathField(blank=True, match='.*.log', path='/home/p-h/git/school-apps/schoolapps/latex', verbose_name='Dateiname zur Logdatei (falls nicht Log-Text)'), + ), + ] diff --git a/biscuit/core/debug/migrations/0004_auto_20190916_1450.py b/biscuit/core/debug/migrations/0004_auto_20190916_1450.py new file mode 100644 index 0000000000000000000000000000000000000000..f0905c799ba5552647595bfa2e8a4cc7d7b44c9c --- /dev/null +++ b/biscuit/core/debug/migrations/0004_auto_20190916_1450.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.4 on 2019-09-16 12:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('debug', '0003_auto_20190818_0910'), + ] + + operations = [ + migrations.AlterField( + model_name='debuglog', + name='filename', + field=models.FilePathField(blank=True, match='.*.log', path='/data/Silas/Daten/school-apps/schoolapps/latex', verbose_name='Dateiname zur Logdatei (falls nicht Log-Text)'), + ), + ] diff --git a/biscuit/core/debug/migrations/__init__.py b/biscuit/core/debug/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/biscuit/core/debug/models.py b/biscuit/core/debug/models.py new file mode 100644 index 0000000000000000000000000000000000000000..831f537a54a336f1f25d7e338dbf3acef75631ba --- /dev/null +++ b/biscuit/core/debug/models.py @@ -0,0 +1,138 @@ +import os +import traceback + +from django.db import models +from django.utils import timezone + +from schoolapps.settings import BASE_DIR + + +class DebugLogGroup(models.Model): + # Meta + id = models.CharField(primary_key=True, blank=False, max_length=100, verbose_name="ID") + name = models.CharField(blank=False, max_length=200, verbose_name="Name") + desc_as_pre = models.CharField(blank=True, max_length=250, verbose_name="Beschreibung, dargestellt als HTML-PRE") + + class Meta: + verbose_name = "Debug-Log-Gruppe" + verbose_name_plural = "Debug-Log-Gruppen" + + def __str__(self): + return self.name or self.id + + def is_successful(self): + """ + :return: Were all operations in this group successful? + """ + successful = True + for log in self.logs.all(): + if not log.is_successful(): + successful = False + return successful + + +DEBUG_LOG_DIR = os.path.join(BASE_DIR, "latex") + + +class DebugLog(models.Model): + # Meta + id = models.CharField(primary_key=True, blank=False, max_length=100, verbose_name="ID") + name = models.CharField(blank=False, max_length=200, verbose_name="Name") + group = models.ForeignKey(DebugLogGroup, on_delete=models.SET_NULL, default=None, null=True, blank=True, + related_name="logs", verbose_name="Gruppe") # If null, it wouldn't be displayed + + # Data + return_code = models.IntegerField(blank=True, null=True, verbose_name="UNIX-Rückgabecode") + filename = models.FilePathField(path=DEBUG_LOG_DIR, match=".*.log", + verbose_name="Dateiname zur Logdatei (falls nicht Log-Text)", blank=True) + log_text = models.TextField(verbose_name="Log-Text (falls nicht Datei)", blank=True) + updated_at = models.DateTimeField(blank=False, default=timezone.now, verbose_name="Aktualisierungszeitpunkt") + + class Meta: + verbose_name = "Debug-Log" + verbose_name_plural = "Debug-Logs" + + def __str__(self): + return self.name or self.id + + def get_file_content(self): + """ + :return: The log text (file or DB) + """ + if self.filename: + print(self.filename) + f = open(os.path.join(DEBUG_LOG_DIR, self.filename), "r") + content = f.read() + f.close() + return content + elif self.log_text: + return self.log_text + else: + return "" + + def is_successful(self): + """ + :return: Was the last operation successful? + """ + return self.return_code == 0 + + +def get_log_group_by_id(id): + """ + Get a log group from DB by given id + :param id: ID of log group + :return: + """ + p, _ = DebugLogGroup.objects.get_or_create(id=id) + return p + + +def register_log_with_filename(id, group_id, filename, return_code): + """ + Register a operation in debugging tool with a log file + + :param id: id of operation + :param group_id: id of group + :param filename: file path (based on latex dir) + :param return_code: UNIX return code + """ + p, _ = DebugLog.objects.get_or_create(id=id) + group = get_log_group_by_id(group_id) + p.group = group + p.return_code = return_code + p.filename = filename + p.updated_at = timezone.now() + p.save() + + +def register_return_0(id, group_id): + """ + Register a operation in debugging tool with an return code of 0 (success) and no log text/log file + + :param id: id of operation + :param group_id: id of group + """ + p, _ = DebugLog.objects.get_or_create(id=id) + group = get_log_group_by_id(group_id) + p.group = group + p.return_code = 0 + p.log_text = "" + p.updated_at = timezone.now() + p.save() + + +def register_traceback(id, group_id): + """ + Register a operation in debugging tool with an return code of 1 (error) and a log text + + :param id: id of operation + :param group_id: id of group + """ + msg = traceback.format_exc() + p, _ = DebugLog.objects.get_or_create(id=id) + group = get_log_group_by_id(group_id) + p.group = group + p.return_code = 1 + p.log_text = msg + p.updated_at = timezone.now() + p.save() diff --git a/biscuit/core/debug/templates/debug/debug.html b/biscuit/core/debug/templates/debug/debug.html new file mode 100644 index 0000000000000000000000000000000000000000..0f6cbc52608ae4b413088d09fa6f1ccd2b9543cf --- /dev/null +++ b/biscuit/core/debug/templates/debug/debug.html @@ -0,0 +1,80 @@ +{% include 'partials/header.html' %} +{% load material_form %} +{% load martortags %} +{% load staticfiles %} +<link rel="stylesheet" href="{% static "css/highlight.min.css" %}"> +<script src="{% static "js/highlight.min.js" %}"></script> +<script>hljs.initHighlightingOnLoad();</script> + +<style> + .debug-li { + line-height: 2; + } + + .debug-icon { + font-size: 2.5rem; + } +</style> + +<main> + <a class="btn-flat waves-effect waves-teal right btn-flat-medium" href="{% url "debug_logs" %}"><i + class="material-icons refresh center">refresh</i></a> + + <h4>Debuggingtool</h4> + + + <h5>Schnellüberblick</h5> + <div class="row"> + {% for group in groups %} + <div class="col s12 m4"> + <div class="card"> + <div class="card-content"> + <i class="material-icons right {{ group.is_successful|yesno:"green,red" }}-text debug-icon">{{ group.is_successful|yesno:"check_circle,error" }}</i> + <span class="card-title">{{ group.name }}</span> + <ul> + {% for log in group.logs.all %} + <li class="debug-li"> + <i class="material-icons right {{ log.is_successful|yesno:"green,red" }}-text">{{ log.is_successful|yesno:"check_circle,error" }}</i> + {{ log.name }} + </li> + {% endfor %} + </ul> + </div> + <div class="card-action"> + <a href="#{{ group.id }}"> + Log anzeigen + </a> + </div> + </div> + + </div> + {% endfor %} + </div> + + + <h5>Logs</h5> + {% for group in groups %} + <div class="card" id="{{ group.id }}"> + <div class="card-content"> + <i class="material-icons right {{ group.is_successful|yesno:"green,red" }}-text medium">{{ group.is_successful|yesno:"check_circle,error" }}</i> + <span class="card-title">{{ group.name }}</span> + <pre>{{ group.desc_as_pre|default:" " }}</pre> + {% for log in group.logs.all %} + <div> + <i class="material-icons right {{ log.is_successful|yesno:"green,red" }}-text small">{{ log.is_successful|yesno:"check_circle,error" }}</i> + + <h5>{{ log.name }} </h5> + <p><i class="material-icons left">access_time</i> {{ log.updated_at }}</p> + <pre><code class="plaintext scroll-fix">RETURN CODE: {{ log.return_code }} + +{{ log.get_file_content }} + </code></pre> + </div> + {% endfor %} + </div> + </div> + {% endfor %} + +</main> + +{% include 'partials/footer.html' %} diff --git a/biscuit/core/debug/tests.py b/biscuit/core/debug/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6 --- /dev/null +++ b/biscuit/core/debug/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/biscuit/core/debug/urls.py b/biscuit/core/debug/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..ee68b8411b7aaece1a9194a0fd7c48e6fa17172b --- /dev/null +++ b/biscuit/core/debug/urls.py @@ -0,0 +1,8 @@ +from django.urls import path + +from . import views + +urlpatterns = [ + path("", views.debugging_tool), + path("logs/", views.debugging_tool, name="debug_logs") +] diff --git a/biscuit/core/debug/views.py b/biscuit/core/debug/views.py new file mode 100644 index 0000000000000000000000000000000000000000..8531e3e3efcd8cc82b3e7a615df4ec4e2f22e970 --- /dev/null +++ b/biscuit/core/debug/views.py @@ -0,0 +1,11 @@ +from django.contrib.auth.decorators import login_required, permission_required +from django.shortcuts import render + +from debug.models import DebugLogGroup + + +@login_required +@permission_required("debug.can_view_debug_log") +def debugging_tool(request): + groups = DebugLogGroup.objects.all() + return render(request, "debug/debug.html", {"groups": groups}) diff --git a/biscuit/core/mailer.py b/biscuit/core/mailer.py new file mode 100644 index 0000000000000000000000000000000000000000..6cd4c0c98cb0927e6aa72c0561a5327c10a67cad --- /dev/null +++ b/biscuit/core/mailer.py @@ -0,0 +1,20 @@ +from django.core.mail import send_mail +from django.template.loader import render_to_string + +SENDER_EMAIL = 'SchoolApps <infoplan@katharineum.de>' + + +def send_mail_with_template(title, receivers, plain_template, html_template, context={}, sender_email=SENDER_EMAIL): + msg_plain = render_to_string(plain_template, context) + msg_html = render_to_string(html_template, context) + + try: + send_mail( + title, + msg_plain, + sender_email, + receivers, + html_message=msg_html, + ) + except Exception as e: + print("[EMAIL PROBLEM] ", e) diff --git a/biscuit/core/meta.py b/biscuit/core/meta.py new file mode 100644 index 0000000000000000000000000000000000000000..9af2f86c59f640d67970a38470a11f3f616d5fbe --- /dev/null +++ b/biscuit/core/meta.py @@ -0,0 +1,64 @@ +import os +from django.conf import settings + +# Build path for copyright +copyright_path = os.path.abspath(os.path.join(settings.BASE_DIR, '..', 'COPYRIGHT.md')) + +# Read copyright from file +with open(copyright_path, "r") as f: + COPYRIGHT = f.read() + +COPYRIGHT_SHORT = "© 2018–2019 Mitglieder der Computer-AG, Katharineum zu Lübeck" + +VERSION = '1.1.4 "Aebli"' + +LICENSE_APACHE_2 = "Apache 2.0 License" +LICENSE_BSD = "2-Clause BSD License" +LICENSE_BSD_3 = "3-Clause BSD License" +LICENSE_MIT = "MIT License" +LICENSE_GPL_V2 = "GNU General Public License v2.0" +LICENSE_GPL_V3 = "GNU General Public License v3.0" + +OPEN_SOURCE_COMPONENTS = [ + # ("Docker (u.a. Engine, CLI, docker-compose)", "https://github.com/docker", LICENSE_APACHE_2, + # "https://github.com/docker/docker/blob/master/LICENSE"), + ("Django", "https://www.djangoproject.com/", "Django BSD License", + "https://github.com/django/django/blob/master/LICENSE"), + ("Python 3", "https://www.python.org/", "PSF LICENSE AGREEMENT FOR PYTHON", + "https://docs.python.org/3/license.html"), + ("jQuery", "https://jquery.com/", LICENSE_MIT, "https://github.com/jquery/jquery/blob/master/LICENSE.txt"), + ("pip", "https://pypi.org/project/pip/", LICENSE_MIT, "https://github.com/pypa/pip/blob/master/LICENSE.txt"), + ("Requests", "https://requests.kennethreitz.org/", LICENSE_APACHE_2, + "https://github.com/psf/requests/blob/master/LICENSE"), + ("django-widget-tweaks", "https://github.com/jazzband/django-widget-tweaks", LICENSE_MIT, + "https://github.com/jazzband/django-widget-tweaks/blob/master/LICENSE"), + ("Materialize CSS", "https://materializecss.com/", LICENSE_MIT, + "https://github.com/Dogfalo/materialize/blob/master/LICENSE"), + ("Material Design Icons", "http://google.github.io/material-design-icons/", LICENSE_APACHE_2, + "https://github.com/google/material-design-icons/blob/master/LICENSE"), + ("highlight.js", "https://highlightjs.org/", LICENSE_BSD_3, + "https://github.com/highlightjs/highlight.js/blob/master/LICENSE"), + ("React", "https://reactjs.org/", LICENSE_MIT, "https://github.com/facebook/react/blob/master/LICENSE"), + ("mysqlclient", "https://github.com/PyMySQL/mysqlclient-python", LICENSE_GPL_V2, + "https://github.com/PyMySQL/mysqlclient-python/blob/master/LICENSE"), + ("django-auth-ldap", "https://github.com/django-auth-ldap/django-auth-ldap", LICENSE_BSD, + "https://github.com/django-auth-ldap/django-auth-ldap/blob/master/LICENSE"), + ("django-dbsettings", "https://github.com/zlorf/django-dbsettings", LICENSE_BSD_3, + "https://github.com/zlorf/django-dbsettings/blob/master/LICENSE"), + ("Django PDB", "https://github.com/HassenPy/django-pdb", "Public Domain", ""), + ("Django Material", "https://github.com/viewflow/django-material", LICENSE_BSD_3, + "https://github.com/viewflow/django-material/blob/master/LICENSE.txt"), + ("Django Filter", "https://github.com/carltongibson/django-filter", LICENSE_BSD_3, + "https://github.com/carltongibson/django-filter/blob/master/LICENSE"), + ("django-react-templatetags", "https://github.com/Frojd/django-react-templatetags", LICENSE_MIT, + "https://github.com/Frojd/django-react-templatetags/blob/develop/LICENSE"), + ("martor", "https://github.com/agusmakmun/django-markdown-editor", LICENSE_GPL_V3, + "https://github.com/agusmakmun/django-markdown-editor/blob/master/LICENSE"), + ("Babel", "https://babeljs.io/", LICENSE_MIT, "https://github.com/babel/babel/blob/master/LICENSE") +] +OPEN_SOURCE_COMPONENTS.sort(key=lambda elem: elem[0].lower()) + + +# Provide vars to all templates via processor +def meta_processor(request): + return {'COPYRIGHT': COPYRIGHT, "COPYRIGHT_SHORT": COPYRIGHT_SHORT, "VERSION": VERSION} diff --git a/biscuit/core/schoolapps/__init__.py b/biscuit/core/schoolapps/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/biscuit/core/schoolapps/example_secure_settings.py b/biscuit/core/schoolapps/example_secure_settings.py new file mode 100644 index 0000000000000000000000000000000000000000..1ba684fa1ecf494f6fe558e673dbbb7e2be21545 --- /dev/null +++ b/biscuit/core/schoolapps/example_secure_settings.py @@ -0,0 +1,33 @@ +# EMAIL +EMAIL_HOST = 'postoffice.katharineum.de' +EMAIL_PORT = 587 +EMAIL_HOST_USER = 'infoplan@katharineum.de' +EMAIL_HOST_PASSWORD = 'grummelPASS1531' +EMAIL_USE_TLS = True + +# SECRET KEY +SECRET_KEY = '_89lg!56$d^sf$22cz1ja_f)x9z(nc*y-x*@j4!!vzmlgi*53u' + +# DATABASES +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'schoolapps', + 'USER': 'www-data', + 'PASSWORD': 'grummelPASS1531', + 'HOST': '', + 'PORT': '' + }, + 'untis': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'Untis', + 'USER': 'www-data', + 'PASSWORD': 'grummelPASS1531', + 'HOST': '', + 'PORT': '' + } +} + +# LDAP +AUTH_LDAP_BIND_DN = "cn=django-agent,dc=example,dc=com" +AUTH_LDAP_BIND_PASSWORD = "phlebotinum" diff --git a/biscuit/core/schoolapps/settings.py b/biscuit/core/schoolapps/settings.py new file mode 100644 index 0000000000000000000000000000000000000000..36fd54aa760b3c7a73a72754b972a28b659b2241 --- /dev/null +++ b/biscuit/core/schoolapps/settings.py @@ -0,0 +1,246 @@ +""" +Django settings for schoolapps project. +""" + +import os +import ldap +from django_auth_ldap.config import LDAPSearch, PosixGroupType, GroupOfNamesType, LDAPGroupType +import logging +from .secure_settings import * + +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +DEBUG = True + +# PDB debugger option +POST_MORTEM = True + +ALLOWED_HOSTS = [ + 'info.katharineum.de', + '178.63.239.184', + '159.69.181.50', + 'localhost', + '127.0.0.1', + '13049d63.ngrok.io' +] + +INTERNAL_IPS = [ + '127.0.0.1', +] + +# Application definition + +INSTALLED_APPS = [ + 'dashboard.apps.DashboardConfig', + "debug.apps.DebugConfig", + 'aub.apps.AubConfig', + 'fibu.apps.FibuConfig', + 'untisconnect.apps.UntisconnectConfig', + 'timetable.apps.TimetableConfig', + 'menu.apps.MenuConfig', + 'support.apps.SupportConfig', + 'faq.apps.FaqConfig', + 'dbsettings', + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'material', + 'django_react_templatetags', + 'martor', + 'widget_tweaks', + 'pwa', + 'templatetags.apps.TemplatetagsConfig', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'schoolapps.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [ + os.path.join(BASE_DIR, 'templates') + ], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + 'django_react_templatetags.context_processors.react_context_processor', + 'meta.meta_processor', + ], + }, + }, +] + +WSGI_APPLICATION = 'schoolapps.wsgi.application' + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +# Internationalization + +LANGUAGE_CODE = 'de-de' +TIME_ZONE = 'Europe/Berlin' +USE_I18N = True +USE_L10N = True +USE_TZ = True + +# Static files (CSS, JavaScript, Images) + +STATIC_URL = '/static/' +STATICFILES_DIRS = [ + os.path.join(BASE_DIR, 'static') +] +STATIC_ROOT = os.path.join(BASE_DIR, 'staticcollect') + +# Redirect to home URL after login (Default redirects to /accounts/profile/) +LOGIN_REDIRECT_URL = '/' + +# EMAIL +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' + +# TIMETABLE +TIMETABLE_WIDTH = 5 +TIMETABLE_HEIGHT = 9 +LESSONS = [('8:00', '1.'), ('8:45', '2.'), ('9:45', '3.'), ('10:35', '4.'), ('11:35', '5.'), + ('12:25', '6.'), ('13:15', '7.'), ('14:05', '8.'), ('14:50', '9.')] +SHORT_WEEK_DAYS = ["Mo", "Di", "Mi", "Do", "Fr"] +LONG_WEEK_DAYS = [("Montag", 0), ("Dienstag", 1), ("Mittwoch", 2), ("Donnerstag", 3), ("Freitag", 4)] + +# LDAP + +# Baseline configuration. +AUTH_LDAP_SERVER_URI = "ldap://127.0.0.1" +AUTH_LDAP_USER_SEARCH = LDAPSearch("dc=skole,dc=skolelinux,dc=no", + ldap.SCOPE_SUBTREE, "(&(objectClass=posixAccount)(uid=%(user)s))") +AUTH_LDAP_GROUP_SEARCH = LDAPSearch("dc=skole,dc=skolelinux,dc=no", ldap.SCOPE_SUBTREE, + "(&(objectClass=posixGroup))") +AUTH_LDAP_GROUP_TYPE = PosixGroupType() +AUTH_LDAP_USER_ATTR_MAP = { + "first_name": "givenName", + "last_name": "sn", + "email": "mail" +} +AUTH_LDAP_USER_FLAGS_BY_GROUP = { + "is_staff": "cn=schoolapps-admins,ou=group,dc=skole,dc=skolelinux,dc=no", + "is_superuser": "cn=schoolapps-admins,ou=group,dc=skole,dc=skolelinux,dc=no", +} + +AUTH_LDAP_ALWAYS_UPDATE_USER = True +AUTH_LDAP_MIRROR_GROUPS = True +AUTH_LDAP_CACHE_GROUPS = True +AUTH_LDAP_GROUP_CACHE_TIMEOUT = 300 + +# Keep ModelBackend around for per-user permissions and maybe a local superuser. +AUTHENTICATION_BACKENDS = ( + 'django_auth_ldap.backend.LDAPBackend', + 'django.contrib.auth.backends.ModelBackend', +) + +if DEBUG: + logger = logging.getLogger('django_auth_ldap') + logger.addHandler(logging.StreamHandler()) + logger.setLevel(logging.DEBUG) + +# Media +MEDIA_URL = '/media/' +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') + +# Use cache for db settings (only on production) +DBSETTINGS_USE_CACHE = not DEBUG + +# Cache configs (only on production) +TEST_MEMCACHE = False +if not DEBUG or TEST_MEMCACHE: + CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', + 'LOCATION': '127.0.0.1:11211', + } + } +else: + CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', + } + } + +# PWA +PWA_APP_NAME = 'SchoolApps' +PWA_APP_DESCRIPTION = "Eine Sammlung an nützlichen Apps für den Schulalltag am Katharineum zu Lübeck" +PWA_APP_THEME_COLOR = '#da1f3d' +PWA_APP_BACKGROUND_COLOR = '#ffffff' +PWA_APP_DISPLAY = 'standalone' +PWA_APP_SCOPE = '/' +PWA_APP_ORIENTATION = 'any' +PWA_APP_START_URL = '/' +PWA_APP_ICONS = [ + { + "src": "/static/icons/android_192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/static/icons/android_512.png", + "sizes": "512x512", + "type": "image/png" + } +] +PWA_APP_SPLASH_SCREEN = [ + { + 'src': '/static/icons/android_512.png', + 'media': '(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)' + } +] +PWA_APP_DIR = 'ltr' +PWA_SERVICE_WORKER_PATH = os.path.join(BASE_DIR, 'static/common', 'serviceworker.js') +PWA_APP_LANG = 'de-DE' + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': { + 'level': 'INFO', + 'class': 'logging.StreamHandler', + }, + 'file': { + 'level': 'DEBUG', + 'class': 'logging.FileHandler', + 'filename': 'log.django', + }, + }, + 'loggers': { + 'django': { + 'handlers': ['console', 'file'], + 'level': os.getenv('DJANGO_LOG_LEVEL', 'DEBUG'), + }, + }, +} diff --git a/biscuit/core/schoolapps/urls.py b/biscuit/core/schoolapps/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..ba42b84d22a00a0aefe57eb87e08ac9daf6fba37 --- /dev/null +++ b/biscuit/core/schoolapps/urls.py @@ -0,0 +1,91 @@ +"""schoolapps URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/2.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" + +from django.conf import settings +from django.conf.urls import include +from django.conf.urls.static import static +from django.contrib import admin +from django.shortcuts import render +from django.urls import path + + +def custom_page_not_found(request, exception): + print(exception) + return render(request, 'common/404.html', context={"martor": False}) + + +handler404 = custom_page_not_found + +urlpatterns = [ + ############# + # Dashboard # + ############# + path('', include('dashboard.urls')), + + ######## + # Auth # + ######## + path('accounts/', include('django.contrib.auth.urls')), + + ####### + # AUB # + ####### + path('aub/', include('aub.urls')), + + ######## + # FIBU # + ######## + path('fibu/', include('fibu.urls')), + + ############# + # TIMETABLE # + ############# + path('timetable/', include('timetable.urls')), + + ######## + # MENU # + ######## + path('menu/', include('menu.urls')), + + ######### + # Admin # + ######### + path("debug/", include("debug.urls")), + path('settings/', include('dbsettings.urls')), + path('admin/', admin.site.urls), + + ########### + # SUPPORT # + ########### + path('support/', include('support.urls')), + + ####### + # FAQ # + ####### + path('faq/', include('faq.urls')), + + path('', include('pwa.urls')), + + path('martor/', include('martor.urls')), + + ####### + # 404 # + ####### + path('404/', custom_page_not_found, name='404'), +] + +urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) +urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/biscuit/core/schoolapps/wsgi.py b/biscuit/core/schoolapps/wsgi.py new file mode 100644 index 0000000000000000000000000000000000000000..15033c6106e4b6ff7de5b3809479bb0c62c3b1ad --- /dev/null +++ b/biscuit/core/schoolapps/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for schoolapps project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "schoolapps.settings") + +application = get_wsgi_application() diff --git a/biscuit/core/static/common/favicon.ico b/biscuit/core/static/common/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..1405a12165083ca70bcdfffeacc527f5986a3785 Binary files /dev/null and b/biscuit/core/static/common/favicon.ico differ diff --git a/biscuit/core/static/common/helper.js b/biscuit/core/static/common/helper.js new file mode 100644 index 0000000000000000000000000000000000000000..2b6837838df14525bf203a3e5b2d4ed2bfd76cb2 --- /dev/null +++ b/biscuit/core/static/common/helper.js @@ -0,0 +1,112 @@ +function formatDate(date) { + return date.getDate() + "." + (date.getMonth() + 1) + "." + date.getFullYear(); +} + + +function addZeros(i) { + if (i < 10) { + return "0" + i; + } else { + return "" + i; + } +} + +function formatDateForDjango(date) { + return "" + date.getFullYear() + "/" + addZeros(date.getMonth() + 1) + "/" + addZeros(date.getDate()) + "/"; + +} + +function getNow() { + return new Date(); +} + +function getNowFormatted() { + return formatDate(getNow()); +} + + +function selectActiveLink() { + var currlocation = $('meta[name="active-loaction"]'); + var url_name = currlocation.attr("content"); + //console.log(url_name); + + var selector = ".url-" + url_name; + console.log(selector); + $(selector).addClass("active"); + $(selector).parent().parent().parent().addClass("active"); +} + +$(document).ready(function () { + selectActiveLink(); + + $("dmc-datetime input").addClass("datepicker"); + $("[data-form-control='date']").addClass("datepicker"); + $("[data-form-control='time']").addClass("timepicker"); + + // Initialize sidenav [MAT] + $(".sidenav").sidenav(); + + // Initialize datepicker [MAT] + $('.datepicker').datepicker({ + format: 'dd.mm.yyyy', + // Translate to German + i18n: { + months: ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'], + monthsShort: ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'], + weekdays: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'], + weekdaysShort: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'], + weekdaysAbbrev: ['S', 'M', 'D', 'M', 'D', 'F', 'S'], + + // Buttons + today: 'Heute', + cancel: 'Abbrechen', + done: 'OK', + }, + + // Set monday as first day of week + firstDay: 1, + autoClose: true + }); + + // Initialize timepicker [MAT] + $('.timepicker').timepicker({ + twelveHour: false, + autoClose: true, + i18n: { + cancel: 'Abbrechen', + clear: 'Löschen', + done: 'OK' + }, + }); + + // Initialize tooltip [MAT] + $('.tooltipped').tooltip(); + + // Initialize select [MAT] + $('select').formSelect(); + + // Initalize print button + $("#print").click(function () { + window.print(); + }); + + // Initialize Collapsible [MAT] + $('.collapsible').collapsible(); + + // Initialize FABs [MAT] + $('.fixed-action-btn').floatingActionButton(); + + // Initialize Modals [MAT] + $('.modal').modal(); + + // Initialize delete button + $(".delete-button").click(function (e) { + if (!confirm("Wirklich löschen?")) { + e.preventDefault(); + } + }); + + if (typeof onFinish !== 'undefined') { + onFinish(); + } +}); diff --git a/biscuit/core/static/common/logo.png b/biscuit/core/static/common/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..005e0b6c64a3475a0ccaa875caaf7673c14b8fd2 Binary files /dev/null and b/biscuit/core/static/common/logo.png differ diff --git a/biscuit/core/static/common/serviceworker.js b/biscuit/core/static/common/serviceworker.js new file mode 100644 index 0000000000000000000000000000000000000000..76a52613b4a8a08e45d06b1fdcc54a74e47f5913 --- /dev/null +++ b/biscuit/core/static/common/serviceworker.js @@ -0,0 +1,115 @@ +//This is the SchoolApps service worker + +const CACHE = "schoolapps-cache"; + +const precacheFiles = [ + '', + '/faq/', +]; + +const offlineFallbackPage = '/offline'; + +const avoidCachingPaths = [ + '/admin', + '/settings', + '/support', + '/tools', + '/faq/ask', + '/aub/apply_for', + '/aub/check1', + '/aub/check2', + '/aktuell.pdf', + '/accounts/login', + '/timetable/aktuell.pdf', + '/api', +]; + +function pathComparer(requestUrl, pathRegEx) { + return requestUrl.match(new RegExp(pathRegEx)); +} + +function comparePaths(requestUrl, pathsArray) { + if (requestUrl) { + for (let index = 0; index < pathsArray.length; index++) { + const pathRegEx = pathsArray[index]; + if (pathComparer(requestUrl, pathRegEx)) { + return true; + } + } + } + + return false; +} + +self.addEventListener("install", function (event) { + console.log("[SchoolApps PWA] Install Event processing."); + + console.log("[SchoolApps PWA] Skipping waiting on install."); + self.skipWaiting(); + + event.waitUntil( + caches.open(CACHE).then(function (cache) { + console.log("[SchoolApps PWA] Caching pages during install."); + + return cache.addAll(precacheFiles).then(function () { + return cache.add(offlineFallbackPage); + }); + }) + ); +}); + +// Allow sw to control of current page +self.addEventListener("activate", function (event) { + console.log("[SchoolApps PWA] Claiming clients for current page."); + event.waitUntil(self.clients.claim()); +}); + +// If any fetch fails, it will look for the request in the cache and serve it from there first +self.addEventListener("fetch", function (event) { + if (event.request.method !== "GET") return; + networkFirstFetch(event); +}); + +function networkFirstFetch(event) { + event.respondWith( + fetch(event.request) + .then(function (response) { + // If request was successful, add or update it in the cache + console.log("[SchoolApps PWA] Network request successful."); + event.waitUntil(updateCache(event.request, response.clone())); + return response; + }) + .catch(function (error) { + console.log("[SchoolApps PWA] Network request failed. Serving content from cache: " + error); + return fromCache(event); + }) + ); +} + +function fromCache(event) { + // Check to see if you have it in the cache + // Return response + // If not in the cache, then return offline fallback page + return caches.open(CACHE).then(function (cache) { + return cache.match(event.request) + .then(function (matching) { + if (!matching || matching.status === 404) { + console.log("[SchoolApps PWA] Cache request failed. Serving offline fallback page."); + // Use the precached offline page as fallback + return caches.match(offlineFallbackPage) + } + + return matching; + }); + }); +} + +function updateCache(request, response) { + if (!comparePaths(request.url, avoidCachingPaths)) { + return caches.open(CACHE).then(function (cache) { + return cache.put(request, response); + }); + } + + return Promise.resolve(); +} diff --git a/biscuit/core/static/common/style.css b/biscuit/core/static/common/style.css new file mode 100644 index 0000000000000000000000000000000000000000..780e9fec90ff9d4abcaf22a9482c247d9ed97b49 --- /dev/null +++ b/biscuit/core/static/common/style.css @@ -0,0 +1,673 @@ +/**********/ +/* COMMON */ +/**********/ + +body { + display: flex; + min-height: 100vh; + flex-direction: column; +} + +.primary-color { + background-color: #da1f3d !important; +} + +.primary-color-text, .primary-color-text a { + color: #da1f3d !important; +} + +/**********/ +/* HEADER */ +/**********/ + +.brand-logo { + margin-left: 10px; +} + +/********/ +/* MAIN */ +/********/ + +main { + padding: 10px 20px; + flex: 1 0 auto; +} + +/***********/ +/* SIDENAV */ +/***********/ + +ul.sidenav.sidenav-fixed li.logo { + margin-top: 32px; + margin-bottom: 50px; +} + +ul.sidenav.sidenav-fixed .brand-logo { + margin: 0; +} + +.logo img { + width: 250px; +} + +header a.sidenav-trigger { + position: absolute; + left: 7.5%; + top: 0; + + height: 64px; + font-size: 38px; + + float: none; + + text-align: center; + color: white; + + z-index: 2; +} + + +@media only screen and (max-width: 993px) { + header div.nav-wrapper { + z-index: -5; + } +} + +header, main, footer { + margin-left: 300px; +} + + +.footer-icon { + font-size: 22px !important; + vertical-align: middle; +} + + +@media only screen and (min-width: 1384px) { + .footer-row-large { + display: flex; + align-items: center; + } + + .footer-row-small { + display: none; + } +} + +@media only screen and (max-width: 1383px) { + .footer-row-large { + display: none; + } + + .footer-row-small { + display: block; + } +} + +ul.footer-ul { + display: inline-block; + text-align: right; + float: right; +} + +.make-it-higher { + vertical-align: middle; + line-height: 36px; +} + +@media only screen and (max-width: 992px) { + header, main, footer { + margin-left: 0; + } +} + +/* Collections */ + +ul.collection .collection-item .title { + font-weight: bold; +} + +.section { + padding: 0; +} + +form .row { + margin-top: 0; + margin-bottom: 0; +} + +/* Badges */ + +span.badge.new::after { + content: ""; +} + +span.badge.new { + font-size: 1rem; + line-height: 26px; + height: 26px; +} + +span.badge.new.no-float { + float: none; + padding: 3px 6px; +} + +span.badge .material-icons { + font-size: 0.9rem; +} + +/*+++++++++++*/ +/* Timetable */ +/*+++++++++++*/ +.smart-plan-badge { + margin: 5px 20px 5px 0; +} + +li.active > a > .sidenav-badge { + background-color: whitesmoke !important; + color: #DA3D56 !important; +} + +.timetable-plan .row, .timetable-plan .col { + display: flex; + padding: 0 .25rem; +} + +.timetable-plan .row { + margin-bottom: .25rem; +} + +.lesson-card, .timetable-title-card { + margin: 0; + display: flex; + flex-grow: 1; + min-height: 65px; +} + +.lesson-card .card-content { + padding: 0; + text-align: center; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; +} + +.lesson-card .card-content div { + padding: 3px; + flex: auto; + width: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.timetable-title-card .card-content { + padding: 10px; + text-align: center; + width: 100%; +} + +.timetable-mobile-title-card { + margin-top: 50px; + margin-bottom: .60rem; +} + +.timetable-mobile-title-card:first-child { + margin-top: -10px; + margin-bottom: .60rem; +} + +.timetable-mobile-title-card .card-content { + padding: 10px; + text-align: center; + width: 100%; +} + +.timetable-mobile-title-card .card-content .card-title { + font-weight: bold; +} + +table.substitutions td, table.substitutions th { + padding: 10px 5px; +} + +.lesson-with-sub { + border: 3px solid red; + border-radius: 3px; +} + +.lesson-with-sub .badge { + margin: 0; +} + +.lesson-with-event { + border: 3px solid #9c27b0; + border-radius: 3px; +} + +.lesson-card a, .substitutions a { + color: inherit; +} + +/*.timetable-time {*/ +/*margin-right: 20px;*/ +/*}*/ + +/*+++++++++*/ +/* Buttons */ +/*+++++++++*/ + +.btn-flat-large { + line-height: 60px; + height: 60px; +} + +.btn-flat-large i { + font-size: 4rem; +} + +.btn-flat-medium { + line-height: 40px; + height: 40px; +} + +.btn-flat-medium i { + font-size: 2rem; +} + +.btn-timetable-quicklaunch { + margin: 1%; + width: 30%; + background-color: rgba(0, 0, 0, 0.05) !important; + color: black; +} + +.btn-timetable-quicklaunch:hover { + background-color: #da1f3d !important; + color: whitesmoke; +} + +.no-margin { + margin: 0 !important; +} + +.valign-middle { + vertical-align: middle; +} + +.valign-top { + vertical-align: top; +} + +.valign-bot { + vertical-align: bottom; +} + +.height-inherit { + height: 100%; +} + +/* Table*/ + +table.striped > tbody > tr:nth-child(odd), table tr.striped { + background-color: rgba(208, 208, 208, 0.5); +} + + +/*+++++++*/ +/* Print */ +/*+++++++*/ +#print-header { + display: none; +} + +.print-icon { + margin-top: 1.52rem; +} + +@media print { + body { + font-size: 15px; + } + + header, main, footer { + margin-left: 0; + } + + ul.sidenav { + display: none !important; + transform: translateX(-105%) !important; + } + + nav { + display: none; + } + + .sidenav-trigger { + display: none; + } + + #print-header { + display: block; + border-bottom: 1px solid; + margin-bottom: 0; + } + + #print-header .col.right-align { + padding: 15px; + } + + main, header { + padding: 0; + } + + footer, footer .footer-copyright, footer .container { + background-color: white !important; + color: black !important; + } + + footer a { + display: none; + } + + .footer-copyright, .footer-copyright .container { + padding: 0 !important; + margin: 0 !important; + } + + .no-print { + display: none; + } +} + +.alert ul, .alert p { + margin: 0; +} + +.alert > p, .alert > div { + margin: 10px; + padding: 10px; + border-left: 5px solid; +} + +.alert.success > p, .alert.success > div { + background-color: #c5e1a5; + border-color: #4caf50; +} + +.alert.error > p, .alert.error > div { + background-color: #ef9a9a; + border-color: #b71c1c; +} + +.alert.primary > p, .alert.primary > div, .alert.info > p, .alert.info > div { + background-color: #ececec; + border-color: #da1f3d; +} + +.alert.warning p, .alert.warning div { + background-color: #ffb74d; + border-color: #e65100; +} + +main .alert p:first-child, main .alert div:first-child { + margin-left: -10px; + margin-right: -10px; +} + +.btn, .btn-large, .btn-small { + background-color: #0f9d58; +} + +.btn:hover, .btn-large:hover, .btn-small { + background-color: #DA1F3D; +} + +/*++++++++++++++++ +FEEDBACK +++++++++++++++++*/ + +.rating { + display: flex; + flex-direction: row-reverse; + flex-wrap: wrap; + width: 100%; + min-width: 150px; + max-width: 600px; + justify-content: flex-end; +} + +@media only screen and (max-width: 992px) { + .rating { + margin-top: 12px; + min-height: 40px; + justify-content: space-around; + } + + ´ +} + +.rating label { + display: flex; + flex: 1; + position: relative; + cursor: pointer; +} + +.rating label:after { + font-family: 'Material Icons'; + -webkit-font-feature-settings: 'liga'; + position: absolute; + color: #777; + top: 0; + left: 0; + width: 100%; + height: 100%; + text-align: center; + font-size: 40px; + content: 'star_border'; +} + +.rating input { + display: none !important; +} + +.rating > input:checked + label:after, +.rating > input:checked ~ label:after { + content: "star"; + color: #FFD700; +} + +.rating > input:hover + label:after, +.rating > input:hover ~ label:after { + content: "star"; + color: #FFD700; +} + +.support-input-mobile label { + line-height: 1rem; +} + +.span-info { + width: 24px; + vertical-align: middle; +} + +i.collapsible-trigger { + margin-right: -10px; + +} + +.collapsible .collapsible-trigger::before { + content: 'arrow_downward'; +} + +.collapsible .collapsible-trigger.v2::before { + content: 'more_vert'; +} + +.collapsible .active .collapsible-trigger::before { + content: 'arrow_upward'; +} + + +.scroll-fix { + max-height: 300px; + overflow: scroll; +} + +.waves-effect.waves-katharineum .waves-ripple { + background-color: rgba(218, 31, 61, 0.65); +} + +.no-margin { + margin: 0 !important; +} + +.no-padding { + padding: 0 !important; +} + +.no-pad-left { + padding-left: 0 !important; +} + +.no-pad-right { + padding-right: 0 !important; +} + +.sidenav a:not(.collapsible-header) { + padding: 0 16px; +} + +ul.sidenav li.logo > a:hover { + background: none !important; +} + +.waves-effect.waves-primary .waves-ripple { + /* The alpha value allows the text and background color + of the button to still show through. */ + background-color: #da1f3d; +} + +.sidenav .collapsible-body > ul:not(.collapsible) > li.active a > i, .sidenav.sidenav-fixed .collapsible-body > ul:not(.collapsible) > li.active a > i { + color: #fff; +} + +.sidenav .collapsible-body > ul:not(.collapsible) > li.active, .sidenav.sidenav-fixed .collapsible-body > ul:not(.collapsible) > li.active { + background-color: #DA3D56; +} + +.card .card-action a:not(.btn):not(.btn-large):not(.btn-small):not(.btn-large):not(.btn-floating) { + color: #da1f3d; +} + +.card .card-action a:not(.btn):not(.btn-large):not(.btn-small):not(.btn-large):not(.btn-floating):hover { + color: #ea4661; +} + +/*section:not(:last-of-type) {*/ +/* border-bottom: solid #bdbdbd 2px;*/ +/*}*/ + +/*++++++++ ++HOLIDAYS+ +++++++++++ */ + +.holiday-badge { + float: left !important; + position: relative; + margin-left: 0% !important; + left: 50%; + transform: translate(-50%); + width: auto; + height: auto !important; + min-height: 26px; +} + +/* Dashboard */ + +.card-action-badge { + float: left !important; + margin-left: 0 !important; + margin-top: -3px; + margin-right: 10px; +} + +.event-card { + padding: 10px; +} + +.event-card .title { + font-size: 18px; + font-weight: 500; +} + + +.flex-row { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; +} + +.hundred-percent { + width: 100%; +} + +.badge-image { + position: absolute; + left: 0; + top: 10px; + z-index: 1; + background-color: #da1f3d; + color: white; + padding: 2px 10px; + border-radius: 0 3px 3px 0; + text-transform: uppercase; + font-weight: 300; +} + +.center-via-flex { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.center2-via-flex { + display: flex; + flex-direction: column; + align-items: center; +} + +.spinner-primary { + border-color: #da1f3d; +} + +.dashboard-cards { + -webkit-column-break-inside: avoid; + column-count: 3; +} + +@media (min-width: 800px) and (max-width: 1460px) { + .dashboard-cards { + column-count: 2; + } +} + +@media (max-width: 800px) { + .dashboard-cards { + column-count: 1; + } +} + + +.dashboard-cards .card { + display: inline-block; + overflow: visible; + width: 100%; +} diff --git a/biscuit/core/static/icons/android_192.png b/biscuit/core/static/icons/android_192.png new file mode 100644 index 0000000000000000000000000000000000000000..3de63ed87e9a3c6389b8cbf686922d5d2b1d38ec Binary files /dev/null and b/biscuit/core/static/icons/android_192.png differ diff --git a/biscuit/core/static/icons/android_512.png b/biscuit/core/static/icons/android_512.png new file mode 100644 index 0000000000000000000000000000000000000000..3de63ed87e9a3c6389b8cbf686922d5d2b1d38ec Binary files /dev/null and b/biscuit/core/static/icons/android_512.png differ diff --git a/biscuit/core/static/icons/apple_114.png b/biscuit/core/static/icons/apple_114.png new file mode 100644 index 0000000000000000000000000000000000000000..859930c3204e69df15c42d5437786ace0c921310 Binary files /dev/null and b/biscuit/core/static/icons/apple_114.png differ diff --git a/biscuit/core/static/icons/apple_152.png b/biscuit/core/static/icons/apple_152.png new file mode 100644 index 0000000000000000000000000000000000000000..d554e0ddb5e53d6f67b7e411c82388e1222cae0d Binary files /dev/null and b/biscuit/core/static/icons/apple_152.png differ diff --git a/biscuit/core/static/icons/apple_180.png b/biscuit/core/static/icons/apple_180.png new file mode 100644 index 0000000000000000000000000000000000000000..69dacfa6d08225ea888812f578cd1b8e9df21789 Binary files /dev/null and b/biscuit/core/static/icons/apple_180.png differ diff --git a/biscuit/core/static/icons/apple_76.png b/biscuit/core/static/icons/apple_76.png new file mode 100644 index 0000000000000000000000000000000000000000..9f89e66cac53188c090f030022cf5047a5ca3ea6 Binary files /dev/null and b/biscuit/core/static/icons/apple_76.png differ diff --git a/biscuit/core/static/icons/favicon_16.png b/biscuit/core/static/icons/favicon_16.png new file mode 100644 index 0000000000000000000000000000000000000000..c6a2906384295d750a2b2f66a7a967bacd5a1159 Binary files /dev/null and b/biscuit/core/static/icons/favicon_16.png differ diff --git a/biscuit/core/static/icons/favicon_32.png b/biscuit/core/static/icons/favicon_32.png new file mode 100644 index 0000000000000000000000000000000000000000..0b3dbb819c40cfa9942139197d8b9d6ed320e562 Binary files /dev/null and b/biscuit/core/static/icons/favicon_32.png differ diff --git a/biscuit/core/static/icons/favicon_48.png b/biscuit/core/static/icons/favicon_48.png new file mode 100644 index 0000000000000000000000000000000000000000..06886725806fa482c887c3443686be0cfde828fe Binary files /dev/null and b/biscuit/core/static/icons/favicon_48.png differ diff --git a/biscuit/core/static/js/dashboard.js b/biscuit/core/static/js/dashboard.js new file mode 100644 index 0000000000000000000000000000000000000000..1604cae664f8a80628172763611379c01a65bd85 --- /dev/null +++ b/biscuit/core/static/js/dashboard.js @@ -0,0 +1,601 @@ +/*** THIS FILE IS GENERATED from react/src/dashboard.js ***/ + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var REFRESH_TIME = 15; + +// function WithCheckCircleIcon(props) { +// return <div className={"col s12"}> +// <i className={"material-icons left green-text"}>check_circle</i> +// {props.children} +// </div> +// } + +var Dashboard = function (_React$Component) { + _inherits(Dashboard, _React$Component); + + function Dashboard() { + _classCallCheck(this, Dashboard); + + var _this = _possibleConstructorReturn(this, (Dashboard.__proto__ || Object.getPrototypeOf(Dashboard)).call(this)); + + _this.updateRefreshTime = function () { + if (_this.state.refreshIn >= 1) { + if (_this.state.timeout) { + window.clearTimeout(_this.state.timeout); + } + var timeout = window.setTimeout(_this.updateRefreshTime, 1000); + _this.setState({ refreshIn: _this.state.refreshIn - 1, timeout: timeout }); + } else { + _this.updateData(); + } + }; + + _this.updateData = function () { + if (_this.state.networkProblems) { + _this.setState({isLoading: true, networkProblems: false}); + } + + var that = _this; + $.getJSON(API_URL, function (data) { + console.log(data); + if (data) { + that.setState(Object.assign({}, data, {refreshIn: REFRESH_TIME + 1, isLoading: false})); + that.updateRefreshTime(); + } + }).fail(function () { + console.log("error"); + that.setState({refreshIn: REFRESH_TIME + 1, networkProblems: true}); + }); + $.getJSON(API_URL + "/my-plan", function (data) { + console.log(data); + if (data && data.lessons) { + that.setState({lessons: data.lessons, holiday: data.holiday}); + } + }).fail(function () { + console.log("error"); + that.setState({networkProblems: true}); + }); + }; + + _this.state = { + refreshIn: REFRESH_TIME, + isLoading: true, + networkProblems: false + }; + return _this; + } + + _createClass(Dashboard, [{ + key: "componentDidMount", + value: function componentDidMount() { + console.log(API_URL); + this.updateData(); + } + }, { + key: "closeNotification", + value: function closeNotification(notification) { + console.log(notification); + $("#not-" + notification.id).addClass("scale-out"); + window.setTimeout(function () { + $("#not-" + notification.id).hide(); + }, 200); + $.getJSON(API_URL + "/notifications/read/" + notification.id); + this.updateData(); + this.setState({ time: new Date() }); + } + }, { + key: "render", + value: function render() { + if (this.state.networkProblems) { + // Show loading screen until first data are loaded + return React.createElement( + "div", + {className: "row center-via-flex container", style: {"height": "20em"}}, + React.createElement( + "i", + {className: "material-icons large"}, + "signal_wifi_off" + ), + React.createElement( + "p", + {className: "flow-text text-center"}, + "Es ist ein Fehler bei der Netzwerkverbindung aufgetreten." + ), + React.createElement( + "button", + {className: "btn-flat grey-text", onClick: this.updateData}, + "Erneuter Versuch in ", + this.state.refreshIn, + " s" + ) + ); + } + + if (this.state.isLoading) { + // Show loading screen until first data are loaded + return React.createElement( + "div", + {className: "row center-via-flex container", style: {"height": "15em"}}, + React.createElement( + "div", + {className: "center2-via-flex"}, + React.createElement( + "div", + { className: "preloader-wrapper big active" }, + React.createElement( + "div", + { className: "spinner-layer spinner-primary" }, + React.createElement( + "div", + { className: "circle-clipper left" }, + React.createElement("div", { className: "circle" }) + ), + React.createElement( + "div", + { className: "gap-patch" }, + React.createElement("div", { className: "circle" }) + ), + React.createElement( + "div", + { className: "circle-clipper right" }, + React.createElement("div", { className: "circle" }) + ) + ) + ), + React.createElement( + "p", + { className: "text-center flow-text" }, + "Deine aktuellen Informationen werden geladen \u2026" + ) + ) + ); + } + + var that = this; + return React.createElement( + "div", + null, + React.createElement( + "button", + { className: "btn-flat right grey-text", onClick: this.updateData }, + React.createElement( + "i", + { className: "material-icons left" }, + "refresh" + ), + "in ", + this.state.refreshIn, + " s" + ), + React.createElement( + "p", + { className: "flow-text" }, + "Moin Moin, ", + this.state.user.full_name !== "" ? this.state.user.full_name : this.state.user.username, + ". Hier findest du alle aktuellen Informationen:" + ), + React.createElement( + "div", + { className: "alert success" }, + React.createElement( + "p", + null, + React.createElement( + "i", + { className: "material-icons left" }, + "report_problem" + ), + "Das neue Dashboard von SchoolApps befindet sich momentan in der ", + React.createElement( + "strong", + null, + "Testphase" + ), + ". Falls Fehler auftreten oder du einen Verbesserungsvorschlag f\xFCr uns hast, schreibe uns bitte unter ", + React.createElement( + "a", + { + href: "mailto:support@katharineum.de" }, + "support@katharineum.de" + ), + "." + ) + ), + this.state.unread_notifications && this.state.unread_notifications.length > 0 ? this.state.unread_notifications.map(function (notification) { + return React.createElement( + "div", + { className: "alert primary scale-transition", id: "not-" + notification.id, + key: notification.id }, + React.createElement( + "div", + null, + React.createElement( + "i", + { className: "material-icons left" }, + "info" + ), + React.createElement( + "div", + { className: "right" }, + React.createElement( + "button", + { className: "btn-flat", onClick: function onClick() { + return that.closeNotification(notification); + } }, + React.createElement( + "i", + { className: "material-icons center" }, + "close" + ) + ) + ), + React.createElement( + "strong", + null, + notification.title + ), + React.createElement( + "p", + null, + notification.description + ) + ) + ); + }) : "", + this.state.plan && this.state.plan.hints.length > 0 ? React.createElement( + "div", + null, + this.state.plan.hints.map(function (hint, idx) { + return React.createElement( + "div", + { className: "alert primary", key: idx }, + React.createElement( + "div", + null, + React.createElement( + "em", + { className: "right hide-on-small-and-down" }, + "Hinweis f\xFCr ", + that.state.date_formatted + ), + React.createElement( + "i", + { className: "material-icons left" }, + "announcement" + ), + React.createElement("p", { dangerouslySetInnerHTML: { __html: hint.html } }), + React.createElement( + "em", + { className: "hide-on-med-and-up" }, + "Hinweis f\xFCr ", + that.state.date_formatted + ) + ) + ); + }) + ) : "", + React.createElement( + "div", + { className: "row" }, + this.state.has_plan ? React.createElement( + "div", + {className: "col s12 m12 l6 xl4"}, + React.createElement( + "div", + {className: "card"}, + React.createElement( + "div", + {className: "card-content"}, + React.createElement( + "span", + {className: "card-title"}, + "Plan ", + this.state.plan.type === 2 ? "der" : "für", + " ", + React.createElement( + "em", + null, + this.state.plan.name + ), + " f\xFCr ", + this.state.date_formatted + ), + this.state.holiday ? React.createElement( + "div", + { className: "card" }, + React.createElement( + "div", + { className: "card-content" }, + React.createElement( + "span", + { + className: "badge new blue center-align holiday-badge" }, + this.state.holiday.name + ), + React.createElement("br", null) + ) + ) : this.state.lessons && this.state.lessons.length > 0 ? React.createElement( + "div", + { className: "timetable-plan" }, + this.state.lessons.map(function (lesson) { + // Show one lesson row + return React.createElement( + "div", + { className: "row" }, + React.createElement( + "div", + { className: "col s4" }, + React.createElement( + "div", + { className: "card timetable-title-card" }, + React.createElement( + "div", + { className: "card-content" }, + React.createElement( + "span", + { className: "card-title left" }, + lesson.time.number_format + ), + React.createElement( + "div", + { + className: "right timetable-time grey-text text-darken-2" }, + React.createElement( + "span", + null, + lesson.time.start_format + ), + React.createElement("br", null), + React.createElement( + "span", + null, + lesson.time.end_format + ) + ) + ) + ) + ), + React.createElement("div", { className: "col s8", + dangerouslySetInnerHTML: { __html: lesson.html } }) + ); + }) + ) : "" + ), + React.createElement( + "div", + { className: "card-action" }, + React.createElement( + "a", + { href: MY_PLAN_URL }, + React.createElement( + "span", + { className: "badge new primary-color card-action-badge" }, + "SMART PLAN" + ), + "anzeigen" + ) + ) + ) + ) : "", + this.state.current_events && this.state.current_events.length > 0 ? React.createElement( + "div", + {className: "col s12 m12 l6 xl4"}, + React.createElement( + "div", + {className: "card "}, + React.createElement( + "div", + {className: "card-content"}, + React.createElement( + "span", + {className: "card-title"}, + "Aktuelle Termine" + ), + this.state.current_events.map(function (event) { + return React.createElement( + "div", + { className: "card-panel event-card" }, + React.createElement( + "span", + { className: "title" }, + event.name + ), + React.createElement("br", null), + event.formatted + ); + }) + ), + React.createElement( + "div", + { className: "card-action" }, + React.createElement( + "a", + { href: "https://katharineum-zu-luebeck.de/aktuelles/termine/", target: "_blank" }, + "Weitere Termine" + ) + ) + ) + ) : "", + this.state.newest_article ? React.createElement( + "div", + {className: "col s12 m12 l6 xl4"}, + React.createElement( + "div", + {className: "card"}, + React.createElement( + "div", + {className: "card-image"}, + React.createElement( + "span", + {className: "badge-image z-depth-2"}, + "Aktuelles von der Homepage" + ), + React.createElement("img", { + src: this.state.newest_article.image_url, + alt: this.state.newest_article.title + }) + ), + React.createElement( + "div", + {className: "card-content"}, + React.createElement("span", { + className: "card-title", + dangerouslySetInnerHTML: {__html: this.state.newest_article.title} + }), + React.createElement("p", {dangerouslySetInnerHTML: {__html: this.state.newest_article.short_text}}) + ), + React.createElement( + "div", + {className: "card-action"}, + React.createElement( + "a", + {href: this.state.newest_article.link, target: "_blank"}, + "Mehr lesen" + ) + ) + ), + React.createElement( + "a", + { + className: "btn hundred-percent primary-color", + href: "https://katharineum-zu-luebeck.de/", + target: "_blank" + }, + "Weitere Artikel", + React.createElement( + "i", + {className: "material-icons right"}, + "arrow_forward" + ) + ) + ) : "" + ), + React.createElement( + "div", + { className: "row" }, + React.createElement( + "div", + { className: "col s12 m6" }, + React.createElement( + "h5", + null, + "Letzte Aktivit\xE4ten" + ), + this.state.activities && this.state.activities.length > 0 ? React.createElement( + "ul", + { className: "collection" }, + this.state.activities.map(function (activity) { + return React.createElement( + "li", + { className: "collection-item", key: activity.id }, + React.createElement( + "span", + { className: "badge new primary-color" }, + activity.app + ), + React.createElement( + "span", + { className: "title" }, + activity.title + ), + React.createElement( + "p", + null, + React.createElement( + "i", + { className: "material-icons left" }, + "access_time" + ), + " ", + activity.created_at + ), + React.createElement( + "p", + null, + activity.description + ) + ); + }) + ) : React.createElement( + "p", + null, + "Noch keine Aktivit\xE4ten vorhanden." + ) + ), + React.createElement( + "div", + { className: "col s12 m6" }, + React.createElement( + "h5", + null, + "Letzte Benachrichtigungen" + ), + this.state.notifications && this.state.notifications.length > 0 ? React.createElement( + "ul", + { className: "collection" }, + this.state.notifications.map(function (notification) { + return React.createElement( + "li", + { className: "collection-item", key: notification.id }, + React.createElement( + "span", + { className: "badge new primary-color" }, + notification.app + ), + React.createElement( + "span", + { className: "title" }, + notification.title + ), + React.createElement( + "p", + null, + React.createElement( + "i", + { className: "material-icons left" }, + "access_time" + ), + " ", + notification.created_at + ), + React.createElement( + "p", + null, + notification.description + ), + notification.link ? React.createElement( + "p", + null, + React.createElement( + "a", + { href: notification.link }, + "Mehr Informationen \u2192" + ) + ) : "" + ); + }) + ) : React.createElement( + "p", + null, + "Noch keine Benachrichtigungen vorhanden." + ) + ) + ) + ); + } + }]); + + return Dashboard; +}(React.Component); + +$(document).ready(function () { + var domContainer = document.querySelector('#dashboard_container'); + ReactDOM.render(React.createElement(Dashboard, null), domContainer); +}); diff --git a/biscuit/core/static/js/rebus.js b/biscuit/core/static/js/rebus.js new file mode 100644 index 0000000000000000000000000000000000000000..752008a3dc5b7e2e43261ea7bf0ada5b51e65722 --- /dev/null +++ b/biscuit/core/static/js/rebus.js @@ -0,0 +1,642 @@ +/*** THIS FILE IS GENERATED from react/src/rebus.js ***/ + +var _createClass = function () { + function defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } + } + + return function (Constructor, protoProps, staticProps) { + if (protoProps) defineProperties(Constructor.prototype, protoProps); + if (staticProps) defineProperties(Constructor, staticProps); + return Constructor; + }; +}(); + +function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } +} + +function _possibleConstructorReturn(self, call) { + if (!self) { + throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); + } + return call && (typeof call === "object" || typeof call === "function") ? call : self; +} + +function _inherits(subClass, superClass) { + if (typeof superClass !== "function" && superClass !== null) { + throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); + } + subClass.prototype = Object.create(superClass && superClass.prototype, { + constructor: { + value: subClass, + enumerable: false, + writable: true, + configurable: true + } + }); + if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; +} + +var OPTIONS_ONLINE_COMMON = ["Portal ist nicht erreichbar", "Fehlermeldung(en) tauchen auf", "Anmeldung funktioniert nicht", "Zugangsdaten vergessen"]; + +var BASIC_OPTIONS = [{ + id: "infrastructureIssues", + name: "Infrastrukturprobleme", + options: [{ + id: "presentationDeviceIssue", + name: "Problem mit Beamer/Fernseher", + helpText: "Bitte wähle aus, wo der Beamer bzw. Fernseher steht!" + }, { + id: "printerIssue", + name: "Problem mit einem Drucker", + helpText: "Bitte nenne uns in der Beschreibung das Modell des Druckers, damit wir genau wissen, welchen Drucker du meinst!" + }, { + id: "subMonitorIssue", + name: "Vertretungsplanmonitor funktioniert nicht", + helpText: "Nenne uns bitte in der Beschreibung ggf. weitere Informationen!" + }, { + id: "aulaIssue", + name: "Problem in der Aula (→Technik-AG)", + helpText: "Deine Anfrage wird direkt an die Technik-AG weitergeleitet." + }, { + id: "wlanIssue", + name: "Probleme mit dem Schul-WLAN (kath-schueler/lehrer)", + helpText: "Nenne uns bitte unbedingt auch den Ort in der Schule, an dem das Problem auftrat." + }] +}, { + id: "onlineIssues", + name: "Webservices", + options: [{ + id: "forum", + name: "Forum (ILIAS)", + options: OPTIONS_ONLINE_COMMON.concat(["Ich kann meinen Kurs bzw. Klasse nicht sehen/finden.", "Ich kann keine Dateien hochladen.", "Es taucht eine weiße Seite auf.", "Ich habe falsche Informationen gefunden."]) + }, { + id: "mail", + name: "Webmail/Mailserver", + options: OPTIONS_ONLINE_COMMON.concat(["Mein E-Mail-Programm funktioniert mit meiner …@katharineum.de-Adresse nicht.", "Ich bekomme keine E-Mails bzw. kann keine senden."]) + }, { + id: "schoolapps", + name: "SchoolApps", + options: OPTIONS_ONLINE_COMMON.concat(["Der Stundenplan/Vertretungsplan ist falsch.", "Ich bin der falschen Klasse zugeordnet.", "Ich habe einen Fehler gefunden."]) + }, { + id: "subOrMenu", + name: "Vertretungsplan/Speiseplan", + options: OPTIONS_ONLINE_COMMON.concat(["Kein Vertretungsplan zu sehen", "Falscher Vertretungsplan zu sehen", "Kein Speiseplan zu sehen", "Falscher Speiseplan zu sehen"]) + }, { + id: "website", + name: "Website (katharineum-zu-luebeck.de)", + options: ["Website nicht erreichbar", "Falsche Inhalte vorhanden", "Typografiefehler"] + + }, { + id: "otherOnlineIssue", + name: "Andere Anwendung" + }] +}, { + id: "deviceIssues", + name: "Probleme am Computer/Notebook", + options: [{ + id: "loginIssue", + name: "Anmeldeproblem/Passwort vergessen" + }, { + id: "internetIssue", + name: "Internetproblem" + }, { + id: "noReaction", + name: "Programm-/Computerabsturz (keine Reaktion)" + }, { + id: "powerOffNoBoot", + name: "Computer/Notebook ist ausgegangen/startet nicht" + }, { + id: "speedIssue", + name: "Computer/Notebook zu langsam" + }, { + id: "noUSB", + name: "USB-Stick wird nicht erkannt" + }, { + id: "noOpenTray", + name: "CD/DVD-Laufwerk öffnet sich nicht" + }, { + id: "noCDDVD", + name: "CD/DVD wird nicht erkannt/abgespielt" + }, { + id: "keyboardMouse", + name: "Tastatur/Maus funktioniert nicht" + }, { + id: "missingHardware", + name: "Tastatur/Maus/Lautsprecher/etc. fehlt" + }, { + id: "missingKeys", + name: "Fehlende Tasten auf der Tastatur" + }, { + id: "hardwareMisc", + name: "Andere Hardware defekt / Äußere Schäden" + }] +}, { + id: "otherIssues", + name: "Andere Probleme", + options: [{ + id: "extra", + name: "Sonstiges" + }] +}]; + +var OTHER_LOCATIONS = ["Notebookwagen 1. Stock/R 2.06", "Notebookwagen 2. Stock/R 2.10", "Notebookwagen 3. Stock/Physik", "Internetcafe", "Infopoint/Sekretariatsvorraum", "Lehrerzimmer (Vorraum)", "Lehrerzimmer (Hauptraum)"]; + +function getCategoryOfOption(option) { + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = BASIC_OPTIONS[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var category = _step.value; + + // console.log(category); + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = undefined; + + try { + for (var _iterator2 = category.options[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var opt = _step2.value; + + // console.log(opt); + if (opt.id === option) { + return category.id; + } + } + } catch (err) { + _didIteratorError2 = true; + _iteratorError2 = err; + } finally { + try { + if (!_iteratorNormalCompletion2 && _iterator2.return) { + _iterator2.return(); + } + } finally { + if (_didIteratorError2) { + throw _iteratorError2; + } + } + } + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } +} + +function getOption(option) { + var _iteratorNormalCompletion3 = true; + var _didIteratorError3 = false; + var _iteratorError3 = undefined; + + try { + for (var _iterator3 = BASIC_OPTIONS[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { + var category = _step3.value; + var _iteratorNormalCompletion4 = true; + var _didIteratorError4 = false; + var _iteratorError4 = undefined; + + try { + for (var _iterator4 = category.options[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { + var opt = _step4.value; + + if (opt.id === option) { + return opt; + } + } + } catch (err) { + _didIteratorError4 = true; + _iteratorError4 = err; + } finally { + try { + if (!_iteratorNormalCompletion4 && _iterator4.return) { + _iterator4.return(); + } + } finally { + if (_didIteratorError4) { + throw _iteratorError4; + } + } + } + } + } catch (err) { + _didIteratorError3 = true; + _iteratorError3 = err; + } finally { + try { + if (!_iteratorNormalCompletion3 && _iterator3.return) { + _iterator3.return(); + } + } finally { + if (_didIteratorError3) { + throw _iteratorError3; + } + } + } +} + +var Select = function (_React$Component) { + _inherits(Select, _React$Component); + + function Select() { + _classCallCheck(this, Select); + + return _possibleConstructorReturn(this, (Select.__proto__ || Object.getPrototypeOf(Select)).apply(this, arguments)); + } + + _createClass(Select, [{ + key: "render", + value: function render() { + return React.createElement( + "select", + {onChange: this.props.onChange, defaultValue: "no", required: this.props.show}, + React.createElement( + "option", + {value: "no", disabled: true}, + "Nichts ausgew\xE4hlt" + ), + this.props.values.map(function (val, i) { + return React.createElement( + "option", + {value: val, key: i}, + val + ); + }), + React.createElement( + "option", + {value: "extra"}, + this.props.defaultValue + ) + ); + } + }]); + + return Select; +}(React.Component); + +Select.propTypes = { + onChange: PropTypes.func.isRequired, + values: PropTypes.array.isRequired, + defaultValue: PropTypes.string, + show: PropTypes.bool.isRequired + +}; + +Select.defaultProps = { + defaultValue: "Sonstiges" +}; + +var Input = function (_React$Component2) { + _inherits(Input, _React$Component2); + + function Input() { + _classCallCheck(this, Input); + + return _possibleConstructorReturn(this, (Input.__proto__ || Object.getPrototypeOf(Input)).apply(this, arguments)); + } + + _createClass(Input, [{ + key: "render", + value: function render() { + return React.createElement( + "div", + { + className: (this.props.show ? "" : "hide ") + "input-field col s12 m12 l4" + }, + React.createElement( + "i", + {className: "material-icons prefix"}, + this.props.icon + ), + this.props.children, + React.createElement( + "label", + null, + this.props.label + ) + ); + } + }]); + + return Input; +}(React.Component); + +Input.propTypes = { + icon: PropTypes.string, + show: PropTypes.bool, + label: PropTypes.string.isRequired, + children: PropTypes.object.isRequired +}; + +Input.defaultProps = { + icon: "list", + show: false +}; + +var REBUSDynSelect = function (_React$Component3) { + _inherits(REBUSDynSelect, _React$Component3); + + function REBUSDynSelect() { + _classCallCheck(this, REBUSDynSelect); + + var _this3 = _possibleConstructorReturn(this, (REBUSDynSelect.__proto__ || Object.getPrototypeOf(REBUSDynSelect)).call(this)); + + _this3._onCategoryChanges = function (e) { + var opt = e.target.value; + var category = getCategoryOfOption(opt); + var option = getOption(opt); + + // Get matching helper text + var helpText = option.helpText || _this3.state.helpText; + if (category === "deviceIssues") { + helpText = "Wähle bitte das Gerät mit dem Problem aus! Bitte vergiss nicht, uns das Problem unten genauer zu beschreiben!"; + } else if (category === "onlineIssues") { + helpText = "Bitte konkretisiere das Problem durch eine Auswahl und gib bitte unten genauere Informationen an."; + } else if (category === "otherIssues") { + helpText = "Da es sich scheinbar um ein seltenes oder noch nicht erfasstes Problem handelt, gib uns bitte besonders viele Informationen."; + } + + // Update state + _this3.setState({ + selectedCategory: category, + selectedOption: option, + step: 1, + helpText: helpText + }); + }; + + _this3._onSetB = function (e) { + var val = e.target.value; + _this3.setState({ + valueB: val, + step: 2 + }); + }; + + _this3._onSetC = function (e) { + var val = e.target.value; + _this3.setState({ + valueC: val, + step: 2 + }); + }; + + _this3.state = { + selectedCategory: "noCategory", + selectedOption: null, + helpText: "Wähle bitte eine Kategorie aus!", + valueB: "", + valueC: "", + step: 0 + }; + return _this3; + } + + _createClass(REBUSDynSelect, [{ + key: "componentDidMount", + value: function componentDidMount() { + // Init materialize selects + var elems = document.querySelectorAll('select'); + M.FormSelect.init(elems, {}); + } + }, { + key: "render", + value: function render() { + var LOCATIONS = this.props.rooms.concat(OTHER_LOCATIONS); + var LOCATIONS_WITH_POSSIBLE_PRESENTATION_DEVICE = this.props.rooms; + LOCATIONS.sort(); + + // console.log(this.state); + var that = this; + var sC = this.state.selectedCategory; + var sO = this.state.selectedOption ? this.state.selectedOption.id : null; + var step = this.state.step; + // console.log(BASIC_OPTIONS[2].options); + return React.createElement( + "div", + {className: "App"}, + React.createElement( + "div", + {className: "row"}, + React.createElement( + "div", + { + className: "input-field col s12 m12 l4" + }, + React.createElement( + "i", + {className: "material-icons prefix"}, + "list" + ), + React.createElement( + "select", + { + onChange: this._onCategoryChanges, defaultValue: "noCategory", className: "validate", + required: true + }, + "-", + React.createElement( + "option", + {value: "noCategory", disabled: true}, + "Keine Kategorie ausgew\xE4hlt" + ), + BASIC_OPTIONS.map(function (category) { + return React.createElement( + "optgroup", + {label: category.name, key: category.id}, + category.options.map(function (option) { + return React.createElement( + "option", + {value: option.id, key: option.id}, + option.name + ); + }) + ); + }) + ), + React.createElement( + "label", + null, + "Kategorie" + ) + ), + React.createElement( + Input, + {label: "Ort des Computer/Notebook", icon: "location_on", show: sC === "deviceIssues"}, + React.createElement(Select, { + onChange: this._onSetB, values: LOCATIONS, defaultValue: "Anderer Ort", + show: sC === "deviceIssues" + }) + ), + React.createElement( + Input, + { + label: "Ort des Beamer/Fernseher", icon: "location_on", + show: sO === "presentationDeviceIssue" + }, + React.createElement(Select, { + onChange: this._onSetB, values: LOCATIONS_WITH_POSSIBLE_PRESENTATION_DEVICE, + defaultValue: "Anderer Raum", show: sO === "presentationDeviceIssue" + }) + ), + React.createElement( + Input, + {label: "Art des Problems", icon: "bug_report", show: sO === "printerIssue"}, + React.createElement(Select, { + onChange: this._onSetB, + values: ["Papierstau", "Toner leer", "Papier leer", "Drucker bekommt keine Daten"], + defaultValue: "Anderes Problem", show: sO === "subMonitorIssue" + }) + ), + React.createElement( + Input, + {label: "Art des Problems", icon: "bug_report", show: sO === "subMonitorIssue"}, + React.createElement(Select, { + onChange: this._onSetB, + values: ["Schwarzer Bildschirm", "Tage wechseln nicht (Eingefroren)"], + defaultValue: "Anderes Problem", show: sO === "subMonitorIssue" + }) + ), + React.createElement( + Input, + {label: "Art des Problems", icon: "bug_report", show: sO === "wlanIssue"}, + React.createElement(Select, { + onChange: this._onSetB, + values: ["Kein Empfang", "Zugangsdaten funktionieren nicht", "Geschwindigkeit zu langsam"], + defaultValue: "Anderes Problem", show: sO === "wlanIssue" + }) + ), + BASIC_OPTIONS[1].options.map(function (opt) { + if (opt.options) { + return React.createElement( + Input, + { + label: "Art des Problems", icon: "bug_report", + show: sC === "onlineIssues" && sO === opt.id, key: opt.id + }, + React.createElement(Select, { + onChange: that._onSetB, + values: opt.options, + defaultValue: "Anderes Problem", show: sC === "onlineIssues" && sO === opt.id, + key: opt.id + }) + ); + } else { + return React.createElement("p", null); + } + }), + React.createElement( + Input, + { + label: "Handelt es sich um einen Beamer oder einen Fernseher?", icon: "tv", + show: sO === "presentationDeviceIssue" && step === 2 + }, + React.createElement(Select, { + onChange: this._onSetC, values: ["Beamer", "Fernseher/Bildschirm"], + defaultValue: "Sonstiges", show: sO === "presentationDeviceIssue" && step === 2 + }) + ), + React.createElement( + Input, + { + label: "Ort des Druckers", icon: "location_on", + show: sO === "printerIssue" && step === 2 + }, + React.createElement(Select, { + onChange: this._onSetC, values: LOCATIONS, + defaultValue: "Anderer Raum", show: sO === "presentationDeviceIssue" + }) + ), + React.createElement( + Input, + { + label: "Um welches WLAN-Netzwerk handelt es sich?", icon: "wifi", + show: sO === "wlanIssue" && step === 2 + }, + React.createElement(Select, { + onChange: this._onSetC, + values: ["kath-schueler", "kath-lehrer", "kath-edu", "kath-gaeste"], + defaultValue: "-", show: sO === "wlanIssue" && step === 2 + }) + ), + React.createElement( + "div", + { + className: (sC === "deviceIssues" && step === 2 ? "" : "hide ") + "input-field col s12 m12 l4" + }, + React.createElement( + "i", + {className: "material-icons prefix"}, + "device_unknown" + ), + React.createElement("input", { + type: "text", id: "valc", onChange: this._onSetC, + required: sC === "deviceIssues" && step === 2, className: "validate" + }), + React.createElement( + "label", + {htmlFor: "valc"}, + "Um welches Ger\xE4t handelt es sich?" + ) + ), + React.createElement( + "div", + {className: "col s12"}, + React.createElement( + "p", + null, + React.createElement( + "i", + {className: "material-icons left"}, + "info" + ), + this.state.helpText + ) + ) + ), + React.createElement( + "div", + null, + React.createElement("input", { + type: "hidden", name: "a", + value: this.state.selectedOption ? this.state.selectedOption.name : "" + }), + React.createElement("input", {type: "hidden", name: "b", value: this.state.valueB}), + React.createElement("input", {type: "hidden", name: "c", value: this.state.valueC}) + ) + ); + } + }]); + + return REBUSDynSelect; +}(React.Component); + +REBUSDynSelect.propTypes = { + rooms: PropTypes.array.isRequired +}; + +$(document).ready(function () { + var domContainer = document.querySelector('#dynselect'); + ReactDOM.render(React.createElement(REBUSDynSelect, props), domContainer); +}); diff --git a/biscuit/core/templates/common/404.html b/biscuit/core/templates/common/404.html new file mode 100644 index 0000000000000000000000000000000000000000..a20f264e20b2cef11dabf9d3c3c10e4547cef9e5 --- /dev/null +++ b/biscuit/core/templates/common/404.html @@ -0,0 +1,135 @@ +{#<!DOCTYPE html>#} +{#<html lang="de">#} +{#<head>#} +{# <meta charset="utf-8">#} +{# <meta http-equiv="X-UA-Compatible" content="IE=edge">#} +{# <meta name="viewport" content="width=device-width,initial-scale=1">#} +{# <meta name="description" content="Selbst programmierte Anwendungen für den Schullaltag am Katharineum zu Lübeck">#} +{# <title>SchoolApps – Katharineum zu Lübeck</title>#} +{##} +{# <!-- Android -->#} +{# <meta name="theme-color" content="#da1f3d">#} +{# <meta name="mobile-web-app-capable" content="yes">#} +{##} +{# <!-- iOS -->#} +{# <meta name="apple-mobile-web-app-title" content="SchoolApps">#} +{# <meta name="apple-mobile-web-app-capable" content="yes">#} +{# <meta name="apple-mobile-web-app-status-bar-style" content="default">#} +{##} +{# <!-- Windows -->#} +{# <meta name="msapplication-navbutton-color" content="#da1f3d">#} +{# <meta name="msapplication-TileColor" content="#da1f3d">#} +{# <meta name="msapplication-TileImage" content="ms-icon-144x144.png">#} +{# <meta name="msapplication-config" content="browserconfig.xml">#} +{##} +{# <!-- Pinned Sites -->#} +{# <meta name="application-name" content="SchoolApps">#} +{# <meta name="msapplication-tooltip" content="SchoolApps">#} +{# <meta name="msapplication-starturl" content="/">#} +{##} +{# <!-- Tap highlighting -->#} +{# <meta name="msapplication-tap-highlight" content="no">#} +{##} +{# <!-- UC Mobile Browser -->#} +{# <meta name="full-screen" content="yes">#} +{# <meta name="browsermode" content="application">#} +{##} +{##} +{# <!-- Main Link Tags -->#} +{# <link href="/static/icons/favicon_16.png" rel="icon" type="image/png" sizes="16x16">#} +{# <link href="/static/icons/favicon_32.png" rel="icon" type="image/png" sizes="32x32">#} +{# <link href="/static/icons/favicon_48.pngg" rel="icon" type="image/png" sizes="48x48">#} +{##} +{# <!-- iOS -->#} +{# <!-- non-retina iPad iOS 7 -->#} +{# <link rel="apple-touch-icon" href="/static/icons/apple_76.png" sizes="76x76">#} +{# <!-- retina iPhone vor iOS 7 -->#} +{# <link rel="apple-touch-icon" href="/static/icons/apple_114.png" sizes="114x114">#} +{# <!-- retina iPad iOS 7 -->#} +{# <link rel="apple-touch-icon" href="/static/icons/apple_152.png" sizes="152x152">#} +{# <!-- retina iPad iOS 7 für iPhone 6 Plus -->#} +{# <link rel="apple-touch-icon" href="/static/icons/apple_180.png" sizes="180x180">#} +{##} +{##} +{# <!-- Pinned Tab -->#} +{##} +{##} +{# <!-- Android -->#} +{# <link href="/static/icons/android_192.png" rel="icon" sizes="192x192">#} +{##} +{# <!-- Others -->#} +{##} +{##} +{##} +{# <!-- Favicon -->#} +{# <link rel="shortcut icon" type="image/x-icon" href="/static/common/favicon.ico">#} +{# <link rel="manifest" href="/static/common/manifest.json">#} +{##} +{# <!--------->#} +{# <!-- CSS -->#} +{# <!--------->#} +{# <link href="/static/css/materialdesignicons-webfont/material-icons.css" rel="stylesheet">#} +{# <link rel="stylesheet" type="text/css" media="screen"#} +{# href="/static/css/materialize.min.css">#} +{# <link rel="stylesheet" type="text/css" href="/static/common/style.css">#} +{# <script src="/static/js/jquery/jquery-3.3.1.slim.min.js"></script>#} +{##} +{##} +{# <!-- location (for "active" in sidenav -->#} +{# <meta name="active-loaction" content="404">#} +{##} +{##} +{#</head>#} +{#<body>#} +{##} +{#<header>#} +{# <!-- Menu button (sidenav) -->#} +{# <div class="container">#} +{# <a href="#" data-target="slide-out" class="top-nav sidenav-trigger hide-on-large-only">#} +{# <i class="material-icons">menu</i>#} +{# </a>#} +{# </div>#} +{##} +{# <!-- Nav bar (logged in as, logout) -->#} +{# <nav class="primary-color">#} +{# <a class="brand-logo" href="/">SchoolApps</a>#} +{# </nav>#} +{##} +{# <div id="print-header" class="row">#} +{# <div class="col s6 logo">#} +{# <img src="/static/common/logo.png">#} +{# </div>#} +{# <div class="col s6 right-align">#} +{# <a href="/"><strong>SchoolApps</strong></a><br>#} +{# Katharineum zu Lübeck#} +{# </div>#} +{# </div>#} +{##} +{# <!-- Main nav (sidenav) -->#} +{# <ul id="slide-out" class="sidenav sidenav-fixed">#} +{# <li class="logo">#} +{# <a id="logo-container" href="/" class="brand-logo">#} +{# <img src="/static/common/logo.png" alt="Logo des Katharineums">#} +{# </a>#} +{# </li>#} +{# </ul>#} +{#</header>#} +{% include "partials/header.html" %} + +<main> + <h3>Leider existiert diese Seite nicht. (Fehler 404)</h3> + + <p class="flow-text"> + Beim Aufrufen dieser Seite ist ein Fehler aufgetreten. Wahrscheinlich existiert die gewünschte Seite unter der Adresse "<code id="url"></code>" nicht. + + Solltest du der Meinung sein, dass diese Seite eigentlich existieren müsste, wende dich bitte an die + <a href="mailto:support@katharineum.de">Computer-AG</a>. + </p> + +</main> + +<script> + document.getElementById("url").innerHTML = window.location.pathname; +</script> + +{% include 'partials/footer.html' %} diff --git a/biscuit/core/templates/common/about.html b/biscuit/core/templates/common/about.html new file mode 100644 index 0000000000000000000000000000000000000000..1cb16028bc60620525ad311638ff5c8f9d78804d --- /dev/null +++ b/biscuit/core/templates/common/about.html @@ -0,0 +1,51 @@ +{% load martortags %} + +{% include "partials/header.html" %} +<main> + + <div class="card"> + <div class="card-content"> + <span class="card-title">Entwickler und Copyright</span> + <p>{{ COPYRIGHT|linebreaksbr|safe_markdown }}</p> + <br> + <p> + GitHub: + <a href="https://github.com/Katharineum/school-apps">https://github.com/Katharineum/school-apps</a> + </p> + <p> + Kontakt: + <a href="mailto:support@katharineum.de">support@katharineum.de</a> + </p> + </div> + </div> + + <div class="card"> + <div class="card-content"> + <span class="card-title">Lizenz</span> +EUPL v1.2 or later + </div> + </div> + <div class="card"> + <div class="card-content"> + <span class="card-title">Verwendete Open-Source-Komponenten</span> + Folgende Open-Source-Komponenten wurden in SchoolApps benutzt: + + <ul class="collection"> + {% for component in components %} + <li class="collection-item"> + <h6>{{ component.0 }}</h6> + <p style="margin-bottom: 10px;"> + <a href="{{ component.1 }}">{{ component.1 }}</a> + · Lizensiert unter der + <a href="{{ component.3 }}">{{ component.2 }}</a> + </p> + </li> + {% endfor %} + </ul> + + Ein Anspruch auf Vollständigkeit wird nicht erhoben. + </div> + </div> +</main> + +{% include "partials/footer.html" %} diff --git a/biscuit/core/templates/common/offline.html b/biscuit/core/templates/common/offline.html new file mode 100644 index 0000000000000000000000000000000000000000..913cb0285293de156c9518da89c5e1f56f3e2040 --- /dev/null +++ b/biscuit/core/templates/common/offline.html @@ -0,0 +1,16 @@ +{% include 'partials/header.html' %} + +<main> + <h3><i class="material-icons left medium" style="font-size: 2.92rem;">signal_wifi_off</i> Es besteht keine + Verbindung zum Internet. </h3> + + <p class="flow-text"> + Beim Aufrufen dieser Seite ist ein Fehler aufgetreten. Vermutlich hast du keine Verbindung zum Internet. + Prüfe, ob dein WLAN oder deine mobilen Daten engeschaltet sind, und probiere es noch einmal. + Wenn du der Meinung bist, dass du über eine Verbindung verfügst, wende dich bitte an die + <a href="mailto:support@katharineum.de">Computer-AG</a> + . + </p> +</main> + +{% include 'partials/footer.html' %} diff --git a/biscuit/core/templates/components/msgbox.html b/biscuit/core/templates/components/msgbox.html new file mode 100644 index 0000000000000000000000000000000000000000..1eb0d0b8ca211e6c1614c4e38d5db6b537cb9a30 --- /dev/null +++ b/biscuit/core/templates/components/msgbox.html @@ -0,0 +1,10 @@ +{% if msg %} + <div class="alert {{ status }}"> + <div> + {% if icon != "" %} + <i class="material-icons left">{{ icon }}</i> + {% endif %} + {{ msg }} + </div> + </div> +{% endif %} diff --git a/biscuit/core/templates/components/react.html b/biscuit/core/templates/components/react.html new file mode 100644 index 0000000000000000000000000000000000000000..452959486f3e4a317b650e1219575c08a4065df2 --- /dev/null +++ b/biscuit/core/templates/components/react.html @@ -0,0 +1,11 @@ +{% 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 %} diff --git a/biscuit/core/templates/mail/email.html b/biscuit/core/templates/mail/email.html new file mode 100644 index 0000000000000000000000000000000000000000..58f6f6b639a423649f425ea12997aa28e6ec7007 --- /dev/null +++ b/biscuit/core/templates/mail/email.html @@ -0,0 +1,13 @@ +{% 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> diff --git a/biscuit/core/templates/mail/email.txt b/biscuit/core/templates/mail/email.txt new file mode 100644 index 0000000000000000000000000000000000000000..c781ae1d5fe821a8c1903eea66f284083d4c1ec4 --- /dev/null +++ b/biscuit/core/templates/mail/email.txt @@ -0,0 +1,9 @@ +Hallo {{ user.name }}, +vielen Dank, dass du SchoolApps benutzt. + +---- +Weitere Informationen findest du hier: +- FORUM (forum.katharineum.de) +---- + +Deine Computer-AG diff --git a/biscuit/core/templates/mail/header.html b/biscuit/core/templates/mail/header.html new file mode 100644 index 0000000000000000000000000000000000000000..ad452e53be3e7ada776b02ce2b64ab605e206b9f --- /dev/null +++ b/biscuit/core/templates/mail/header.html @@ -0,0 +1,8 @@ +<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> diff --git a/biscuit/core/templates/mail/notification.html b/biscuit/core/templates/mail/notification.html new file mode 100644 index 0000000000000000000000000000000000000000..a5619f9aa49bdc693e106a8b5cca888a5d8b29a4 --- /dev/null +++ b/biscuit/core/templates/mail/notification.html @@ -0,0 +1,15 @@ +{% 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> diff --git a/biscuit/core/templates/mail/notification.txt b/biscuit/core/templates/mail/notification.txt new file mode 100644 index 0000000000000000000000000000000000000000..ca927a4fd764d6d1e02d225a9dc43a201adf695d --- /dev/null +++ b/biscuit/core/templates/mail/notification.txt @@ -0,0 +1,13 @@ +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 diff --git a/biscuit/core/templates/martor/editor.html b/biscuit/core/templates/martor/editor.html new file mode 100644 index 0000000000000000000000000000000000000000..dccea1c07d93851a03392b1fef97228098912b9e --- /dev/null +++ b/biscuit/core/templates/martor/editor.html @@ -0,0 +1,71 @@ +{% load i18n %} +<div class="main-martor main-martor-{{ field_name }} card" data-field-name="{{ field_name }}"> + <div class="section-martor card-content"> + <div class="row"> + <ul class="tabs col s12 m3 primary-color-text"> + <li class="tab"> + <a class="item" data-tab="editor-tab-{{ field_name }}" href="#editor-{{ field_name }}"> + <i class="material-icons">edit</i> + {# {% trans "Editor" %}#} + </a> + </li> + <li class="tab"> + <a class="item" data-tab="preview-tab-{{ field_name }}" href="#preview-{{ field_name }}"> + <i class="material-icons">slideshow</i> + {# {% trans "Preview" %}#} + </a> + </li> + + </ul> + <div class="col s12 m9"> + {% include "martor/toolbar.html" %} + </div> + </div> + <div class="col s12 tab segment" data-tab="editor-tab-{{ field_name }}" + id="editor-{{ field_name }}"> + <div class="ui active dimmer upload-progress" style="display:none"> + <div class="ui text loader">{% trans "Uploading... please wait..." %}</div> + </div> + + <div id="martor-{{ field_name }}" class="martor-field martor-field-{{ field_name }}"></div> + {{ martor }} + <i class="angle double down grey icon expand-editor"></i> + </div> + <div class="martor-preview col s12 tab segment" data-tab="preview-tab-{{ field_name }}" + id="preview-{{ field_name }}"> + <p>{% trans "Nothing to preview" %}</p> + </div> + </div><!-- end /.section-martor --> + + {% include 'martor/guide.html' %} + {% include 'martor/emoji.html' %} + <script type="text/javascript"> + $(document).ready(function () { + $('.tabs').tabs(); + $('.dropdown-trigger').dropdown(); + $('.modal').modal(); + }); + </script> + <style type="text/css"> + .main-martor .card-content { + margin: 0; + padding: 0; + } + + .martor-toolbar .btn-flat { + padding: 0 6px; + } + + .maximize::before { + content: "fullscreen"; + } + + .minimize::before { + content: "fullscreen_exit"; + } + + .main-martor .tab a i { + line-height: inherit; + } + </style> +</div> diff --git a/biscuit/core/templates/martor/emoji.html b/biscuit/core/templates/martor/emoji.html new file mode 100644 index 0000000000000000000000000000000000000000..61bc0a1843efa512bc15147b786d21e94758b2a5 --- /dev/null +++ b/biscuit/core/templates/martor/emoji.html @@ -0,0 +1,14 @@ +{% load i18n %} +<div class="ui large modal scrolling transition modal-emoji"> + <i class="close icon"></i> + <div class="header"><i class="help circle icon"></i> {% trans "Select Emoji to Insert" %}</div> + <div class="content emoji-content-base"> + <div class="ui segment emoji-loader-init"> + <div class="ui active inverted dimmer"> + <div class="ui text loader">{% trans "Preparing emojis..." %}</div> + </div> + </div> + <div class="ui grid emoji-content-body"> + </div> + </div> +</div> diff --git a/biscuit/core/templates/martor/guide.html b/biscuit/core/templates/martor/guide.html new file mode 100644 index 0000000000000000000000000000000000000000..60d35b70403144b39a63df6b4d7ee084cc844268 --- /dev/null +++ b/biscuit/core/templates/martor/guide.html @@ -0,0 +1,176 @@ +{% load i18n static %} +<div class="ui medium modal scrolling transition modal-help-guide modal" id="modal-{{ field_name }}"> + {# <i class="close icon"></i>#} + + <div class="modal-content"> + <a href="#" class="modal-close btn-flat right"><i class="material-icons">close</i></a> + + <h4><i class="help circle icon"></i> {% trans "Markdown Guide" %}</h4> + <p>{% blocktrans with doc_url='http://commonmark.org/help/' %}This site is powered by Markdown. For full + documentation, + <a href="{{ doc_url }}" target="_blank">click here</a>{% endblocktrans %}</p> + <table class="ui celled table markdown-reference"> + <thead> + <tr> + <th>{% trans "Code" %}</th> + <th>{% trans "Or" %}</th> + <th>Linux/Windows</th> + <th>Mac OS</th> + <th>{% trans "... to Get" %}</th> + </tr> + </thead> + <tbody> + {# <tr>#} + {# <td>:emoji_name:</td>#} + {# <td>—</td>#} + {# <td>—</td>#} + {# <td>—</td>#} + {# <td><img class="marked-emoji" src="{% static 'plugins/images/heart.png' %}"></td>#} + {# </tr>#} + {# <tr>#} + {# <td>@[username]</td>#} + {# <td>—</td>#} + {# <td>Ctrl+M</td>#} + {# <td>Command+M</td>#} + {# <td><a href="#">@username</a></td>#} + {# </tr>#} + {# <tr>#} + {# <td colspan="5"></td>#} + {# </tr>#} + <tr> + <td>*Italic*</td> + <td>_Italic_</td> + <td>Ctrl+I</td> + <td>Command+I</td> + <td><em>Italic</em></td> + </tr> + <tr> + <td>**Bold**</td> + <td>__Bold__</td> + <td>Ctrl+B</td> + <td>Command+B</td> + <td><strong>Bold</strong></td> + </tr> + <tr> + <td>++Underscores++</td> + <td>—</td> + <td>Shift+U</td> + <td>Option+U</td> + <td> + <ins>Underscores</ins> + </td> + </tr> + <tr> + <td>~~Strikethrough~~</td> + <td>—</td> + <td>Shift+S</td> + <td>Option+S</td> + <td> + <del>Strikethrough</del> + </td> + </tr> + <tr> + <td># Heading 1</td> + <td>Heading 1<br> =========</td> + <td>Ctrl+Alt+1</td> + <td>Command+Option+1</td> + <td><h1>Heading 1</h1></td> + </tr> + <tr> + <td>## Heading 2</td> + <td>Heading 2<br> -----------</td> + <td>Ctrl+Alt+2</td> + <td>Command+Option+2</td> + <td><h2>Heading 2</h2></td> + </tr> + <tr> + <td>[Link](http://a.com)</td> + <td>[Link][1]<br> ⁝<br> [1]: http://b.org</td> + <td>Ctrl+L</td> + <td>Command+L</td> + <td> + <a href="http://commonmark.org/">Link</a> + </td> + </tr> + {# <tr>#} + {# <td></td>#} + {# <td>![Image][1]<br> ⁝<br> [1]: http://url/b.jpg</td>#} + {# <td>Ctrl+Shift+I</td>#} + {# <td>Command+Option+I</td>#} + {# <td><img src="{% static 'plugins/images/commonmark.png' %}" width="36" height="36" alt="Markdown"></td>#} + {# </tr>#} + <tr> + <td>> Blockquote</td> + <td>—</td> + <td>Ctrl+Q</td> + <td>Command+Q</td> + <td> + <blockquote>Blockquote</blockquote> + </td> + </tr> + <tr> + <td>A paragraph.<br><br> A paragraph after 1 blank line.</td> + <td>—</td> + <td>—</td> + <td>—</td> + <td><p>A paragraph.</p> + <p>A paragraph after 1 blank line.</p></td> + </tr> + <tr> + <td><p>* List<br> * List<br> * List</p></td> + <td><p> - List<br> - List<br> - List<br></p></td> + <td>Ctrl+U</td> + <td>Command+U</td> + <td> + <ul> + <li>List</li> + <li>List</li> + <li>List</li> + </ul> + </td> + </tr> + <tr> + <td><p> 1. One<br> 2. Two<br> 3. Three</p></td> + <td><p> 1) One<br> 2) Two<br> 3) Three</p></td> + <td>Ctrl+Shift+O</td> + <td>Command+Option+O</td> + <td> + <ol> + <li>One</li> + <li>Two</li> + <li>Three</li> + </ol> + </td> + </tr> + <tr> + <td>Horizontal Rule<br><br> -----------</td> + <td>Horizontal Rule<br><br> ***********</td> + <td>Ctrl+H</td> + <td>Command+H</td> + <td>Horizontal Rule + <hr> + </td> + </tr> + <tr> + <td>`Inline code` with backticks</td> + <td>—</td> + <td>Ctrl+Alt+C</td> + <td>Command+Option+C</td> + <td><code>Inline code</code> with backticks</td> + </tr> + <tr> + <td>```<br> def whatever(foo):<br> return foo<br>```</td> + <td><b>with tab / 4 spaces</b><br>....def whatever(foo):<br>.... return foo</td> + <td>Ctrl+Alt+P</td> + <td>Command+Option+P</td> + <td> + <pre>def whatever(foo):<br/> return foo</pre> + </td> + </tr> + </tbody> + </table> + </div> + <div class="modal-footer"> + <a href="#" class="modal-close waves-effect waves-green btn-flat">Close</a> + </div> +</div> diff --git a/biscuit/core/templates/martor/toolbar.html b/biscuit/core/templates/martor/toolbar.html new file mode 100644 index 0000000000000000000000000000000000000000..da37e7a4d5c8936f34b28e8975846cc24fa172b1 --- /dev/null +++ b/biscuit/core/templates/martor/toolbar.html @@ -0,0 +1,89 @@ +{% load i18n %} +<div class="ui right floated item martor-toolbar"> + <a class="markdown-selector btn-flat markdown-bold" title="{% trans 'Bold' %} (Ctrl+B)"> + <i class="material-icons">format_bold</i> + </a> + <a class="markdown-selector btn-flat markdown-italic" title="{% trans 'Italic' %} (Ctrl+I)"> + <i class="material-icons">format_italic</i> + </a> + <a class="markdown-selector btn-flat markdown-horizontal" + title="{% trans 'Horizontal Line' %} (Ctrl+H)"> + <i class="material-icons">remove</i> + </a> + + + <a class="dropdown-trigger btn-flat" title="{% trans 'Heading' %}" data-target="dropdown-heading-{{ field_name }}"> + <i class="material-icons">format_size</i> + </a> + <ul class="dropdown-content" id="dropdown-heading-{{ field_name }}"> + <li> + <a class=" markdown-selector markdown-h1" title="{% trans 'H' %} 1 (Ctrl+Alt+1)">{% trans 'H' %} 1</a> + </li> + <li> + <a class=" markdown-selector markdown-h2" title="{% trans 'H' %} 2 (Ctrl+Alt+2)">{% trans 'H' %} 2</a> + </li> + <li> + <a class=" markdown-selector markdown-h3" title="{% trans 'H' %} 3 (Ctrl+Alt+3)">{% trans 'H' %} 3</a> + </li> + </ul> + + + <a class="dropdown-trigger btn-flat" title="{% trans 'Pre or Code' %}" + data-target="dropdown-precode-{{ field_name }}"> + <i class="material-icons">code</i> + </a> + + <ul class="dropdown-content" id="dropdown-precode-{{ field_name }}"> + <li> + <a class="item markdown-selector markdown-pre" title="{% trans 'Pre' %} (Ctrl+Alt+P)">{% trans 'Pre' %}</a> + </li> + <li> + <a class="item markdown-selector markdown-code" + title="{% trans 'Code' %} (Ctrl+Alt+C)">{% trans 'Code' %}</a> + </li> + </ul> + + + <a class="markdown-selector btn-flat markdown-blockquote" + title="{% trans 'Quote' %} (Ctrl+Q)"> + <i class="material-icons">format_quote</i> + </a> + <a class="markdown-selector btn-flat markdown-unordered-list" + title="{% trans 'Unordered List' %} (Ctrl+U)"> + <i class="material-icons">format_list_bulleted</i> + </a> + <a class="markdown-selector btn-flat markdown-ordered-list" + title="{% trans 'Ordered List' %} (Ctrl+Shift+O)"> + <i class="material-icons">format_list_numbered</i> + </a> + + <a class="markdown-selector btn-flat markdown-link" title="{% trans 'URL/Link' %} (Ctrl+L)"> + <i class="material-icons">insert_link</i> + </a> + {# <a class="markdown-selector btn-flat markdown-image-link"#} + {# title="{% trans 'Insert Image Link' %} (Ctrl+Shift+I)">#} + {# <i class="material-icons">insert_photo</i>#} + {# </a>#} + {# <a class="markdown-selector btn-flat markdown-image-upload"#} + {# title="{% trans 'Upload an Image' %}">#} + {# <i class="material-icons">file_upload</i>#} + {# <input name="markdown-image-upload" class="button" type="file" accept="image/*"#} + {# title="{% trans 'Upload an Image' %}">#} + {# </a>#} + {# <a class="markdown-selector btn-flat markdown-emoji" title="{% trans 'Insert Emoji' %}">#} + {# <i class="material-icons">face</i>#} + {# </a>#} + {# <a class="markdown-selector btn-flat markdown-direct-mention"#} + {# title="{% trans 'Direct Mention a User' %} (Ctrl+M)">#} + {# <i class="material-icons">people</i>#} + {# </a>#} + + <a class="markdown-selector btn-flat markdown-toggle-maximize" + title="{% trans 'Full Screen' %}"> + <i class="material-icons maximize icon"></i> + </a> + <a class="markdown-selector btn-flat markdown-help modal-trigger" + title="{% trans 'Markdown Guide (Help)' %}" href="#modal-{{ field_name }}"> + <i class="material-icons">help</i> + </a> +</div> diff --git a/biscuit/core/templates/partials/footer.html b/biscuit/core/templates/partials/footer.html new file mode 100644 index 0000000000000000000000000000000000000000..b682bd0cfd699c9148522b80308bd3e1e2c62d66 --- /dev/null +++ b/biscuit/core/templates/partials/footer.html @@ -0,0 +1,76 @@ +{% 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 }} · {{ 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> diff --git a/biscuit/core/templates/partials/header.html b/biscuit/core/templates/partials/header.html new file mode 100644 index 0000000000000000000000000000000000000000..8fe9a3f0cf2afef973a3009e3daeef4cd1cba4ce --- /dev/null +++ b/biscuit/core/templates/partials/header.html @@ -0,0 +1,441 @@ +{% load static %} +{% load pwa %} +{% load url_name %} + +<!DOCTYPE html> +<html lang="de"> +<head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width,initial-scale=1"> + <meta name="description" content="Selbst programmierte Anwendungen für den Schullaltag am Katharineum zu Lübeck"> + <title>SchoolApps – Katharineum zu Lübeck</title> + + <!-- Android --> + <meta name="theme-color" content="#da1f3d"> + <meta name="mobile-web-app-capable" content="yes"> + + <!-- iOS --> + <meta name="apple-mobile-web-app-title" content="SchoolApps"> + <meta name="apple-mobile-web-app-capable" content="yes"> + <meta name="apple-mobile-web-app-status-bar-style" content="default"> + + <!-- Windows --> + <meta name="msapplication-navbutton-color" content="#da1f3d"> + <meta name="msapplication-TileColor" content="#da1f3d"> + <meta name="msapplication-TileImage" content="ms-icon-144x144.png"> + <meta name="msapplication-config" content="browserconfig.xml"> + + <!-- Pinned Sites --> + <meta name="application-name" content="SchoolApps"> + <meta name="msapplication-tooltip" content="SchoolApps"> + <meta name="msapplication-starturl" content="/"> + + <!-- Tap highlighting --> + <meta name="msapplication-tap-highlight" content="no"> + + <!-- UC Mobile Browser --> + <meta name="full-screen" content="yes"> + <meta name="browsermode" content="application"> + + + <!-- Main Link Tags --> + <link href="{% static "icons/favicon_16.png" %}" rel="icon" type="image/png" sizes="16x16"> + <link href="{% static "icons/favicon_32.png" %}" rel="icon" type="image/png" sizes="32x32"> + <link href="{% static "icons/favicon_48.png" %}" rel="icon" type="image/png" sizes="48x48"> + + <!-- iOS --> + <!-- non-retina iPad iOS 7 --> + <link rel="apple-touch-icon" href="{% static "icons/apple_76.png" %}" sizes="76x76"> + <!-- retina iPhone vor iOS 7 --> + <link rel="apple-touch-icon" href="{% static "icons/apple_114.png" %}" sizes="114x114"> + <!-- retina iPad iOS 7 --> + <link rel="apple-touch-icon" href="{% static "icons/apple_152.png" %}" sizes="152x152"> + <!-- retina iPad iOS 7 für iPhone 6 Plus --> + <link rel="apple-touch-icon" href="{% static "icons/apple_180.png" %}" sizes="180x180"> + + + <!-- Pinned Tab --> + {# <link href="path/to/icon.svg" rel="mask-icon" size="any" color="red">#} + + <!-- Android --> + <link href="{% static "icons/android_192.png" %}" rel="icon" sizes="192x192"> + + <!-- Others --> + {# <link href="favicon.icon" rel="shortcut icon" type="image/x-icon">#} + + + <!-- Favicon --> + <link rel="shortcut icon" type="image/x-icon" href="{% static 'common/favicon.ico' %}"> + + <!-- PWA --> + {% progressive_web_app_meta %} + + <!---------> + <!-- CSS --> + <!---------> + <link href="{% static 'css/materialdesignicons-webfont/material-icons.css' %}" rel="stylesheet"> + <link rel="stylesheet" type="text/css" media="screen" + href="{% static 'css/materialize.min.css' %}"> + <link rel="stylesheet" type="text/css" href="{% static 'common/style.css' %}"> + <script src="{% static 'js/jquery/jquery-3.3.1.min.js' %}"></script> + + + <!-- location (for "active" in sidenav --> + <meta name="active-loaction" content="{{ request|url_name }}"> + + {% if martor %} + <link href="{% static 'plugins/css/ace.min.css' %}" type="text/css" media="all" rel="stylesheet"/> + <link href="{% static 'plugins/css/resizable.min.css' %}" type="text/css" media="all" rel="stylesheet"/> + <link href="{% static 'martor/css/martor.min.css' %}" type="text/css" media="all" rel="stylesheet"/> + {% endif %} +</head> +<body> +<header> + <!-- Menu button (sidenav) --> + <div class="container"> + <a href="#" data-target="slide-out" class="top-nav sidenav-trigger hide-on-large-only"> + <i class="material-icons">menu</i> + </a> + </div> + + <!-- Nav bar (logged in as, logout) --> + <nav class="primary-color"> + <a class="brand-logo" href="/">SchoolApps</a> + + <div class="nav-wrapper"> + <ul id="nav-mobile" class="right hide-on-med-and-down"> + {% if user.is_authenticated %} + <li>Angemeldet als {{ user.get_username }}</li> + <li> + <a href="{% url 'logout' %}">Abmelden <i class="material-icons right">exit_to_app</i></a> + </li> + {% endif %} + </ul> + </div> + </nav> + + <div id="print-header" class="row"> + <div class="col s6 logo"> + <img src="{% static 'common/logo.png' %}"> + </div> + <div class="col s6 right-align"> + <a href="/"><strong>SchoolApps</strong></a> + <br> + Katharineum zu Lübeck + </div> + </div> + + <!-- Main nav (sidenav) --> + <ul id="slide-out" class="sidenav sidenav-fixed"> + <li class="logo"> + <a id="logo-container" href="/" class="brand-logo"> + <img src="{% static 'common/logo.png' %}" alt="Logo des Katharineums"> + </a> + </li> + + {% if user.is_authenticated %} + <li class="url-dashboard"> + <a href="{% url 'dashboard' %}"> + <i class="material-icons">home</i> Dashboard + </a> + </li> + <li> + <div class="divider"></div> + </li> + + <li class="no-padding"> + <ul class="collapsible collapsible-accordion"> + + {% if perms.aub.apply_for_aub or perms.aub.check1_aub or perms.aub.check2_aub %} + <li class="bold url-aub_index url-aub_details url-ab_edit url-aub_apply_for urlaub_applied_for"> + <a class="collapsible-header waves-effect waves-primary" href="{% url 'aub_index' %}"><i + class="material-icons">business_center</i> + Unterrichtsbefreiungen + </a> + <div class="collapsible-body"> + <ul> + {% if perms.aub.check1_aub %} + <li class="url-aub_check1"> + <a href="{% url 'aub_check1' %}"><i class="material-icons">done</i> Anträge + genehmigen 1 + </a> + </li> + {% endif %} + {% if perms.aub.check2_aub %} + <li class="url-aub_check2"> + <a href="{% url 'aub_check2' %}"><i class="material-icons">done_all</i> + Anträge + genehmigen 2 + </a> + </li> + {% endif %} + {% if perms.aub.view_archive %} + <li class="url-aub_archive"> + <a href="{% url 'aub_archive' %}"><i class="material-icons">archive</i> + Archiv + </a> + </li> + {% endif %} + </ul> + </div> + </li> + + <li> + <div class="divider"></div> + </li> + {% endif %} + {% if perms.fibu.request_booking or perms.fibu.manage_booking or perms.fibu.manage_costcenter or perms.fibu.manage.account %} + <li class="bold url-fibu_index url-fibu_bookings_user_edit"> + <a class="collapsible-header waves-effect waves-primary" href="{% url 'fibu_index' %}"><i + class="material-icons">euro_symbol</i> + Finanzen + </a> + <div class="collapsible-body"> + <ul> + {% if perms.fibu.check_booking %} + <li class="url-fibu_bookings_check"> + <a href="{% url 'fibu_bookings_check' %}"><i class="material-icons">done_all</i>Anträge + prüfen + </a> + </li> + {% endif %} + {% if perms.fibu.manage_booking %} + <li class="url-fibu_bookings url-fibu_bookings_edit url-fibu_bookings_new"> + <a href="{% url 'fibu_bookings' %}"><i class="material-icons">receipt</i>Buchungen + </a> + </li> + {% endif %} + {% if perms.fibu.manage_costcenter %} + <li class="url-fibu_cost_centers url-fibu_cost_centers_edit"> + <a href="{% url 'fibu_cost_centers' %}"><i class="material-icons">done</i>Kostenstellen + </a> + </li> + <li class="url-fibu_accounts url-fibu_accounts_edit"> + <a href="{% url 'fibu_accounts' %}"><i class="material-icons">done</i>Buchungskonten + </a> + </li> + {% endif %} + {% if perms.fibu.manage_booking %} + <li class="url-fibu_reports url-fibu_reports_expenses"> + <a href="{% url 'fibu_reports' %}"><i class="material-icons">list</i>Berichte</a> + </li> + {% endif %} + </ul> + </div> + </li> + <li> + <div class="divider"></div> + </li> + {% endif %} + + {% if perms.timetable.show_plan %} + <li class="bold"> + <a class="collapsible-header waves-effect waves-primary"><i class="material-icons">school</i> + Stundenplan + </a> + <div class="collapsible-body"> + <ul> + <li class="url-timetable_my_plan"> + <a href="{% url 'timetable_my_plan' %}" style="padding-right: 10px;"> + <i class="material-icons">person</i> Mein Plan + <span class="badge new primary-color sidenav-badge">SMART PLAN</span> + </a> + </li> + {# <li>#} + {# <a href="{% url 'timetable_admin_all' %}">#} + {# <i class="material-icons">grid_on</i> Alle Pläne#} + {# </a>#} + {# </li>#} + <li class="url-timetable_quicklaunch url-timetable_smart_plan url-timetable_regular_plan url-timetable_smart_plan_week"> + <a href="{% url 'timetable_quicklaunch' %}"> + <i class="material-icons">grid_on</i> Alle Pläne + </a> + </li> + <li class="url-timetable_substitutions_date url-timetable_substitutions"> + <a href="{% url 'timetable_substitutions' %}"> + <i class="material-icons">update</i> Vertretungsplan + </a> + </li> + {% if perms.timetable.view_hint %} + <li class="url-timetable_hints url-timetable_add_hint url-timetable_edit_hint url-timetable_delete_hint"> + <a href="{% url 'timetable_hints' %}"> + <i class="material-icons">announcement</i> Hinweismanagement + </a> + </li> + {% endif %} + {% if perms.debug.can_view_debug_log %} + <li class="url-debug_logs url-debug"> + <a href="{% url 'debug_logs' %}"> + <i class="material-icons">error</i> Debuggingtool + </a> + </li> + {% endif %} + </ul> + </div> + </li> + + <li> + <div class="divider"></div> + </li> + {% endif %} + + <li> + <a href="{% url 'menu_show_current' %}" target="_blank"> + <i class="material-icons">picture_as_pdf</i> Aktueller Speiseplan + </a> + </li> + + {% if perms.menu.add_menu %} + <li class="url-menu_index url-menu_upload url-menu_index_msg"> + <a href="{% url 'menu_index' %}"> + <i class="material-icons">restaurant_menu</i> Speiseplan hochladen + </a> + </li> + {% endif %} + + <li> + <div class="divider"></div> + </li> + + <li class="bold"> + <a class="collapsible-header waves-effect waves-primary"><i class="material-icons">help</i> Support</a> + <div class="collapsible-body"> + <ul> + <li class="url-rebus"> + <a href="{% url 'rebus' %}"> + <i class="material-icons">bug_report</i> Fehler melden + </a> + </li> + <li class="url-feedback"> + <a href="{% url 'feedback' %}"> + <i class="material-icons">feedback</i> Feedback + </a> + </li> + <li class="url-faq"> + <a href="{% url 'faq' %}"> + <i class="material-icons">question_answer</i>FAQ + </a> + </li> + </ul> + </div> + </li> + {% endif %} + + {% if not user.is_authenticated %} + <li class="url-faq"> + <a href="{% url 'faq' %}"> + <i class="material-icons">question_answer</i>FAQ + </a> + </li> + {% endif %} + + {% if user.is_authenticated %} + {% if user.is_superuser %} + <li> + <div class="divider"></div> + </li> + <li class="bold"> + <a class="collapsible-header waves-effect waves-primary"><i class="material-icons">security</i>Administration + </a> + <div class="collapsible-body"> + <ul> + <li id="tools"> + <a href="{% url "tools" %}"> + <i class="material-icons">build</i> Tools + </a> + </li> + <li> + <a href="/admin/"> + <i class="material-icons">dashboard</i> Django-Administration + </a> + </li> + <li> + <a href="/settings/"> + <i class="material-icons">settings</i> Einstellungen + </a> + </li> + </ul> + </div> + </li> + {% endif %} + + <li> + <div class="divider"></div> + </li> + + <li> + <a href="{% url 'logout' %}"> + <i class="material-icons">exit_to_app</i> Abmelden + </a> + </li> + + </ul> + </li> + {% else %} + + <li class="url-login"> + <a href="{% url 'login' %}"> + <i class="material-icons">lock_open</i> Anmelden + </a> + </li> + {% endif %} + </ul> +</header> +{#<header class="alert success">#} +{# <p>#} +{# <i class="material-icons left">info</i>#} +{# Du befindest dich in der ersten veröffentlichten Version von SchoolApps. Daher kann es immer mal wieder noch zu#} +{# bislang unvorhergesehenen#} +{# Problemen kommen. Es würde uns sehr helfen, wenn du uns dann über#} +{# <a href="mailto:support@katharineum.de">support@katharineum.de</a>#} +{# schreibst oder die in SchoolApps integrierten#} +{# <a href="{% url 'rebus' %}">Fehlermelde-</a>#} +{# und#} +{# <a href="{% url 'feedback' %}">Feedbackfunktionen</a>#} +{# nutzt.#} +{# </p>#} +{#</header>#} + +{% if messages %} + <header> + {% for message in messages %} + <div class="alert {% if message.tags %}{{ message.tags }}{% else %}info{% endif %}"> + <p> + {% if message.tags == "success" %} + <i class="material-icons left">check_circle</i> + {% elif message.tags == "info" %} + <i class="material-icons left">info</i> + {% elif message.tags == "warning" %} + <i class="material-icons left">warning</i> + {% elif message.tags == "error" %} + <i class="material-icons left">error</i> + {% endif %} + {{ message }} + </p> + </div> + {% endfor %} + </header> +{% endif %} + +{% if user.is_authenticated %} + <div class="fixed-action-btn"> + <a class="btn-floating btn-large green"> + <i class="large material-icons">help</i> + </a> + <ul> + <li> + <a class="btn-floating tooltipped red" data-position="left" data-tooltip="Fehler melden" + href="{% url 'rebus' %}"><i class="material-icons">bug_report</i></a> + </li> + <li> + <a class="btn-floating tooltipped yellow darken-1" data-position="left" data-tooltip="FAQ" + href="{% url 'faq' %}"><i class="material-icons">question_answer</i></a> + </li> + <li> + <a class="btn-floating tooltipped blue" data-position="left" data-tooltip="Frage stellen" + href="{% url 'ask-faq' %}"><i class="material-icons">chat</i></a> + </li> + </ul> + </div> +{% endif %} diff --git a/biscuit/core/templates/partials/paper/footer.html b/biscuit/core/templates/partials/paper/footer.html new file mode 100644 index 0000000000000000000000000000000000000000..1dc3a88b8281509e4eb8d18212b4513d32189845 --- /dev/null +++ b/biscuit/core/templates/partials/paper/footer.html @@ -0,0 +1,32 @@ +{% load static %} +<footer> + <div class="left"> + Katharineum zu Lübeck + </div> + + <div class="right"> + Umsetzung: {{ COPYRIGHT_SHORT }} + </div> +</footer> +</div> +</td> +</tr> +</tbody> +<tfoot> +<tr class="no-border"> + <td> + <div class="footer-space"> </div> + </td> +</tr> +</tfoot> +</table> +</main> + +<!----------------> +<!-- 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> diff --git a/biscuit/core/templates/partials/paper/header.html b/biscuit/core/templates/partials/paper/header.html new file mode 100644 index 0000000000000000000000000000000000000000..845672ab118cc0112332f292b7899661a6b64ebd --- /dev/null +++ b/biscuit/core/templates/partials/paper/header.html @@ -0,0 +1,144 @@ +{% load static %} +{% load pwa %} +{% load url_name %} + +<!DOCTYPE html> +<html lang="de"> +<head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width,initial-scale=1"> + <meta name="description" content="Selbst programmierte Anwendungen für den Schullaltag am Katharineum zu Lübeck"> + <title>SchoolApps – Katharineum zu Lübeck</title> + + <!---------> + <!-- CSS --> + <!---------> + <link href="{% static 'css/materialdesignicons-webfont/material-icons.css' %}" rel="stylesheet"> + <link rel="stylesheet" type="text/css" href="{% static 'css/materialize.min.css' %}"> + <link rel="stylesheet" type="text/css" href="{% static 'css/paper.css' %}"> + <link rel="stylesheet" type="text/css" href="{% static 'common/style.css' %}"> + <script src="{% static 'js/jquery/jquery-3.3.1.min.js' %}"></script> + + + <style> + body { + font-family: Cabin, sans-serif; + } + + @page { + size: A4; + padding: 30mm; + } + + header { + display: block; + width: 190mm; + } + + + #print-header { + display: block !important; + border-bottom: 1px solid; + margin-bottom: 0; + height: 22mm; + background: white; + } + + header, main, footer { + margin: 0; + } + + #print-header .col.right-align { + padding: 15px; + } + + .sheet { + padding: 10mm; + } + + + .header-space, .footer-space { + height: 0; + } + + .print-layout-table td { + padding: 0; + } + + .print-layout-table .no-border { + border: 0; + } + + + footer { + margin-top: 5mm; + text-align: center; + width: 190mm; + + } + + header .row, header .col { + padding: 0 !important; + margin: 0 !important; + } + + @media print { + .header-space { + height: 32mm; + } + + .footer-space { + height: 20mm + } + + header, footer { + height: 22mm; + } + + header { + position: fixed; + top: 10mm; + } + + footer { + position: fixed; + bottom: 0; + } + + @page { + @bottom-center { + content: "Seite " counter(page) " von " counter(pages); + } + } + } + </style> +</head> +<body class="A4"> + + +<div style="margin-top: -10mm;"></div> +<main class="sheet infinitive"> + <table class="print-layout-table"> + <thead> + <tr class="no-border"> + <td> + <div class="header-space"> </div> + </td> + </tr> + </thead> + <tbody> + <tr class="no-border"> + <td> + <div class="content"> + <header> + <div id="print-header" class="row"> + <div class="col s6 logo"> + <img src="{% static 'common/logo.png' %}"> + </div> + <div class="col s6 right-align"> + <h5>SchoolApps</h5> + {% now "j. F Y H:i" %} + </div> + </div> + </header> diff --git a/biscuit/core/templates/registration/logged_out.html b/biscuit/core/templates/registration/logged_out.html new file mode 100644 index 0000000000000000000000000000000000000000..48a7d1535dfb6071d29e3f2fee1fd2247650b763 --- /dev/null +++ b/biscuit/core/templates/registration/logged_out.html @@ -0,0 +1,8 @@ +{% include 'partials/header.html' %} + +<main> + <p class="flow-text">Du bist nun abgemeldet.</p> + <a href="{% url 'login' %}">Wieder anmelden?</a> +</main> + +{% include 'partials/footer.html' %} diff --git a/biscuit/core/templatetags/apps.py b/biscuit/core/templatetags/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..6f48c994917130290f2c4400fca753c4df11b06f --- /dev/null +++ b/biscuit/core/templatetags/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class TemplatetagsConfig(AppConfig): + name = 'templatetags' diff --git a/biscuit/core/templatetags/templatetags/__init__.py b/biscuit/core/templatetags/templatetags/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/biscuit/core/templatetags/templatetags/copy_filter.py b/biscuit/core/templatetags/templatetags/copy_filter.py new file mode 100644 index 0000000000000000000000000000000000000000..9ee6181b430174b1f8dec5eff3276531f7e7e44b --- /dev/null +++ b/biscuit/core/templatetags/templatetags/copy_filter.py @@ -0,0 +1,8 @@ +import copy as copylib + +from django import template + +register = template.Library() + +register.filter("copy", copylib.copy) +register.filter("deepcopy", copylib.deepcopy) diff --git a/biscuit/core/templatetags/templatetags/msg_box.py b/biscuit/core/templatetags/templatetags/msg_box.py new file mode 100644 index 0000000000000000000000000000000000000000..216883cc1f64bc9539cd2bea1941c0c066552fe3 --- /dev/null +++ b/biscuit/core/templatetags/templatetags/msg_box.py @@ -0,0 +1,6 @@ +from django.template.loader_tags import register + + +@register.inclusion_tag("components/msgbox.html") +def msg_box(msg, status="success", icon="info"): + return {"msg": msg, "status": status, "icon": icon} diff --git a/biscuit/core/templatetags/templatetags/tex.py b/biscuit/core/templatetags/templatetags/tex.py new file mode 100644 index 0000000000000000000000000000000000000000..333c3d31d819eb4c3b41ef6513d3b2b9df3ff30e --- /dev/null +++ b/biscuit/core/templatetags/templatetags/tex.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +""" +Django filters, needed when creating LaTeX files with the django template language + +Written by Rocco Schulz (http://is-gr8.com/), modified by SchoolApps-Team +""" +from django.template.defaultfilters import stringfilter, register +from django.template.loader import render_to_string + + +@register.filter +@stringfilter +def brackets(value): + """ + surrounds the value with { } + You have to use this filter whenever you would need something like + {{{ field }}} in a template. + """ + return "{%s}" % value + + +REPLACEMENTS = { + "§": "\\textsection{}", + "$": "\\textdollar{}", + "LaTeX": "\\LaTeX \\ ", + " TeX": " \\TeX \\ ", + "€": "\\euro", + ">": "$>$", + "<": "$<$" +} + +ESCAPES = ("&", "{", "}", "%") + + +@register.filter +@stringfilter +def texify(value): + """ + escapes/replaces special character with appropriate latex commands + """ + tex_value = [] + + # escape special symbols + for char in value: + tex_value.append("%s" % ("\\%s" % char if char in ESCAPES else char)) + tex_value = "".join(tex_value) + + # replace symbols / words with latex commands + for key, value in REPLACEMENTS.items(): + tex_value = tex_value.replace(key, value) + + return "%s" % tex_value diff --git a/biscuit/core/templatetags/templatetags/url_name.py b/biscuit/core/templatetags/templatetags/url_name.py new file mode 100644 index 0000000000000000000000000000000000000000..20f63c673b00f0d736652db62937f9dc68947db2 --- /dev/null +++ b/biscuit/core/templatetags/templatetags/url_name.py @@ -0,0 +1,15 @@ +from django.urls import resolve +from django import template + +register = template.Library() + + +def get_url_name(request): # Only one argument. + """Gets url_name""" + try: + return resolve(request.path_info).url_name + except Exception as e: + return e + + +register.filter("url_name", get_url_name) diff --git a/biscuit/core/userinformation.py b/biscuit/core/userinformation.py new file mode 100644 index 0000000000000000000000000000000000000000..69bd84938444dcfe31aa44e4259d7c6d7d84672f --- /dev/null +++ b/biscuit/core/userinformation.py @@ -0,0 +1,64 @@ +import re + + +class UserInformation: + OTHER = 0 + TEACHER = 1 + STUDENT = 2 + + @staticmethod + def regexr(regex, groups): + reg = re.compile(regex) + return reg.findall("\n".join(groups)) + + @staticmethod + def user_groups(user): + raw_groups = user.groups.all() + groups = [group.name for group in raw_groups] + # print(groups) + return groups + + @staticmethod + def user_type(user): + groups = UserInformation.user_groups(user) + if "teachers" in groups: + return UserInformation.TEACHER + elif "students" in groups: + return UserInformation.STUDENT + else: + return UserInformation.OTHER + + @staticmethod + def _user_type_formatted(user_type): + return "Lehrer" if user_type == UserInformation.TEACHER else ( + "Schüler" if user_type == UserInformation.STUDENT else "Sonstiges Mitglied") + + @staticmethod + def user_type_formatted(user): + user_type = UserInformation.user_type(user) + return UserInformation._user_type_formatted(user_type) + + @staticmethod + def user_classes(user): + groups = UserInformation.user_groups(user) + classes = UserInformation.regexr(r"class_(\w{1,3})", groups) + return classes + + @staticmethod + def user_courses(user): + groups = UserInformation.user_groups(user) + classes = UserInformation.regexr(r"course_(.{1,10})", groups) + return classes + + @staticmethod + def user_subjects(user): + groups = UserInformation.user_groups(user) + classes = UserInformation.regexr(r"subject_(\w{1,3})", groups) + return classes + + @staticmethod + def user_has_wifi(user): + groups = UserInformation.user_groups(user) + if "teachers" in groups or "students-wifi" in groups: + return True + return False diff --git a/biscuit/core/util/helper.py b/biscuit/core/util/helper.py new file mode 100644 index 0000000000000000000000000000000000000000..04b910255cffad5db90f9c4077b9016a21ae5b82 --- /dev/null +++ b/biscuit/core/util/helper.py @@ -0,0 +1,22 @@ +import os +from uuid import uuid4 + +from django.template.loader_tags import register + + +def path_and_rename(instance, filename): + upload_to = 'menus' + ext = filename.split('.')[-1].lower() + # get filename + if instance.pk: + filename = '{}.{}'.format(instance.pk, ext) + else: + # set filename as random string + filename = '{}.{}'.format(uuid4().hex, ext) + # return the whole path to the file + return os.path.join(upload_to, filename) + + +@register.inclusion_tag("components/msgbox.html") +def msg_box(msg, status="success", icon="info"): + return {"msg": msg, "status": status, "icon": icon} diff --git a/biscuit/core/util/network.py b/biscuit/core/util/network.py new file mode 100644 index 0000000000000000000000000000000000000000..6c81df3330006f4ba8f591615d1c3aa06d939bcd --- /dev/null +++ b/biscuit/core/util/network.py @@ -0,0 +1,167 @@ +import re + +import requests +from django.utils import timezone, formats +from ics import Calendar +from requests import RequestException + +from dashboard import settings +from dashboard.caches import LATEST_ARTICLE_CACHE, CURRENT_EVENTS_CACHE + +WP_DOMAIN: str = "https://katharineum-zu-luebeck.de" + +VS_COMPOSER_REGEX = re.compile(r"\[[\w\s\d!\"§$%&/()=?#'+*~’¸´`;,·.:…\-_–]*\]") + + +def get_newest_articles(domain: str = WP_DOMAIN, + limit: int = 5, + author_whitelist: list = None, + author_blacklist: list = None, + category_whitelist: list = None, + category_blacklist: list = None + ): + """ + This function returns the newest articles/posts of a WordPress site. + + :param domain: The domain to get the newest posts from (for example https://wordpress.com). Don't put a slash (/) at the end! + :param limit: if 0: all posts will be shown, else nly the certain number + :param author_whitelist: If this list is filled, only articles which are written by one of this authors will be shown + :param author_blacklist: If the author's id (an integer) is in this list, the article won't be shown + :param category_whitelist: If this list is filled, only articles which are in one of this categories will be shown + :param category_blacklist: If the category's id (an integer) is in this list, the article won't be shown + :return: a list of the newest posts/articles + """ + # Make mutable default arguments unmutable + if category_whitelist is None: + category_whitelist = [] + if category_blacklist is None: + category_blacklist = [] + if author_whitelist is None: + author_whitelist = [] + if author_blacklist is None: + author_blacklist = [] + + suffix: str = "/wp-json/wp/v2/posts" + url: str = domain + suffix + try: + site: requests.request = requests.get(url, timeout=10) + data: dict = site.json() + except RequestException as e: + print("E", str(e)) + return [] + posts: list = [] + + for post in data: + if post["author"] not in author_blacklist: + if len(author_whitelist) > 0 and post["author"] not in author_whitelist: + continue + + if post["categories"][0] not in category_blacklist: + if len(category_whitelist) > 0 and post["categories"][0] not in category_whitelist: + continue + + # Now get the link to the image + if post["_links"].get("wp:featuredmedia", False): + media_site: requests.request = requests.get(post["_links"]["wp:featuredmedia"][0]["href"]).json() + image_url: str = media_site["guid"]["rendered"] + else: + image_url: str = "" + + # Replace VS composer tags if activated + if settings.latest_article_settings.replace_vs_composer_stuff: + excerpt = VS_COMPOSER_REGEX.sub("", post["excerpt"]["rendered"]) + else: + excerpt = post["excerpt"]["rendered"] + + posts.append( + { + "title": post["title"]["rendered"], + "short_text": excerpt, + "link": post["link"], + "image_url": image_url, + } + ) + if len(posts) >= limit and limit >= 0: + break + + return posts + + +@LATEST_ARTICLE_CACHE.decorator +def get_newest_article_from_news(domain=WP_DOMAIN): + newest_articles: list = get_newest_articles(domain=domain, limit=1, category_whitelist=[1, 27]) + if len(newest_articles) > 0: + return newest_articles[0] + else: + return None + + +def get_current_events(calendar: Calendar, limit: int = 5) -> list: + """ + Get upcoming events from calendar + :param calendar: The calendar object + :param limit: Count of events + :return: List of upcoming events + """ + i: int = 0 + events: list = [] + for event in calendar.timeline.start_after(timezone.now()): + # Check for limit + if i >= limit: + break + i += 1 + + # Create formatted dates and times for begin and end + begin_date_formatted = formats.date_format(event.begin) + end_date_formatted = formats.date_format(event.end) + begin_time_formatted = formats.time_format(event.begin.time()) + end_time_formatted = formats.time_format(event.end.time()) + + if event.begin.date() == event.end.date(): + # Event is only on one day + formatted = begin_date_formatted + + if not event.all_day: + # No all day event + formatted += " " + begin_time_formatted + + if event.begin.time != event.end.time(): + # Event has an end time + formatted += " – " + end_time_formatted + + else: + # Event is on multiple days + if event.all_day: + # Event is all day + formatted = "{} – {}".format(begin_date_formatted, end_date_formatted) + else: + # Event has begin and end times + formatted = "{} {} – {} {}".format(begin_date_formatted, begin_time_formatted, end_date_formatted, + end_time_formatted) + + events.append({ + "name": event.name, + "begin_timestamp": event.begin.timestamp, + "end_timestamp": event.end.timestamp, + "formatted": formatted + }) + + return events + + +@CURRENT_EVENTS_CACHE.decorator +def get_current_events_with_cal(limit: int = 5) -> list: + # Get URL + calendar_url: str = settings.current_events_settings.calendar_url + if calendar_url is None or calendar_url == "": + return [] + + # Get ICS + try: + calendar: Calendar = Calendar(requests.get(calendar_url, timeout=3).text) + except RequestException as e: + print("E", str(e)) + return [] + + # Get events + return get_current_events(calendar, settings.current_events_settings.events_count) diff --git a/react/babel.sh b/react/babel.sh new file mode 100755 index 0000000000000000000000000000000000000000..0a8185fa487cda7d479d31ccec1e698b9b471ae1 --- /dev/null +++ b/react/babel.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +npx babel --watch src/ --out-dir ../schoolapps/static/js/ --presets react-app/prod diff --git a/react/package.json b/react/package.json new file mode 100644 index 0000000000000000000000000000000000000000..f2e83345d6861242fe1349e2abdb0b7358653a4d --- /dev/null +++ b/react/package.json @@ -0,0 +1,16 @@ +{ + "name": "react", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "babel-cli": "^6.26.0", + "babel-preset-react-app": "^3.1.2" + } +} diff --git a/react/src/dashboard.js b/react/src/dashboard.js new file mode 100644 index 0000000000000000000000000000000000000000..9611e47d5ed6334353e5d9129b83d3c15998f574 --- /dev/null +++ b/react/src/dashboard.js @@ -0,0 +1,406 @@ +/*** source for biscuit/core/static/js/dashboard.js ***/ + +const REFRESH_TIME = 15; + +// function WithCheckCircleIcon(props) { +// return <div className={"col s12"}> +// <i className={"material-icons left green-text"}>check_circle</i> +// {props.children} +// </div> +// } + +class Dashboard extends React.Component { + constructor() { + super(); + this.state = { + refreshIn: REFRESH_TIME, + isLoading: true, + networkProblems: false + }; + } + + updateRefreshTime = () => { + if (this.state.refreshIn >= 1) { + if (this.state.timeout) { + window.clearTimeout(this.state.timeout); + } + const timeout = window.setTimeout(this.updateRefreshTime, 1000); + this.setState({refreshIn: this.state.refreshIn - 1, timeout: timeout}); + } else { + this.updateData(); + } + }; + + updateData = () => { + if (this.state.networkProblems) { + this.setState({isLoading: true, networkProblems: false}); + } + + const that = this; + $.getJSON(API_URL, (data) => { + console.log(data); + if (data) { + that.setState({...data, refreshIn: REFRESH_TIME + 1, isLoading: false}); + that.updateRefreshTime(); + } + }).fail(() => { + console.log("error"); + that.setState({refreshIn: REFRESH_TIME + 1, networkProblems: true}); + }); + $.getJSON(API_URL + "/my-plan", (data) => { + console.log(data); + if (data && data.lessons) { + that.setState({lessons: data.lessons, holiday: data.holiday}); + } + }).fail(() => { + console.log("error"); + that.setState({networkProblems: true}); + }); + }; + + componentDidMount() { + console.log(API_URL); + this.updateData(); + } + + closeNotification(notification) { + console.log(notification); + $("#not-" + notification.id).addClass("scale-out"); + window.setTimeout(() => { + $("#not-" + notification.id).hide(); + }, 200); + $.getJSON(API_URL + "/notifications/read/" + notification.id); + this.updateData(); + this.setState({time: new Date()}); + } + + render() { + if (this.state.networkProblems) { + // Show loading screen until first data are loaded + return <div className={"row center-via-flex container"} style={{"height": "20em"}}> + <i className={"material-icons large"}>signal_wifi_off</i> + <p className={"flow-text text-center"}>Es ist ein Fehler bei der Netzwerkverbindung aufgetreten.</p> + <button className={"btn-flat grey-text"} onClick={this.updateData}> + Erneuter Versuch in {this.state.refreshIn} s + </button> + </div>; + } + + if (this.state.isLoading) { + // Show loading screen until first data are loaded + return <div className={"row center-via-flex container"} style={{"height": "15em"}}> + <div className={"center2-via-flex"}> + <div className="preloader-wrapper big active"> + <div className="spinner-layer spinner-primary"> + <div className="circle-clipper left"> + <div className="circle"/> + </div> + <div className="gap-patch"> + <div className="circle"/> + </div> + <div className="circle-clipper right"> + <div className="circle"/> + </div> + </div> + </div> + <p className={"text-center flow-text"}>Deine aktuellen Informationen werden geladen …</p> + </div> + </div>; + } + + + const that = this; + return <div> + {/* REFRESH BUTTON*/} + <button className={"btn-flat right grey-text"} onClick={this.updateData}> + <i className={"material-icons left"}>refresh</i> + in {this.state.refreshIn} s + </button> + + {/* GREETINGS */} + <p className="flow-text">Moin + Moin, {this.state.user.full_name !== "" ? this.state.user.full_name : this.state.user.username}. Hier + findest du alle aktuellen Informationen:</p> + + <div className={"alert success"}> + <p> + <i className={"material-icons left"}>report_problem</i> + Das neue Dashboard von SchoolApps befindet sich momentan in der <strong>Testphase</strong>. Falls + Fehler auftreten oder du einen Verbesserungsvorschlag für uns hast, schreibe uns bitte unter <a + href={"mailto:support@katharineum.de"}>support@katharineum.de</a>. + </p> + </div> + + {/* UNREAD NOTIFICATIONS*/} + {this.state.unread_notifications && this.state.unread_notifications.length > 0 ? + this.state.unread_notifications.map(function (notification) { + return <div className={"alert primary scale-transition"} id={"not-" + notification.id} + key={notification.id}> + <div> + {/* Info icon */} + <i className={"material-icons left"}>info</i> + + <div className={"right"}> + {/* Button for marking as read*/} + <button className={"btn-flat"} onClick={() => that.closeNotification(notification)}> + <i className={"material-icons center"}>close</i> + </button> + </div> + + {/* Notification title and desc */} + <strong>{notification.title}</strong> + <p>{notification.description}</p> + </div> + </div>; + }) : ""} + + {/* HINTS */} + {this.state.plan && this.state.plan.hints.length > 0 ? + <div> + {this.state.plan.hints.map(function (hint, idx) { + return <div className="alert primary" key={idx}> + <div> + <em className="right hide-on-small-and-down"> + Hinweis für {that.state.date_formatted} + </em> + + <i className="material-icons left">announcement</i> + <p dangerouslySetInnerHTML={{__html: hint.html}}/> + + <em className="hide-on-med-and-up"> + Hinweis für {that.state.date_formatted} + </em> + </div> + </div>; + })} + </div> : ""} + + {/* CARDS */} + <div className={"row"}> + {/*<div className={"dashboard-cards"}>*/} + + {/* MY PLAN */} + {this.state.has_plan ? <div className="col s12 m12 l6 xl4"> + <div className={"card"}> + <div className="card-content"> + {/* Show individualized title */} + <span className="card-title"> + Plan {this.state.plan.type === 2 ? "der" : "für"} <em> + {this.state.plan.name}</em> für {this.state.date_formatted} + </span> + + {/* Show plan */} + {this.state.holiday ? <div className={"card"}> + <div className={"card-content"}> + {/*<i className={"material-icons medium left"}>no_meeting_room</i>*/} + <span + className="badge new blue center-align holiday-badge">{this.state.holiday.name}</span> + <br/> + </div> + </div> : + (this.state.lessons && this.state.lessons.length > 0 ? + <div className={"timetable-plan"}> + {this.state.lessons.map(function (lesson) { + // Show one lesson row + return <div className="row"> + {/* Show time information*/} + <div className="col s4"> + <div className="card timetable-title-card"> + <div className="card-content"> + {/* Lesson number*/} + <span className="card-title left"> + {lesson.time.number_format} + </span> + + {/* Times */} + <div + className="right timetable-time grey-text text-darken-2"> + <span>{lesson.time.start_format}</span> + <br/> + <span>{lesson.time.end_format}</span> + </div> + </div> + </div> + </div> + + {/* Show lesson content (via generated HTML by Django) */} + <div className={"col s8"} + dangerouslySetInnerHTML={{__html: lesson.html}}/> + </div>; + })} + </div> : "")} + </div> + <div className="card-action"> + <a href={MY_PLAN_URL}> + <span className="badge new primary-color card-action-badge">SMART PLAN</span> + anzeigen + </a> + </div> + </div> + </div> : ""} + + {/* MY STATUS */} + {/*<div className="card">*/} + {/* <div className="card-content">*/} + {/* <span className="card-title">Mein Status</span>*/} + {/* <div className={"row"}>*/} + {/* <WithCheckCircleIcon>*/} + {/* {this.state.user_type_formatted}*/} + {/* </WithCheckCircleIcon>*/} + + {/* {this.state.user_type === 1 || this.state.user_type === 2 ? <WithCheckCircleIcon>*/} + {/* Meine Klassen: {this.state.classes.join(", ")}*/} + {/* </WithCheckCircleIcon> : ""}*/} + + {/* {this.state.user_type === 1 || this.state.user_type === 2 ? <WithCheckCircleIcon>*/} + {/* Meine Kurse: {this.state.courses.join(", ")}*/} + {/* </WithCheckCircleIcon> : ""}*/} + + {/* {this.state.user_type === 1 ? <WithCheckCircleIcon>*/} + {/* Meine Fächer: {this.state.subjects.join(", ")}*/} + {/* </WithCheckCircleIcon> : ""}*/} + {/* {this.state.user_type === 1 || this.state.has_wifi ?*/} + {/* <WithCheckCircleIcon>WLAN</WithCheckCircleIcon> : <div className={"col"}>*/} + {/* <i className={"material-icons left red-text"}>cancel</i>*/} + {/* Kein WLAN*/} + {/* </div>}*/} + {/* </div>*/} + {/* </div>*/} + {/*</div>*/} + + {/* CURRENT EVENTS*/} + {this.state.current_events && this.state.current_events.length > 0 ? + <div className={"col s12 m12 l6 xl4"}> + <div className="card "> + <div className="card-content"> + <span className="card-title">Aktuelle Termine</span> + {this.state.current_events.map(function (event) { + return <div className="card-panel event-card"> + <span className={"title"}>{event.name}</span> + <br/> + {event.formatted} + </div>; + })} + </div> + <div className="card-action"> + <a href="https://katharineum-zu-luebeck.de/aktuelles/termine/" target={"_blank"}> + Weitere Termine + </a> + </div> + </div> + </div> + : ""} + + {/* EXAMS */} + {/*<div className="card">*/} + {/* <div className="card-content">*/} + {/* <span className="card-title">Klausuren der <em>Eb</em></span>*/} + {/* <div className="card-panel event-card">*/} + {/* <span className={"title"}>Sextanereinschulung</span>*/} + {/* <br/>*/} + {/* 28.Aug. 2019 18:30 - 22:00*/} + {/* </div>*/} + {/* <div className="card-panel event-card">*/} + {/* <span className={"title"}>Sextanereinschulung</span>*/} + {/* <br/>*/} + {/* 28.Aug. 2019 18:30 - 22:00*/} + {/* </div>*/} + {/* </div>*/} + {/* <div className="card-action">*/} + {/* <a href="https://katharineum-zu-luebeck.de/aktuelles/termine/">Alle Klausuren</a>*/} + {/* </div>*/} + {/*</div>*/} + + {/* NEWEST ARTICLE FROM HOMEPAGE*/} + {this.state.newest_article ? + <div className={"col s12 m12 l6 xl4"}> + <div className="card"> + {/* Image with badge and title */} + <div className="card-image"> + <span className={"badge-image z-depth-2"}>Aktuelles von der Homepage</span> + <img src={this.state.newest_article.image_url} + alt={this.state.newest_article.title}/> + + </div> + + + {/* Short text */} + <div className="card-content"> + <span className="card-title" + dangerouslySetInnerHTML={{__html: this.state.newest_article.title}}/> + + <p dangerouslySetInnerHTML={{__html: this.state.newest_article.short_text}}/> + </div> + + {/* Link to article */} + <div className="card-action"> + <a href={this.state.newest_article.link} target={"_blank"}>Mehr lesen</a> + </div> + </div> + + {/* Link to homepage */} + <a className={"btn hundred-percent primary-color"} + href={"https://katharineum-zu-luebeck.de/"} + target={"_blank"}> + Weitere Artikel + <i className={"material-icons right"}>arrow_forward</i> + </a> + </div> + : ""} + </div> + {/*</div>*/} + + {/* ACITIVITIES */} + <div className={"row"}> + <div className="col s12 m6"> + <h5>Letzte Aktivitäten</h5> + {this.state.activities && this.state.activities.length > 0 ? <ul className={"collection"}> + {this.state.activities.map((activity) => { + return <li className={"collection-item"} key={activity.id}> + <span className="badge new primary-color">{activity.app}</span> + <span className="title">{activity.title}</span> + <p> + <i className="material-icons left">access_time</i> {activity.created_at} + </p> + <p> + {activity.description} + </p> + </li>; + })} + </ul> : <p> + Noch keine Aktivitäten vorhanden. + </p>} + </div> + + {/* NOTIFICATIONS */} + <div className="col s12 m6"> + <h5>Letzte Benachrichtigungen</h5> + {this.state.notifications && this.state.notifications.length > 0 ? <ul className={"collection"}> + {this.state.notifications.map((notification) => { + return <li className={"collection-item"} key={notification.id}> + <span className="badge new primary-color">{notification.app}</span> + <span className="title">{notification.title}</span> + <p> + <i className="material-icons left">access_time</i> {notification.created_at} + </p> + <p> + {notification.description} + </p> + {notification.link ? <p> + <a href={notification.link}>Mehr Informationen →</a> + </p> : ""} + </li>; + })} + </ul> : <p> + Noch keine Benachrichtigungen vorhanden. + </p>} + </div> + </div> + </div>; + } +} + +$(document).ready(function () { + const domContainer = document.querySelector('#dashboard_container'); + ReactDOM.render(<Dashboard/>, domContainer); +}); diff --git a/react/src/rebus.js b/react/src/rebus.js new file mode 100644 index 0000000000000000000000000000000000000000..7a24db362e93ca9c0d70694c9af8615dd236245f --- /dev/null +++ b/react/src/rebus.js @@ -0,0 +1,455 @@ +/*** source for biscuit/core/static/js/rebus.js ***/ + +const OPTIONS_ONLINE_COMMON = [ + "Portal ist nicht erreichbar", + "Fehlermeldung(en) tauchen auf", + "Anmeldung funktioniert nicht", + "Zugangsdaten vergessen" +]; + +const BASIC_OPTIONS = [ + { + id: "infrastructureIssues", + name: "Infrastrukturprobleme", + options: [ + { + id: "presentationDeviceIssue", + name: "Problem mit Beamer/Fernseher", + helpText: "Bitte wähle aus, wo der Beamer bzw. Fernseher steht!" + }, + { + id: "printerIssue", + name: "Problem mit einem Drucker", + helpText: "Bitte nenne uns in der Beschreibung das Modell des Druckers, damit wir genau wissen, welchen Drucker du meinst!" + }, + { + id: "subMonitorIssue", + name: "Vertretungsplanmonitor funktioniert nicht", + helpText: "Nenne uns bitte in der Beschreibung ggf. weitere Informationen!" + }, + { + id: "aulaIssue", + name: "Problem in der Aula (→Technik-AG)", + helpText: "Deine Anfrage wird direkt an die Technik-AG weitergeleitet." + }, + { + id: "wlanIssue", + name: "Probleme mit dem Schul-WLAN (kath-schueler/lehrer)", + helpText: "Nenne uns bitte unbedingt auch den Ort in der Schule, an dem das Problem auftrat." + }, + ] + }, + { + id: "onlineIssues", + name: "Webservices", + options: [ + { + id: "forum", + name: "Forum (ILIAS)", + options: OPTIONS_ONLINE_COMMON.concat([ + "Ich kann meinen Kurs bzw. Klasse nicht sehen/finden.", + "Ich kann keine Dateien hochladen.", + "Es taucht eine weiße Seite auf.", + "Ich habe falsche Informationen gefunden.", + ]) + }, + { + id: "mail", + name: "Webmail/Mailserver", + options: OPTIONS_ONLINE_COMMON.concat([ + "Mein E-Mail-Programm funktioniert mit meiner …@katharineum.de-Adresse nicht.", + "Ich bekomme keine E-Mails bzw. kann keine senden." + ]) + }, + { + id: "schoolapps", + name: "SchoolApps", + options: OPTIONS_ONLINE_COMMON.concat([ + "Der Stundenplan/Vertretungsplan ist falsch.", + "Ich bin der falschen Klasse zugeordnet.", + "Ich habe einen Fehler gefunden." + ]) + }, + { + id: "subOrMenu", + name: "Vertretungsplan/Speiseplan", + options: OPTIONS_ONLINE_COMMON.concat([ + "Kein Vertretungsplan zu sehen", + "Falscher Vertretungsplan zu sehen", + "Kein Speiseplan zu sehen", + "Falscher Speiseplan zu sehen" + ]) + }, + { + id: "website", + name: "Website (katharineum-zu-luebeck.de)", + options: [ + "Website nicht erreichbar", + "Falsche Inhalte vorhanden", + "Typografiefehler" + ] + + }, + { + id: "otherOnlineIssue", + name: "Andere Anwendung" + }, + ] + }, + { + id: "deviceIssues", + name: "Probleme am Computer/Notebook", + options: [ + { + id: "loginIssue", + name: "Anmeldeproblem/Passwort vergessen" + }, + { + id: "internetIssue", + name: "Internetproblem" + }, + { + id: "noReaction", + name: "Programm-/Computerabsturz (keine Reaktion)" + }, + { + id: "powerOffNoBoot", + name: "Computer/Notebook ist ausgegangen/startet nicht" + }, + { + id: "speedIssue", + name: "Computer/Notebook zu langsam" + }, + { + id: "noUSB", + name: "USB-Stick wird nicht erkannt" + }, + { + id: "noOpenTray", + name: "CD/DVD-Laufwerk öffnet sich nicht" + }, + { + id: "noCDDVD", + name: "CD/DVD wird nicht erkannt/abgespielt" + }, + { + id: "keyboardMouse", + name: "Tastatur/Maus funktioniert nicht" + }, + { + id: "missingHardware", + name: "Tastatur/Maus/Lautsprecher/etc. fehlt" + }, + { + id: "missingKeys", + name: "Fehlende Tasten auf der Tastatur" + }, + { + id: "hardwareMisc", + name: "Andere Hardware defekt / Äußere Schäden" + } + + + ] + }, + + { + id: "otherIssues", + name: "Andere Probleme", + options: [ + { + id: "extra", + name: "Sonstiges" + } + ] + }, +]; + + +const OTHER_LOCATIONS = [ + "Notebookwagen 1. Stock/R 2.06", + "Notebookwagen 2. Stock/R 2.10", + "Notebookwagen 3. Stock/Physik", + "Internetcafe", + "Infopoint/Sekretariatsvorraum", + "Lehrerzimmer (Vorraum)", + "Lehrerzimmer (Hauptraum)" +]; + + +function getCategoryOfOption(option) { + for (const category of BASIC_OPTIONS) { + // console.log(category); + for (const opt of category.options) { + // console.log(opt); + if (opt.id === option) { + return category.id; + } + } + } +} + + +function getOption(option) { + for (const category of BASIC_OPTIONS) { + for (const opt of category.options) { + if (opt.id === option) { + return opt; + } + } + } +} + +class Select extends React.Component { + render() { + return <select onChange={this.props.onChange} defaultValue={"no"} required={this.props.show}> + <option value={"no"} disabled={true}>Nichts ausgewählt</option> + {this.props.values.map(function (val, i) { + return <option value={val} key={i}>{val}</option>; + })} + <option value={"extra"}>{this.props.defaultValue}</option> + </select> + } +} + +Select.propTypes = { + onChange: PropTypes.func.isRequired, + values: PropTypes.array.isRequired, + defaultValue: PropTypes.string, + show: PropTypes.bool.isRequired + +}; + +Select.defaultProps = { + defaultValue: "Sonstiges" +}; + +class Input extends React.Component { + render() { + return <div + className={(this.props.show ? "" : "hide ") + "input-field col s12 m12 l4" + }> + <i className={"material-icons prefix"}>{this.props.icon}</i> + {this.props.children} + <label>{this.props.label}</label> + </div>; + } +} + +Input.propTypes = { + icon: PropTypes.string, + show: PropTypes.bool, + label: PropTypes.string.isRequired, + children: PropTypes.object.isRequired +}; + +Input.defaultProps = { + icon: "list", + show: false, +}; + +class REBUSDynSelect extends React.Component { + constructor() { + super(); + this.state = { + selectedCategory: "noCategory", + selectedOption: null, + helpText: "Wähle bitte eine Kategorie aus!", + valueB: "", + valueC: "", + step: 0 + } + } + + componentDidMount() { + // Init materialize selects + const elems = document.querySelectorAll('select'); + M.FormSelect.init(elems, {}); + } + + _onCategoryChanges = (e) => { + const opt = e.target.value; + const category = getCategoryOfOption(opt); + const option = getOption(opt); + + // Get matching helper text + let helpText = option.helpText || this.state.helpText; + if (category === "deviceIssues") { + helpText = "Wähle bitte das Gerät mit dem Problem aus! Bitte vergiss nicht, uns das Problem unten genauer zu beschreiben!" + } else if (category === "onlineIssues") { + helpText = "Bitte konkretisiere das Problem durch eine Auswahl und gib bitte unten genauere Informationen an." + } else if (category === "otherIssues") { + helpText = "Da es sich scheinbar um ein seltenes oder noch nicht erfasstes Problem handelt, gib uns bitte besonders viele Informationen." + } + + // Update state + this.setState({ + selectedCategory: category, + selectedOption: option, + step: 1, + helpText: helpText + }) + }; + + _onSetB = (e) => { + const val = e.target.value; + this.setState({ + valueB: val, + step: 2 + }) + }; + + _onSetC = (e) => { + const val = e.target.value; + this.setState({ + valueC: val, + step: 2 + }) + }; + + render() { + let LOCATIONS = this.props.rooms.concat(OTHER_LOCATIONS); + let LOCATIONS_WITH_POSSIBLE_PRESENTATION_DEVICE = this.props.rooms; + LOCATIONS.sort(); + + // console.log(this.state); + const that = this; + const sC = this.state.selectedCategory; + const sO = this.state.selectedOption ? this.state.selectedOption.id : null; + const step = this.state.step; + // console.log(BASIC_OPTIONS[2].options); + return ( + <div className="App"> + <div className={"row"}> + < div + className="input-field col s12 m12 l4"> + <i className={"material-icons prefix"}>list</i> + <select onChange={this._onCategoryChanges} defaultValue={"noCategory"} className={"validate"} + required={true}>- + <option value={"noCategory"} disabled={true}>Keine Kategorie ausgewählt</option> + {BASIC_OPTIONS.map(function (category) { + return <optgroup label={category.name} key={category.id}> + {category.options.map(function (option) { + return <option value={option.id} key={option.id}>{option.name}</option>; + })} + </optgroup>; + })} + {/*<option value={"extra"}>Sonstiges</option>*/} + </select> + <label>Kategorie</label> + </div> + + {/* Section B – Device Issues*/} + <Input label={"Ort des Computer/Notebook"} icon={"location_on"} show={sC === "deviceIssues"}> + <Select onChange={this._onSetB} values={LOCATIONS} defaultValue={"Anderer Ort"} + show={sC === "deviceIssues"}/> + </Input> + + {/* Section B – Presentation Device Issues */} + <Input label={"Ort des Beamer/Fernseher"} icon={"location_on"} + show={sO === "presentationDeviceIssue"}> + <Select onChange={this._onSetB} values={LOCATIONS_WITH_POSSIBLE_PRESENTATION_DEVICE} + defaultValue={"Anderer Raum"} show={sO === "presentationDeviceIssue"}/> + </Input> + + {/* Section B – Printer Issue */} + <Input label={"Art des Problems"} icon={"bug_report"} show={sO === "printerIssue"}> + <Select onChange={this._onSetB} + values={["Papierstau", "Toner leer", "Papier leer", "Drucker bekommt keine Daten"]} + defaultValue={"Anderes Problem"} show={sO === "printerIssue"}/> + </Input> + + {/* Section B – Substitution Monitor Issue */} + <Input label={"Art des Problems"} icon={"bug_report"} show={sO === "subMonitorIssue"}> + <Select onChange={this._onSetB} + values={["Schwarzer Bildschirm", "Tage wechseln nicht (Eingefroren)"]} + defaultValue={"Anderes Problem"} show={sO === "subMonitorIssue"}/> + </Input> + + {/* Section B – WLAN Issue */} + <Input label={"Art des Problems"} icon={"bug_report"} show={sO === "wlanIssue"}> + <Select onChange={this._onSetB} + values={["Kein Empfang", "Zugangsdaten funktionieren nicht", "Geschwindigkeit zu langsam"]} + defaultValue={"Anderes Problem"} show={sO === "wlanIssue"}/> + </Input> + + {/* Section B – Online Issue*/} + {BASIC_OPTIONS[1].options.map(function (opt) { + if (opt.options) { + return <Input label={"Art des Problems"} icon={"bug_report"} + show={sC === "onlineIssues" && sO === opt.id} key={opt.id}> + <Select onChange={that._onSetB} + values={opt.options} + defaultValue={"Anderes Problem"} show={sC === "onlineIssues" && sO === opt.id} + key={opt.id}/> + </Input>; + } else { + return <p/>; + } + })} + + + {/* Section C – Presentation Device Issues */} + <Input label={"Handelt es sich um einen Beamer oder einen Fernseher?"} icon={"tv"} + show={sO === "presentationDeviceIssue" && step === 2}> + <Select onChange={this._onSetC} values={["Beamer", "Fernseher/Bildschirm"]} + defaultValue={"Sonstiges"} show={sO === "presentationDeviceIssue" && step === 2}/> + </Input> + + {/* Section C – Presentation Device Issues */} + <Input label={"Ort des Druckers"} icon={"location_on"} + show={sO === "printerIssue" && step === 2}> + <Select onChange={this._onSetC} values={LOCATIONS} + defaultValue={"Anderer Raum"} show={sO === "printerIssue"}/> + </Input> + + {/* Section C – WLAN Issue */} + <Input label={"Um welches WLAN-Netzwerk handelt es sich?"} icon={"wifi"} + show={sO === "wlanIssue" && step === 2}> + <Select onChange={this._onSetC} + values={["kath-schueler", "kath-lehrer", "kath-edu", "kath-gaeste"]} + defaultValue={"-"} show={sO === "wlanIssue" && step === 2}/> + </Input> + + {/* Section C – Device Issue */} + <div + className={(sC === "deviceIssues" && step === 2 ? "" : "hide ") + "input-field col s12 m12 l4" + }> + <i className={"material-icons prefix"}>device_unknown</i> + <input type={"text"} id={"valc"} onChange={this._onSetC} + required={sC === "deviceIssues" && step === 2} className={"validate"}/> + <label htmlFor="valc">Um welches Gerät handelt es sich?</label> + </div> + + {/* Helper Text */} + <div className={"col s12"}> + <p> + <i className={"material-icons left"}>info</i> + {this.state.helpText} + </p> + </div> + + + </div> + + {/* Prepare values for Django */} + <div> + <input type={"hidden"} name={"a"} + value={this.state.selectedOption ? this.state.selectedOption.name : ""}/> + <input type={"hidden"} name={"b"} value={this.state.valueB}/> + <input type={"hidden"} name={"c"} value={this.state.valueC}/> + </div> + + </div> + ); + } +} + +REBUSDynSelect.propTypes = { + rooms: PropTypes.array.isRequired +}; + +$(document).ready(function () { + const domContainer = document.querySelector('#dynselect'); + ReactDOM.render(<REBUSDynSelect {...props}/>, domContainer); +}); diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b767d1c91300b74fb4bce041688565787ae8949 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +mysqlclient +django-pdb +django-filter +django_react_templatetags +PyPDF2 +martor +django-pwa +django_widget_tweaks +ics