Compare commits
10 commits
638ae328ea
...
696e603476
Author | SHA1 | Date | |
---|---|---|---|
696e603476 | |||
3caecf63f6 | |||
d64efb663a | |||
73e019de9a | |||
7a70163e76 | |||
|
43fe9aea31 | ||
|
4b496a7d4f | ||
|
d248c99242 | ||
|
34f3367a08 | ||
|
d2e6b65a12 |
50 changed files with 3563 additions and 534 deletions
0
.gitignore
vendored
Executable file → Normal file
0
.gitignore
vendored
Executable file → Normal file
0
azube/__init__.py
Executable file → Normal file
0
azube/__init__.py
Executable file → Normal file
0
azube/asgi.py
Executable file → Normal file
0
azube/asgi.py
Executable file → Normal file
0
azube/settings.py
Executable file → Normal file
0
azube/settings.py
Executable file → Normal file
0
azube/urls.py
Executable file → Normal file
0
azube/urls.py
Executable file → Normal file
0
azube/wsgi.py
Executable file → Normal file
0
azube/wsgi.py
Executable file → Normal file
0
core/__init__.py
Executable file → Normal file
0
core/__init__.py
Executable file → Normal file
0
core/admin.py
Executable file → Normal file
0
core/admin.py
Executable file → Normal file
0
core/apps.py
Executable file → Normal file
0
core/apps.py
Executable file → Normal file
41
core/azure_auth.py
Executable file → Normal file
41
core/azure_auth.py
Executable file → Normal 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
37
core/doc_gen.py
Executable file → Normal 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
|
||||||
|
|
|
@ -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
1
core/migrations/0001_initial.py
Executable file → Normal 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
1
core/migrations/0002_alter_berichtsheft_id.py
Executable file → Normal 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"),
|
||||||
]
|
]
|
||||||
|
|
1
core/migrations/0003_group_azureuser_alter_berichtsheft_user.py
Executable file → Normal file
1
core/migrations/0003_group_azureuser_alter_berichtsheft_user.py
Executable file → Normal 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"),
|
||||||
|
|
1
core/migrations/0004_remove_berichtsheft_approved_by.py
Executable file → Normal file
1
core/migrations/0004_remove_berichtsheft_approved_by.py
Executable file → Normal 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"),
|
||||||
]
|
]
|
||||||
|
|
1
core/migrations/0005_user_alter_berichtsheft_user_delete_azureuser.py
Executable file → Normal file
1
core/migrations/0005_user_alter_berichtsheft_user_delete_azureuser.py
Executable file → Normal 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
1
core/migrations/0006_berichtsheft_num.py
Executable file → Normal 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"),
|
||||||
]
|
]
|
||||||
|
|
28
core/migrations/0007_approval.py
Normal file
28
core/migrations/0007_approval.py
Normal 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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
17
core/migrations/0008_berichtsheft_needs_rewrite.py
Normal file
17
core/migrations/0008_berichtsheft_needs_rewrite.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
21
core/migrations/0009_berichtsheft_kind.py
Normal file
21
core/migrations/0009_berichtsheft_kind.py
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
17
core/migrations/0010_berichtsheft_department.py
Normal file
17
core/migrations/0010_berichtsheft_department.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
20
core/migrations/0011_berichtsheftdraft.py
Normal file
20
core/migrations/0011_berichtsheftdraft.py
Normal 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()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
17
core/migrations/0012_berichtsheft_image.py
Normal file
17
core/migrations/0012_berichtsheft_image.py
Normal 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
0
core/migrations/__init__.py
Executable file → Normal file
48
core/models.py
Executable file → Normal file
48
core/models.py
Executable file → Normal 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()
|
||||||
|
|
|
@ -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
196
core/reports.py
Normal 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
11
core/styles.py
Normal 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>'
|
13
core/templates/component/report.html
Normal file
13
core/templates/component/report.html
Normal 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
3
core/templates/head.html
Normal 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">
|
12
core/templates/htmx/reports.html
Normal file
12
core/templates/htmx/reports.html
Normal 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
71
core/templates/index.html
Executable file → Normal 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>
|
||||||
|
|
48
core/templates/report.html
Normal file
48
core/templates/report.html
Normal 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>
|
2464
core/templates/report_template/weekly.html
Normal file
2464
core/templates/report_template/weekly.html
Normal file
File diff suppressed because it is too large
Load diff
8
core/templates/settings.html
Normal file
8
core/templates/settings.html
Normal 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
70
core/templates/shell.html
Normal 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()">
|
||||||
|
☰
|
||||||
|
</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>
|
|
@ -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
161
core/templates/write.html
Executable file → Normal 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>
|
||||||
|
|
13
core/templatetags/access.py
Normal file
13
core/templatetags/access.py
Normal 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 ""
|
25
core/templatetags/add_attr.py
Normal file
25
core/templatetags/add_attr.py
Normal 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
11
core/templatetags/b64.py
Normal 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")
|
12
core/templatetags/markdown.py
Normal file
12
core/templatetags/markdown.py
Normal 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"]
|
||||||
|
)
|
27
core/templatetags/set_content.py
Normal file
27
core/templatetags/set_content.py
Normal 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
0
core/tests.py
Executable file → Normal file
8
core/urls.py
Executable file → Normal file
8
core/urls.py
Executable file → Normal 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
36
core/util.py
Executable file → Normal 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
231
core/views.py
Executable file → Normal 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
0
manage.py
Executable file → Normal file
7
requirements.txt
Executable file → Normal file
7
requirements.txt
Executable file → Normal file
|
@ -1,2 +1,7 @@
|
||||||
Django~=4.2.11
|
Django~=4.2.11
|
||||||
psycopg2
|
psycopg2
|
||||||
|
markdown
|
||||||
|
bleach
|
||||||
|
pillow
|
||||||
|
weasyprint
|
||||||
|
pdf2image
|
Loading…
Add table
Add a link
Reference in a new issue