Compare commits

...

10 commits

Author SHA1 Message Date
696e603476
report pdf + export options + work 2024-12-06 15:07:03 +01:00
3caecf63f6
work+roles 2024-12-06 14:04:17 +01:00
d64efb663a
image feature + refactor 2024-12-06 12:10:30 +01:00
73e019de9a
htmx single page application + work 2024-12-05 17:03:42 +01:00
7a70163e76
work + report draft 2024-12-05 13:45:51 +01:00
Angelo Rodriguez
43fe9aea31 work done 2024-12-04 16:36:57 +01:00
Angelo Rodriguez
4b496a7d4f work 2024-12-04 14:04:12 +01:00
Angelo Rodriguez
d248c99242 work+htmx 2024-12-04 11:41:57 +01:00
Angelo Rodriguez
34f3367a08 work 2024-12-04 09:37:01 +01:00
Angelo Rodriguez
d2e6b65a12 work 2024-12-03 15:15:45 +01:00
50 changed files with 3563 additions and 534 deletions

0
.gitignore vendored Executable file → Normal file
View file

0
azube/__init__.py Executable file → Normal file
View file

0
azube/asgi.py Executable file → Normal file
View file

0
azube/settings.py Executable file → Normal file
View file

0
azube/urls.py Executable file → Normal file
View file

0
azube/wsgi.py Executable file → Normal file
View file

0
core/__init__.py Executable file → Normal file
View file

0
core/admin.py Executable file → Normal file
View file

0
core/apps.py Executable file → Normal file
View file

41
core/azure_auth.py Executable file → Normal file
View file

@ -1,8 +1,17 @@
import datetime
import json import json
import base64 import base64
from core.models import Berichtsheft 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: class AzureUser:
@ -33,8 +42,32 @@ class AzureUser:
self.id = "anon" self.id = "anon"
def reports(self): 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 # 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

37
core/doc_gen.py Executable file → Normal file
View file

@ -1,12 +1,35 @@
import json 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: def gen_doc_html(report: Berichtsheft):
definition = json.loads(open(f"{template}.json").read()) return render_to_string(f"report_template/{report.kind}.html", report.vars())
content = open(f"{template}.html").read()
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

View file

@ -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

1
core/migrations/0001_initial.py Executable file → Normal file
View file

@ -6,7 +6,6 @@ import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [

1
core/migrations/0002_alter_berichtsheft_id.py Executable file → Normal file
View file

@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("core", "0001_initial"), ("core", "0001_initial"),
] ]

View file

@ -8,7 +8,6 @@ import django.utils.timezone
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("auth", "0012_alter_user_first_name_max_length"), ("auth", "0012_alter_user_first_name_max_length"),
("core", "0002_alter_berichtsheft_id"), ("core", "0002_alter_berichtsheft_id"),

View file

@ -4,7 +4,6 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("core", "0003_group_azureuser_alter_berichtsheft_user"), ("core", "0003_group_azureuser_alter_berichtsheft_user"),
] ]

View file

@ -7,7 +7,6 @@ import django.utils.timezone
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("auth", "0012_alter_user_first_name_max_length"), ("auth", "0012_alter_user_first_name_max_length"),
("core", "0004_remove_berichtsheft_approved_by"), ("core", "0004_remove_berichtsheft_approved_by"),

1
core/migrations/0006_berichtsheft_num.py Executable file → Normal file
View file

@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("core", "0005_user_alter_berichtsheft_user_delete_azureuser"), ("core", "0005_user_alter_berichtsheft_user_delete_azureuser"),
] ]

View file

@ -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",
),
),
],
),
]

View file

@ -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),
),
]

View file

@ -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,
),
),
]

View file

@ -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),
),
]

View file

@ -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()),
],
),
]

View file

@ -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),
),
]

0
core/migrations/__init__.py Executable file → Normal file
View file

48
core/models.py Executable file → Normal file
View file

@ -1,6 +1,8 @@
from django.db import models from django.db import models
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.contrib.auth.models import Group as BuiltinGroup from django.contrib.auth.models import Group as BuiltinGroup
from .util import get_week_range
import base64
class User(AbstractUser): class User(AbstractUser):
@ -21,22 +23,66 @@ class Group(BuiltinGroup):
proxy = True proxy = True
class ReportKind(models.TextChoices):
WEEKLY = "weekly"
DAILY = "daily"
class Berichtsheft(models.Model): class Berichtsheft(models.Model):
id = models.AutoField(primary_key=True) id = models.AutoField(primary_key=True)
user = models.TextField() user = models.TextField()
kind = models.CharField(
max_length=20, choices=ReportKind.choices, default=ReportKind.WEEKLY
)
num = models.PositiveBigIntegerField(default=0) num = models.PositiveBigIntegerField(default=0)
year = models.PositiveIntegerField() year = models.PositiveIntegerField()
week = models.PositiveSmallIntegerField() week = models.PositiveSmallIntegerField()
department = models.CharField(max_length=160, default="")
content = models.JSONField() content = models.JSONField()
needs_rewrite = models.BooleanField(default=False)
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
image = models.BinaryField(null=True, blank=True)
def __str__(self): def __str__(self):
return f"Berichtsheft: {self.user}, Year: {self.year}, Week: {self.week}" 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) id = models.AutoField(primary_key=True)
user = models.TextField() user = models.TextField()
report = models.ForeignKey( report = models.ForeignKey(
Berichtsheft, on_delete=models.CASCADE, related_name="report" 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()

View file

@ -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

196
core/reports.py Normal file
View file

@ -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)

11
core/styles.py Normal file
View file

@ -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'<span class="block text-base font-medium font-bold text-gray-700 mr-1 mb-3">{txt}</span>'

View file

@ -0,0 +1,13 @@
<a class="{{ style.card }}"
href="/report/{{ report.id }}"
hx-get="/report/{{ report.id }}" hx-target="#main_content" hx-push-url="true" hx-swap="innerHTML"
>
{% if report.is_approved %}
<div
class="absolute top-0 right-0 translate-y-3 -translate-x-3 bg-green-500 text-white rounded-full w-6 h-6 flex items-center justify-center scale-[1.5]"
>
</div>
{% endif %}
<img src="/report/{{ report.id }}/png" class="w-full h-full object-cover">
</a>

3
core/templates/head.html Normal file
View file

@ -0,0 +1,3 @@
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/htmx.org@2.0.3" integrity="sha384-0895/pl2MU10Hqc6jd4RvrthNlDiE9U1tWmX7WRESftEDRosgxNsQG/Ze9YMRzHq" crossorigin="anonymous"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">

View file

@ -0,0 +1,12 @@
{% if reports|length != 0 %}
{% for report in reports %}
{% include 'component/report.html' with report=report %}
{% endfor %}
<div hx-get="/reports?page={{ next }}"
hx-trigger="revealed"
hx-swap="outerHTML">
{% endif %}

71
core/templates/index.html Executable file → Normal file
View file

@ -1,34 +1,45 @@
<html> {{ title|safe }}
<head>
<title>Azube</title>
<style>
.grid-container {
display: inline-grid;
grid-template-columns: repeat(3, 1fr);
gap: 5px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 0.5rem;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
}
</style>
</head>
<body>
{{ user.display_name }}
<h1> Deine Berichtshefte </h1> {% if late_reports > 1 %}
<div class="bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4 mr-10 ml-10 mt-2" role="alert">
<p class="font-bold"> Du bist nicht aktuell! </p>
<p> Du hast noch {{ late_reports }} Berichtshefte nachzuschreiben. </p>
</div>
{% endif %}
{% if reports|length == 0 %} {% if reports|length == 0 %}
<button onclick="location = '/write'">
Füge dein erstes Berichtsheft hinzu!
</button>
{% endif %}
<h3 class="text-xl justify-center flex m-10 p-5">wau, such empty!</h3>
<button onclick="location = '/write'" class="{{ style.red_btn }} justify-center">
Füge dein erstes Berichtsheft hinzu!
</button>
{% else %}
<div
class="mb-5 -mt-5 flex items-center justify-center grid sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5"
>
{% for report in reports %} {% for report in reports %}
<div class="grid-container">
<p> {{ report }} </p> {% if forloop.first %} {% if report.week != week_now %}
</div> <a class="{{ style.card }} flex items-center justify-center" title="Neues Berichtsheft"
{% endfor %} href="/write" hx-get="/write" hx-target="#main_content" hx-push-url="true" hx-swap="innerHTML"
>
</body> <span class="text-9xl font-bold"> + </span>
</html> </a>
{% endif %} {% endif %}
{% include 'component/report.html' with report=report %}
{% endfor %}
<div hx-get="/reports?page=2"
hx-trigger="revealed"
hx-swap="outerHTML">
{% endif %}
</div>
</div>

View file

@ -0,0 +1,48 @@
{{ title|safe }}
<main class="flex-grow w-full max-w-4xl mx-auto p-6 bg-white shadow-lg rounded-lg mt-6">
<div class="mb-4 flex justify-between">
<div>
<p class="text-lg font-medium text-gray-700">
Berichtsheft <span class="font-semibold">{{ report.year }}</span> / <span class="font-semibold">{{ report.week }}</span>
</p>
<p class="text-gray-700">
Is Approved?
<span class="{% if report.is_approved %}text-green-500 font-semibold{% else %}text-red-500 font-semibold{% endif %}">
{{ report.is_approved }}
</span>
</p>
</div>
<div class="mt-3">
<a href="/report/{{ report.id }}/pdf" class="{{ STYLE.red_btn }}">
Download
</a>
</div>
</div>
<div class="border-t border-gray-200 mt-4 pt-4">
<h2 class="text-lg font-medium text-gray-800 mb-2">Content:</h2>
<dl class="space-y-2">
<div class="flex items-start space-x-4 justify-between">
<div>
{% load access markdown %}
{% for key, value in report.content.items %}
<div class="justify-between items-start">
<h4 class="text-sm font-medium text-gray-600">{{ form.display_names|access:key|safe }}</h4>
<div class="text-sm text-gray-800 mb-4">{{ value|markdown|safe }}</div>
</div>
{% endfor %}
</div>
{% if report.image is not None %}
{% load b64 %}
<div class="w-36">
<img src="data:image/*;base64,{{ report.image|b64 }}" width="128"
class="-mt-8 w-32 h-32 border-2 border-black cursor-pointer rounded-lg shadow-lg hover:shadow-2xl transition-all duration-300">
</div>
{% endif %}
</div>
</dl>
</div>
</main>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,8 @@
{{ title|safe }}
<p class="text-lg font-bold mx-auto">
Benutzer:
</p>
<p> {{ user.display_name }} </p>
<p> {{ user.role.value|capfirst }} </p>

70
core/templates/shell.html Normal file
View file

@ -0,0 +1,70 @@
<html>
<head>
<title id="page_title">{{ title }}</title>
{% include 'head.html' %}
</head>
<body class="bg-gray-50 text-gray-800 min-h-screen">
<!-- Main wrapper that will hold the header, sidebar, and content -->
<div class="flex flex-col min-h-screen">
<header class="w-full bg-red-600 text-white flex py-4 shadow-md max-h-20 h-full max-h-20">
<button id="menu-toggle" class="text-2xl focus:outline-none ml-5 -mt-1" onclick="toggleSidepanel()">
&#9776;
</button>
<h1 class="text-2xl font-bold {% if center is not None and center %} text-center {% else %}ml-5{% endif %} flex-1" id="page_banner_title">{{ title }}</h1>
</header>
<!-- Main content container with sidebar and main content -->
<div class="flex flex-1">
<!-- Sidebar -->
<div id="sidepanel" class="w-0 transition-all duration-250 ease-in-out bg-red-600 shadow-md text-white">
<ul class="space-y-2 font-medium">
<li>
<a href="/" onclick="toggleSidepanel()" class="rounded-lg lg:bg-transparent flex w-full items-center p-2 text-gray-900 rounded-lg dark:text-white hover:bg-red-100 dark:hover:bg-red-700 group"
hx-get="/" hx-target="#main_content" hx-push-url="true" hx-swap="innerHTML"
>
<span class="ms-3">Home</span>
</a>
<a href="/settings" onclick="toggleSidepanel()" class="rounded-lg lg:bg-transparent flex w-full items-center p-2 text-gray-900 rounded-lg dark:text-white hover:bg-red-100 dark:hover:bg-red-700 group"
hx-get="/settings" hx-target="#main_content" hx-push-url="true" hx-swap="innerHTML"
>
<span class="ms-3">Settings</span>
</a>
</li>
</ul>
</div>
<!-- Main content -->
<div class="flex-1 p-4" id="main_content">
{{ main_content|safe }}
</div>
</div>
</div>
<!-- Script to toggle the sidebar -->
<script>
function toggleSidepanel() {
const sidepanel = document.getElementById('sidepanel');
if (sidepanel.classList.contains('w-0')) {
sidepanel.classList.remove('w-0');
sidepanel.classList.add('w-60');
sidepanel.classList.add('p-4');
sidepanel.querySelectorAll('button').forEach(button => {
button.classList.add('bg-red-700');
});
} else {
sidepanel.classList.add('w-0');
sidepanel.classList.remove('w-60');
sidepanel.classList.remove('p-4');
sidepanel.querySelectorAll('button').forEach(button => {
button.classList.remove('bg-red-700');
});
}
}
</script>
</body>
</html>

View file

@ -1,384 +0,0 @@
<html>
<head>
<title> Test </title>
</head>
<body>
<script>
// Modal-Elemente holen
const modal = document.getElementById('berichtModal');
const btnNew = document.querySelector('.btn-new');
const btnClose = document.querySelector('.btn-close');
const btnSave = document.querySelector('.btn-save');
const titelInput = document.getElementById('titel');
const textInput = document.getElementById('text');
// Funktion, um Sterne zu erzeugen
function createStars() {
const starContainer = document.createElement('div');
starContainer.classList.add('stars'); // Container für Sterne
document.body.appendChild(starContainer); // Sterncontainer zum Body hinzufügen
// Anzahl der Sterne, die du erzeugen möchtest
const numStars = 50;
// Zufällige Sterne erzeugen
for (let i = 0; i < numStars; i++) {
const star = document.createElement('div');
star.classList.add('star');
// Zufällige Positionen und Größen für Sterne
const size = Math.random() * 3 + 1; // Größe zwischen 1 und 4px
star.style.width = `${size}px`;
star.style.height = `${size}px`;
// Zufällige Position
const x = Math.random() * window.innerWidth; // Zufällige horizontale Position
const y = Math.random() * window.innerHeight; // Zufällige vertikale Position
star.style.left = `${x}px`;
star.style.top = `${y}px`;
// Zufällige Verzögerung für den Twinkle-Effekt
const delay = Math.random() * 2; // Zufällige Verzögerung zwischen 0 und 2s
star.style.animationDelay = `${delay}s`;
starContainer.appendChild(star); // Stern zum Container hinzufügen
}
}
// Funktion zum Entfernen der Sterne
function removeStars() {
const starContainer = document.querySelector('.stars');
if (starContainer) {
starContainer.remove(); // Entfernt den Container mit den Sternen
}
}
// Funktion zum Öffnen des Modals
function openModal() {
modal.classList.add('show'); // Modal anzeigen mit 'show' Klasse
document.body.classList.add('modal-open'); // Hintergrund unscharf machen
createStars(); // Sterne erzeugen, wenn das Modal geöffnet wird
}
// Funktion zum Schließen des Modals
function closeModal() {
modal.classList.remove('show'); // Modal ausblenden durch Entfernen der 'show' Klasse
document.body.classList.remove('modal-open'); // Hintergrund zurücksetzen
removeStars(); // Sterne entfernen, wenn das Modal geschlossen wird
}
// Funktion zum Speichern eines neuen Berichtshefts
function saveReport() {
const titel = titelInput.value;
const text = textInput.value;
if (titel && text) {
// Hier kannst du das neue Berichtsheft speichern (z.B. in LocalStorage oder in einer Datenbank)
alert(`Berichtsheft gespeichert!\nTitel: ${titel}\nText: ${text}`);
// Modal schließen nach dem Speichern
closeModal();
// Eingabefelder zurücksetzen
titelInput.value = '';
textInput.value = '';
} else {
alert('Bitte fülle alle Felder aus!');
}
}
// Event-Listener für das Öffnen des Modals
btnNew.addEventListener('click', openModal);
// Event-Listener für das Schließen des Modals
btnClose.addEventListener('click', closeModal);
// Event-Listener für das Speichern des Berichtshefts
btnSave.addEventListener('click', saveReport);
// Wenn der Benutzer außerhalb des Modals klickt, soll es ebenfalls geschlossen werden
window.addEventListener('click', function(event) {
if (event.target === modal) {
closeModal();
}
});
// Mausbewegung über das Grid für 3D-Rotation
const reportElements = document.querySelectorAll('.bericht');
function handleMouseMove(event) {
reportElements.forEach((element) => {
const rect = element.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const deltaX = (event.clientX - centerX) / rect.width;
const deltaY = (event.clientY - centerY) / rect.height;
const rotateX = deltaY * 15;
const rotateY = deltaX * -25;
const moveX = deltaX * 40;
// Nur das angeklickte Element wird rotiert und skaliert
if (element.matches(':hover')) {
element.style.transform = `scale(1.05) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`;
element.style.zIndex = 10; // Erhöht den Stapelwert des Elements, um es hervorzuheben
} else {
element.style.transform = 'scale(1)'; // Andere Elemente bleiben unverändert
element.style.zIndex = 1; // Standardstapelwert
}
});
}
// Event-Listener für Mausbewegung
document.querySelector('.container').addEventListener('mousemove', handleMouseMove);
</script>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
box-sizing: border-box;
background-color: #f4f4f4;
}
header {
background-color: #333;
color: white;
padding: 10px 20px;
text-align: center;
}
.container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
padding: 20px;
perspective: 1500px; /* 3D-Effekt */
}
/* Stil für die Berichtselemente */
.bericht {
background-color: white;
padding: 15px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: transform 0.1s ease, box-shadow 0.1s ease;
position: relative;
will-change: transform;
}
.add-new {
display: flex;
justify-content: center;
align-items: center;
}
.btn-new {
width: 60px;
height: 60px;
background-color: #28a745;
color: white;
font-size: 30px;
border: none;
border-radius: 50%;
cursor: pointer;
transition: background-color 0.3s, transform 0.2s;
display: flex;
justify-content: center;
align-items: center;
}
.btn-new:hover {
background-color: #218838;
transform: scale(1.1);
}
/* Modal für neues Berichtsheft */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
transition: opacity 0.3s ease;
}
.modal.show {
display: flex;
opacity: 1;
z-index: 1000; /* Modal über anderen Inhalten */
}
/* Hintergrundunschärfe beim Öffnen des Modals */
.modal.show::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.3);
filter: blur(10px); /* Hintergrund unscharf */
z-index: -1;
}
/* Modal-Inhalt */
.modal-content {
background-color: white;
padding: 40px;
border-radius: 8px;
width: 500px;
max-width: 90%;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transform: scale(1.05); /* Kleinere Zoom-Animation */
transition: transform 0.3s ease;
}
body.modal-open {
filter: blur(5px); /* Unschärfe des Hintergrunds */
transition: filter 0.3s ease;
}
/* Modal bleibt scharf und im Vordergrund */
.modal.show {
z-index: 100; /* Stellt sicher, dass das Modal im Vordergrund ist */
}
/* Eingabefelder im Modal */
.modal input,
.modal textarea {
width: 100%;
padding: 10px;
margin: 10px 0;
border-radius: 5px;
border: 1px solid #ccc;
font-size: 16px;
}
/* Buttons im Modal */
.modal-buttons {
display: flex;
justify-content: space-between;
}
.modal button {
padding: 10px 15px;
background-color: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s;
}
.modal button:hover {
background-color: #0056b3;
}
/* Schließen-Button im Modal */
.btn-close {
background-color: #dc3545;
}
.btn-close:hover {
background-color: #c82333;
}
/* Speichern-Button im Modal */
.btn-save {
background-color: #28a745;
}
.btn-save:hover {
background-color: #218838;
}
/* Animation für die Sterne */
.stars {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 999;
}
.star {
position: absolute;
width: 3px;
height: 3px;
background-color: #fff;
border-radius: 50%;
opacity: 0;
animation: star-animation 1s infinite;
}
/* Sterneffekt im Hintergrund */
body.modal-open .stars {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 50; /* Überlagert den Hintergrund, aber unter dem Modal */
pointer-events: none; /* Verhindert Interaktionen mit den Sternen */
}
.star {
position: absolute;
background-color: white;
border-radius: 50%;
animation: twinkle 2s infinite alternate;
opacity: 0.8;
}
@keyframes twinkle {
0% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
/* Modal-Styling */
#berichtModal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 100; /* Modal ganz oben */
background-color: white;
padding: 20px;
border-radius: 8px;
width: 400px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.modal-content {
background-color: white;
padding: 15px;
border-radius: 8px;
}
.modal-open #berichtModal {
z-index: 1000; /* Sicherstellen, dass das Modal immer oben bleibt */
}
</style>
<h1> Deine Berichtshefte </h1>
<div class="modal-content stars">
<p class="bericht" > Berichtsheft 1 </p>
</div>
<button class="modal"> Add new I guess </button>
</body>
</html>

161
core/templates/write.html Executable file → Normal file
View file

@ -1,50 +1,127 @@
<html> <main class="flex-grow w-full max-w-4xl mx-auto p-6 bg-white shadow-lg rounded-lg mt-6">
<head>
<title> Neues Berichtsheft </title> {{ title|safe }}
</head>
<body> <div class="mb-6">
<h1> Neues Berichtsheft </h1> <p class="text-lg font-medium">{{ user.display_name }}</p>
<p class="text-gray-600">
Berichtsheft <span class="font-semibold">{{ year }}</span> / <span class="font-semibold">{{ week }}</span>
</p>
<p class="text-gray-600">
Von: <span class="font-semibold">{{ start_date|date:"d.m.Y" }}</span> bis: <span class="font-semibold">{{ end_date|date:"d.m.Y" }}</span>
</p>
</div>
<p> Berichtsheft {{ year }} / {{ week }} </p> <form method="post" class="space-y-4" enctype="multipart/form-data">
<form method="post"> {% load access set_content %}
{% for var in definition.vars %}
<div> {% for field in form %}
<label for="{{ var.name }}"> {{ var.display_name }}:</label> <div class="mb-4">
<input type="text" id="{{ var.name }}" name="{{ var.name }}" <label for="{{ field.id_for_label }}">
{{ field.label|safe }}
{% if var.name == "week" %} </label>
value = "{{ week }}"
{% if field.id_for_label == "id_department" %}
{% if draft is not None %}
{{ field|set_content:draft.department }}
{% else %}
{{ field }}
{% endif %} {% endif %}
{% if var.name == "year" %} {% else %}
value = "{{ year }}" {% with content=draft.content|access:field.id_for_label %}
{% endif %} {{ field|set_content:content }}
{% endwith %}
{% if var.name == "start_date" %} {% endif %}
value = "{{ start_date|date:"d.m.Y" }}" {% if field.errors %}
{% endif %} <p class="text-red-500 text-sm">{{ field.errors|join:", " }}</p>
{% 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 %}
>
</div> </div>
{% endfor %} {% endfor %}
<button> Submit </button>
{% csrf_token %} {% csrf_token %}
</form>
<button
type="submit"
class="w-full bg-red-600 text-white py-2 px-4 rounded-lg shadow hover:bg-red-700 focus:ring focus:ring-red-400">
Submit
</button>
</form>
</body> <script>
</html> function updateDraft() {
const formData = {};
{% for field in form %}
var field = document.getElementById("{{ field.id_for_label }}");
formData["{{ field.id_for_label }}"] = field.value;
{% endfor %}
fetch("/draft", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCSRFToken(),
},
body: JSON.stringify(formData),
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log('Updated Draft:', data);
})
.catch(error => {
console.error('Error:', error);
});
}
function debounce(func, delay) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), delay);
};
}
function getCSRFToken() {
const name = 'csrftoken';
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
cookie = cookie.trim();
if (cookie.startsWith(name + '=')) {
return cookie.substring(name.length + 1);
}
}
return null;
}
function setupListeners() {
const textareas = document.querySelectorAll('textarea');
const inputs = document.querySelectorAll('input');
const debouncedSend = debounce(() => updateDraft(), 500);
textareas.forEach(textarea => {
textarea.addEventListener('input', () => debouncedSend());
});
inputs.forEach(input => {
if (input.type === 'text' || input.type === 'email' || input.type === 'password' || input.type === 'hidden') {
input.addEventListener('input', () => debouncedSend());
}
});
}
setupListeners();
</script>
</main>

View file

@ -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 ""

View file

@ -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)

11
core/templatetags/b64.py Normal file
View file

@ -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")

View file

@ -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"]
)

View file

@ -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)

0
core/tests.py Executable file → Normal file
View file

8
core/urls.py Executable file → Normal file
View file

@ -4,5 +4,11 @@ from . import views
urlpatterns = [ urlpatterns = [
path("", views.index, name="index"), path("", views.index, name="index"),
path("write", views.write_new_report, name="write"), path("write", views.write_new_report, name="write"),
path("test", views.test, name="test"), path("report/<int:report_id>", views.report_detail_page, name="report_detail"),
path("report/<int:report_id>/pdf", views.report_pdf_route, name="report_pdf"),
path("report/<int:report_id>/html", views.report_html_route, name="report_html"),
path("report/<int:report_id>/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"),
] ]

36
core/util.py Executable file → Normal file
View file

@ -1,5 +1,9 @@
import datetime 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): def next_date(year: int, week: int) -> (int, int):
if week >= 52: if week >= 52:
@ -30,3 +34,35 @@ def get_week_range(p_year, p_week):
).date() ).date()
lastdayofweek = firstdayofweek + datetime.timedelta(days=6.9) lastdayofweek = firstdayofweek + datetime.timedelta(days=6.9)
return firstdayofweek, lastdayofweek return firstdayofweek, lastdayofweek
def is_htmx_request(request) -> bool:
return request.headers.get("HX-Request") is not None
def title(t):
return f"""
<script>
document.getElementById('page_title').innerHTML = '{t}';
document.getElementById('page_banner_title').innerHTML = '{t}';
</script>
"""
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,
},
)

231
core/views.py Executable file → Normal file
View file

@ -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 core.util import get_week_range, next_date
from .azure_auth import AzureUser from .azure_auth import AzureUser
from .models import Berichtsheft 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 datetime
import json
# Create your views here. # Create your views here.
@ -20,34 +26,45 @@ def write_new_report(request):
def write_new_report_post(request): def write_new_report_post(request):
user = AzureUser(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( report = Berichtsheft(
user=user.id, user=user.id,
num=int(values.pop("num_doc", "")), kind=user.get_report_kind(),
year=int(values.pop("year", "")), num=int(current_num),
week=int(values.pop("week", "")), year=int(current_year),
content=values, week=int(current_week),
department=report_form.cleaned_data["department"],
content=report_form.content_values(),
image=img,
) )
report.save() report.save()
# Clear draft
BerichtsheftDraft.objects.filter(user=user.id).delete()
return redirect("/") return redirect("/")
def write_new_report_get(request): def get_current_report_values(latest):
user = AzureUser(request) 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: if latest is not None:
current_year, current_week = next_date(latest.year, latest.week) current_year, current_week = next_date(latest.year, latest.week)
else: 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) start_date, end_date = get_week_range(current_year, current_week)
@ -56,9 +73,37 @@ def write_new_report_get(request):
else: else:
current_num = 1 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, request,
"write.html", "write.html",
{ {
@ -68,8 +113,11 @@ def write_new_report_get(request):
"start_date": start_date, "start_date": start_date,
"end_date": end_date, "end_date": end_date,
"current_num": current_num, "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) user = AzureUser(request)
# Get all berichtshefte # 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): def reports_list(request):
return render(request, "test.html", {}) 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

0
manage.py Executable file → Normal file
View file

7
requirements.txt Executable file → Normal file
View file

@ -1,2 +1,7 @@
Django~=4.2.11 Django~=4.2.11
psycopg2 psycopg2
markdown
bleach
pillow
weasyprint
pdf2image