diff --git a/CHANGELOG.md b/CHANGELOG.md index b571aebe84110cc57230d90c9f2d69d691fac54e..2dc8e229104aee482fd7bba92d1c47979d611d77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,4 +76,11 @@ Weitere Verbesserungen der vorangegeangen Versionen dieses Releases. + Einfachere Einbindung von React für REBUS ## 1.1.2 "Aebli" -* Bugfix in der PWA (zuerst Netzwerk, dann Cache) \ No newline at end of file +* Bugfix in der PWA (zuerst Netzwerk, dann Cache) + +## 1.1.3 "Aebli" +* Bugfix in REBUS (Submit wieder erlaubt) (#335) +* Klassenlehrkräfte werden im Plan angezeigt (#334) + +## 1.1.4 "Aebli" +* Bugfix in der PWA (Offline-Fallback-Page wird nicht mehr bei teils aktiver Netzwerkverbindung fälschlicherweise angezeigt) \ No newline at end of file diff --git a/INSTALL.md b/INSTALL.md index 64d0f5baf2e338d30adb42f025ece35dc18c5e7a..05e7f01bb908b0de69941f43a75cd935c2a443bd 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -33,7 +33,6 @@ GRANT SELECT ON Untis.* TO 'www-data'@'localhost'; ### UNTIS-Beispieldaten importieren Zum Testen kann die Datei `untiskath.sql` vom Forum in die Datenbank `Untis` importiert werden. - ### SchoolApps clonen ``` git clone git@github.com:Katharineum/school-apps.git @@ -49,7 +48,6 @@ pip install -r requirements.txt - `example_secure_settings.py` zu `secure_settings.py` kopieren und anpassen (hier müssen auch die passenden DB-Zugangsdaten eingetragen werden) - ### Migrations durchführen/auflösen Leider kommt es bei einer Erstinstallation von SchoolApps immer noch zu Problemen mit den Migrations. Sollte es Schwierigkeiten geben, @hansegucker kontaktieren. diff --git a/README.md b/README.md index d95f8391b9751c61248844e996ebab6516e7e3ed..731e35747099e550067dcbffad69d490de7b8d3b 100755 --- a/README.md +++ b/README.md @@ -2,6 +2,6 @@ SchoolApps ist ein modulares System zum Bereitstellen von speziellen Anwendungen für den Schulalltag. Besonderer Fokus liegt dabei auf dem Stundenplansystem. -© 2018 by [Jonathan Weth](mailto:wethjo@katharineum.de), [Frank-Poetzsch-Heffter](mailto:p-h@katharineum.de) +© 2018 by [Jonathan Weth](mailto:wethjo@katharineum.de), [Frank Poetzsch-Heffter](mailto:p-h@katharineum.de) -© 2019 by [Hangzhi Yu](mailto:yuha@katharineum.de), [Julian Leucker](mailto:leuckeju@katharineum.de), [Jonathan Weth](mailto:wethjo@katharineum.de), [Frank-Poetzsch-Heffter](mailto:p-h@katharineum.de) \ No newline at end of file +© 2019 by [Hangzhi Yu](mailto:yuha@katharineum.de), [Julian Leucker](mailto:leuckeju@katharineum.de), [Jonathan Weth](mailto:wethjo@katharineum.de), [Frank Poetzsch-Heffter](mailto:p-h@katharineum.de) diff --git a/react/src/rebus.js b/react/src/rebus.js index a0fd50b4bf9a7aa02d3a0886de069bc0aa15ba4f..5799a15cbc881d4580a2c441bde3ceff19e0071c 100755 --- a/react/src/rebus.js +++ b/react/src/rebus.js @@ -222,9 +222,7 @@ function getCategoryOfOption(option) { function getOption(option) { for (const category of BASIC_OPTIONS) { - // console.log(category); for (const opt of category.options) { - // console.log(opt); if (opt.id === option) { return opt; } @@ -388,7 +386,7 @@ class REBUSDynSelect extends React.Component { <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 === "subMonitorIssue"}/> + defaultValue={"Anderes Problem"} show={sO === "printerIssue"}/> </Input> {/* Section B – Substitution Monitor Issue */} @@ -432,7 +430,7 @@ class REBUSDynSelect extends React.Component { <Input label={"Ort des Druckers"} icon={"location_on"} show={sO === "printerIssue" && step === 2}> <Select onChange={this._onSetC} values={LOCATIONS} - defaultValue={"Anderer Raum"} show={sO === "presentationDeviceIssue"}/> + defaultValue={"Anderer Raum"} show={sO === "printerIssue"}/> </Input> {/* Section C – WLAN Issue */} diff --git a/requirements.txt b/requirements.txt index 95162f578fab706905d1cf1eb69c600d9ee99046..e760b444ea85230fa371ede00451160a5bbd353e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ requests mysqlclient -django +django<3.0 django-auth-ldap django-dbsettings django-pdb diff --git a/schoolapps/aub/forms.py b/schoolapps/aub/forms.py index 245e2619bd5b7d2afa9cec7906fd1c61fd0ea9cd..1b59bbffc20e41edab82c12146d5e75e7763c9fa 100755 --- a/schoolapps/aub/forms.py +++ b/schoolapps/aub/forms.py @@ -4,6 +4,7 @@ from django.utils import timezone from datetime import datetime from material import Layout, Row, Fieldset from aub.models import Aub +# from tinymce.widgets import TinyMCE class ApplyForAUBForm(forms.ModelForm): @@ -17,6 +18,7 @@ class ApplyForAUBForm(forms.ModelForm): to_date = forms.DateField(label='Datum', input_formats=['%d.%m.%Y']) to_lesson = forms.ChoiceField(label='Stunde', choices=lessons, required=False, widget=forms.Select(attrs={'onchange': 'setTime(this)'})) to_time = forms.TimeField(label='Zeit', input_formats=['%H:%M'], initial=initial_to_time) + # description = forms.CharField(label='Bitte begründen Sie Ihren Antrag.', widget=TinyMCE(attrs={'cols': 80, 'rows': 30})) description = forms.CharField(label='Bitte begründen Sie Ihren Antrag.') layout = Layout(Fieldset('Von', @@ -33,6 +35,9 @@ class ApplyForAUBForm(forms.ModelForm): model = Aub fields = ('id', 'from_date', 'from_time', 'to_date', 'to_time', 'description') + class Media: + js = ('/media/tinymce/jscripts/tiny_mce/tiny_mce.js', '',) + def clean(self): cleaned_data = super().clean() diff --git a/schoolapps/aub/models.py b/schoolapps/aub/models.py index e9193afc05c98914f97f43ede0cfe91fdc08af1a..7366bd652640e2b1b84de11b692cd4549a126cff 100755 --- a/schoolapps/aub/models.py +++ b/schoolapps/aub/models.py @@ -45,7 +45,6 @@ class Aub(models.Model): created_at = models.DateTimeField(default=timezone.now, verbose_name="Erstellungszeitpunkt") def getStatus(self): - print(self.status, self.created_by, self.id) return status_list[self.status] def __str__(self): diff --git a/schoolapps/aub/templates/aub/index.html b/schoolapps/aub/templates/aub/index.html index 59defc97477ddb85664572ee5463a189c971d488..ce9d67f5818fa788249e524cf97fa8d164471b0e 100755 --- a/schoolapps/aub/templates/aub/index.html +++ b/schoolapps/aub/templates/aub/index.html @@ -33,7 +33,7 @@ <div class="col s12 m4"> <p> {% if aub.status == 0 %} - <form action="{% url 'aub_edit' aub.id %}" class="right"> + <form action="{% url 'aub_edit' aub.id %}" class="left"> {% csrf_token %} <button type="submit" name="edit" class="waves-effect waves-light btn-flat btn-flat-medium" title="Bearbeiten"> <i class="material-icons center green-text">create</i> @@ -41,7 +41,7 @@ </form> {% endif %} {% if aub.status == 0 or aub.status == 1 %} - <form action="" method="POST" class="right"> + <form action="" method="POST" class="left"> {% csrf_token %} <input type="hidden" value="{{ aub.id }}" name="aub-id"> <button type="submit" onclick="return confirm('Wollen Sie den Antrag wirklich löschen?')" name="cancel" class="waves-effect waves-light btn-flat btn-flat-medium" title="Löschen"> diff --git a/schoolapps/aub/views.py b/schoolapps/aub/views.py index ba1a759e7cd7b711d3da9ee0a2058375d98c798e..578c8d89c0dd4cc9c9fe8e808f05e4c243198321 100755 --- a/schoolapps/aub/views.py +++ b/schoolapps/aub/views.py @@ -196,7 +196,10 @@ def check2(request): def archive(request): order_crit = '-from_date' if 'created_by' in request.GET: - item = int(request.GET['created_by']) + try: + item = int(request.GET['created_by']) + except ValueError: + item = '' aub_list = Aub.objects.filter((Q(status__exact=2) | Q(status__exact=3)) & Q(created_by=item)).order_by(order_crit) else: aub_list = Aub.objects.filter(Q(status__exact=2) | Q(status__exact=3)).order_by(order_crit) diff --git a/schoolapps/dashboard/migrations/0002_cache.py b/schoolapps/dashboard/migrations/0002_cache.py index 55ee00afe0e0c57fac5e0e62773131ec36a52309..1d146b35c7961c326f2b7de0bcf33ea6bdb552a5 100644 --- a/schoolapps/dashboard/migrations/0002_cache.py +++ b/schoolapps/dashboard/migrations/0002_cache.py @@ -13,8 +13,8 @@ class Migration(migrations.Migration): name='Cache', fields=[ ('id', - models.CharField(max_length=200, primary_key=True, serialize=False, unique=True, verbose_name='ID')), - ('name', models.CharField(max_length=200, verbose_name='Name')), + 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={ diff --git a/schoolapps/dashboard/models.py b/schoolapps/dashboard/models.py index 3aaf75ae4c872092683b26809d6cbbc0155da8b1..8a26ce363f72ef0867614a389fc1c7b1062ee16f 100755 --- a/schoolapps/dashboard/models.py +++ b/schoolapps/dashboard/models.py @@ -11,7 +11,7 @@ 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=200) + title = models.CharField(max_length=150) description = models.TextField(max_length=500) app = models.CharField(max_length=100) @@ -24,7 +24,7 @@ class Activity(models.Model): class Notification(models.Model): user = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name="notifications") - title = models.CharField(max_length=200) + title = models.CharField(max_length=150) description = models.TextField(max_length=500) link = models.URLField(blank=True) @@ -48,8 +48,8 @@ def register_notification(user, title, description, app="SchoolApps", link=""): class Cache(models.Model): - id = models.CharField(max_length=200, unique=True, primary_key=True, verbose_name="ID") - name = models.CharField(max_length=200, verbose_name="Name") + 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") diff --git a/schoolapps/fibu/README.md b/schoolapps/fibu/README.md new file mode 100755 index 0000000000000000000000000000000000000000..bca3b98cf3f604d0009e59c57ce83a6fd698f3c8 --- /dev/null +++ b/schoolapps/fibu/README.md @@ -0,0 +1,30 @@ +# SchoolApps: FiBu (Finanzbuchhaltung) +## Workflow + +1. Berechtigungen setzen + + Anwender: request_booking + + Buchhalter: manage_booking + + Manager: manage_costcenter, manage_account + +2. Manager richtet Kostenstellen (z.B. Schulträger-konsumtiv, +Schulträger-investiv, Elternverein, Fundraising, ...) sowie +Buchungskonten (z.B. Klassenraum-Festwert, Geschäftszimmer, +Fachschaft Mathematik, ...) ein und setzt dabei Budgets fest, +die in der Summe den Haushaltsplan ergeben. + +3. Anwender stellt Anträge für Ausgaben. + +4. Manager bewilligt Anträge unter Zuordnung zum passenden +Buchungskonto oder lehnt sie ab. + +5. Anwender gibt für bewilligte Anträge Bestellung auf und +ändert den Status der Buchung entsprechend. + +6. Anwender reicht Rechnung im Geschäftszimmer ein und ändert +den Status der Buchung entsprechend. + +7. Buchhalter vervollständigt die Buchung, weist die Rechnung +an und ändert den Status auf bezahlt. + +8. Buchhalter und Manager kontrollieren durch Aufruf von Berichten +den Haushalt. \ No newline at end of file diff --git a/schoolapps/fibu/__init__.py b/schoolapps/fibu/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/schoolapps/fibu/admin.py b/schoolapps/fibu/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..9f16fa184564f6a7a31fcaec3458e6e16b26c43c --- /dev/null +++ b/schoolapps/fibu/admin.py @@ -0,0 +1,6 @@ +from django.contrib import admin +from .models import CostCenter, Account, Booking + +admin.site.register(CostCenter) +admin.site.register(Account) +admin.site.register(Booking) diff --git a/schoolapps/fibu/apps.py b/schoolapps/fibu/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..f98393e1c0883b1ad219582feb398b0381e45263 --- /dev/null +++ b/schoolapps/fibu/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class FibuConfig(AppConfig): + name = 'fibu' diff --git a/schoolapps/fibu/decorators.py b/schoolapps/fibu/decorators.py new file mode 100644 index 0000000000000000000000000000000000000000..63210079b3262886d437ca6cd785810f6ac9c7df --- /dev/null +++ b/schoolapps/fibu/decorators.py @@ -0,0 +1,24 @@ +from django.contrib.auth.decorators import user_passes_test +from django.contrib.auth import REDIRECT_FIELD_NAME + +from .models import Booking + + +# prevent to show booking details from foreign users +def check_own_booking_verification(user): + return Booking.objects.all().filter(created_by=user) + + +def check_own_booking(function=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url=None): + """ + Decorator for views that checks that the user only gets his own bookings, redirecting + to the dashboard if necessary. + """ + actual_decorator = user_passes_test( + check_own_booking_verification, + login_url=login_url, + redirect_field_name=redirect_field_name + ) + if function: + return actual_decorator(function) + return actual_decorator diff --git a/schoolapps/fibu/filters.py b/schoolapps/fibu/filters.py new file mode 100644 index 0000000000000000000000000000000000000000..e6ca80862169f0d0fdd037163f23b0ffdfaedd34 --- /dev/null +++ b/schoolapps/fibu/filters.py @@ -0,0 +1,25 @@ +from django.contrib.auth.models import User +import django_filters +from .models import Booking +from django.db.utils import ProgrammingError + + +def get_fibu_users(): + """ Find all users who requests a boooking """ + try: + fibu_users = Booking.objects.values_list('contact') + users = list(User.objects.filter(id__in=fibu_users)) + # user_ids = [(str(user.id),user.username) for user in users] + user_ids = [(str(user.id), user.last_name + ', ' + user.first_name) for user in users] + user_ids_sorted = sorted(user_ids, key=lambda user: user[1]) + return user_ids_sorted + except ProgrammingError: + return [] + + +class BookingFilter(django_filters.FilterSet): + contact = django_filters.ChoiceFilter(label='Von', choices=get_fibu_users()) + + class Meta: + model = Booking + fields = ['contact', ] diff --git a/schoolapps/fibu/forms.py b/schoolapps/fibu/forms.py new file mode 100644 index 0000000000000000000000000000000000000000..9a4854092ee7068770b0275eee4f3b3ab6887471 --- /dev/null +++ b/schoolapps/fibu/forms.py @@ -0,0 +1,62 @@ +from django import forms +from django.utils import timezone +from material import Layout, Row, Fieldset + +from .models import Booking, CostCenter, Account + + +class SimpleBookingForm(forms.ModelForm): + description = forms.CharField(label='Beschreibung – Was soll angeschafft werden?') + planned_amount = forms.IntegerField( + label='Erwarteter Betrag – Welcher Betrag ist erforderlich?', help_text="in Euro, ohne Komma") + justification = forms.CharField(label='Begründung – Begründe ggf. deinen Antrag.', required=False) + + layout = Layout(Row('description', 'planned_amount'), Row('justification')) + + class Meta: + model = Booking + fields = ['id', 'description', 'planned_amount', 'justification'] + + +class CheckBookingForm(forms.ModelForm): + account = forms.ModelChoiceField(Account.objects.filter().order_by('cost_center', 'name')) + + class Meta: + model = Booking + fields = ['account', ] + + +class CompleteBookingForm(forms.ModelForm): + accounts = Account.objects.filter().order_by('cost_center', 'name') + account = forms.ModelChoiceField(queryset=accounts) + submission_date = forms.DateField(label='Bearbeitungsdatum', initial=timezone.now()) + + layout = Layout(Fieldset("Allgemeines", + Row('description', 'justification'), + Row("contact", "planned_amount"), + Row('account', 'status') + ), + Fieldset('Details', + Row('firma', 'invoice_number', 'amount'), + Row('invoice_date', 'maturity', 'submission_date', 'booking_date'), + Row('payout_number', 'upload') + ) + ) + + class Meta: + model = Booking + fields = ['id', 'description', 'planned_amount', 'justification', 'account', 'contact', 'invoice_date', + 'invoice_number', 'firma', 'amount', 'submission_date', 'payout_number', 'booking_date', + 'maturity', 'upload', 'status'] + + +class CostCenterForm(forms.ModelForm): + class Meta: + model = CostCenter + fields = ['id', 'name', 'year'] + + +class AccountForm(forms.ModelForm): + class Meta: + model = Account + fields = ['id', 'name', 'cost_center', 'income', 'budget'] diff --git a/schoolapps/fibu/migrations/0001_initial.py b/schoolapps/fibu/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..acff540c447b1c52a06949b1e1a9b78d20edded1 --- /dev/null +++ b/schoolapps/fibu/migrations/0001_initial.py @@ -0,0 +1,72 @@ +# Generated by Django 2.2.8 on 2019-12-27 06:08 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Account', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(default='', max_length=20)), + ('income', models.BooleanField(default=False)), + ('budget', models.DecimalField(decimal_places=2, default=0.0, max_digits=9)), + ('saldo', models.DecimalField(decimal_places=2, default=0.0, max_digits=9)), + ('rest', models.DecimalField(decimal_places=2, default=0.0, max_digits=9)), + ], + options={ + 'permissions': [('manage_account', 'Can manage account')], + }, + ), + migrations.CreateModel( + name='CostCenter', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=30)), + ('year', models.IntegerField(choices=[(2020, '2020'), (2021, '2021'), (2022, '2022'), (2023, '2023')], + default=2019, verbose_name='Jahr')), + ], + options={ + 'permissions': [('manage_costcenter', 'Can manage costcenter')], + }, + ), + migrations.CreateModel( + name='Booking', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('invoice_date', models.DateField(default='2000-12-31')), + ('invoice_number', models.CharField(default='0', max_length=20)), + ('firma', models.CharField(default='', max_length=30)), + ('description', models.CharField(max_length=50)), + ('amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=9)), + ('planned_amount', models.IntegerField()), + ('submission_date', models.DateField(default='2000-12-31')), + ('justification', models.CharField(blank=True, max_length=2000, null=True)), + ('payout_number', models.IntegerField(default=0)), + ('booking_date', models.DateField(default='2000-12-31')), + ('maturity', models.DateField(default='2000-12-31')), + ('upload', models.FileField(blank=True, default=None, null=True, upload_to='uploads/fibu/%Y/')), + ('status', models.IntegerField(choices=[(0, 'beantragt'), (1, 'bewilligt'), (2, 'abgelehnt'), (3, 'bestellt'), (4, 'eingereicht'), (5, 'bezahlt')], default=0, verbose_name='Status')), + ('account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='fibu.Account')), + ('contact', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bookings', to=settings.AUTH_USER_MODEL, verbose_name='Erstellt von')), + ], + options={ + 'permissions': [('manage_booking', 'Can manage bookings'), ('request_booking', 'Can request a booking')], + }, + ), + migrations.AddField( + model_name='account', + name='costcenter', + field=models.ForeignKey(default='', on_delete=django.db.models.deletion.CASCADE, to='fibu.CostCenter'), + ), + ] diff --git a/schoolapps/fibu/migrations/0002_auto_20191228_1357.py b/schoolapps/fibu/migrations/0002_auto_20191228_1357.py new file mode 100644 index 0000000000000000000000000000000000000000..50c6c27ac779af2362fd1a5ca6372f0ea76f7694 --- /dev/null +++ b/schoolapps/fibu/migrations/0002_auto_20191228_1357.py @@ -0,0 +1,90 @@ +# Generated by Django 2.2.6 on 2019-12-28 12:57 + +import datetime +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ('fibu', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='account', + options={'permissions': [('manage_account', 'Can manage account')], 'verbose_name': 'Buchungskonto', + 'verbose_name_plural': 'Buchungskonten'}, + ), + migrations.AlterModelOptions( + name='booking', + options={ + 'permissions': [('manage_booking', 'Can manage bookings'), ('request_booking', 'Can request a booking'), + ('check_booking', 'Can check bookings')], 'verbose_name': 'Buchung', + 'verbose_name_plural': 'Buchungen'}, + ), + migrations.AlterModelOptions( + name='costcenter', + options={'permissions': [('manage_costcenter', 'Can manage costcenter')], 'verbose_name': 'Kostenstelle', + 'verbose_name_plural': 'Kostenstellen'}, + ), + migrations.AlterField( + model_name='account', + name='budget', + field=models.DecimalField(decimal_places=2, default=0.0, max_digits=9, verbose_name='Budget'), + ), + migrations.AlterField( + model_name='account', + name='costcenter', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fibu.CostCenter', + verbose_name='Kostenstelle'), + ), + migrations.AlterField( + model_name='account', + name='income', + field=models.BooleanField(default=False, verbose_name='Einnahmekonto'), + ), + migrations.AlterField( + model_name='account', + name='name', + field=models.CharField(max_length=20, verbose_name='Buchungskonto'), + ), + migrations.AlterField( + model_name='booking', + name='booking_date', + field=models.DateField(default=datetime.date.today), + ), + migrations.AlterField( + model_name='booking', + name='invoice_date', + field=models.DateField(default=datetime.date.today), + ), + migrations.AlterField( + model_name='booking', + name='maturity', + field=models.DateField(default=datetime.date.today), + ), + migrations.AlterField( + model_name='booking', + name='status', + field=models.IntegerField( + choices=[(0, 'beantragt'), (1, 'abgelehnt'), (2, 'bewilligt'), (3, 'bestellt'), (4, 'eingereicht'), + (5, 'bezahlt')], default=0, verbose_name='Status'), + ), + migrations.AlterField( + model_name='booking', + name='submission_date', + field=models.DateField(default=datetime.date.today), + ), + migrations.AlterField( + model_name='costcenter', + name='name', + field=models.CharField(max_length=30, verbose_name='Kostenstelle'), + ), + migrations.AlterField( + model_name='costcenter', + name='year', + field=models.IntegerField(choices=[(2019, '2019'), (2020, '2020'), (2021, '2021'), (2022, '2022')], + default=2019, verbose_name='Jahr'), + ), + ] diff --git a/schoolapps/fibu/migrations/0003_auto_20191228_1553.py b/schoolapps/fibu/migrations/0003_auto_20191228_1553.py new file mode 100644 index 0000000000000000000000000000000000000000..6674b3d95d25eb5451050e7c10fefb65149ead28 --- /dev/null +++ b/schoolapps/fibu/migrations/0003_auto_20191228_1553.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.8 on 2019-12-28 14:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('fibu', '0002_auto_20191228_1357'), + ] + + operations = [ + migrations.AlterField( + model_name='account', + name='budget', + field=models.IntegerField(default=0, verbose_name='Budget'), + ), + ] diff --git a/schoolapps/fibu/migrations/__init__.py b/schoolapps/fibu/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/schoolapps/fibu/models.py b/schoolapps/fibu/models.py new file mode 100644 index 0000000000000000000000000000000000000000..8611ca69c7738929db51b8cd97a02da7f8c5cb5e --- /dev/null +++ b/schoolapps/fibu/models.py @@ -0,0 +1,108 @@ +from django.utils import timezone +from django.db import models +from django.contrib.auth.models import User +from datetime import date + +current_year = timezone.now().year +YEARS = [(x, str(x)) for x in range(current_year, current_year + 4)] + + +class Status: + def __init__(self, name, style_class): + self.name = name + self.style_class = style_class + + def __str__(self): + return self.name + + +status_list = [ + Status(name='beantragt', style_class='red'), + Status(name='abgelehnt', style_class='black'), + Status(name='bewilligt', style_class='orange'), + Status(name='bestellt', style_class='yellow darken-1'), + Status(name='eingereicht', style_class='blue'), + Status(name='bezahlt', style_class='green'), +] + +status_choices = [(x, val.name) for x, val in enumerate(status_list)] + + +class CostCenter(models.Model): + # Kostenstellen z.B. Schulträger-konsumtiv, Schulträger-investiv, Elternverein, ... + name = models.CharField(max_length=30, blank=False, verbose_name="Kostenstelle") + year = models.IntegerField(default=timezone.now().year, choices=YEARS, blank=False, verbose_name="Jahr") + + def __str__(self): + return self.name + + class Meta: + verbose_name = "Kostenstelle" + verbose_name_plural = "Kostenstellen" + permissions = [ + ('manage_costcenter', 'Can manage costcenter'), + ] + + +class Account(models.Model): + # Buchungskonten, z.B. Fachschaften, Sekretariat, Schulleiter, Kopieren, Tafelnutzung + name = models.CharField(max_length=20, blank=False, verbose_name="Buchungskonto") + cost_center = models.ForeignKey(to=CostCenter, on_delete=models.CASCADE, blank=False, verbose_name="Kostenstelle") + income = models.BooleanField(default=False, + verbose_name="Einnahmekonto") # True, wenn es sich um ein Einnahmekonto handelt + budget = models.IntegerField(default=0, verbose_name="Budget") + saldo = models.DecimalField(max_digits=9, decimal_places=2, default=0.00) + rest = models.DecimalField(max_digits=9, decimal_places=2, default=0.00) + + def __str__(self): + return "{}: {}".format(self.cost_center, self.name) + + class Meta: + verbose_name = "Buchungskonto" + verbose_name_plural = "Buchungskonten" + permissions = [ + ('manage_account', 'Can manage account'), + ] + + +class Booking(models.Model): + # General information + account = models.ForeignKey(to=Account, on_delete=models.SET_NULL, blank=True, null=True, + verbose_name="Buchungskonto") + contact = models.ForeignKey(to=User, related_name='bookings', on_delete=models.SET_NULL + , verbose_name="Erstellt von", blank=True, null=True) + description = models.CharField(max_length=50, verbose_name="Beschreibung") + justification = models.CharField(max_length=2000, blank=True, null=True, verbose_name="Begründung") + planned_amount = models.IntegerField(verbose_name="Erwarteter Betrag", help_text="ganze Euro") + + # Details + invoice_date = models.DateField(blank=True, null=True, verbose_name="Rechnungsdatum") + invoice_number = models.CharField(max_length=20, blank=True, null=True, verbose_name="Rechnungsnummer") + firma = models.CharField(max_length=30, blank=True, null=True, verbose_name="Firma") + amount = models.DecimalField(max_digits=9, decimal_places=2, default=0.00, verbose_name="Betrag") + payout_number = models.IntegerField(blank=True, null=True, verbose_name="Auszahlungsnummer") + + submission_date = models.DateField(blank=True, null=True, verbose_name="Bearbeitungsdatum") + booking_date = models.DateField(default=date.today, verbose_name="Buchungsdatum") + maturity = models.DateField(blank=True, null=True, verbose_name="Fälligkeit") + + upload = models.FileField(upload_to='uploads/fibu/%Y/', default=None, blank=True, null=True, + verbose_name="Scan der Rechnung") + + # Meta information + status = models.IntegerField(default=0, choices=status_choices, verbose_name="Status") + + def get_status(self): + return status_list[self.status] + + def __str__(self): + return "{} ({})".format(self.description, self.account) + + class Meta: + verbose_name = "Buchung" + verbose_name_plural = "Buchungen" + permissions = [ + ('manage_booking', 'Can manage bookings'), + ('request_booking', 'Can request a booking'), + ('check_booking', 'Can check bookings'), + ] diff --git a/schoolapps/fibu/templates/fibu/account/edit.html b/schoolapps/fibu/templates/fibu/account/edit.html new file mode 100644 index 0000000000000000000000000000000000000000..185d5f80fdc5ac53d3c5c824c21717a7f7578e6c --- /dev/null +++ b/schoolapps/fibu/templates/fibu/account/edit.html @@ -0,0 +1,23 @@ +{% include 'partials/header.html' %} +{% load material_form %} + +<main> + <h4>Buchungskonto bearbeiten</h4> + + <form method="POST" action=""> + {% csrf_token %} + {% form form=form %} + {% endform %} + + <span class="right"> + <button type="submit" class="waves-effect waves-light btn green"> + <i class="material-icons left">save</i> Änderungen übernehmen + </button> + <a href="{% url 'fibu_accounts' %}" class="waves-effect waves-light btn red"> + <i class="material-icons left">cancel</i> Abbrechen + </a> + </span> + </form> + +</main> +{% include 'partials/footer.html' %} diff --git a/schoolapps/fibu/templates/fibu/account/index.html b/schoolapps/fibu/templates/fibu/account/index.html new file mode 100755 index 0000000000000000000000000000000000000000..b12b41f7025d30f4b8f2971cafc268f095351e7d --- /dev/null +++ b/schoolapps/fibu/templates/fibu/account/index.html @@ -0,0 +1,76 @@ +{% include 'partials/header.html' %} +{% load material_form %} + +<main> + <a class="waves-effect waves-light btn green modal-trigger right" href="#new-modal"> + <i class="material-icons left">add</i> Buchungskonto anlegen + </a> + + <h4>Buchungskonten</h4> + + {% if form.errors %} + <script> + var onFinish = function () { + $("#new-modal").modal("open"); + }; + </script> + {% endif %} + + <form method="POST"> + <div id="new-modal" class="modal"> + <div class="modal-content"> + <h5>Neues Buchungskonto anlegen</h5> + {% csrf_token %} + {% form form=form %} + {% endform %} + </div> + <div class="modal-footer"> + <button type="submit" class="waves-effect waves-light btn green"> + <i class="material-icons left">save</i> Buchungskonto anlegen + </button> + </div> + </div> + </form> + + <table> + <thead> + <tr> + <th>Buchungskonto</th> + <th>Kostenstelle</th> + <th class="right-align">erwartete Einnahmen</th> + <th class="right-align">erwartete Ausgaben</th> + <th>Aktionen</th> + </tr> + </thead> + <tbody> + {% for account in accounts %} + <tr> + <td>{{ account.name }}</td> + <td>{{ account.cost_center }}</td> + {% if account.income %} + <td class="green-text right-align">{{ account.budget }} €</td> + <td></td> + {% else %} + <td></td> + <td class="red-text right-align">{{ account.budget }} €</td> + {% endif %} + <td class="right-align"> + <a href="{% url 'fibu_accounts_edit' account.id %}" + class="waves-effect waves-light btn-flat btn-flat-medium left" title="Bearbeiten"> + <i class="material-icons center green-text">create</i> + </a> + <form action="" method="POST" class="left"> + {% csrf_token %} + <input type="hidden" value="{{ account.id }}" name="id"> + <button type="submit" onclick="return confirm('Buchungskonto wirklich löschen?')" + name="cancel" class="waves-effect waves-light btn-flat btn-flat-medium" title="Löschen"> + <i class="material-icons center red-text">cancel</i> + </button> + </form> + </td> + </tr> + {% endfor %} + </tbody> + </table> +</main> +{% include 'partials/footer.html' %} diff --git a/schoolapps/fibu/templates/fibu/booking/book.html b/schoolapps/fibu/templates/fibu/booking/book.html new file mode 100644 index 0000000000000000000000000000000000000000..0f44c48c719451b596ccf484d9ef90ba84a70b25 --- /dev/null +++ b/schoolapps/fibu/templates/fibu/booking/book.html @@ -0,0 +1,23 @@ +{% include 'partials/header.html' %} +{% load material_form %} + +<main> + <h4>Buchung bearbeiten</h4> + + <form method="POST"> + {% csrf_token %} + {% form form=form %} + {% endform %} + + <span class="right"> + <button type="submit" class="waves-effect waves-light btn green"> + <i class="material-icons left">save</i> Änderungen übernehmen + </button> + <a href="{% url 'fibu_bookings' %}" class="waves-effect waves-light btn red"> + <i class="material-icons left">cancel</i> Abbrechen + </a> + </span> + </form> +</main> + +{% include 'partials/footer.html' %} diff --git a/schoolapps/fibu/templates/fibu/booking/check.html b/schoolapps/fibu/templates/fibu/booking/check.html new file mode 100755 index 0000000000000000000000000000000000000000..9dad97f53214556cb9152fc5093440dc5c13caf5 --- /dev/null +++ b/schoolapps/fibu/templates/fibu/booking/check.html @@ -0,0 +1,44 @@ +{% include 'partials/header.html' %} +{% load material_form %} + +<main> + <h4>Anträge prüfen</h4> + {% if not filter.qs %} + <span class="flow-text center-align"> + Keine offenen Anträge vorhanden + </span> + {% else %} + <div class="collection-item row"> + <div class="col s2"><strong>Antragsteller</strong></div> + <div class="col s3"><strong>Anschaffungswunsch</strong></div> + <div class="col s2 right-align"><strong>Geplante Kosten</strong></div> + <div class="col s3"><strong>Buchungskonto</strong></div> + <div class="col s2"><strong>Aktionen</strong></div> + </div> + + {% for booking in filter.qs %} + <div class="collection-item row"> + <form method="POST"> + {% csrf_token %} + <input type="hidden" value="{{ booking.id }}" name="booking-id"> + <div class="col s2">{{ booking.contact.get_full_name }}</div> + <div class="col s3">{{ booking.description }}</div> + <div class="col s2 right-align">{{ booking.planned_amount }} €</div> + <div class="col s3">{{ form.account }}</div> + <div class="col s2"> + <button type="submit" name="allow" + class="waves-effect waves-light btn-flat btn-flat-medium" title="Annehmen"> + <i class="material-icons center green-text">check_circle</i> + </button> + <button type="submit" name="deny" + class="waves-effect waves-light btn-flat btn-flat-medium" title="Ablehnen"> + <i class="material-icons center red-text">not_interested</i> + </button> + </div> + </form> + </div> + {% endfor %} + {% endif %} +</main> + +{% include 'partials/footer.html' %} diff --git a/schoolapps/fibu/templates/fibu/booking/edit.html b/schoolapps/fibu/templates/fibu/booking/edit.html new file mode 100644 index 0000000000000000000000000000000000000000..4fabb87a6fc09dbb9c0358efd787a0c611337c1f --- /dev/null +++ b/schoolapps/fibu/templates/fibu/booking/edit.html @@ -0,0 +1,24 @@ +{% include 'partials/header.html' %} +{% load material_form %} + +<main> + <h4>Antrag bearbeiten</h4> + + <form method="POST" action=""> + {% csrf_token %} + {% form form=form %} + {% part form.planned_amount prefix %}<i class="material-icons prefix">euro_symbol</i>{% endpart %} + {% endform %} + + <div class="right"> + <button type="submit" class="waves-effect waves-light btn green"> + <i class="material-icons left">save</i> Änderungen übernehmen + </button> + <a href="{% url 'fibu_index' %}" class="waves-effect waves-light btn red"> + <i class="material-icons left">cancel</i> Abbrechen + </a> + </div> + </form> +</main> + +{% include 'partials/footer.html' %} diff --git a/schoolapps/fibu/templates/fibu/booking/index.html b/schoolapps/fibu/templates/fibu/booking/index.html new file mode 100755 index 0000000000000000000000000000000000000000..3ad4fd6ebf9f6ae4ae26f842a3d5d285aa7e1381 --- /dev/null +++ b/schoolapps/fibu/templates/fibu/booking/index.html @@ -0,0 +1,68 @@ +{% include 'partials/header.html' %} +{% load material_form %} + +<main> + <div class="right"> + {% if is_archive %} + <a href="{% url 'fibu_bookings' %}" class="waves-effect waves-light btn grey">Zur aktuellen Übersicht</a> + {% else %} + <a href="{% url 'fibu_bookings_archive' "archive" %}" class="waves-effect waves-light btn grey">Zum Archiv + </a> + {% endif %} + + <a href="{% url 'fibu_bookings_new' %}" class="waves-effect waves-light btn green">Neue Buchung anlegen</a> + </div> + + <h4>{% if is_archive %}Buchungsarchiv{% else %}Aktuelle Buchungen{% endif %}</h4> + + <table> + <thead> + <tr> + <th>Artikelbeschreibung</th> + <th class="right-align">erwarteter Betrag</th> + <th class="right-align">Rechnungsbetrag</th> + <th>Buchungskonto</th> + <th>Antragsteller</th> + <th>Status</th> + <th>Aktionen</th> + </tr> + </thead> + {% for booking in bookings %} + <tr> + <td> + <a href="{% url "fibu_bookings_edit" booking.id %}">{{ booking.description }}</a> + </td> + <td class="right-align">{{ booking.planned_amount }} €</td> + <td class="right-align">{{ booking.amount }} €</td> + <td>{{ booking.account|default:"" }}</td> + <td>{{ booking.contact.get_full_name }}</td> + <td> + <span class="badge new center-align {{ booking.get_status.style_class }}"> + {{ booking.get_status.name }} + </span> + </td> + <td> + <span class="left"> + <a class="waves-effect waves-light btn-flat btn-flat-medium green-text" title="Bearbeiten" + href="{% url "fibu_bookings_edit" booking.id %}"> + <i class="material-icons">edit</i> + </a> + </span> + {% if booking.status < 2 %} + <form method="POST" class="left"> + {% csrf_token %} + <input type="hidden" value="{{ booking.id }}" name="booking-id"> + <button type="submit" + onclick="return confirm('Wirklich löschen?')" + name="cancel" class="waves-effect waves-light btn-flat btn-flat-medium red-text" + title="Löschen"> + <i class="material-icons center">cancel</i> + </button> + </form> + {% endif %} + </td> + </tr> + {% endfor %} + </table> +</main> +{% include 'partials/footer.html' %} diff --git a/schoolapps/fibu/templates/fibu/booking/new.html b/schoolapps/fibu/templates/fibu/booking/new.html new file mode 100644 index 0000000000000000000000000000000000000000..d79cdd2595c1950ee2ec505e0dd8923be165fbdb --- /dev/null +++ b/schoolapps/fibu/templates/fibu/booking/new.html @@ -0,0 +1,23 @@ +{% include 'partials/header.html' %} +{% load material_form %} + +<main> + <h4>Neue Buchung</h4> + + <form method="POST"> + {% csrf_token %} + {% form form=form %} + {% endform %} + + <span class="right"> + <button type="submit" class="waves-effect waves-light btn green"> + <i class="material-icons left">save</i> Buchung anlegen + </button> + <a href="{% url 'fibu_bookings' %}" class="waves-effect waves-light btn red"> + <i class="material-icons left">cancel</i> Abbrechen + </a> + </span> + </form> +</main> + +{% include 'partials/footer.html' %} diff --git a/schoolapps/fibu/templates/fibu/cost_center/edit.html b/schoolapps/fibu/templates/fibu/cost_center/edit.html new file mode 100644 index 0000000000000000000000000000000000000000..e251f5a3333100c1860bea9c12ce16cb92131f45 --- /dev/null +++ b/schoolapps/fibu/templates/fibu/cost_center/edit.html @@ -0,0 +1,23 @@ +{% include 'partials/header.html' %} +{% load material_form %} + +<main> + <h4>Kostenstelle bearbeiten</h4> + + <form method="POST"> + {% csrf_token %} + {% form form=form %} + {% endform %} + + <span class="right"> + <button type="submit" class="waves-effect waves-light btn green"> + <i class="material-icons left">save</i> Änderungen übernehmen + </button> + <a href="{% url 'fibu_cost_centers' %}" class="waves-effect waves-light btn red"> + <i class="material-icons left">cancel</i> Abbrechen + </a> + </span> + </form> +</main> + +{% include 'partials/footer.html' %} diff --git a/schoolapps/fibu/templates/fibu/cost_center/index.html b/schoolapps/fibu/templates/fibu/cost_center/index.html new file mode 100755 index 0000000000000000000000000000000000000000..87d6b9af09f482dcdf2a5e49de7b341d42076f77 --- /dev/null +++ b/schoolapps/fibu/templates/fibu/cost_center/index.html @@ -0,0 +1,72 @@ +{% include 'partials/header.html' %} +{% load material_form %} + +<main> + <a class="waves-effect waves-light btn green modal-trigger right" href="#new-modal"> + <i class="material-icons left">add</i> Kostenstelle anlegen + </a> + + <h4>Kostenstellen</h4> + + {% if form.errors %} + <script> + var onFinish = function () { + $("#new-modal").modal("open"); + }; + </script> + {% endif %} + + <form method="POST"> + <div id="new-modal" class="modal"> + <div class="modal-content"> + <h5>Neue Kostenstelle anlegen</h5> + {% csrf_token %} + {% form form=form %} + {% endform %} + </div> + <div class="modal-footer"> + <button type="submit" class="waves-effect waves-light btn green"> + <i class="material-icons left">save</i> Kostenstelle anlegen + </button> + </div> + </div> + </form> + + <table> + <thead> + <tr> + <th>Kostenstelle</th> + <th>Jahr</th> + <th>Aktionen</th> + </tr> + </thead> + {% for cost_center in cost_centers %} + <tr> + <td>{{ cost_center.name }}</td> + <td>{{ cost_center.year }}</td> + <td> + <form action="{% url 'fibu_cost_centers_edit' cost_center.id %}" class="left"> + {% csrf_token %} + <input type="hidden" value="{{ cost_center.id }}" name="id"> + <button type="submit" name="edit" + class="waves-effect waves-light btn-flat btn-flat-medium" title="Bearbeiten"> + <i class="material-icons center green-text">create</i> + </button> + </form> + <form action="" method="POST" class="left"> + {% csrf_token %} + <input type="hidden" value="{{ cost_center.id }}" name="id"> + <button type="submit" + onclick="return confirm('Wollen Sie die Kostenstelle wirklich löschen?')" + name="cancel" class="waves-effect waves-light btn-flat btn-flat-medium" + title="Löschen"> + <i class="material-icons center red-text">cancel</i> + </button> + </form> + </td> + </tr> + {% endfor %} + </table> +</main> + +{% include 'partials/footer.html' %} diff --git a/schoolapps/fibu/templates/fibu/index.html b/schoolapps/fibu/templates/fibu/index.html new file mode 100755 index 0000000000000000000000000000000000000000..3eca9a5d3ba7980a266d30c79d0f0661f0141d15 --- /dev/null +++ b/schoolapps/fibu/templates/fibu/index.html @@ -0,0 +1,108 @@ +{% include 'partials/header.html' %} +{% load material_form %} + +<main> + <a class="waves-effect waves-light btn green modal-trigger right" href="#new-modal"> + <i class="material-icons left">add</i> Antrag stellen + </a> + <h4>Meine Anträge</h4> + + + {% if form.errors %} + <script> + var onFinish = function () { + $("#new-modal").modal("open"); + }; + </script> + {% endif %} + + <form method="POST"> + <div id="new-modal" class="modal"> + <div class="modal-content"> + <h5>Neuen Antrag stellen</h5> + {% csrf_token %} + {% form form=form %} + {% part form.planned_amount prefix %}<i class="material-icons prefix">euro_symbol</i> + {% endpart %} + {% endform %} + </div> + <div class="modal-footer"> + <button type="submit" class="waves-effect waves-light btn green"> + <i class="material-icons left">send</i> Antrag stellen + </button> + </div> + </div> + </form> + +<div class="collection-item row"> + <div class="col s5"><strong>Beschreibung</strong></div> + <div class="col s2 right-align"><strong>erwarteter Betrag</strong></div> + <div class="col s2 right-align"><strong>Rechnungsbetrag</strong></div> + <div class="col s1 center-align"><strong>Status</strong></div> + <div class="col s2 center-align"><strong>Aktionen</strong></div> + </div> + <div class="collection"> + {% for booking in bookings %} + <div class="collection-item row"> + <div class="col s5">{{ booking.description }}</div> + <div class="col s2 right-align">{{ booking.planned_amount }} €</div> + <div class="col s2 right-align">{{ booking.amount }} €</div> + <div class="col s1"> + <span class="badge new {{ booking.get_status.style_class }}">{{ booking.get_status.name }}</span> + </div> + <div class="col s2"> + {# Delete #} + {% if booking.status < 3 %} + <form action="" method="POST" class="right"> + {% csrf_token %} + <input type="hidden" value="{{ booking.id }}" name="booking-id"> + <button type="submit" + onclick="return confirm('Wollen Sie den Antrag wirklich zurücknehmen?')" + name="cancel" class="waves-effect waves-light btn-flat btn-flat-medium" + title="Antrag zurücknehmen"> + <i class="material-icons center red-text">cancel</i> + </button> + </form> + {% endif %} + + {# Edit #} + {% if booking.status == 0 %} + <form action="{% url 'fibu_bookings_user_edit' booking.id %}" class="right"> + {% csrf_token %} + <input type="hidden" value="{{ booking.id }}" name="booking-id"> + <button type="submit" name="edit" + class="waves-effect waves-light btn-flat btn-flat-medium right" + title="Bearbeiten"> + <i class="material-icons center green-text">create</i> + </button> + </form> + {% endif %} + + <form action="" method="POST" class="right"> + {% csrf_token %} + <input type="hidden" value="{{ booking.id }}" name="booking-id"> + <input type="hidden" value="{{ booking.status }}" name="booking-status"> + + + {% if booking.status == 2 %} + <button type="submit" name="ordered" + class="waves-effect waves-light btn-flat btn-flat-medium left" + title="Status auf 'bestellt' ändern"> + <i class="material-icons center green-text">shopping_cart</i> + </button> + + + {% elif booking.status == 3 %} + <button type="submit" name="submit-invoice" + class="waves-effect waves-light btn-flat btn-flat-medium left" + title="Status auf 'Rechnung eingereicht' ändern"> + <i class="material-icons center green-text">description</i> + </button> + {% endif %} + </form> + </div> + </div> + {% endfor %} + </div> +</main> +{% include 'partials/footer.html' %} diff --git a/schoolapps/fibu/templates/fibu/reports/expenses.html b/schoolapps/fibu/templates/fibu/reports/expenses.html new file mode 100755 index 0000000000000000000000000000000000000000..c35fb8ec1d032c9ad8f1943005f1b0f02d1eda9b --- /dev/null +++ b/schoolapps/fibu/templates/fibu/reports/expenses.html @@ -0,0 +1,36 @@ +{% include 'partials/header.html' %} +{% load material_form %} + +<main> + <h4>Schlussrechnung</h4> + + <div class="collection"> + {% for cost_center, accounts in cost_center_accounts.items %} + <h5 class="collection-item">{{ cost_center }}</h5> + + <div class="collection"> + <div class="row"> + <span class="col s5 m5 white-text grey">Buchungskonto</span> + <span class="col s2 m2 white-text grey right-align">Budget</span> + <span class="col s1 m1 white-text grey right-align">Einnahmen</span> + <span class="col s1 m1 white-text grey right-align">Ausgaben</span> + <span class="col s1 m1 white-text grey right-align">Reste</span> + </div> + {% for account in accounts %} + <div class="collection-item row"> + <span class="col s5 m5">{{ account.name }}</span> + <span class="col s2 m2 right-align">{{ account.budget }} €</span> + <span class="col s1 m1 right-align"> + {% if account.income %}{{ account.saldo }} €{% endif %} + </span> + <span class="col s1 m1 right-align"> + {% if not account.income %}{{ account.saldo }} €{% endif %} + </span> + <span class="col s1 m1 right-align">{{ account.rest }} €</span> + </div> + {% endfor %} + </div> + {% endfor %} + </div> +</main> +{% include 'partials/footer.html' %} diff --git a/schoolapps/fibu/templates/fibu/reports/index.html b/schoolapps/fibu/templates/fibu/reports/index.html new file mode 100755 index 0000000000000000000000000000000000000000..5399308743095cfc65dfe4b93de840618b9a16e4 --- /dev/null +++ b/schoolapps/fibu/templates/fibu/reports/index.html @@ -0,0 +1,11 @@ +{% include 'partials/header.html' %} +{% load material_form %} + +<main> + <h4>Berichte</h4> + + <div class="collection"> + <a href="{% url "fibu_reports_expenses" %}" class="collection-item">Ausgaben</a> + </div> +</main> +{% include 'partials/footer.html' %} diff --git a/schoolapps/fibu/urls.py b/schoolapps/fibu/urls.py new file mode 100755 index 0000000000000000000000000000000000000000..42d8911ac49f9eca2867a53769ce15dc159d033b --- /dev/null +++ b/schoolapps/fibu/urls.py @@ -0,0 +1,19 @@ +from django.urls import path +from . import views + + +urlpatterns = [ + path('', views.index, name='fibu_index'), + path('<int:pk>/', views.user_edit, name='fibu_bookings_user_edit'), + path('bookings/check/', views.check, name='fibu_bookings_check'), + path('bookings/new/', views.new_booking, name='fibu_bookings_new'), + path('bookings/', views.booking, name='fibu_bookings'), + path('bookings/<str:is_archive>/', views.booking, name='fibu_bookings_archive'), + path('bookings/<int:pk>/edit/', views.book, name='fibu_bookings_edit'), + path('costcenters/', views.cost_centers, name='fibu_cost_centers'), + path('costcenters/edit/<int:pk>/', views.cost_center_edit, name='fibu_cost_centers_edit'), + path('accounts/', views.account, name='fibu_accounts'), + path('accounts/<int:pk>/', views.account_edit, name='fibu_accounts_edit'), + path('reports/', views.reports, name='fibu_reports'), + path('reports/expenses/', views.expenses, name='fibu_reports_expenses'), +] diff --git a/schoolapps/fibu/views.py b/schoolapps/fibu/views.py new file mode 100644 index 0000000000000000000000000000000000000000..ea3c3afcc38ce7b70949c2d8e5fdfde15ae7b514 --- /dev/null +++ b/schoolapps/fibu/views.py @@ -0,0 +1,266 @@ +from django.contrib import messages +from django.contrib.auth.decorators import login_required, permission_required +from django.db.models import Sum +from django.urls import reverse +from django.shortcuts import render, redirect, get_object_or_404 +from .models import Booking, CostCenter, Account +from .filters import BookingFilter +from .forms import SimpleBookingForm, CheckBookingForm, CompleteBookingForm, CostCenterForm, AccountForm + + +@login_required +@permission_required('fibu.request_booking') +def index(request): + if request.method == 'POST': + if 'booking-id' in request.POST: + booking_id = request.POST['booking-id'] + booking = get_object_or_404(Booking, pk=booking_id) + + if 'cancel' in request.POST: + booking.delete() + print('Eintrag gelöscht') + return redirect('fibu_index') + + elif 'ordered' in request.POST: + Booking.objects.filter(id=booking_id).update(status=3) + return redirect('fibu_index') + + elif 'submit-invoice' in request.POST: + Booking.objects.filter(id=booking_id).update(status=4) + return redirect('fibu_index') + + form = SimpleBookingForm(instance=booking) + else: + form = SimpleBookingForm(request.POST) + else: + form = SimpleBookingForm() + + if form.is_valid(): + description = form.cleaned_data['description'] + planned_amount = form.cleaned_data['planned_amount'] + justification = form.cleaned_data['justification'] + booking = Booking(description=description, planned_amount=planned_amount, contact=request.user, + justification=justification) + booking.save() + + messages.success(request, "Der Antrag wurde erfolgreich übermittelt.") + + return redirect('fibu_index') + + bookings = Booking.objects.filter(contact=request.user).order_by('status') + + context = {'bookings': bookings, 'form': form} + return render(request, 'fibu/index.html', context) + + +@login_required +@permission_required('fibu.request_booking') +def user_edit(request, pk): + booking = get_object_or_404(Booking, pk=pk) + form = SimpleBookingForm(instance=booking) + + if request.method == 'POST': + form = SimpleBookingForm(request.POST, instance=booking) + + if form.is_valid(): + form.save() + + messages.success(request, "Die Änderungen am Antrag wurden erfolgreich übernommen.") + return redirect(reverse('fibu_index')) + + context = {'form': form} + return render(request, 'fibu/booking/edit.html', context) + + +@login_required +@permission_required('fibu.check_booking') +def check(request): + if request.method == 'POST': + if 'booking-id' in request.POST: + booking_id = request.POST['booking-id'] + if 'allow' in request.POST: + if "account" in request.POST: + account = request.POST['account'] + Booking.objects.filter(id=booking_id).update(status=2, account=account) + messages.success(request, "Der Antrag wurde angenommen.") + else: + messages.error(request, "Bitte wähle ein Buchungskonto aus, um den Antrag anzunehmen.") + elif 'deny' in request.POST: + Booking.objects.filter(id=booking_id).update(status=1) + messages.success(request, "Der Antrag wurde abgelehnt.") + + booking_list = Booking.objects.filter(status=0).order_by('submission_date') + bookings = BookingFilter(request.GET, queryset=booking_list) + form = CheckBookingForm() + return render(request, 'fibu/booking/check.html', {'filter': bookings, 'form': form}) + + +@login_required +@permission_required('fibu.manage_booking') +def booking(request, is_archive=""): + is_archive = is_archive == "archive" + if is_archive: + bookings = Booking.objects.filter(status=5).order_by('-status') + else: + bookings = Booking.objects.filter(status__lt=5).order_by('-status') + context = {'bookings': bookings, 'is_archive': is_archive} + return render(request, 'fibu/booking/index.html', context) + + +@login_required +@permission_required('fibu.manage_booking') +def book(request, pk): + booking = get_object_or_404(Booking, pk=pk) + form = CompleteBookingForm(instance=booking) + template = 'fibu/booking/book.html' + if request.method == 'POST': + form = CompleteBookingForm(request.POST, request.FILES, instance=booking) + if form.is_valid(): + form.save() + messages.success(request, "Die Änderungen an der Buchung wurden erfolgreich übernommen.") + return redirect(reverse('fibu_bookings')) + context = {'form': form} + return render(request, template, context) + + +@login_required +@permission_required('fibu.manage_booking') +def new_booking(request): + form = CompleteBookingForm() + template = 'fibu/booking/new.html' + if request.method == 'POST': + form = CompleteBookingForm(request.POST, request.FILES) + if form.is_valid(): + form.save() + messages.success(request, "Die Buchung wurde erfolgreich angelegt.") + + return redirect(reverse('fibu_bookings')) + context = {'form': form} + return render(request, template, context) + + +@login_required +@permission_required('fibu.manage_costcenter') +def cost_centers(request): + form = CostCenterForm() + + if request.method == 'POST': + if 'id' in request.POST and 'cancel' in request.POST: + cost_center_id = request.POST['id'] + cost_center = CostCenter.objects.get(id=cost_center_id) + cost_center.delete() + + messages.success(request, "Die Kostenstelle wurde erfolgreich gelöscht.") + + return redirect('fibu_cost_centers') + else: + form = CostCenterForm(request.POST) + + if form.is_valid(): + form.save() + messages.success(request, "Die Kostenstelle wurde erfolgreich angelegt.") + return redirect('fibu_cost_centers') + + cost_centers = CostCenter.objects.filter() + + context = {'cost_centers': cost_centers, 'form': form} + return render(request, 'fibu/cost_center/index.html', context) + + +@login_required +@permission_required('fibu.manage_costcenter') +def cost_center_edit(request, pk): + cost_center = get_object_or_404(CostCenter, pk=pk) + form = CostCenterForm(instance=cost_center) + + if request.method == 'POST': + form = CostCenterForm(request.POST, instance=cost_center) + + if form.is_valid(): + form.save() + + messages.success(request, "Die Änderungen an der Kostenstelle wurden erfolgreich übernommen.") + + return redirect(reverse('fibu_cost_centers')) + + context = {'form': form} + return render(request, 'fibu/cost_center/edit.html', context) + + +@login_required +@permission_required('fibu.manage_account') +def account(request): + form = AccountForm() + + if request.method == 'POST': + if 'account-id' in request.POST and 'cancel' in request.POST: + account_id = request.POST['id'] + account = Account.objects.get(id=account_id) + account.delete() + + messages.success(request, "Das Buchungskonto wurde erfolgreich gelöscht") + return redirect('account') + else: + form = AccountForm(request.POST) + + if form.is_valid(): + form.save() + + messages.success(request, "Das Buchungskonto wurde erfolgreich angelegt.") + return redirect('account') + + accounts = Account.objects.filter().order_by('cost_center', '-income', 'name') + context = {'accounts': accounts, 'form': form} + return render(request, 'fibu/account/index.html', context) + + +@login_required +@permission_required('fibu.manage_account') +def account_edit(request, pk): + account = get_object_or_404(Account, pk=pk) + form = AccountForm(instance=account) + + if request.method == 'POST': + form = AccountForm(request.POST, instance=account) + + if form.is_valid(): + form.save() + + messages.success(request, "Die Änderungen am Buchungskonto wurden erfolgreich übernommen.") + return redirect(reverse('account')) + + context = {'form': form} + return render(request, 'fibu/account/edit.html', context) + + +@login_required +@permission_required('fibu.manage_booking') +def reports(request): + return render(request, 'fibu/reports/index.html') + + +@login_required +@permission_required('fibu.manage_booking') +def expenses(request): + cost_centers = CostCenter.objects.filter() + cost_center_accounts = {} + account_rests = {} + for cost_center in cost_centers: + accounts = Account.objects.filter(cost_center=cost_center) + # update saldo + for account in accounts: + saldo = Booking.objects.filter(account=account).aggregate(Sum('amount')) + saldo = saldo['amount__sum'] + try: + rest = account.budget - saldo + except: + rest = account.budget + try: + Account.objects.filter(id=account.id).update(saldo=saldo, rest=rest) + except: + Account.objects.filter(id=account.id).update(saldo=0, rest=account.budget) + + cost_center_accounts[cost_center.name] = list( + Account.objects.filter(cost_center=cost_center).order_by('-income')) + context = {'cost_center_accounts': cost_center_accounts, 'account_rests': account_rests} + return render(request, 'fibu/reports/expenses.html', context) diff --git a/schoolapps/meta.py b/schoolapps/meta.py index 17d6f3a65229465c39108f62d4241abf443076a4..9af2f86c59f640d67970a38470a11f3f616d5fbe 100644 --- a/schoolapps/meta.py +++ b/schoolapps/meta.py @@ -10,7 +10,7 @@ with open(copyright_path, "r") as f: COPYRIGHT_SHORT = "© 2018–2019 Mitglieder der Computer-AG, Katharineum zu Lübeck" -VERSION = '1.1.2 "Aebli"' +VERSION = '1.1.4 "Aebli"' LICENSE_APACHE_2 = "Apache 2.0 License" LICENSE_BSD = "2-Clause BSD License" diff --git a/schoolapps/schoolapps/settings.py b/schoolapps/schoolapps/settings.py index 5239dc663f357b6cd3550a7833320ac967c16022..d6a2219b1b0f2cf7cc374e2db8a14020cb85bda5 100755 --- a/schoolapps/schoolapps/settings.py +++ b/schoolapps/schoolapps/settings.py @@ -34,6 +34,7 @@ INSTALLED_APPS = [ 'dashboard.apps.DashboardConfig', "debug.apps.DebugConfig", 'aub.apps.AubConfig', + 'fibu.apps.FibuConfig', 'untisconnect.apps.UntisconnectConfig', 'timetable.apps.TimetableConfig', 'menu.apps.MenuConfig', diff --git a/schoolapps/schoolapps/urls.py b/schoolapps/schoolapps/urls.py index 2e24ef70e17da0a8ab6183c2319ad83b4cd1db07..ba42b84d22a00a0aefe57eb87e08ac9daf6fba37 100755 --- a/schoolapps/schoolapps/urls.py +++ b/schoolapps/schoolapps/urls.py @@ -45,6 +45,11 @@ urlpatterns = [ ####### path('aub/', include('aub.urls')), + ######## + # FIBU # + ######## + path('fibu/', include('fibu.urls')), + ############# # TIMETABLE # ############# diff --git a/schoolapps/static/common/helper.js b/schoolapps/static/common/helper.js index 235e9e61f6c87828a7e7e182cd8c98ced9d81a87..d552d9a659aa5d479f32486800a98d61e50a6972 100644 --- a/schoolapps/static/common/helper.js +++ b/schoolapps/static/common/helper.js @@ -96,10 +96,17 @@ $(document).ready(function () { // 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(); + } }); \ No newline at end of file diff --git a/schoolapps/static/common/serviceworker.js b/schoolapps/static/common/serviceworker.js index 25c0ca0d31ec83b7e1339728608f3c09191ede4c..babd95c4dea5ab66a59b5b0c7eccdba48bed8524 100644 --- a/schoolapps/static/common/serviceworker.js +++ b/schoolapps/static/common/serviceworker.js @@ -1,22 +1,19 @@ -//This is the SchoolApps service worker with Advanced caching +//This is the SchoolApps service worker const CACHE = "schoolapps-cache"; const precacheFiles = [ - '/', - '/faq', + '', + '/faq/', ]; const offlineFallbackPage = '/offline'; -const cacheFirstPaths = [ - '/faq', -]; - const avoidCachingPaths = [ '/admin', '/settings', '/support', + '/tools', '/faq/ask', '/aub/apply_for', '/aub/check1', @@ -45,14 +42,14 @@ function comparePaths(requestUrl, pathsArray) { } self.addEventListener("install", function (event) { - console.log("[SchoolApps PWA] Install Event processing"); + console.log("[SchoolApps PWA] Install Event processing."); - console.log("[SchoolApps PWA] Skip waiting on install"); + 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"); + console.log("[SchoolApps PWA] Caching pages during install."); return cache.addAll(precacheFiles).then(function () { return cache.add(offlineFallbackPage); @@ -63,84 +60,43 @@ self.addEventListener("install", function (event) { // Allow sw to control of current page self.addEventListener("activate", function (event) { - console.log("[SchoolApps PWA] Claiming clients for current page"); + 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; - - if (comparePaths(event.request.url, cacheFirstPaths)) { - cacheFirstFetch(event); - } else { - networkFirstFetch(event); - } + networkFirstFetch(event); }); -function cacheFirstFetch(event) { - event.respondWith( - fromCache(event.request).then( - function (response) { - // The response was found in the cache so we respond with it and update the entry - - // This is where we call the server to get the newest version of the - // file to use the next time we show view - event.waitUntil( - fetch(event.request).then(function (response) { - return updateCache(event.request, response); - }) - ); - - return response; - }, - function () { - // The response was not found in the cache so we look for it on the server - return fetch(event.request) - .then(function (response) { - // If request was successful, add or update it in the cache - event.waitUntil(updateCache(event.request, response.clone())); - - return response; - }) - .catch(function (error) { - // The following validates that the request was for a navigation to a new document - if (event.request.destination !== "document" || event.request.mode !== "navigate") { - return; - } - - console.log("[SchoolApps PWA] Network request failed and no cache." + error); - // Use the precached offline page as fallback - return caches.match(offlineFallbackPage) - }); - } - ) - ); -} - 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.request); + return fromCache(event); }) ); } -function fromCache(request) { +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(request).then(function (matching) { + return cache.match(event.request) + .then(function (matching) { if (!matching || matching.status === 404) { - return caches.match(offlineFallbackPage); + console.log("[SchoolApps PWA] Cache request failed. Serving offline fallback page."); + // Use the precached offline page as fallback + return caches.match(offlineFallbackPage) } return matching; diff --git a/schoolapps/static/common/style.css b/schoolapps/static/common/style.css index e94e7d303657fe61f2f7abbd23e0b8fd46803736..c418d66e7b209a29b46122e9e8d9afae5e5ea911 100644 --- a/schoolapps/static/common/style.css +++ b/schoolapps/static/common/style.css @@ -249,7 +249,7 @@ table.substitutions td, table.substitutions th { } .lesson-card a, .substitutions a { - color: black; + color: inherit; } /*.timetable-time {*/ @@ -312,10 +312,11 @@ table.substitutions td, table.substitutions th { /* Table*/ -table.striped > tbody > tr:nth-child(odd) { +table.striped > tbody > tr:nth-child(odd), table tr.striped { background-color: rgba(208, 208, 208, 0.5); } + /*+++++++*/ /* Print */ /*+++++++*/ @@ -402,7 +403,7 @@ table.striped > tbody > tr:nth-child(odd) { border-color: #b71c1c; } -.alert.primary > p, .alert.primary > div { +.alert.primary > p, .alert.primary > div, .alert.info > p, .alert.info > div { background-color: #ececec; border-color: #da1f3d; } diff --git a/schoolapps/static/common/workbox-sw.js b/schoolapps/static/common/workbox-sw.js deleted file mode 100644 index 61b3289a81a12f15841f55e57ace9063bafa3e51..0000000000000000000000000000000000000000 --- a/schoolapps/static/common/workbox-sw.js +++ /dev/null @@ -1,2 +0,0 @@ -!function(){"use strict";try{self["workbox:sw:4.3.1"]&&_()}catch(t){}const t="https://storage.googleapis.com/workbox-cdn/releases/4.3.1",e={backgroundSync:"background-sync",broadcastUpdate:"broadcast-update",cacheableResponse:"cacheable-response",core:"core",expiration:"expiration",googleAnalytics:"offline-ga",navigationPreload:"navigation-preload",precaching:"precaching",rangeRequests:"range-requests",routing:"routing",strategies:"strategies",streams:"streams"};self.workbox=new class{constructor(){return this.v={},this.t={debug:"localhost"===self.location.hostname,modulePathPrefix:null,modulePathCb:null},this.s=this.t.debug?"dev":"prod",this.o=!1,new Proxy(this,{get(t,s){if(t[s])return t[s];const o=e[s];return o&&t.loadModule(`workbox-${o}`),t[s]}})}setConfig(t={}){if(this.o)throw new Error("Config must be set before accessing workbox.* modules");Object.assign(this.t,t),this.s=this.t.debug?"dev":"prod"}loadModule(t){const e=this.i(t);try{importScripts(e),this.o=!0}catch(s){throw console.error(`Unable to import module '${t}' from '${e}'.`),s}}i(e){if(this.t.modulePathCb)return this.t.modulePathCb(e,this.t.debug);let s=[t];const o=`${e}.${this.s}.js`,r=this.t.modulePathPrefix;return r&&""===(s=r.split("/"))[s.length-1]&&s.splice(s.length-1,1),s.push(o),s.join("/")}}}(); -//# sourceMappingURL=workbox-sw.js.map diff --git a/schoolapps/static/css/paper.css b/schoolapps/static/css/paper.css new file mode 100644 index 0000000000000000000000000000000000000000..9e5b66db1a7f87dca6123920a4b03bb54e0270a6 --- /dev/null +++ b/schoolapps/static/css/paper.css @@ -0,0 +1,134 @@ +/* MIT © Tsutomu Kawamura */ +/* Edited by SchoolApps Team */ + +@page { + margin: 0 +} + +body { + margin: 0 +} + +.sheet { + margin: 0; + overflow: hidden; + position: relative; + box-sizing: border-box; + page-break-after: always; +} + +/** Paper sizes **/ +body.A3 .sheet { + width: 297mm; + height: 419mm +} + +body.A3.landscape .sheet { + width: 420mm; + height: 296mm +} + +body.A4 .sheet { + width: 210mm; + height: 296mm +} + +body.A4.landscape .sheet { + width: 297mm; + height: 209mm +} + +body.A5 .sheet { + width: 148mm; + height: 209mm +} + +body.A5.landscape .sheet { + width: 210mm; + height: 147mm +} + +body.letter .sheet { + width: 216mm; + height: 279mm +} + +body.letter.landscape .sheet { + width: 280mm; + height: 215mm +} + +body.legal .sheet { + width: 216mm; + height: 356mm +} + +body.legal.landscape .sheet { + width: 357mm; + height: 215mm +} + +.sheet.infinitive { + height: auto !important; +} + +/** Padding area **/ +.sheet.padding-10mm { + padding: 10mm +} + +.sheet.padding-15mm { + padding: 15mm +} + +.sheet.padding-20mm { + padding: 20mm +} + +.sheet.padding-25mm { + padding: 25mm +} + +/** For screen preview **/ +@media screen { + body { + background: #e0e0e0 + } + + .sheet { + background: white; + box-shadow: 0 .5mm 2mm rgba(0, 0, 0, .3); + margin: 5mm auto; + } +} + +/** Fix for Chrome issue #273306 **/ +@media print { + body.A3.landscape { + width: 420mm + } + + body.A3, body.A4.landscape { + width: 297mm + } + + body.A4, body.A5.landscape { + width: 210mm + } + + body.A5 { + width: 148mm + } + + body.letter, body.legal { + width: 216mm + } + + body.letter.landscape { + width: 280mm + } + + body.legal.landscape { + width: 357mm + } +} diff --git a/schoolapps/static/js/react/prop-types.development.js b/schoolapps/static/js/react/prop-types.development.js new file mode 100644 index 0000000000000000000000000000000000000000..fba1dfd762c08a9987fc18cff24d9b61f6dc6f1a --- /dev/null +++ b/schoolapps/static/js/react/prop-types.development.js @@ -0,0 +1,917 @@ +(function (f) { + if (typeof exports === "object" && typeof module !== "undefined") { + module.exports = f() + } else if (typeof define === "function" && define.amd) { + define([], f) + } else { + var g; + if (typeof window !== "undefined") { + g = window + } else if (typeof global !== "undefined") { + g = global + } else if (typeof self !== "undefined") { + g = self + } else { + g = this + } + g.PropTypes = f() + } +})(function () { + var define, module, exports; + return (function e(t, n, r) { + function s(o, u) { + if (!n[o]) { + if (!t[o]) { + var a = typeof require == "function" && require; + if (!u && a) return a(o, !0); + if (i) return i(o, !0); + var f = new Error("Cannot find module '" + o + "'"); + throw f.code = "MODULE_NOT_FOUND", f + } + var l = n[o] = {exports: {}}; + t[o][0].call(l.exports, function (e) { + var n = t[o][1][e]; + return s(n ? n : e) + }, l, l.exports, e, t, n, r) + } + return n[o].exports + } + + var i = typeof require == "function" && require; + for (var o = 0; o < r.length; o++) s(r[o]); + return s + })({ + 1: [function (require, module, exports) { + /** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + + 'use strict'; + + var printWarning = function () { + }; + + if ("development" !== 'production') { + var ReactPropTypesSecret = require('./lib/ReactPropTypesSecret'); + var loggedTypeFailures = {}; + + printWarning = function (text) { + var message = 'Warning: ' + text; + if (typeof console !== 'undefined') { + console.error(message); + } + try { + // --- Welcome to debugging React --- + // This error was thrown as a convenience so that you can use this stack + // to find the callsite that caused this warning to fire. + throw new Error(message); + } catch (x) { + } + }; + } + + /** + * Assert that the values match with the type specs. + * Error messages are memorized and will only be shown once. + * + * @param {object} typeSpecs Map of name to a ReactPropType + * @param {object} values Runtime values that need to be type-checked + * @param {string} location e.g. "prop", "context", "child context" + * @param {string} componentName Name of the component for error messages. + * @param {?Function} getStack Returns the component stack. + * @private + */ + function checkPropTypes(typeSpecs, values, location, componentName, getStack) { + if ("development" !== 'production') { + for (var typeSpecName in typeSpecs) { + if (typeSpecs.hasOwnProperty(typeSpecName)) { + var error; + // Prop type validation may throw. In case they do, we don't want to + // fail the render phase where it didn't fail before. So we log it. + // After these have been cleaned up, we'll let them throw. + try { + // This is intentionally an invariant that gets caught. It's the same + // behavior as without this statement except with a better message. + if (typeof typeSpecs[typeSpecName] !== 'function') { + var err = Error( + (componentName || 'React class') + ': ' + location + ' type `' + typeSpecName + '` is invalid; ' + + 'it must be a function, usually from the `prop-types` package, but received `' + typeof typeSpecs[typeSpecName] + '`.' + ); + err.name = 'Invariant Violation'; + throw err; + } + error = typeSpecs[typeSpecName](values, typeSpecName, componentName, location, null, ReactPropTypesSecret); + } catch (ex) { + error = ex; + } + if (error && !(error instanceof Error)) { + printWarning( + (componentName || 'React class') + ': type specification of ' + + location + ' `' + typeSpecName + '` is invalid; the type checker ' + + 'function must return `null` or an `Error` but returned a ' + typeof error + '. ' + + 'You may have forgotten to pass an argument to the type checker ' + + 'creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and ' + + 'shape all require an argument).' + ) + + } + if (error instanceof Error && !(error.message in loggedTypeFailures)) { + // Only monitor this failure once because there tends to be a lot of the + // same error. + loggedTypeFailures[error.message] = true; + + var stack = getStack ? getStack() : ''; + + printWarning( + 'Failed ' + location + ' type: ' + error.message + (stack != null ? stack : '') + ); + } + } + } + } + } + + module.exports = checkPropTypes; + + }, {"./lib/ReactPropTypesSecret": 5}], + 2: [function (require, module, exports) { + /** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + + 'use strict'; + + var ReactPropTypesSecret = require('./lib/ReactPropTypesSecret'); + + function emptyFunction() { + } + + module.exports = function () { + function shim(props, propName, componentName, location, propFullName, secret) { + if (secret === ReactPropTypesSecret) { + // It is still safe when called from React. + return; + } + var err = new Error( + 'Calling PropTypes validators directly is not supported by the `prop-types` package. ' + + 'Use PropTypes.checkPropTypes() to call them. ' + + 'Read more at http://fb.me/use-check-prop-types' + ); + err.name = 'Invariant Violation'; + throw err; + }; + shim.isRequired = shim; + + function getShim() { + return shim; + }; + // Important! + // Keep this list in sync with production version in `./factoryWithTypeCheckers.js`. + var ReactPropTypes = { + array: shim, + bool: shim, + func: shim, + number: shim, + object: shim, + string: shim, + symbol: shim, + + any: shim, + arrayOf: getShim, + element: shim, + instanceOf: getShim, + node: shim, + objectOf: getShim, + oneOf: getShim, + oneOfType: getShim, + shape: getShim, + exact: getShim + }; + + ReactPropTypes.checkPropTypes = emptyFunction; + ReactPropTypes.PropTypes = ReactPropTypes; + + return ReactPropTypes; + }; + + }, {"./lib/ReactPropTypesSecret": 5}], + 3: [function (require, module, exports) { + /** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + + 'use strict'; + + var assign = require('object-assign'); + + var ReactPropTypesSecret = require('./lib/ReactPropTypesSecret'); + var checkPropTypes = require('./checkPropTypes'); + + var printWarning = function () { + }; + + if ("development" !== 'production') { + printWarning = function (text) { + var message = 'Warning: ' + text; + if (typeof console !== 'undefined') { + console.error(message); + } + try { + // --- Welcome to debugging React --- + // This error was thrown as a convenience so that you can use this stack + // to find the callsite that caused this warning to fire. + throw new Error(message); + } catch (x) { + } + }; + } + + function emptyFunctionThatReturnsNull() { + return null; + } + + module.exports = function (isValidElement, throwOnDirectAccess) { + /* global Symbol */ + var ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator; + var FAUX_ITERATOR_SYMBOL = '@@iterator'; // Before Symbol spec. + + /** + * Returns the iterator method function contained on the iterable object. + * + * Be sure to invoke the function with the iterable as context: + * + * var iteratorFn = getIteratorFn(myIterable); + * if (iteratorFn) { + * var iterator = iteratorFn.call(myIterable); + * ... + * } + * + * @param {?object} maybeIterable + * @return {?function} + */ + function getIteratorFn(maybeIterable) { + var iteratorFn = maybeIterable && (ITERATOR_SYMBOL && maybeIterable[ITERATOR_SYMBOL] || maybeIterable[FAUX_ITERATOR_SYMBOL]); + if (typeof iteratorFn === 'function') { + return iteratorFn; + } + } + + /** + * Collection of methods that allow declaration and validation of props that are + * supplied to React components. Example usage: + * + * var Props = require('ReactPropTypes'); + * var MyArticle = React.createClass({ + * propTypes: { + * // An optional string prop named "description". + * description: Props.string, + * + * // A required enum prop named "category". + * category: Props.oneOf(['News','Photos']).isRequired, + * + * // A prop named "dialog" that requires an instance of Dialog. + * dialog: Props.instanceOf(Dialog).isRequired + * }, + * render: function() { ... } + * }); + * + * A more formal specification of how these methods are used: + * + * type := array|bool|func|object|number|string|oneOf([...])|instanceOf(...) + * decl := ReactPropTypes.{type}(.isRequired)? + * + * Each and every declaration produces a function with the same signature. This + * allows the creation of custom validation functions. For example: + * + * var MyLink = React.createClass({ + * propTypes: { + * // An optional string or URI prop named "href". + * href: function(props, propName, componentName) { + * var propValue = props[propName]; + * if (propValue != null && typeof propValue !== 'string' && + * !(propValue instanceof URI)) { + * return new Error( + * 'Expected a string or an URI for ' + propName + ' in ' + + * componentName + * ); + * } + * } + * }, + * render: function() {...} + * }); + * + * @internal + */ + + var ANONYMOUS = '<<anonymous>>'; + + // Important! + // Keep this list in sync with production version in `./factoryWithThrowingShims.js`. + var ReactPropTypes = { + array: createPrimitiveTypeChecker('array'), + bool: createPrimitiveTypeChecker('boolean'), + func: createPrimitiveTypeChecker('function'), + number: createPrimitiveTypeChecker('number'), + object: createPrimitiveTypeChecker('object'), + string: createPrimitiveTypeChecker('string'), + symbol: createPrimitiveTypeChecker('symbol'), + + any: createAnyTypeChecker(), + arrayOf: createArrayOfTypeChecker, + element: createElementTypeChecker(), + instanceOf: createInstanceTypeChecker, + node: createNodeChecker(), + objectOf: createObjectOfTypeChecker, + oneOf: createEnumTypeChecker, + oneOfType: createUnionTypeChecker, + shape: createShapeTypeChecker, + exact: createStrictShapeTypeChecker, + }; + + /** + * inlined Object.is polyfill to avoid requiring consumers ship their own + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is + */ + + /*eslint-disable no-self-compare*/ + function is(x, y) { + // SameValue algorithm + if (x === y) { + // Steps 1-5, 7-10 + // Steps 6.b-6.e: +0 != -0 + return x !== 0 || 1 / x === 1 / y; + } else { + // Step 6.a: NaN == NaN + return x !== x && y !== y; + } + } + + /*eslint-enable no-self-compare*/ + + /** + * We use an Error-like object for backward compatibility as people may call + * PropTypes directly and inspect their output. However, we don't use real + * Errors anymore. We don't inspect their stack anyway, and creating them + * is prohibitively expensive if they are created too often, such as what + * happens in oneOfType() for any type before the one that matched. + */ + function PropTypeError(message) { + this.message = message; + this.stack = ''; + } + + // Make `instanceof Error` still work for returned errors. + PropTypeError.prototype = Error.prototype; + + function createChainableTypeChecker(validate) { + if ("development" !== 'production') { + var manualPropTypeCallCache = {}; + var manualPropTypeWarningCount = 0; + } + + function checkType(isRequired, props, propName, componentName, location, propFullName, secret) { + componentName = componentName || ANONYMOUS; + propFullName = propFullName || propName; + + if (secret !== ReactPropTypesSecret) { + if (throwOnDirectAccess) { + // New behavior only for users of `prop-types` package + var err = new Error( + 'Calling PropTypes validators directly is not supported by the `prop-types` package. ' + + 'Use `PropTypes.checkPropTypes()` to call them. ' + + 'Read more at http://fb.me/use-check-prop-types' + ); + err.name = 'Invariant Violation'; + throw err; + } else if ("development" !== 'production' && typeof console !== 'undefined') { + // Old behavior for people using React.PropTypes + var cacheKey = componentName + ':' + propName; + if ( + !manualPropTypeCallCache[cacheKey] && + // Avoid spamming the console because they are often not actionable except for lib authors + manualPropTypeWarningCount < 3 + ) { + printWarning( + 'You are manually calling a React.PropTypes validation ' + + 'function for the `' + propFullName + '` prop on `' + componentName + '`. This is deprecated ' + + 'and will throw in the standalone `prop-types` package. ' + + 'You may be seeing this warning due to a third-party PropTypes ' + + 'library. See https://fb.me/react-warning-dont-call-proptypes ' + 'for details.' + ); + manualPropTypeCallCache[cacheKey] = true; + manualPropTypeWarningCount++; + } + } + } + if (props[propName] == null) { + if (isRequired) { + if (props[propName] === null) { + return new PropTypeError('The ' + location + ' `' + propFullName + '` is marked as required ' + ('in `' + componentName + '`, but its value is `null`.')); + } + return new PropTypeError('The ' + location + ' `' + propFullName + '` is marked as required in ' + ('`' + componentName + '`, but its value is `undefined`.')); + } + return null; + } else { + return validate(props, propName, componentName, location, propFullName); + } + } + + var chainedCheckType = checkType.bind(null, false); + chainedCheckType.isRequired = checkType.bind(null, true); + + return chainedCheckType; + } + + function createPrimitiveTypeChecker(expectedType) { + function validate(props, propName, componentName, location, propFullName, secret) { + var propValue = props[propName]; + var propType = getPropType(propValue); + if (propType !== expectedType) { + // `propValue` being instance of, say, date/regexp, pass the 'object' + // check, but we can offer a more precise error message here rather than + // 'of type `object`'. + var preciseType = getPreciseType(propValue); + + return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type ' + ('`' + preciseType + '` supplied to `' + componentName + '`, expected ') + ('`' + expectedType + '`.')); + } + return null; + } + + return createChainableTypeChecker(validate); + } + + function createAnyTypeChecker() { + return createChainableTypeChecker(emptyFunctionThatReturnsNull); + } + + function createArrayOfTypeChecker(typeChecker) { + function validate(props, propName, componentName, location, propFullName) { + if (typeof typeChecker !== 'function') { + return new PropTypeError('Property `' + propFullName + '` of component `' + componentName + '` has invalid PropType notation inside arrayOf.'); + } + var propValue = props[propName]; + if (!Array.isArray(propValue)) { + var propType = getPropType(propValue); + return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type ' + ('`' + propType + '` supplied to `' + componentName + '`, expected an array.')); + } + for (var i = 0; i < propValue.length; i++) { + var error = typeChecker(propValue, i, componentName, location, propFullName + '[' + i + ']', ReactPropTypesSecret); + if (error instanceof Error) { + return error; + } + } + return null; + } + + return createChainableTypeChecker(validate); + } + + function createElementTypeChecker() { + function validate(props, propName, componentName, location, propFullName) { + var propValue = props[propName]; + if (!isValidElement(propValue)) { + var propType = getPropType(propValue); + return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type ' + ('`' + propType + '` supplied to `' + componentName + '`, expected a single ReactElement.')); + } + return null; + } + + return createChainableTypeChecker(validate); + } + + function createInstanceTypeChecker(expectedClass) { + function validate(props, propName, componentName, location, propFullName) { + if (!(props[propName] instanceof expectedClass)) { + var expectedClassName = expectedClass.name || ANONYMOUS; + var actualClassName = getClassName(props[propName]); + return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type ' + ('`' + actualClassName + '` supplied to `' + componentName + '`, expected ') + ('instance of `' + expectedClassName + '`.')); + } + return null; + } + + return createChainableTypeChecker(validate); + } + + function createEnumTypeChecker(expectedValues) { + if (!Array.isArray(expectedValues)) { + "development" !== 'production' ? printWarning('Invalid argument supplied to oneOf, expected an instance of array.') : void 0; + return emptyFunctionThatReturnsNull; + } + + function validate(props, propName, componentName, location, propFullName) { + var propValue = props[propName]; + for (var i = 0; i < expectedValues.length; i++) { + if (is(propValue, expectedValues[i])) { + return null; + } + } + + var valuesString = JSON.stringify(expectedValues); + return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of value `' + propValue + '` ' + ('supplied to `' + componentName + '`, expected one of ' + valuesString + '.')); + } + + return createChainableTypeChecker(validate); + } + + function createObjectOfTypeChecker(typeChecker) { + function validate(props, propName, componentName, location, propFullName) { + if (typeof typeChecker !== 'function') { + return new PropTypeError('Property `' + propFullName + '` of component `' + componentName + '` has invalid PropType notation inside objectOf.'); + } + var propValue = props[propName]; + var propType = getPropType(propValue); + if (propType !== 'object') { + return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type ' + ('`' + propType + '` supplied to `' + componentName + '`, expected an object.')); + } + for (var key in propValue) { + if (propValue.hasOwnProperty(key)) { + var error = typeChecker(propValue, key, componentName, location, propFullName + '.' + key, ReactPropTypesSecret); + if (error instanceof Error) { + return error; + } + } + } + return null; + } + + return createChainableTypeChecker(validate); + } + + function createUnionTypeChecker(arrayOfTypeCheckers) { + if (!Array.isArray(arrayOfTypeCheckers)) { + "development" !== 'production' ? printWarning('Invalid argument supplied to oneOfType, expected an instance of array.') : void 0; + return emptyFunctionThatReturnsNull; + } + + for (var i = 0; i < arrayOfTypeCheckers.length; i++) { + var checker = arrayOfTypeCheckers[i]; + if (typeof checker !== 'function') { + printWarning( + 'Invalid argument supplied to oneOfType. Expected an array of check functions, but ' + + 'received ' + getPostfixForTypeWarning(checker) + ' at index ' + i + '.' + ); + return emptyFunctionThatReturnsNull; + } + } + + function validate(props, propName, componentName, location, propFullName) { + for (var i = 0; i < arrayOfTypeCheckers.length; i++) { + var checker = arrayOfTypeCheckers[i]; + if (checker(props, propName, componentName, location, propFullName, ReactPropTypesSecret) == null) { + return null; + } + } + + return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` supplied to ' + ('`' + componentName + '`.')); + } + + return createChainableTypeChecker(validate); + } + + function createNodeChecker() { + function validate(props, propName, componentName, location, propFullName) { + if (!isNode(props[propName])) { + return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` supplied to ' + ('`' + componentName + '`, expected a ReactNode.')); + } + return null; + } + + return createChainableTypeChecker(validate); + } + + function createShapeTypeChecker(shapeTypes) { + function validate(props, propName, componentName, location, propFullName) { + var propValue = props[propName]; + var propType = getPropType(propValue); + if (propType !== 'object') { + return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type `' + propType + '` ' + ('supplied to `' + componentName + '`, expected `object`.')); + } + for (var key in shapeTypes) { + var checker = shapeTypes[key]; + if (!checker) { + continue; + } + var error = checker(propValue, key, componentName, location, propFullName + '.' + key, ReactPropTypesSecret); + if (error) { + return error; + } + } + return null; + } + + return createChainableTypeChecker(validate); + } + + function createStrictShapeTypeChecker(shapeTypes) { + function validate(props, propName, componentName, location, propFullName) { + var propValue = props[propName]; + var propType = getPropType(propValue); + if (propType !== 'object') { + return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type `' + propType + '` ' + ('supplied to `' + componentName + '`, expected `object`.')); + } + // We need to check all keys in case some are required but missing from + // props. + var allKeys = assign({}, props[propName], shapeTypes); + for (var key in allKeys) { + var checker = shapeTypes[key]; + if (!checker) { + return new PropTypeError( + 'Invalid ' + location + ' `' + propFullName + '` key `' + key + '` supplied to `' + componentName + '`.' + + '\nBad object: ' + JSON.stringify(props[propName], null, ' ') + + '\nValid keys: ' + JSON.stringify(Object.keys(shapeTypes), null, ' ') + ); + } + var error = checker(propValue, key, componentName, location, propFullName + '.' + key, ReactPropTypesSecret); + if (error) { + return error; + } + } + return null; + } + + return createChainableTypeChecker(validate); + } + + function isNode(propValue) { + switch (typeof propValue) { + case 'number': + case 'string': + case 'undefined': + return true; + case 'boolean': + return !propValue; + case 'object': + if (Array.isArray(propValue)) { + return propValue.every(isNode); + } + if (propValue === null || isValidElement(propValue)) { + return true; + } + + var iteratorFn = getIteratorFn(propValue); + if (iteratorFn) { + var iterator = iteratorFn.call(propValue); + var step; + if (iteratorFn !== propValue.entries) { + while (!(step = iterator.next()).done) { + if (!isNode(step.value)) { + return false; + } + } + } else { + // Iterator will provide entry [k,v] tuples rather than values. + while (!(step = iterator.next()).done) { + var entry = step.value; + if (entry) { + if (!isNode(entry[1])) { + return false; + } + } + } + } + } else { + return false; + } + + return true; + default: + return false; + } + } + + function isSymbol(propType, propValue) { + // Native Symbol. + if (propType === 'symbol') { + return true; + } + + // 19.4.3.5 Symbol.prototype[@@toStringTag] === 'Symbol' + if (propValue['@@toStringTag'] === 'Symbol') { + return true; + } + + // Fallback for non-spec compliant Symbols which are polyfilled. + if (typeof Symbol === 'function' && propValue instanceof Symbol) { + return true; + } + + return false; + } + + // Equivalent of `typeof` but with special handling for array and regexp. + function getPropType(propValue) { + var propType = typeof propValue; + if (Array.isArray(propValue)) { + return 'array'; + } + if (propValue instanceof RegExp) { + // Old webkits (at least until Android 4.0) return 'function' rather than + // 'object' for typeof a RegExp. We'll normalize this here so that /bla/ + // passes PropTypes.object. + return 'object'; + } + if (isSymbol(propType, propValue)) { + return 'symbol'; + } + return propType; + } + + // This handles more types than `getPropType`. Only used for error messages. + // See `createPrimitiveTypeChecker`. + function getPreciseType(propValue) { + if (typeof propValue === 'undefined' || propValue === null) { + return '' + propValue; + } + var propType = getPropType(propValue); + if (propType === 'object') { + if (propValue instanceof Date) { + return 'date'; + } else if (propValue instanceof RegExp) { + return 'regexp'; + } + } + return propType; + } + + // Returns a string that is postfixed to a warning about an invalid type. + // For example, "undefined" or "of type array" + function getPostfixForTypeWarning(value) { + var type = getPreciseType(value); + switch (type) { + case 'array': + case 'object': + return 'an ' + type; + case 'boolean': + case 'date': + case 'regexp': + return 'a ' + type; + default: + return type; + } + } + + // Returns class name of the object, if any. + function getClassName(propValue) { + if (!propValue.constructor || !propValue.constructor.name) { + return ANONYMOUS; + } + return propValue.constructor.name; + } + + ReactPropTypes.checkPropTypes = checkPropTypes; + ReactPropTypes.PropTypes = ReactPropTypes; + + return ReactPropTypes; + }; + + }, {"./checkPropTypes": 1, "./lib/ReactPropTypesSecret": 5, "object-assign": 6}], + 4: [function (require, module, exports) { + /** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + + if ("development" !== 'production') { + var REACT_ELEMENT_TYPE = (typeof Symbol === 'function' && + Symbol.for && + Symbol.for('react.element')) || + 0xeac7; + + var isValidElement = function (object) { + return typeof object === 'object' && + object !== null && + object.$$typeof === REACT_ELEMENT_TYPE; + }; + + // By explicitly using `prop-types` you are opting into new development behavior. + // http://fb.me/prop-types-in-prod + var throwOnDirectAccess = true; + module.exports = require('./factoryWithTypeCheckers')(isValidElement, throwOnDirectAccess); + } else { + // By explicitly using `prop-types` you are opting into new production behavior. + // http://fb.me/prop-types-in-prod + module.exports = require('./factoryWithThrowingShims')(); + } + + }, {"./factoryWithThrowingShims": 2, "./factoryWithTypeCheckers": 3}], + 5: [function (require, module, exports) { + /** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + + 'use strict'; + + var ReactPropTypesSecret = 'SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED'; + + module.exports = ReactPropTypesSecret; + + }, {}], + 6: [function (require, module, exports) { + /* + object-assign + (c) Sindre Sorhus + @license MIT + */ + + 'use strict'; + /* eslint-disable no-unused-vars */ + var getOwnPropertySymbols = Object.getOwnPropertySymbols; + var hasOwnProperty = Object.prototype.hasOwnProperty; + var propIsEnumerable = Object.prototype.propertyIsEnumerable; + + function toObject(val) { + if (val === null || val === undefined) { + throw new TypeError('Object.assign cannot be called with null or undefined'); + } + + return Object(val); + } + + function shouldUseNative() { + try { + if (!Object.assign) { + return false; + } + + // Detect buggy property enumeration order in older V8 versions. + + // https://bugs.chromium.org/p/v8/issues/detail?id=4118 + var test1 = new String('abc'); // eslint-disable-line no-new-wrappers + test1[5] = 'de'; + if (Object.getOwnPropertyNames(test1)[0] === '5') { + return false; + } + + // https://bugs.chromium.org/p/v8/issues/detail?id=3056 + var test2 = {}; + for (var i = 0; i < 10; i++) { + test2['_' + String.fromCharCode(i)] = i; + } + var order2 = Object.getOwnPropertyNames(test2).map(function (n) { + return test2[n]; + }); + if (order2.join('') !== '0123456789') { + return false; + } + + // https://bugs.chromium.org/p/v8/issues/detail?id=3056 + var test3 = {}; + 'abcdefghijklmnopqrst'.split('').forEach(function (letter) { + test3[letter] = letter; + }); + if (Object.keys(Object.assign({}, test3)).join('') !== + 'abcdefghijklmnopqrst') { + return false; + } + + return true; + } catch (err) { + // We don't expect any of the above to throw, but better to be safe. + return false; + } + } + + module.exports = shouldUseNative() ? Object.assign : function (target, source) { + var from; + var to = toObject(target); + var symbols; + + for (var s = 1; s < arguments.length; s++) { + from = Object(arguments[s]); + + for (var key in from) { + if (hasOwnProperty.call(from, key)) { + to[key] = from[key]; + } + } + + if (getOwnPropertySymbols) { + symbols = getOwnPropertySymbols(from); + for (var i = 0; i < symbols.length; i++) { + if (propIsEnumerable.call(from, symbols[i])) { + to[symbols[i]] = from[symbols[i]]; + } + } + } + } + + return to; + }; + + }, {}] + }, {}, [4])(4) +}); \ No newline at end of file diff --git a/schoolapps/static/js/react/prop-types.production.min.js b/schoolapps/static/js/react/prop-types.production.min.js new file mode 100644 index 0000000000000000000000000000000000000000..128d0111a92844041d9066d971ee0d46d2546f8b --- /dev/null +++ b/schoolapps/static/js/react/prop-types.production.min.js @@ -0,0 +1 @@ +!function(f){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=f();else if("function"==typeof define&&define.amd)define([],f);else{var g;g="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,g.PropTypes=f()}}(function(){return function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a="function"==typeof require&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}for(var i="function"==typeof require&&require,o=0;o<r.length;o++)s(r[o]);return s}({1:[function(require,module){"use strict";function emptyFunction(){}var ReactPropTypesSecret=require(3);module.exports=function(){function e(e,r,t,o,n,p){if(p!==ReactPropTypesSecret){var c=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw c.name="Invariant Violation",c}}function r(){return e}e.isRequired=e;var t={array:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:r,element:e,instanceOf:r,node:e,objectOf:r,oneOf:r,oneOfType:r,shape:r,exact:r};return t.checkPropTypes=emptyFunction,t.PropTypes=t,t}},{3:3}],2:[function(require,module){module.exports=require(1)()},{1:1}],3:[function(require,module){"use strict";var ReactPropTypesSecret="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED";module.exports=ReactPropTypesSecret},{}]},{},[2])(2)}); \ No newline at end of file diff --git a/schoolapps/static/js/rebus.js b/schoolapps/static/js/rebus.js index d07b96a864c131e7804e5eef5afbf1046ec9a211..467e1a71a1c965cc1cb04f9ad72e094527e030cc 100755 --- a/schoolapps/static/js/rebus.js +++ b/schoolapps/static/js/rebus.js @@ -243,8 +243,6 @@ function getOption(option) { try { for (var _iterator3 = BASIC_OPTIONS[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { var category = _step3.value; - - // console.log(category); var _iteratorNormalCompletion4 = true; var _didIteratorError4 = false; var _iteratorError4 = undefined; @@ -253,7 +251,6 @@ function getOption(option) { for (var _iterator4 = category.options[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { var opt = _step4.value; - // console.log(opt); if (opt.id === option) { return opt; } diff --git a/schoolapps/support/templates/support/rebus.html b/schoolapps/support/templates/support/rebus.html index c9ad6405d2a36b7abb979698c88c39811791211f..cf21afca5ce750b778e8d33294d772e6f4c3ec57 100644 --- a/schoolapps/support/templates/support/rebus.html +++ b/schoolapps/support/templates/support/rebus.html @@ -30,7 +30,8 @@ <div class="input-field col s12 support-input-mobile"> <i class="material-icons prefix">mode_edit</i> <textarea id="{{ form.long_description.id_for_label }}" name="{{ form.long_description.html_name }}" - class="materialize-textarea"></textarea> + class="materialize-textarea"> + </textarea> <label for="{{ form.long_description.id_for_label }}">Bitte beschreibe dein Problem <strong>genauer</strong> (optional) diff --git a/schoolapps/templates/partials/header.html b/schoolapps/templates/partials/header.html index 83d1fa36dfbef936af0ab8f91ccea244f3682c64..a0c320fd86156d421695a267b7582ebd839b7063 100755 --- a/schoolapps/templates/partials/header.html +++ b/schoolapps/templates/partials/header.html @@ -120,7 +120,8 @@ <img src="{% static 'common/logo.png' %}"> </div> <div class="col s6 right-align"> - <a href="/"><strong>SchoolApps</strong></a><br> + <a href="/"><strong>SchoolApps</strong></a> + <br> Katharineum zu Lübeck </div> </div> @@ -147,19 +148,13 @@ <ul class="collapsible collapsible-accordion"> {% if perms.aub.apply_for_aub or perms.aub.check1_aub or perms.aub.check2_aub %} - <li class="bold"> - <a class="collapsible-header waves-effect waves-primary"><i class="material-icons">assignment</i> - Lehrerfunktionen + <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.apply_for_aub %} - <li class="url-aub_index url-aub_details url-ab_edit url-aub_apply_for urlaub_applied_for"> - <a href="{% url 'aub_index' %}"><i class="material-icons">business_center</i> - Unterrichtsbefreiungen - </a> - </li> - {% endif %} {% if perms.aub.check1_aub %} <li class="url-aub_check1"> <a href="{% url 'aub_check1' %}"><i class="material-icons">done</i> Anträge @@ -169,7 +164,8 @@ {% 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 + <a href="{% url 'aub_check2' %}"><i class="material-icons">done_all</i> + Anträge genehmigen 2 </a> </li> @@ -189,6 +185,49 @@ <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"> @@ -235,11 +274,11 @@ </ul> </div> </li> - {% endif %} - <li> - <div class="divider"></div> - </li> + <li> + <div class="divider"></div> + </li> + {% endif %} <li> <a href="{% url 'menu_show_current' %}" target="_blank"> @@ -247,7 +286,6 @@ </a> </li> - {% if perms.menu.add_menu %} <li class="url-menu_index url-menu_upload url-menu_index_msg"> <a href="{% url 'menu_index' %}"> @@ -359,6 +397,27 @@ {# </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"> diff --git a/schoolapps/templates/partials/paper/footer.html b/schoolapps/templates/partials/paper/footer.html new file mode 100755 index 0000000000000000000000000000000000000000..1dc3a88b8281509e4eb8d18212b4513d32189845 --- /dev/null +++ b/schoolapps/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/schoolapps/templates/partials/paper/header.html b/schoolapps/templates/partials/paper/header.html new file mode 100755 index 0000000000000000000000000000000000000000..4b92397ddc86886771e811b1ec34ad72e2c776e2 --- /dev/null +++ b/schoolapps/templates/partials/paper/header.html @@ -0,0 +1,146 @@ +{% 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/schoolapps/timetable/templates/timetable/hintsinsubprint.html b/schoolapps/timetable/templates/timetable/hintsinsubprint.html new file mode 100644 index 0000000000000000000000000000000000000000..d39760fac1dd3843c4f286c252c8088193443c91 --- /dev/null +++ b/schoolapps/timetable/templates/timetable/hintsinsubprint.html @@ -0,0 +1,16 @@ +{% load martortags %} +{% if c.hints %} + {% for hint in c.hints %} + <div class="alert primary"> + <div> + <strong> + {{ hint.classes_formatted }}{% if hint.teachers and hint.classes.all %}, Lehrkräfte{% endif %}: + </strong> + + <i class="material-icons left">announcement</i> + + {{ hint.text|safe_markdown }} + </div> + </div> + {% endfor %} +{% endif %} diff --git a/schoolapps/timetable/templates/timetable/plan.html b/schoolapps/timetable/templates/timetable/plan.html index 9874ea7b4aa8e2484a6a8c9b505af70a16d5562b..07a8aeef0fc284bfe37ebff502863a262e5cc49f 100755 --- a/schoolapps/timetable/templates/timetable/plan.html +++ b/schoolapps/timetable/templates/timetable/plan.html @@ -43,6 +43,19 @@ <h3> Stundenplan <i>{{ el }}</i> </h3> + + {# Show class teacher and deputy class teacher #} + {% if type == 2 and el.teachers %} + <h5>Klassenlehrkräfte: + {% for teacher in el.teachers %} + + <span data-position="bottom" class="tooltipped" + data-tooltip="{{ teacher }}"> + <a href="{% url "timetable_smart_plan" "teacher" teacher.id %}"> + {{ teacher.shortcode }}</a></span>{% if not forloop.last %},{% endif %} + {% endfor %} + </h5> + {% endif %} </div> {# Show print button only if not on mobile #} <div class="col s4 m6 l4 xl3 right align-right no-print"> diff --git a/schoolapps/timetable/templates/timetable/substitutionprint.html b/schoolapps/timetable/templates/timetable/substitutionprint.html new file mode 100755 index 0000000000000000000000000000000000000000..39363d68a5b46e52b00d3099716362fb8aa9784e --- /dev/null +++ b/schoolapps/timetable/templates/timetable/substitutionprint.html @@ -0,0 +1,110 @@ +{% load common %} +{% include 'partials/paper/header.html' %} + +<script type="text/javascript"> + var dest = "/timetable/substitutions/"; +</script> + +<style> + table.substitutions td, table.substitutions th { + padding: 0 2px; + } + + span.badge.new { + font-size: 0.9rem; + line-height: 20px; + height: 20px; + margin: 2px; + letter-spacing: 0.3pt; + } +</style> + +{% for c in days %} + <h4>Vertretungen {{ c.date|date:"l, j. F Y" }}</h4> + + + {% include "timetable/hintsinsubprint.html" %} + + <div style="margin-bottom: 20px"> + {% if c.header_info.is_box_needed %} + {% for row in c.header_info.rows %} + <div class="row no-margin"> + <div class="col s3 no-padding"> + <strong>{{ row.0 }}</strong> + </div> + <div class="col s9 no-padding"> + {{ row.1 }} + </div> + </div> + {% endfor %} + {% endif %} + </div> + + <table class="substitutions"> + <thead> + <tr> + <th><i class="material-icons">people</i></th> + <th><i class="material-icons">access_time</i></th> + <th>Lehrer</th> + <th>Fach</th> + <th>Raum</th> + <th>Hinweis</th> + <th></th> + </tr> + </thead> + <tbody> + {% if not c.sub_table %} + <td colspan="7"> + <p class="flow-text center"> + Keine Vertretungen vorhanden + </p> + </td> + {% endif %} + + {% set color_background = 1 %} + {% set last_classes = "" %} + + {% for sub in c.sub_table %} + + {# Color groups of classes in grey/white #} + {% if last_classes != sub.classes %} + {% if color_background %}{% set color_background = 0 %} + {% else %}{% set color_background = 1 %} + {% endif %} + {% endif %} + {% set last_classes = sub.classes %} + + + <tr class="{{ sub.color }}-text {% if color_background %}striped{% endif %}"> + <td> + {{ sub.classes }} + </td> + <td> + <strong> + {{ sub.lesson }} + </strong> + </td> + <td> + {% include "timetable/subs/teacher.html" %} + </td> + <td> + {% include "timetable/subs/subject.html" %} + </td> + <td> + {% include "timetable/subs/room.html" %} + </td> + <td> + {% if sub.badge %} + <span class="badge new green">{{ sub.badge }}</span> + {% endif %} + <em>{{ sub.text|default:"" }}</em> + </td> + </tr> + {% endfor %} + </tbody> + </table> + +{% endfor %} + + +{% include 'partials/paper/footer.html' %} diff --git a/schoolapps/timetable/urls.py b/schoolapps/timetable/urls.py index 1a8f979a2854425a642fe53b460ef7db7dd60d99..610ffb897ded39e7aa399ba6f326d15b3fe51881 100755 --- a/schoolapps/timetable/urls.py +++ b/schoolapps/timetable/urls.py @@ -23,12 +23,17 @@ try: path('substitutions/', views.substitutions, name='timetable_substitutions'), path('substitutions/<int:year>/<int:month>/<int:day>/', views.substitutions, name='timetable_substitutions_date'), + path('substitutions/<int:year>/<int:month>/<int:day>/print/', views.substitutions_print, + name='timetable_substitutions_date_print'), + path('substitutions/print/', views.substitutions_print, + name='timetable_substitutions_print'), path('aktuell.pdf', views.sub_pdf, name="timetable_substitutions_pdf"), path('<str:plan_date>-aktuell.pdf', views.sub_pdf, name="timetable_substitutions_pdf_date") ] -except (Terms.DoesNotExist, Schoolyear.DoesNotExist, ProgrammingError, OperationalError): +except (Terms.DoesNotExist, Schoolyear.DoesNotExist, ProgrammingError, OperationalError) as e: from . import fallback_view + print(e) urlpatterns = [ path('hints', fallback_view.fallback, name="timetable_hints"), @@ -46,5 +51,10 @@ except (Terms.DoesNotExist, Schoolyear.DoesNotExist, ProgrammingError, Operation path('substitutions/', fallback_view.fallback, name='timetable_substitutions'), path('substitutions/<int:year>/<int:month>/<int:day>/', fallback_view.fallback, name='timetable_substitutions_date'), - path('aktuell.pdf', fallback_view.fallback, name="timetable_substitutions_pdf") + path('substitutions/<int:year>/<int:month>/<int:day>/<str:print_view>/', fallback_view.fallback, + name='timetable_substitutions_date_print'), + path('substitutions/<str:print_view>/', fallback_view.fallback, + name='timetable_substitutions_print'), + path('aktuell.pdf', fallback_view.fallback, name="timetable_substitutions_pdf"), + path('<str:plan_date>-aktuell.pdf', fallback_view.fallback, name="timetable_substitutions_pdf_date") ] diff --git a/schoolapps/timetable/views.py b/schoolapps/timetable/views.py index c716d63dc3f0ed51e93e273ffc3c5d501b6d0c57..16d7ef2d35a7e1fdca49e601813cd3f05dcfdd86 100755 --- a/schoolapps/timetable/views.py +++ b/schoolapps/timetable/views.py @@ -328,19 +328,7 @@ def sub_pdf(request, plan_date=None): return FileResponse(file, content_type="application/pdf") -@login_required -@permission_required("timetable.show_plan") -@cache_page(SUBS_VIEW_CACHE.expiration_time) -def substitutions(request, year=None, month=None, day=None): - """Show substitutions in a classic view""" - - date, time = find_out_what_is_today(year, month, day) - - # Get next weekday if it is a weekend - next_weekday = get_next_weekday_with_time(date, time) - if next_weekday != date: - return redirect("timetable_substitutions_date", next_weekday.year, next_weekday.month, next_weekday.day) - +def get_subs_context(request, date): # Get subs and generate table events = get_all_events_by_date(date) subs = get_substitutions_by_date(date) @@ -354,7 +342,7 @@ def substitutions(request, year=None, month=None, day=None): header_info = get_header_information(subs, date, events) hints = list(get_all_hints_by_time_period(date, date)) - context = { + return { "subs": subs, "sub_table": sub_table, "date": date, @@ -363,7 +351,51 @@ def substitutions(request, year=None, month=None, day=None): "hints": hints, } - return render(request, 'timetable/substitution.html', context) + +@login_required +@permission_required("timetable.show_plan") +@cache_page(SUBS_VIEW_CACHE.expiration_time) +def substitutions(request, year=None, month=None, day=None): + """Show substitutions in a classic view""" + + date, time = find_out_what_is_today(year, month, day) + + # Get next weekday if it is a weekend + next_weekday = get_next_weekday_with_time(date, time) + if next_weekday != date: + return redirect("timetable_substitutions_date", next_weekday.year, next_weekday.month, next_weekday.day) + + context = get_subs_context(request, date) + + template_name = 'timetable/substitution.html' + + return render(request, template_name, context) + + +@login_required +@permission_required("timetable.show_plan") +@cache_page(SUBS_VIEW_CACHE.expiration_time) +def substitutions_print(request, year=None, month=None, day=None): + """Show substitutions in a classic view""" + + date, time = find_out_what_is_today(year, month, day) + + # Get next weekday if it is a weekend + next_weekday = get_next_weekday_with_time(date, time) + if next_weekday != date: + return redirect("timetable_substitutions_date_print", next_weekday.year, next_weekday.month, next_weekday.day) + + second_date = get_next_weekday(date + datetime.timedelta(days=1)) + context1 = get_subs_context(request, date) + context2 = get_subs_context(request, second_date) + + context = { + "days": [context1, context2] + } + + template_name = 'timetable/substitutionprint.html' + + return render(request, template_name, context) ################### diff --git a/schoolapps/untisconnect/api.py b/schoolapps/untisconnect/api.py index 313067603011e3d4a493980e98c36c0e60e4c686..767e5b599a9eb51ecf26c683228de34fa714dd7c 100755 --- a/schoolapps/untisconnect/api.py +++ b/schoolapps/untisconnect/api.py @@ -1,6 +1,7 @@ from django.conf import settings -from untisconnect.api_helper import get_term_by_ids, run_using, untis_date_to_date, date_to_untis_date +from untisconnect.api_helper import get_term_by_ids, run_using, untis_date_to_date, date_to_untis_date, \ + untis_split_first from . import models from timetable.settings import untis_settings @@ -8,8 +9,6 @@ TYPE_TEACHER = 0 TYPE_ROOM = 1 TYPE_CLASS = 2 -from datetime import date - def run_all(obj, filter_term=True): return run_default_filter(run_using(obj).all(), filter_term=filter_term) @@ -118,6 +117,7 @@ class Class(object): self.text1 = None self.text2 = None self.room = None + self.teachers = [] def __str__(self): if self.filled: @@ -138,6 +138,9 @@ class Class(object): self.name = db_obj.name self.text1 = db_obj.longname self.text2 = db_obj.text + teacher_ids = untis_split_first(db_obj.teacherids, int) + self.teachers = [get_teacher_by_id(t_id) for t_id in teacher_ids] + print(self.teachers) # print(db_obj.room_id) if db_obj.room_id != 0: # print("RAUM")