diff --git a/.gitignore b/.gitignore old mode 100755 new mode 100644 diff --git a/azube/__init__.py b/azube/__init__.py old mode 100755 new mode 100644 diff --git a/azube/asgi.py b/azube/asgi.py old mode 100755 new mode 100644 diff --git a/azube/settings.py b/azube/settings.py old mode 100755 new mode 100644 diff --git a/azube/urls.py b/azube/urls.py old mode 100755 new mode 100644 diff --git a/azube/wsgi.py b/azube/wsgi.py old mode 100755 new mode 100644 diff --git a/core/__init__.py b/core/__init__.py old mode 100755 new mode 100644 diff --git a/core/admin.py b/core/admin.py old mode 100755 new mode 100644 diff --git a/core/apps.py b/core/apps.py old mode 100755 new mode 100644 diff --git a/core/azure_auth.py b/core/azure_auth.py old mode 100755 new mode 100644 index 3797d8a..2dee84e --- a/core/azure_auth.py +++ b/core/azure_auth.py @@ -1,8 +1,17 @@ +import datetime import json import base64 from core.models import Berichtsheft -from core.report_templates import ReportTemplates +from core.reports import DailyReport, WeeklyReport, choose_report_kind +import core.util +from enum import Enum + + +class Roles(Enum): + TRAINEE = "trainee" + MANAGER = "manager" + ADMIN = "admin" class AzureUser: @@ -33,8 +42,32 @@ class AzureUser: self.id = "anon" def reports(self): - return Berichtsheft.objects.filter(user=self.id) + return Berichtsheft.objects.filter(user=self.id).order_by("-year", "-week") - def get_report_template(self): + def get_report_kind(self): # TODO : Implement - return ReportTemplates.get_template("weekly") + return "weekly" + + def get_report_kind_form(self, request=None, files=None): + return choose_report_kind(self.get_report_kind(), request, files) + + def latest_report(self): + return self.reports().order_by("-year", "-week").first() + + def late_reports(self) -> int: + year_now, week_now, _ = datetime.datetime.today().isocalendar() + count = 0 + latest = self.latest_report() + + new_year, new_week = (latest.year, latest.week) + + while week_now != new_week or (week_now == new_week and year_now != new_year): + count += 1 + new_year, new_week = core.util.next_date(new_year, new_week) + + return count + + @property + def role(self) -> Roles: + # TODO : Implement + return Roles.TRAINEE diff --git a/core/doc_gen.py b/core/doc_gen.py old mode 100755 new mode 100644 index e6b3040..7999cf8 --- a/core/doc_gen.py +++ b/core/doc_gen.py @@ -1,12 +1,35 @@ import json +from weasyprint import HTML, CSS +from io import BytesIO +from pdf2image import convert_from_bytes +from PIL import Image +from django.conf import settings +from django.template.loader import render_to_string +from .models import Berichtsheft +from io import BytesIO -def gen_doc(template: str, vars: dict) -> str: - definition = json.loads(open(f"{template}.json").read()) - content = open(f"{template}.html").read() +def gen_doc_html(report: Berichtsheft): + return render_to_string(f"report_template/{report.kind}.html", report.vars()) - for var in definition["vars"]: - var_r = var["name"].upper() - content = content.replace(f"[[{var_r}]]", vars[var["name"]]) - return content +def gen_doc_pdf(report: Berichtsheft): + main_content = gen_doc_html(report) + + pdf_buffer = BytesIO() + HTML( + string=main_content, + ).write_pdf(pdf_buffer, stylesheets=[CSS(string="@page { size: A4; margin: 1cm }")]) + pdf_buffer.seek(0) + return pdf_buffer + + +def gen_doc_png(report: Berichtsheft): + pdf_buffer = gen_doc_pdf(report) + pages = convert_from_bytes(pdf_buffer.getvalue(), 300) + + for page in pages: + img_byte_array = BytesIO() + page.save(img_byte_array, format="PNG") + img_byte_array.seek(0) + return img_byte_array diff --git a/core/forms.py b/core/forms.py deleted file mode 100755 index be9c917..0000000 --- a/core/forms.py +++ /dev/null @@ -1,5 +0,0 @@ -def extract_post_values(post, vars) -> dict: - res = {} - for var in vars: - res[var["name"]] = post.get(var["name"], "") - return res diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py old mode 100755 new mode 100644 index eaacbb1..7be4550 --- a/core/migrations/0001_initial.py +++ b/core/migrations/0001_initial.py @@ -6,7 +6,6 @@ import django.db.models.deletion class Migration(migrations.Migration): - initial = True dependencies = [ diff --git a/core/migrations/0002_alter_berichtsheft_id.py b/core/migrations/0002_alter_berichtsheft_id.py old mode 100755 new mode 100644 index 21edbf1..2ccae28 --- a/core/migrations/0002_alter_berichtsheft_id.py +++ b/core/migrations/0002_alter_berichtsheft_id.py @@ -4,7 +4,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("core", "0001_initial"), ] diff --git a/core/migrations/0003_group_azureuser_alter_berichtsheft_user.py b/core/migrations/0003_group_azureuser_alter_berichtsheft_user.py old mode 100755 new mode 100644 index 6050d9c..8cc8a8e --- a/core/migrations/0003_group_azureuser_alter_berichtsheft_user.py +++ b/core/migrations/0003_group_azureuser_alter_berichtsheft_user.py @@ -8,7 +8,6 @@ import django.utils.timezone class Migration(migrations.Migration): - dependencies = [ ("auth", "0012_alter_user_first_name_max_length"), ("core", "0002_alter_berichtsheft_id"), diff --git a/core/migrations/0004_remove_berichtsheft_approved_by.py b/core/migrations/0004_remove_berichtsheft_approved_by.py old mode 100755 new mode 100644 index aa1f29b..78b4b3f --- a/core/migrations/0004_remove_berichtsheft_approved_by.py +++ b/core/migrations/0004_remove_berichtsheft_approved_by.py @@ -4,7 +4,6 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ ("core", "0003_group_azureuser_alter_berichtsheft_user"), ] diff --git a/core/migrations/0005_user_alter_berichtsheft_user_delete_azureuser.py b/core/migrations/0005_user_alter_berichtsheft_user_delete_azureuser.py old mode 100755 new mode 100644 index cf368b3..f52a13b --- a/core/migrations/0005_user_alter_berichtsheft_user_delete_azureuser.py +++ b/core/migrations/0005_user_alter_berichtsheft_user_delete_azureuser.py @@ -7,7 +7,6 @@ import django.utils.timezone class Migration(migrations.Migration): - dependencies = [ ("auth", "0012_alter_user_first_name_max_length"), ("core", "0004_remove_berichtsheft_approved_by"), diff --git a/core/migrations/0006_berichtsheft_num.py b/core/migrations/0006_berichtsheft_num.py old mode 100755 new mode 100644 index 985fc22..4ab5e4d --- a/core/migrations/0006_berichtsheft_num.py +++ b/core/migrations/0006_berichtsheft_num.py @@ -4,7 +4,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("core", "0005_user_alter_berichtsheft_user_delete_azureuser"), ] diff --git a/core/migrations/0007_approval.py b/core/migrations/0007_approval.py new file mode 100644 index 0000000..1cb35ba --- /dev/null +++ b/core/migrations/0007_approval.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.16 on 2024-12-03 12:41 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0006_berichtsheft_num"), + ] + + operations = [ + migrations.CreateModel( + name="Approval", + fields=[ + ("id", models.AutoField(primary_key=True, serialize=False)), + ("user", models.TextField()), + ( + "report", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="report", + to="core.berichtsheft", + ), + ), + ], + ), + ] diff --git a/core/migrations/0008_berichtsheft_needs_rewrite.py b/core/migrations/0008_berichtsheft_needs_rewrite.py new file mode 100644 index 0000000..a67dc54 --- /dev/null +++ b/core/migrations/0008_berichtsheft_needs_rewrite.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.16 on 2024-12-03 12:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0007_approval"), + ] + + operations = [ + migrations.AddField( + model_name="berichtsheft", + name="needs_rewrite", + field=models.BooleanField(default=False), + ), + ] diff --git a/core/migrations/0009_berichtsheft_kind.py b/core/migrations/0009_berichtsheft_kind.py new file mode 100644 index 0000000..016887d --- /dev/null +++ b/core/migrations/0009_berichtsheft_kind.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.16 on 2024-12-03 14:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0008_berichtsheft_needs_rewrite"), + ] + + operations = [ + migrations.AddField( + model_name="berichtsheft", + name="kind", + field=models.CharField( + choices=[("weekly", "Weekly"), ("daily", "Daily")], + default="weekly", + max_length=20, + ), + ), + ] diff --git a/core/migrations/0010_berichtsheft_department.py b/core/migrations/0010_berichtsheft_department.py new file mode 100644 index 0000000..57c931f --- /dev/null +++ b/core/migrations/0010_berichtsheft_department.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.16 on 2024-12-03 15:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0009_berichtsheft_kind"), + ] + + operations = [ + migrations.AddField( + model_name="berichtsheft", + name="department", + field=models.CharField(default="", max_length=160), + ), + ] diff --git a/core/migrations/0011_berichtsheftdraft.py b/core/migrations/0011_berichtsheftdraft.py new file mode 100644 index 0000000..9f73ecf --- /dev/null +++ b/core/migrations/0011_berichtsheftdraft.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.17 on 2024-12-05 10:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0010_berichtsheft_department"), + ] + + operations = [ + migrations.CreateModel( + name="BerichtsheftDraft", + fields=[ + ("user", models.TextField(primary_key=True, serialize=False)), + ("department", models.CharField(default="", max_length=160)), + ("content", models.JSONField()), + ], + ), + ] diff --git a/core/migrations/0012_berichtsheft_image.py b/core/migrations/0012_berichtsheft_image.py new file mode 100644 index 0000000..b6e5aed --- /dev/null +++ b/core/migrations/0012_berichtsheft_image.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.17 on 2024-12-06 09:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0011_berichtsheftdraft"), + ] + + operations = [ + migrations.AddField( + model_name="berichtsheft", + name="image", + field=models.BinaryField(blank=True, null=True), + ), + ] diff --git a/core/migrations/__init__.py b/core/migrations/__init__.py old mode 100755 new mode 100644 diff --git a/core/models.py b/core/models.py old mode 100755 new mode 100644 index 8a57865..96df7f8 --- a/core/models.py +++ b/core/models.py @@ -1,6 +1,8 @@ from django.db import models from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import Group as BuiltinGroup +from .util import get_week_range +import base64 class User(AbstractUser): @@ -21,22 +23,66 @@ class Group(BuiltinGroup): proxy = True +class ReportKind(models.TextChoices): + WEEKLY = "weekly" + DAILY = "daily" + + class Berichtsheft(models.Model): id = models.AutoField(primary_key=True) user = models.TextField() + kind = models.CharField( + max_length=20, choices=ReportKind.choices, default=ReportKind.WEEKLY + ) num = models.PositiveBigIntegerField(default=0) year = models.PositiveIntegerField() week = models.PositiveSmallIntegerField() + department = models.CharField(max_length=160, default="") content = models.JSONField() + needs_rewrite = models.BooleanField(default=False) created = models.DateTimeField(auto_now_add=True) + image = models.BinaryField(null=True, blank=True) def __str__(self): return f"Berichtsheft: {self.user}, Year: {self.year}, Week: {self.week}" + @property + def is_approved(self): + approvals = Approval.objects.filter(report=self.id) + return len(approvals) >= 2 -class Approval: + def vars(self): + start_date, end_date = get_week_range(self.year, self.week) + + vars = { + "user": self.user, + "kind": self.kind, + "num": self.num, + "year": self.year, + "week": self.week, + "department": self.department, + "start_date": start_date, + "end_date": end_date, + "image": base64.b64encode(self.image).decode("utf-8") + if self.image is not None + else None, + } + + for key, val in self.content.items(): + vars[key] = val + + return vars + + +class Approval(models.Model): id = models.AutoField(primary_key=True) user = models.TextField() report = models.ForeignKey( Berichtsheft, on_delete=models.CASCADE, related_name="report" ) + + +class BerichtsheftDraft(models.Model): + user = models.TextField(primary_key=True) + department = models.CharField(max_length=160, default="") + content = models.JSONField() diff --git a/core/report_templates.py b/core/report_templates.py deleted file mode 100755 index a5f2073..0000000 --- a/core/report_templates.py +++ /dev/null @@ -1,29 +0,0 @@ -class ReportTemplates: - report_global = { - "vars": [ - {"name": "name", "display_name": "Name"}, - {"name": "num_doc", "display_name": "Berichtsheft Nummer"}, - {"name": "year", "display_name": "Jahr"}, - {"name": "week", "display_name": "Kalenderwoche"}, - {"name": "start_date", "display_name": "Von"}, - {"name": "end_date", "display_name": "bis"}, - {"name": "department", "display_name": "Abteilung"}, - ] - } - - def get_template(id) -> dict: - match id: - case "weekly": - weekly = { - "vars": [ - { - "name": "company_text", - "display_name": "Betriebliche Tätigkeiten", - }, - {"name": "week_topic", "display_name": "Thema der Woche"}, - {"name": "school_text", "display_name": "Berufsschule"}, - ] - } - - weekly["vars"].extend(ReportTemplates.report_global["vars"]) - return weekly diff --git a/core/reports.py b/core/reports.py new file mode 100644 index 0000000..fa637af --- /dev/null +++ b/core/reports.py @@ -0,0 +1,196 @@ +from django import forms + +from core.styles import label_span, STYLE + + +class WeeklyReport(forms.Form): + department = forms.CharField( + label=label_span("Abteilung"), + max_length=150, + widget=forms.TextInput( + attrs={ + "placeholder": "Abteilung", + "class": STYLE["text-input"], + } + ), + ) + company_text = forms.CharField( + label=label_span("Betriebliche Tätigkeiten"), + max_length=300, + widget=forms.Textarea( + attrs={ + "class": STYLE["text-input"], + "rows": 5, + "placeholder": "Betriebliche Tätigkeiten", + } + ), + ) + week_topic = forms.CharField( + label=label_span("Thema der Woche"), + max_length=600, + widget=forms.Textarea( + attrs={ + "class": STYLE["text-input"], + "rows": 10, + "placeholder": "Thema der Woche", + } + ), + ) + school_text = forms.CharField( + label=label_span("Berufsschule"), + max_length=300, + widget=forms.Textarea( + attrs={ + "class": STYLE["text-input"], + "rows": 5, + "placeholder": "Berufsschule", + } + ), + ) + img = forms.ImageField( + label=label_span("Bild"), + required=False, + widget=forms.ClearableFileInput( + attrs={ + "class": "block w-full p-2 text-sm text-gray-900 border border-gray-300 rounded-lg cursor-pointer bg-gray-50 dark:text-gray-400 focus:outline-none dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400", + "accept": "image/*", + } + ), + ) + + @property + def display_names(self): + return { + "department": self.fields["department"].label, + "company_text": self.fields["company_text"].label, + "week_topic": self.fields["week_topic"].label, + "school_text": self.fields["school_text"].label, + } + + def content_values(self) -> dict: + if self.is_valid(): + return { + "company_text": self.cleaned_data["company_text"], + "week_topic": self.cleaned_data["week_topic"], + "school_text": self.cleaned_data["school_text"], + } + + +class DailyReport(forms.Form): + department = forms.CharField( + label=label_span("Abteilung"), + max_length=150, + widget=forms.TextInput( + attrs={ + "placeholder": "Abteilung", + "class": STYLE["text-input"], + } + ), + ) + week_topic = forms.CharField( + label=label_span("Thema der Woche"), + max_length=600, + widget=forms.Textarea( + attrs={ + "class": STYLE["text-input"], + "rows": 5, + "placeholder": "Thema der Woche", + } + ), + ) + monday_text = forms.CharField( + label=label_span("Montag"), + max_length=300, + widget=forms.Textarea( + attrs={ + "class": STYLE["text-input"], + "rows": 5, + "placeholder": "Montag", + } + ), + ) + tuesday_text = forms.CharField( + label=label_span("Dienstag"), + max_length=300, + widget=forms.Textarea( + attrs={ + "class": STYLE["text-input"], + "rows": 5, + "placeholder": "Dienstag", + } + ), + ) + wednesday_text = forms.CharField( + label=label_span("Mittwoch"), + max_length=300, + widget=forms.Textarea( + attrs={ + "class": STYLE["text-input"], + "rows": 5, + "placeholder": "Mittwoch,", + } + ), + ) + thursday_text = forms.CharField( + label=label_span("Donnerstag"), + max_length=300, + widget=forms.Textarea( + attrs={ + "class": STYLE["text-input"], + "rows": 5, + "placeholder": "Donnerstag", + } + ), + ) + friday_text = forms.CharField( + label=label_span("Freitag"), + max_length=300, + widget=forms.Textarea( + attrs={ + "class": STYLE["text-input"], + "rows": 5, + "placeholder": "Freitag", + } + ), + ) + img = forms.ImageField( + label=label_span("Bild"), + required=False, + widget=forms.ClearableFileInput( + attrs={ + "class": "block w-full p-2 text-sm text-gray-900 border border-gray-300 rounded-lg cursor-pointer bg-gray-50 dark:text-gray-400 focus:outline-none dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400", + "accept": "image/*", + } + ), + ) + + @property + def display_names(self) -> dict: + return { + "department": self.fields["department"].label, + "week_topic": self.fields["week_topic"].label, + "monday_text": self.fields["monday_text"].label, + "tuesday_text": self.fields["tuesday_text"].label, + "wednesday_text": self.fields["wednesday_text"].label, + "thursday_text": self.fields["thursday_text"].label, + "friday_text": self.fields["friday_text"].label, + } + + def content_values(self) -> dict: + if self.is_valid(): + return { + "week_topic": self.cleaned_data["week_topic"], + "monday_text": self.cleaned_data["monday_text"], + "tuesday_text": self.cleaned_data["tuesday_text"], + "wednesday_text": self.cleaned_data["wednesday_text"], + "thursday_text": self.cleaned_data["thursday_text"], + "friday_text": self.cleaned_data["friday_text"], + } + + +def choose_report_kind(kind, request=None, files=None): + match kind: + case "weekly": + return WeeklyReport(request, files) + case "daily": + return DailyReport(request, files) diff --git a/core/styles.py b/core/styles.py new file mode 100644 index 0000000..7f9ce5a --- /dev/null +++ b/core/styles.py @@ -0,0 +1,11 @@ +# Tailwind Styles + +STYLE = { + "red_btn": "text-white bg-red-700 hover:bg-red-800 focus:outline-none focus:ring-4 focus:ring-red-300 font-medium rounded-full text-sm px-5 py-2.5 text-center me-2 mb-2 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-900", + "card": "bg-white drop-shadow-md mx-auto mt-5 aspect-[2/3] hover:drop-shadow-xl hover:scale-[0.85] scale-[0.8] lg:hover:scale-[0.95] lg:scale-[0.9] transition-all duration-50 transform ease-in-out w-60 sm:w-80 flex items-center justify-center", + "text-input": "w-full p-2 border border-gray-300 rounded-lg focus:ring focus:ring-blue-400 mb-5", +} + + +def label_span(txt): + return f'{txt}' diff --git a/core/templates/component/report.html b/core/templates/component/report.html new file mode 100644 index 0000000..331469c --- /dev/null +++ b/core/templates/component/report.html @@ -0,0 +1,13 @@ + + {% if report.is_approved %} +
+ ✓ +
+ {% endif %} + +
diff --git a/core/templates/head.html b/core/templates/head.html new file mode 100644 index 0000000..e1d2240 --- /dev/null +++ b/core/templates/head.html @@ -0,0 +1,3 @@ + + + diff --git a/core/templates/htmx/reports.html b/core/templates/htmx/reports.html new file mode 100644 index 0000000..aee0210 --- /dev/null +++ b/core/templates/htmx/reports.html @@ -0,0 +1,12 @@ +{% if reports|length != 0 %} + {% for report in reports %} + + {% include 'component/report.html' with report=report %} + + {% endfor %} + +
+ + {% endif %} diff --git a/core/templates/index.html b/core/templates/index.html old mode 100755 new mode 100644 index ab7cf7f..7655f66 --- a/core/templates/index.html +++ b/core/templates/index.html @@ -1,34 +1,45 @@ - - - Azube - - - - {{ user.display_name }} + {{ title|safe }} -

Deine Berichtshefte

+ {% if late_reports > 1 %} + + + + {% endif %} - {% if reports|length == 0 %} - - {% endif %} + {% if reports|length == 0 %} +

wau, such empty!

+ + + {% else %} + +
{% for report in reports %} -
-

{{ report }}

-
- {% endfor %} - - - \ No newline at end of file + + {% if forloop.first %} {% if report.week != week_now %} + + + + + {% endif %} {% endif %} + + {% include 'component/report.html' with report=report %} + + {% endfor %} + +
+ + {% endif %} + +
+
diff --git a/core/templates/report.html b/core/templates/report.html new file mode 100644 index 0000000..f47c82f --- /dev/null +++ b/core/templates/report.html @@ -0,0 +1,48 @@ +{{ title|safe }} + +
+
+
+

+ Berichtsheft {{ report.year }} / {{ report.week }} +

+

+ Is Approved? + + {{ report.is_approved }} + +

+
+ +
+ +
+

Content:

+
+
+
+ {% load access markdown %} + {% for key, value in report.content.items %} +
+

{{ form.display_names|access:key|safe }}

+
{{ value|markdown|safe }}
+
+ {% endfor %} +
+ + {% if report.image is not None %} + {% load b64 %} +
+ +
+ {% endif %} + +
+
+
+
diff --git a/core/templates/report_template/weekly.html b/core/templates/report_template/weekly.html new file mode 100644 index 0000000..6c05a5c --- /dev/null +++ b/core/templates/report_template/weekly.html @@ -0,0 +1,2464 @@ +{% load markdown %} + + + + + + + + + + + + + + + + + + + + +
+ +

 

+ +

 

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

 

+
+

 

+
+

Name: {{ user }}

+
+

Ausbildungsnachweis Nr. {{ num }}

+
+

 

+
+

 

+
+

 

+
+

 

+
+

Ausbildungsjahr: {{ year }}

+
+

Firma:

+
+

Ausbildungsabteilung: {{ department }}

+
+

 

+
+

 

+
+

 

+
+

 

+
+

CANCOM GmbH

+
+

 

+
+

Woche: {{ week }}

+
+

Messerschmittstr. 20

+
+

 

+
+

 

+
+

89343 Jettingen-Scheppach

+
+

 

+
+

 

+
+

 

+
+

 

+
+

vom: {{ start_date }} bis: {{ end_date }}

+
+

 

+
+

 

+
+

 

+
+

Betriebliche + Tätigkeit (bitte + Ausbildungsverlauf mit der zeitlichen und sachlichen Gliederung abgleichen):

+

 

+

 

+

{{ company_text|markdown|safe }}        

+

 

+

 

+

 

+
+

Stunden

+
+

Thema der Woche:

+

  {{ week_topic|markdown|safe }}

+

 

+

 

+

 

+
+

 

+
+

 

+
+

 

+
+

Berufsschule (Themen des + Unterrichts)

+

 

+

{{ school_text|markdown|safe }}

+

 

+

 

+

 

+
+

 

+
+

 

+
+

 

+
+

8

+
+

Gesamtstunden

+
+

40

+
+

 

+
+

 

+
+

 

+
+

Für + die

+
+

                   

+
+

 

+
+

 

+
+

 

+
+

Richtigkeit

+
+

____________________

+
+

____________________

+
+

____________________

+
+

____________________

+
+

 

+
+

Datum

+
+

Auszubildender

+
+

Datum

+
+

Ausbilder

+
+

 

+
+

 

+
+

 

+
+ +
+ +

 

+ +
+ + + + diff --git a/core/templates/settings.html b/core/templates/settings.html new file mode 100644 index 0000000..9d14eb4 --- /dev/null +++ b/core/templates/settings.html @@ -0,0 +1,8 @@ +{{ title|safe }} + +

+ Benutzer: +

+ +

{{ user.display_name }}

+

{{ user.role.value|capfirst }}

diff --git a/core/templates/shell.html b/core/templates/shell.html new file mode 100644 index 0000000..a4402a5 --- /dev/null +++ b/core/templates/shell.html @@ -0,0 +1,70 @@ + + + {{ title }} + {% include 'head.html' %} + + + + +
+
+ + +

{{ title }}

+
+ + +
+ +
+ +
+ + +
+ {{ main_content|safe }} +
+
+
+ + + + + + diff --git a/core/templates/test.html b/core/templates/test.html deleted file mode 100755 index 31ea0fc..0000000 --- a/core/templates/test.html +++ /dev/null @@ -1,384 +0,0 @@ - - - Test - - - - - - - -

Deine Berichtshefte

- - - - - - \ No newline at end of file diff --git a/core/templates/write.html b/core/templates/write.html old mode 100755 new mode 100644 index 7d3ccbe..3ec6783 --- a/core/templates/write.html +++ b/core/templates/write.html @@ -1,50 +1,127 @@ - - - Neues Berichtsheft - - -

Neues Berichtsheft

+
+ + {{ title|safe }} + +
+

{{ user.display_name }}

+

+ Berichtsheft {{ year }} / {{ week }} +

+

+ Von: {{ start_date|date:"d.m.Y" }} bis: {{ end_date|date:"d.m.Y" }} +

+
-

Berichtsheft {{ year }} / {{ week }}

+
- - {% for var in definition.vars %} -
- - + + + {% if field.id_for_label == "id_department" %} + + {% if draft is not None %} + {{ field|set_content:draft.department }} + {% else %} + {{ field }} {% endif %} - {% if var.name == "year" %} - value = "{{ year }}" - {% endif %} - - {% if var.name == "start_date" %} - value = "{{ start_date|date:"d.m.Y" }}" - {% endif %} - - {% if var.name == "end_date" %} - value = "{{ end_date|date:"d.m.Y" }}" - {% endif %} - - {% if var.name == "num_doc" %} - value = "{{ current_num }}" - {% endif %} - - {% if var.name == "name" %} - value = "{{ user.display_name }}" - {% endif %} - - > + {% else %} + {% with content=draft.content|access:field.id_for_label %} + {{ field|set_content:content }} + {% endwith %} +{% endif %} + {% if field.errors %} +

{{ field.errors|join:", " }}

+ {% endif %}
- {% endfor %} - - + {% endfor %} {% csrf_token %} -
+ + + - - + + +
diff --git a/core/templatetags/access.py b/core/templatetags/access.py new file mode 100644 index 0000000..1cd9369 --- /dev/null +++ b/core/templatetags/access.py @@ -0,0 +1,13 @@ +from django import template + +register = template.Library() + + +@register.filter(name="access") +def access(value, arg): + if value == None: + return "" + try: + return value[arg] + except: + return "" diff --git a/core/templatetags/add_attr.py b/core/templatetags/add_attr.py new file mode 100644 index 0000000..51e77b7 --- /dev/null +++ b/core/templatetags/add_attr.py @@ -0,0 +1,25 @@ +from django import template + +register = template.Library() + + +@register.filter(name="add_attr") +def add_attr(field, css): + """Adds custom attributes (like class) to form fields without overwriting existing attributes.""" + # Retrieve existing attributes (if any) + attrs = field.field.widget.attrs.copy() + + # Split the CSS string into key-value pairs + definition = css.split(",") + + for d in definition: + if ":" not in d: + # If no key-value pair is provided, default to 'class' + attrs["class"] = f"{attrs.get('class', '')} {d}".strip() + else: + # If a key-value pair is provided (e.g., 'data-test:123'), add it + key, val = d.split(":") + attrs[key.strip()] = val.strip() + + # Return the widget with the updated attributes + return field.as_widget(attrs=attrs) diff --git a/core/templatetags/b64.py b/core/templatetags/b64.py new file mode 100644 index 0000000..6c82718 --- /dev/null +++ b/core/templatetags/b64.py @@ -0,0 +1,11 @@ +from django import template +import base64 + +register = template.Library() + + +@register.filter(name="b64") +def b64(value): + if value is None: + return "" + return base64.b64encode(value).decode("utf-8") diff --git a/core/templatetags/markdown.py b/core/templatetags/markdown.py new file mode 100644 index 0000000..3728b52 --- /dev/null +++ b/core/templatetags/markdown.py @@ -0,0 +1,12 @@ +from django import template +import markdown +import bleach + +register = template.Library() + + +@register.filter(name="markdown") +def markdown_tag(value): + return markdown.markdown( + bleach.clean(value, tags=[], attributes=[]), extensions=["nl2br"] + ) diff --git a/core/templatetags/set_content.py b/core/templatetags/set_content.py new file mode 100644 index 0000000..0c5dc22 --- /dev/null +++ b/core/templatetags/set_content.py @@ -0,0 +1,27 @@ +from django import template +from django import forms + +register = template.Library() + + +@register.filter(name="set_content") +def set_content(field, content): + """Sets the content of input and textarea fields, and custom attributes like class, without overwriting existing attributes.""" + + # Retrieve existing attributes (if any) + attrs = field.field.widget.attrs.copy() + + # Check if the field is a text input or textarea + is_input_or_textarea = isinstance( + field.field.widget, (forms.widgets.TextInput, forms.widgets.Textarea) + ) + + # If the field is an input or textarea, set the content as value or text + if is_input_or_textarea: + attrs["value"] = content.strip() # Set the value to the provided content + + if isinstance(field.field.widget, forms.widgets.Textarea): + field.initial = content.strip() + + # Return the widget with the updated attributes + return field.as_widget(attrs=attrs) diff --git a/core/tests.py b/core/tests.py old mode 100755 new mode 100644 diff --git a/core/urls.py b/core/urls.py old mode 100755 new mode 100644 index 6a9c653..4e7c018 --- a/core/urls.py +++ b/core/urls.py @@ -4,5 +4,11 @@ from . import views urlpatterns = [ path("", views.index, name="index"), path("write", views.write_new_report, name="write"), - path("test", views.test, name="test"), + path("report/", views.report_detail_page, name="report_detail"), + path("report//pdf", views.report_pdf_route, name="report_pdf"), + path("report//html", views.report_html_route, name="report_html"), + path("report//png", views.report_png_route, name="report_png"), + path("reports", views.reports_list, name="reports_list"), + path("draft", views.report_draft_update, name="report_draft_update"), + path("settings", views.settings_page, name="settings"), ] diff --git a/core/util.py b/core/util.py old mode 100755 new mode 100644 index c93ebe8..b383732 --- a/core/util.py +++ b/core/util.py @@ -1,5 +1,9 @@ import datetime +from django.template.loader import render_to_string +from django.shortcuts import render +from django.http import HttpResponse + def next_date(year: int, week: int) -> (int, int): if week >= 52: @@ -30,3 +34,35 @@ def get_week_range(p_year, p_week): ).date() lastdayofweek = firstdayofweek + datetime.timedelta(days=6.9) return firstdayofweek, lastdayofweek + + +def is_htmx_request(request) -> bool: + return request.headers.get("HX-Request") is not None + + +def title(t): + return f""" + + """ + + +def htmx_request(request, content_template, vars, page_title): + vars["title"] = title(page_title) + vars["htmx"] = is_htmx_request(request) + + main_content = render_to_string(content_template, vars, request=request) + + if is_htmx_request(request): + return HttpResponse(main_content, content_type="text/html") + else: + return render( + request, + "shell.html", + { + "title": page_title, + "main_content": main_content, + }, + ) diff --git a/core/views.py b/core/views.py old mode 100755 new mode 100644 index 7f760ec..a477744 --- a/core/views.py +++ b/core/views.py @@ -1,11 +1,17 @@ -from django.shortcuts import redirect, render +from django.http import HttpResponse +from django.shortcuts import get_object_or_404, redirect, render -from core.forms import extract_post_values -from core.report_templates import ReportTemplates from core.util import get_week_range, next_date from .azure_auth import AzureUser from .models import Berichtsheft +from django.core.paginator import Paginator +from django.template.loader import render_to_string +from core.styles import STYLE +from .util import is_htmx_request, title, htmx_request +from .reports import choose_report_kind +from .doc_gen import gen_doc_html, gen_doc_pdf, gen_doc_png import datetime +import json # Create your views here. @@ -20,34 +26,45 @@ def write_new_report(request): def write_new_report_post(request): user = AzureUser(request) - definition = user.get_report_template() + report_form = user.get_report_kind_form(request.POST, request.FILES) - values = extract_post_values(request.POST, definition["vars"]) + if not report_form.is_valid(): + return HttpResponse("Bad Request", status=400) - # TODO : Input Validation + # TODO : Implement values + + current_year, current_week, _, _, current_num = get_current_report_values( + user.reports().order_by("-year", "-week").first() + ) + + upload = report_form.cleaned_data["img"] + img = upload.read() if upload is not None else None report = Berichtsheft( user=user.id, - num=int(values.pop("num_doc", "")), - year=int(values.pop("year", "")), - week=int(values.pop("week", "")), - content=values, + kind=user.get_report_kind(), + num=int(current_num), + year=int(current_year), + week=int(current_week), + department=report_form.cleaned_data["department"], + content=report_form.content_values(), + image=img, ) report.save() + + # Clear draft + BerichtsheftDraft.objects.filter(user=user.id).delete() + return redirect("/") -def write_new_report_get(request): - user = AzureUser(request) +def get_current_report_values(latest): + year_now, week_now, _ = datetime.datetime.today().isocalendar() - definition = user.get_report_template() - - # Get the latest year and week - latest = user.reports().order_by("-year", "-week").first() if latest is not None: current_year, current_week = next_date(latest.year, latest.week) else: - current_year, current_week, _ = datetime.datetime.today().isocalendar() + current_year, current_week = year_now, week_now start_date, end_date = get_week_range(current_year, current_week) @@ -56,9 +73,37 @@ def write_new_report_get(request): else: current_num = 1 - # TODO : Cookies for persistent saves + return current_year, current_week, start_date, end_date, current_num - return render( + +def write_new_report_get(request): + user = AzureUser(request) + + report_kind = user.get_report_kind() + + # Get the latest year and week + latest = user.latest_report() + + year_now, week_now, _ = datetime.datetime.today().isocalendar() + + # Report for this week already exists + if latest is not None: + if latest.year == year_now and latest.week == week_now: + return redirect(f"/report/{latest.id}") + + ( + current_year, + current_week, + start_date, + end_date, + current_num, + ) = get_current_report_values(latest) + + draft = BerichtsheftDraft.objects.filter(user=user.id).first() + + form = user.get_report_kind_form() + + return htmx_request( request, "write.html", { @@ -68,8 +113,11 @@ def write_new_report_get(request): "start_date": start_date, "end_date": end_date, "current_num": current_num, - "definition": definition, + "report_kind": report_kind, + "form": form, + "draft": draft, }, + "Neues Berichtsheft", ) @@ -77,10 +125,145 @@ def index(request): user = AzureUser(request) # Get all berichtshefte - all_reports = user.reports() + reports = Paginator(user.reports(), 30).get_page(1) - return render(request, "index.html", {"user": user, "reports": all_reports}) + year_now, week_now, _ = datetime.datetime.today().isocalendar() + + return htmx_request( + request, + "index.html", + { + "user": user, + "reports": reports, + "week_now": week_now, + "late_reports": user.late_reports(), + "style": STYLE, + }, + "Berichtshefte", + ) -def test(request): - return render(request, "test.html", {}) +def reports_list(request): + user = AzureUser(request) + + p = Paginator(user.reports(), 30) + + try: + page_num = int(request.GET.get("page")) + except: + return HttpResponse("Page should be a number", 400) + + if p.num_pages >= page_num: + objs = p.get_page(page_num) + else: + objs = [] + + return render( + request, + "htmx/reports.html", + {"reports": objs, "next": page_num + 1, "style": STYLE}, + ) + + +def report_detail_page(request, report_id): + user = AzureUser(request) + + report = get_object_or_404(Berichtsheft, id=report_id) + form = choose_report_kind(report.kind) + + if report.user != user.id: + return HttpResponse("Nah", status=401) + + return htmx_request( + request, + "report.html", + {"report": report, "form": form, "STYLE": STYLE}, + f"Berichtsheft {report.num}", + ) + + +from django.shortcuts import get_object_or_404 +from django.http import JsonResponse +from .models import BerichtsheftDraft + + +def report_draft_update(request): + if request.method == "POST": + user = AzureUser(request) + + data = json.loads(request.body) + + department = "" + content = {} + + for key, val in data.items(): + if key == "id_department": + department = val + continue + content[key] = val + + draft, created = BerichtsheftDraft.objects.update_or_create( + user=user.id, defaults={"department": department, "content": content} + ) + + return JsonResponse( + { + "status": "success", + "message": "Draft updated" if not created else "Draft created", + } + ) + + return JsonResponse({"error": "Invalid request method"}, status=400) + + +def settings_page(request): + user = AzureUser(request) + + return htmx_request( + request, + "settings.html", + {"user": user}, + "Einstellungen", + ) + + +def report_pdf_route(request, report_id): + user = AzureUser(request) + + report = get_object_or_404(Berichtsheft, id=report_id) + form = choose_report_kind(report.kind) + + if report.user != user.id: + return HttpResponse("Nah", status=401) + + pdf_buffer = gen_doc_pdf(report) + response = HttpResponse(pdf_buffer, content_type="application/pdf") + return response + + +def report_html_route(request, report_id): + user = AzureUser(request) + + report = get_object_or_404(Berichtsheft, id=report_id) + form = choose_report_kind(report.kind) + + if report.user != user.id: + return HttpResponse("Nah", status=401) + + html = gen_doc_html(report) + response = HttpResponse(html) + return response + + +def report_png_route(request, report_id): + user = AzureUser(request) + + report = get_object_or_404(Berichtsheft, id=report_id) + form = choose_report_kind(report.kind) + + if report.user != user.id: + return HttpResponse("Nah", status=401) + + png = gen_doc_png(report) + response = HttpResponse(png, content_type="image/png") + return response diff --git a/manage.py b/manage.py old mode 100755 new mode 100644 diff --git a/requirements.txt b/requirements.txt old mode 100755 new mode 100644 index 7a4844b..5b5aab3 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,7 @@ Django~=4.2.11 -psycopg2 \ No newline at end of file +psycopg2 +markdown +bleach +pillow +weasyprint +pdf2image \ No newline at end of file