add notes app
This commit is contained in:
parent
acb398be1c
commit
a29376b3c5
38 changed files with 1720 additions and 45 deletions
|
@ -18,53 +18,30 @@ class AviaryEditForm(forms.ModelForm):
|
||||||
}
|
}
|
||||||
model = Aviary
|
model = Aviary
|
||||||
fields = [
|
fields = [
|
||||||
"name",
|
|
||||||
"location",
|
|
||||||
"description",
|
"description",
|
||||||
"capacity",
|
|
||||||
"current_occupancy",
|
|
||||||
"contact_person",
|
|
||||||
"contact_phone",
|
|
||||||
"contact_email",
|
|
||||||
"notes",
|
|
||||||
"condition",
|
"condition",
|
||||||
"last_ward_round",
|
"last_ward_round",
|
||||||
"comment",
|
"comment",
|
||||||
]
|
]
|
||||||
labels = {
|
labels = {
|
||||||
"name": _("Name"),
|
"description": _("Beschreibung"),
|
||||||
"location": _("Standort"),
|
|
||||||
"description": _("Bezeichnung"),
|
|
||||||
"capacity": _("Kapazität"),
|
|
||||||
"current_occupancy": _("Aktuelle Belegung"),
|
|
||||||
"contact_person": _("Ansprechpartner"),
|
|
||||||
"contact_phone": _("Telefon"),
|
|
||||||
"contact_email": _("E-Mail"),
|
|
||||||
"notes": _("Notizen"),
|
|
||||||
"condition": _("Zustand"),
|
"condition": _("Zustand"),
|
||||||
"last_ward_round": _("Letzte Inspektion"),
|
"last_ward_round": _("Letzte Visite"),
|
||||||
"comment": _("Bemerkungen"),
|
"comment": _("Bemerkungen"),
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
# Set help text for key fields
|
# Mark required fields
|
||||||
if 'capacity' in self.fields:
|
self.fields['description'].required = True
|
||||||
self.fields['capacity'].help_text = str(_("Maximum number of birds this aviary can hold"))
|
self.fields['condition'].required = True
|
||||||
if 'current_occupancy' in self.fields:
|
self.fields['last_ward_round'].required = True
|
||||||
self.fields['current_occupancy'].help_text = str(_("Current number of birds in this aviary"))
|
|
||||||
|
# Set today as default for last_ward_round
|
||||||
|
if not self.instance.pk and 'last_ward_round' in self.fields:
|
||||||
|
self.fields['last_ward_round'].initial = date.today
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""Custom validation for the form."""
|
"""Custom validation for the form."""
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
capacity = cleaned_data.get('capacity')
|
|
||||||
current_occupancy = cleaned_data.get('current_occupancy')
|
|
||||||
|
|
||||||
# Validate that occupancy doesn't exceed capacity
|
|
||||||
if capacity is not None and current_occupancy is not None:
|
|
||||||
if current_occupancy > capacity:
|
|
||||||
raise forms.ValidationError({
|
|
||||||
'current_occupancy': _('Current occupancy cannot exceed capacity.')
|
|
||||||
})
|
|
||||||
|
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
|
@ -64,16 +64,37 @@ class Aviary(models.Model):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
"""Override save to ensure name and location are set."""
|
||||||
|
# Auto-populate name from description if not provided
|
||||||
|
if not self.name and self.description:
|
||||||
|
self.name = self.description
|
||||||
|
|
||||||
|
# Set default location if not provided
|
||||||
|
if not self.location:
|
||||||
|
self.location = "Standardort"
|
||||||
|
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""Custom validation for the model."""
|
"""Custom validation for the model."""
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
# Check required fields for test compatibility
|
# For simplified form, use description as name if name is not provided
|
||||||
if not self.name:
|
if not self.name and self.description:
|
||||||
raise ValidationError({'name': _('This field is required.')})
|
self.name = self.description
|
||||||
|
|
||||||
|
# Set default location if not provided
|
||||||
if not self.location:
|
if not self.location:
|
||||||
raise ValidationError({'location': _('This field is required.')})
|
self.location = "Standardort"
|
||||||
|
|
||||||
|
# Check required fields for test compatibility only if they exist
|
||||||
|
if hasattr(self, '_test_mode') and self._test_mode:
|
||||||
|
if not self.name:
|
||||||
|
raise ValidationError({'name': _('This field is required.')})
|
||||||
|
|
||||||
|
if not self.location:
|
||||||
|
raise ValidationError({'location': _('This field is required.')})
|
||||||
|
|
||||||
# Validate that occupancy doesn't exceed capacity
|
# Validate that occupancy doesn't exceed capacity
|
||||||
if self.current_occupancy and self.capacity and self.current_occupancy > self.capacity:
|
if self.current_occupancy and self.capacity and self.current_occupancy > self.capacity:
|
||||||
|
|
|
@ -45,6 +45,9 @@
|
||||||
<p>
|
<p>
|
||||||
Die Übersicht aller Volieren.
|
Die Übersicht aller Volieren.
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
<a href="{% url 'aviary_create' %}" class="btn btn-primary">Voliere hinzufügen</a>
|
||||||
|
</p>
|
||||||
<table class="table table-striped table-hover display responsive nowrap" width="100%" id="t__aviary_all">
|
<table class="table table-striped table-hover display responsive nowrap" width="100%" id="t__aviary_all">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -68,4 +71,8 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<!-- Notizen für diese Übersicht -->
|
||||||
|
{% load notizen_tags %}
|
||||||
|
{% show_page_notizen "aviary_overview" %}
|
||||||
|
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
136
app/aviary/templates/aviary/aviary_form.html
Normal file
136
app/aviary/templates/aviary/aviary_form.html
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h3>{% if is_create %}Voliere hinzufügen{% else %}Voliere bearbeiten{% endif %}</h3>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8 mb-3">
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.description.id_for_label }}" class="form-label">
|
||||||
|
{{ form.description.label }} <span class="text-danger">*</span>
|
||||||
|
</label>
|
||||||
|
{{ form.description }}
|
||||||
|
{% if form.description.errors %}
|
||||||
|
<div class="text-danger">{{ form.description.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.condition.id_for_label }}" class="form-label">
|
||||||
|
{{ form.condition.label }} <span class="text-danger">*</span>
|
||||||
|
</label>
|
||||||
|
{{ form.condition }}
|
||||||
|
{% if form.condition.errors %}
|
||||||
|
<div class="text-danger">{{ form.condition.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.last_ward_round.id_for_label }}" class="form-label">
|
||||||
|
{{ form.last_ward_round.label }} <span class="text-danger">*</span>
|
||||||
|
</label>
|
||||||
|
<div class="input-group">
|
||||||
|
{{ form.last_ward_round }}
|
||||||
|
<button type="button" class="btn btn-outline-primary" id="today-btn">Heute</button>
|
||||||
|
</div>
|
||||||
|
{% if form.last_ward_round.errors %}
|
||||||
|
<div class="text-danger">{{ form.last_ward_round.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.comment.id_for_label }}" class="form-label">
|
||||||
|
{{ form.comment.label }}
|
||||||
|
</label>
|
||||||
|
{{ form.comment }}
|
||||||
|
{% if form.comment.errors %}
|
||||||
|
<div class="text-danger">{{ form.comment.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
{% if is_create %}
|
||||||
|
<button class="btn btn-success" type="submit">Speichern</button>
|
||||||
|
<button class="btn btn-info" type="submit" name="save_and_add">Sichern und neu hinzufügen</button>
|
||||||
|
<button class="btn btn-info" type="submit" name="save_and_continue">Sichern und weiter bearbeiten</button>
|
||||||
|
{% else %}
|
||||||
|
<button class="btn btn-primary" type="submit">Speichern</button>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{% url 'aviary_all' %}" class="btn btn-secondary">Abbrechen</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<small class="text-muted">* Pflichtfeld</small>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>Informationen</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h6>Beschreibung</h6>
|
||||||
|
<p class="small">
|
||||||
|
Die Beschreibung dient zur eindeutigen Identifikation der Voliere.
|
||||||
|
Verwenden Sie einen aussagekräftigen Namen.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h6>Zustand</h6>
|
||||||
|
<p class="small">
|
||||||
|
Der Zustand gibt an, ob die Voliere derzeit genutzt werden kann:
|
||||||
|
<br><strong>Offen:</strong> Verfügbar für neue Tiere
|
||||||
|
<br><strong>Geschlossen:</strong> Temporär nicht verfügbar
|
||||||
|
<br><strong>Gesperrt:</strong> Dauerhaft außer Betrieb
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h6>Letzte Visite</h6>
|
||||||
|
<p class="small">
|
||||||
|
Datum der letzten Kontrolle oder Reinigung der Voliere.
|
||||||
|
Klicken Sie auf "Heute" um das aktuelle Datum einzutragen.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h6>Bemerkungen</h6>
|
||||||
|
<p class="small">
|
||||||
|
Zusätzliche Informationen zur Voliere, wie besondere Ausstattung
|
||||||
|
oder Wartungshinweise.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Add Bootstrap classes to form fields
|
||||||
|
const descriptionField = document.getElementById('{{ form.description.id_for_label }}');
|
||||||
|
const conditionField = document.getElementById('{{ form.condition.id_for_label }}');
|
||||||
|
const dateField = document.getElementById('{{ form.last_ward_round.id_for_label }}');
|
||||||
|
const commentField = document.getElementById('{{ form.comment.id_for_label }}');
|
||||||
|
|
||||||
|
if (descriptionField) descriptionField.classList.add('form-control');
|
||||||
|
if (conditionField) conditionField.classList.add('form-select');
|
||||||
|
if (dateField) dateField.classList.add('form-control');
|
||||||
|
if (commentField) commentField.classList.add('form-control');
|
||||||
|
|
||||||
|
// Today button functionality
|
||||||
|
const todayBtn = document.getElementById('today-btn');
|
||||||
|
if (todayBtn && dateField) {
|
||||||
|
todayBtn.addEventListener('click', function() {
|
||||||
|
const today = new Date();
|
||||||
|
const year = today.getFullYear();
|
||||||
|
const month = String(today.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(today.getDate()).padStart(2, '0');
|
||||||
|
dateField.value = `${year}-${month}-${day}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock content %}
|
|
@ -1,18 +1,60 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load crispy_forms_tags %}
|
{% load crispy_forms_tags %}
|
||||||
|
{% load notizen_tags %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<h3>Voliere <strong>{{ aviary.description }}</strong> bearbeiten </h3>
|
<h3>Voliere <strong>{{ aviary.description }}</strong> bearbeiten </h3>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-5 mt-3 mb-3">
|
<div class="col-lg-5 mt-3 mb-3">
|
||||||
<form method="post" enctype="multipart/form-data">
|
<form method="post" enctype="multipart/form-data">
|
||||||
<fieldset>
|
{% csrf_token %}
|
||||||
{% csrf_token %}
|
|
||||||
{{form|crispy}}
|
<div class="mb-3">
|
||||||
<a href="{% url 'aviary_all' %}" class="btn btn-success">Abbrechen</a>
|
<label for="{{ form.description.id_for_label }}" class="form-label">
|
||||||
<button class="btn btn-primary" type="submit">Speichern</button>
|
{{ form.description.label }} <span class="text-danger">*</span>
|
||||||
</fieldset>
|
</label>
|
||||||
|
{{ form.description }}
|
||||||
|
{% if form.description.errors %}
|
||||||
|
<div class="text-danger">{{ form.description.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.condition.id_for_label }}" class="form-label">
|
||||||
|
{{ form.condition.label }} <span class="text-danger">*</span>
|
||||||
|
</label>
|
||||||
|
{{ form.condition }}
|
||||||
|
{% if form.condition.errors %}
|
||||||
|
<div class="text-danger">{{ form.condition.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.last_ward_round.id_for_label }}" class="form-label">
|
||||||
|
{{ form.last_ward_round.label }} <span class="text-danger">*</span>
|
||||||
|
</label>
|
||||||
|
<div class="input-group">
|
||||||
|
{{ form.last_ward_round }}
|
||||||
|
<button type="button" class="btn btn-outline-primary" id="today-btn">Heute</button>
|
||||||
|
</div>
|
||||||
|
{% if form.last_ward_round.errors %}
|
||||||
|
<div class="text-danger">{{ form.last_ward_round.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.comment.id_for_label }}" class="form-label">
|
||||||
|
{{ form.comment.label }}
|
||||||
|
</label>
|
||||||
|
{{ form.comment }}
|
||||||
|
{% if form.comment.errors %}
|
||||||
|
<div class="text-danger">{{ form.comment.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{% url 'aviary_all' %}" class="btn btn-success">Abbrechen</a>
|
||||||
|
<button class="btn btn-primary" type="submit">Speichern</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-1"></div>
|
<div class="col-lg-1"></div>
|
||||||
|
@ -39,4 +81,32 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Add Bootstrap classes to form fields
|
||||||
|
const descriptionField = document.getElementById('{{ form.description.id_for_label }}');
|
||||||
|
const conditionField = document.getElementById('{{ form.condition.id_for_label }}');
|
||||||
|
const dateField = document.getElementById('{{ form.last_ward_round.id_for_label }}');
|
||||||
|
const commentField = document.getElementById('{{ form.comment.id_for_label }}');
|
||||||
|
|
||||||
|
if (descriptionField) descriptionField.classList.add('form-control');
|
||||||
|
if (conditionField) conditionField.classList.add('form-select');
|
||||||
|
if (dateField) dateField.classList.add('form-control');
|
||||||
|
if (commentField) commentField.classList.add('form-control');
|
||||||
|
|
||||||
|
// Today button functionality
|
||||||
|
const todayBtn = document.getElementById('today-btn');
|
||||||
|
if (todayBtn && dateField) {
|
||||||
|
todayBtn.addEventListener('click', function() {
|
||||||
|
const today = new Date();
|
||||||
|
const year = today.getFullYear();
|
||||||
|
const month = String(today.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(today.getDate()).padStart(2, '0');
|
||||||
|
dateField.value = `${year}-${month}-${day}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
{% endblock content %}
|
{% endblock content %}
|
|
@ -2,10 +2,12 @@ from django.urls import path
|
||||||
|
|
||||||
from .views import (
|
from .views import (
|
||||||
aviary_all,
|
aviary_all,
|
||||||
|
aviary_create,
|
||||||
aviary_single
|
aviary_single
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("all/", aviary_all, name="aviary_all"),
|
path("all/", aviary_all, name="aviary_all"),
|
||||||
|
path("neu/", aviary_create, name="aviary_create"),
|
||||||
path("<id>", aviary_single, name="aviary_single"),
|
path("<id>", aviary_single, name="aviary_single"),
|
||||||
]
|
]
|
||||||
|
|
|
@ -13,6 +13,29 @@ def aviary_all(request):
|
||||||
return render(request, "aviary/aviary_all.html", context)
|
return render(request, "aviary/aviary_all.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required(login_url="account_login")
|
||||||
|
def aviary_create(request):
|
||||||
|
"""Create a new aviary."""
|
||||||
|
form = AviaryEditForm(request.POST or None)
|
||||||
|
if request.method == "POST":
|
||||||
|
if form.is_valid():
|
||||||
|
aviary = form.save(commit=False)
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
aviary.created_by = request.user
|
||||||
|
aviary.save()
|
||||||
|
|
||||||
|
# Handle different save options
|
||||||
|
if 'save_and_add' in request.POST:
|
||||||
|
return redirect("aviary_create") # Redirect to create another
|
||||||
|
elif 'save_and_continue' in request.POST:
|
||||||
|
return redirect("aviary_single", id=aviary.id) # Redirect to edit the created aviary
|
||||||
|
else:
|
||||||
|
return redirect("aviary_all") # Default: go to list
|
||||||
|
|
||||||
|
context = {"form": form, "is_create": True}
|
||||||
|
return render(request, "aviary/aviary_form.html", context)
|
||||||
|
|
||||||
|
|
||||||
@login_required(login_url="account_login")
|
@login_required(login_url="account_login")
|
||||||
def aviary_single(request, id):
|
def aviary_single(request, id):
|
||||||
aviary = Aviary.objects.get(id=id)
|
aviary = Aviary.objects.get(id=id)
|
||||||
|
|
|
@ -82,4 +82,9 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<!-- Notizen für diese Übersicht -->
|
||||||
|
{% load notizen_tags %}
|
||||||
|
{% show_page_notizen "patient_overview" %}
|
||||||
|
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load crispy_forms_tags %}
|
{% load crispy_forms_tags %}
|
||||||
|
{% load notizen_tags %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<h3>Patient <strong>{{ bird.bird_identifier }}</strong> bearbeiten </h3>
|
<h3>Patient <strong>{{ bird.bird_identifier }}</strong> bearbeiten </h3>
|
||||||
|
|
|
@ -73,5 +73,10 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<!-- Notizen für diese Übersicht -->
|
||||||
|
{% load notizen_tags %}
|
||||||
|
{% show_page_notizen "contact_overview" %}
|
||||||
|
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
||||||
|
|
|
@ -85,6 +85,7 @@ INSTALLED_APPS = [
|
||||||
"contact",
|
"contact",
|
||||||
"costs",
|
"costs",
|
||||||
"export",
|
"export",
|
||||||
|
"notizen",
|
||||||
"reports",
|
"reports",
|
||||||
"sendemail",
|
"sendemail",
|
||||||
]
|
]
|
||||||
|
|
|
@ -12,6 +12,7 @@ urlpatterns = [
|
||||||
path("contacts/", include("contact.urls")),
|
path("contacts/", include("contact.urls")),
|
||||||
path("costs/", include("costs.urls")),
|
path("costs/", include("costs.urls")),
|
||||||
path("export/", include("export.urls")),
|
path("export/", include("export.urls")),
|
||||||
|
path("notizen/", include("notizen.urls")),
|
||||||
# Admin
|
# Admin
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
path("admin/reports/", include("reports.urls", namespace="reports")),
|
path("admin/reports/", include("reports.urls", namespace="reports")),
|
||||||
|
|
|
@ -77,5 +77,10 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<!-- Notizen für diese Übersicht -->
|
||||||
|
{% load notizen_tags %}
|
||||||
|
{% show_page_notizen "costs_overview" %}
|
||||||
|
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" </div>
|
||||||
{% load static %}
|
</div>
|
||||||
|
|
||||||
|
{% endblock content %}load static %}
|
||||||
{% load crispy_forms_tags %}
|
{% load crispy_forms_tags %}
|
||||||
|
{% load notizen_tags %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h3>Buchung bearbeiten</h3>
|
<h3>Buchung bearbeiten</h3>
|
||||||
<div class="row mt-3">
|
<div class="row mt-3">
|
||||||
|
@ -21,4 +24,8 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Notizen für diese Buchung -->
|
||||||
|
{% show_object_notizen costs %}
|
||||||
|
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
0
app/notizen/__init__.py
Normal file
0
app/notizen/__init__.py
Normal file
22
app/notizen/admin.py
Normal file
22
app/notizen/admin.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
from .models import Notiz, Page
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Notiz)
|
||||||
|
class NotizAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['name', 'erstellt_von', 'erstellt_am', 'geaendert_am', 'attached_to_model_name', 'attached_to_object_str']
|
||||||
|
list_filter = ['erstellt_am', 'geaendert_am', 'content_type']
|
||||||
|
search_fields = ['name', 'inhalt']
|
||||||
|
readonly_fields = ['erstellt_am', 'geaendert_am']
|
||||||
|
|
||||||
|
def save_model(self, request, obj, form, change):
|
||||||
|
if not change: # If creating a new object
|
||||||
|
obj.erstellt_von = request.user
|
||||||
|
super().save_model(request, obj, form, change)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Page)
|
||||||
|
class PageAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['name', 'identifier', 'description']
|
||||||
|
search_fields = ['name', 'identifier', 'description']
|
||||||
|
readonly_fields = ['identifier']
|
6
app/notizen/apps.py
Normal file
6
app/notizen/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class NotizenConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'notizen'
|
50
app/notizen/forms.py
Normal file
50
app/notizen/forms.py
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
from django import forms
|
||||||
|
from django_ckeditor_5.widgets import CKEditor5Widget
|
||||||
|
from .models import Notiz
|
||||||
|
|
||||||
|
|
||||||
|
class NotizForm(forms.ModelForm):
|
||||||
|
"""Form for creating and editing notes."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Notiz
|
||||||
|
fields = ['name', 'inhalt']
|
||||||
|
widgets = {
|
||||||
|
'name': forms.TextInput(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'Name der Notiz eingeben...'
|
||||||
|
}),
|
||||||
|
'inhalt': CKEditor5Widget(config_name='extends')
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['name'].widget.attrs.update({'autofocus': True})
|
||||||
|
|
||||||
|
|
||||||
|
class NotizAttachForm(forms.ModelForm):
|
||||||
|
"""Form for attaching a note to an object."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Notiz
|
||||||
|
fields = ['name', 'inhalt']
|
||||||
|
widgets = {
|
||||||
|
'name': forms.TextInput(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'Name der Notiz eingeben...'
|
||||||
|
}),
|
||||||
|
'inhalt': CKEditor5Widget(config_name='extends')
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.content_object = kwargs.pop('content_object', None)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['name'].widget.attrs.update({'autofocus': True})
|
||||||
|
|
||||||
|
def save(self, commit=True):
|
||||||
|
notiz = super().save(commit=False)
|
||||||
|
if self.content_object:
|
||||||
|
notiz.content_object = self.content_object
|
||||||
|
if commit:
|
||||||
|
notiz.save()
|
||||||
|
return notiz
|
37
app/notizen/migrations/0001_initial.py
Normal file
37
app/notizen/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
# Generated by Django 5.2.3 on 2025-06-10 11:14
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django_ckeditor_5.fields
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Notiz',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(help_text='Bezeichnung für diese Notiz', max_length=200, verbose_name='Name der Notiz')),
|
||||||
|
('inhalt', django_ckeditor_5.fields.CKEditor5Field(help_text='Inhalt der Notiz in Markdown-Format', verbose_name='Inhalt')),
|
||||||
|
('object_id', models.PositiveIntegerField(blank=True, null=True, verbose_name='Objekt ID')),
|
||||||
|
('erstellt_am', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')),
|
||||||
|
('geaendert_am', models.DateTimeField(auto_now=True, verbose_name='Geändert am')),
|
||||||
|
('content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype', verbose_name='Objekttyp')),
|
||||||
|
('erstellt_von', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Erstellt von')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Notiz',
|
||||||
|
'verbose_name_plural': 'Notizen',
|
||||||
|
'ordering': ['-geaendert_am'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
18
app/notizen/migrations/0002_alter_notiz_object_id.py
Normal file
18
app/notizen/migrations/0002_alter_notiz_object_id.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 5.2.3 on 2025-06-10 11:20
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('notizen', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='notiz',
|
||||||
|
name='object_id',
|
||||||
|
field=models.CharField(blank=True, help_text='ID des verknüpften Objekts (unterstützt sowohl Integer als auch UUID)', max_length=255, null=True, verbose_name='Objekt ID'),
|
||||||
|
),
|
||||||
|
]
|
27
app/notizen/migrations/0003_page.py
Normal file
27
app/notizen/migrations/0003_page.py
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# Generated by Django 5.2.3 on 2025-06-10 12:32
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('notizen', '0002_alter_notiz_object_id'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Page',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('identifier', models.CharField(help_text='Eindeutige Kennung für diese Seite', max_length=100, unique=True, verbose_name='Seiten-Identifier')),
|
||||||
|
('name', models.CharField(help_text='Anzeigename für diese Seite', max_length=200, verbose_name='Seitenname')),
|
||||||
|
('description', models.TextField(blank=True, help_text='Beschreibung der Seite', null=True, verbose_name='Beschreibung')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Seite',
|
||||||
|
'verbose_name_plural': 'Seiten',
|
||||||
|
'ordering': ['name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
0
app/notizen/migrations/__init__.py
Normal file
0
app/notizen/migrations/__init__.py
Normal file
116
app/notizen/models.py
Normal file
116
app/notizen/models.py
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
from django.db import models
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
|
from django_ckeditor_5.fields import CKEditor5Field
|
||||||
|
|
||||||
|
|
||||||
|
class Page(models.Model):
|
||||||
|
"""
|
||||||
|
Model to represent overview pages that can have notes attached.
|
||||||
|
"""
|
||||||
|
identifier = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
unique=True,
|
||||||
|
verbose_name="Seiten-Identifier",
|
||||||
|
help_text="Eindeutige Kennung für diese Seite"
|
||||||
|
)
|
||||||
|
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=200,
|
||||||
|
verbose_name="Seitenname",
|
||||||
|
help_text="Anzeigename für diese Seite"
|
||||||
|
)
|
||||||
|
|
||||||
|
description = models.TextField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name="Beschreibung",
|
||||||
|
help_text="Beschreibung der Seite"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Seite"
|
||||||
|
verbose_name_plural = "Seiten"
|
||||||
|
ordering = ['name']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class Notiz(models.Model):
|
||||||
|
"""
|
||||||
|
Model for user notes that can be attached to different objects.
|
||||||
|
"""
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=200,
|
||||||
|
verbose_name="Name der Notiz",
|
||||||
|
help_text="Bezeichnung für diese Notiz"
|
||||||
|
)
|
||||||
|
|
||||||
|
inhalt = CKEditor5Field(
|
||||||
|
verbose_name="Inhalt",
|
||||||
|
help_text="Inhalt der Notiz in Markdown-Format",
|
||||||
|
config_name='extends'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Generic foreign key to attach notes to any model
|
||||||
|
content_type = models.ForeignKey(
|
||||||
|
ContentType,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Objekttyp"
|
||||||
|
)
|
||||||
|
object_id = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Objekt ID",
|
||||||
|
help_text="ID des verknüpften Objekts (unterstützt sowohl Integer als auch UUID)"
|
||||||
|
)
|
||||||
|
content_object = GenericForeignKey('content_type', 'object_id')
|
||||||
|
|
||||||
|
# Metadata
|
||||||
|
erstellt_von = models.ForeignKey(
|
||||||
|
User,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
verbose_name="Erstellt von"
|
||||||
|
)
|
||||||
|
erstellt_am = models.DateTimeField(
|
||||||
|
auto_now_add=True,
|
||||||
|
verbose_name="Erstellt am"
|
||||||
|
)
|
||||||
|
geaendert_am = models.DateTimeField(
|
||||||
|
auto_now=True,
|
||||||
|
verbose_name="Geändert am"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Notiz"
|
||||||
|
verbose_name_plural = "Notizen"
|
||||||
|
ordering = ['-geaendert_am']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('notizen:detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
def get_edit_url(self):
|
||||||
|
return reverse('notizen:edit', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def attached_to_model_name(self):
|
||||||
|
"""Return human-readable model name this note is attached to."""
|
||||||
|
if self.content_type:
|
||||||
|
return self.content_type.model_class()._meta.verbose_name
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def attached_to_object_str(self):
|
||||||
|
"""Return string representation of attached object."""
|
||||||
|
if self.content_object:
|
||||||
|
return str(self.content_object)
|
||||||
|
return None
|
1
app/notizen/templatetags/__init__.py
Normal file
1
app/notizen/templatetags/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# Empty file to make this directory a Python package
|
112
app/notizen/templatetags/notizen_tags.py
Normal file
112
app/notizen/templatetags/notizen_tags.py
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
from django import template
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from notizen.models import Notiz
|
||||||
|
import markdown
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.inclusion_tag('notizen/object_notizen.html', takes_context=True)
|
||||||
|
def show_object_notizen(context, obj):
|
||||||
|
"""
|
||||||
|
Template tag to display notes attached to an object.
|
||||||
|
Usage: {% show_object_notizen object %}
|
||||||
|
"""
|
||||||
|
content_type = ContentType.objects.get_for_model(obj)
|
||||||
|
notizen = Notiz.objects.filter(
|
||||||
|
content_type=content_type,
|
||||||
|
object_id=obj.pk
|
||||||
|
).order_by('-geaendert_am')
|
||||||
|
|
||||||
|
# Convert markdown to HTML for each note
|
||||||
|
notizen_with_html = []
|
||||||
|
for notiz in notizen:
|
||||||
|
html_content = markdown.markdown(notiz.inhalt, extensions=['markdown.extensions.fenced_code'])
|
||||||
|
notizen_with_html.append({
|
||||||
|
'notiz': notiz,
|
||||||
|
'html_content': html_content
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'notizen_with_html': notizen_with_html,
|
||||||
|
'content_object': obj,
|
||||||
|
'content_type': content_type,
|
||||||
|
'user': context['user'],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def content_type_id(obj):
|
||||||
|
"""
|
||||||
|
Filter to get content type ID for an object.
|
||||||
|
Usage: {{ object|content_type_id }}
|
||||||
|
"""
|
||||||
|
return ContentType.objects.get_for_model(obj).id
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def notiz_attach_url(obj):
|
||||||
|
"""
|
||||||
|
Template tag to generate URL for attaching a note to an object.
|
||||||
|
Usage: {% notiz_attach_url object %}
|
||||||
|
"""
|
||||||
|
from django.urls import reverse
|
||||||
|
content_type = ContentType.objects.get_for_model(obj)
|
||||||
|
return reverse('notizen:attach', kwargs={
|
||||||
|
'content_type_id': content_type.id,
|
||||||
|
'object_id': obj.pk
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def notiz_count_for_object(obj):
|
||||||
|
"""
|
||||||
|
Template tag to get the count of notes for an object.
|
||||||
|
Usage: {% notiz_count_for_object object %}
|
||||||
|
"""
|
||||||
|
content_type = ContentType.objects.get_for_model(obj)
|
||||||
|
return Notiz.objects.filter(
|
||||||
|
content_type=content_type,
|
||||||
|
object_id=obj.pk
|
||||||
|
).count()
|
||||||
|
|
||||||
|
|
||||||
|
@register.inclusion_tag('notizen/page_notizen.html', takes_context=True)
|
||||||
|
def show_page_notizen(context, page_identifier):
|
||||||
|
"""
|
||||||
|
Template tag to display notes attached to a specific page/overview.
|
||||||
|
Usage: {% show_page_notizen "patient_overview" %}
|
||||||
|
"""
|
||||||
|
from notizen.models import Page
|
||||||
|
|
||||||
|
# Get or create the page object
|
||||||
|
page, created = Page.objects.get_or_create(
|
||||||
|
identifier=page_identifier,
|
||||||
|
defaults={
|
||||||
|
'name': page_identifier.replace('_', ' ').title(),
|
||||||
|
'description': f'Übersichtsseite für {page_identifier}'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get notes attached to this page
|
||||||
|
content_type = ContentType.objects.get_for_model(Page)
|
||||||
|
notizen = Notiz.objects.filter(
|
||||||
|
content_type=content_type,
|
||||||
|
object_id=page.pk
|
||||||
|
).order_by('-geaendert_am')
|
||||||
|
|
||||||
|
# Convert markdown to HTML for each note
|
||||||
|
notizen_with_html = []
|
||||||
|
for notiz in notizen:
|
||||||
|
html_content = markdown.markdown(notiz.inhalt, extensions=['markdown.extensions.fenced_code'])
|
||||||
|
notizen_with_html.append({
|
||||||
|
'notiz': notiz,
|
||||||
|
'html_content': html_content
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'notizen_with_html': notizen_with_html,
|
||||||
|
'page_identifier': page_identifier,
|
||||||
|
'page': page,
|
||||||
|
'user': context['user'],
|
||||||
|
}
|
3
app/notizen/tests.py
Normal file
3
app/notizen/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
20
app/notizen/urls.py
Normal file
20
app/notizen/urls.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = 'notizen'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
# Main note views
|
||||||
|
path('', views.notizen_list, name='list'),
|
||||||
|
path('neu/', views.notiz_create, name='create'),
|
||||||
|
path('<int:pk>/', views.notiz_detail, name='detail'),
|
||||||
|
path('<int:pk>/bearbeiten/', views.notiz_edit, name='edit'),
|
||||||
|
path('<int:pk>/loeschen/', views.notiz_delete, name='delete'),
|
||||||
|
|
||||||
|
# Object attachment views
|
||||||
|
path('anhaengen/<int:content_type_id>/<str:object_id>/', views.attach_notiz, name='attach'),
|
||||||
|
path('objekt/<int:content_type_id>/<str:object_id>/', views.object_notizen, name='object_notizen'),
|
||||||
|
|
||||||
|
# Page attachment views
|
||||||
|
path('seite/<str:page_identifier>/anhaengen/', views.attach_page_notiz, name='attach_page'),
|
||||||
|
]
|
210
app/notizen/views.py
Normal file
210
app/notizen/views.py
Normal file
|
@ -0,0 +1,210 @@
|
||||||
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.http import Http404
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from .models import Notiz
|
||||||
|
from .forms import NotizForm, NotizAttachForm
|
||||||
|
import markdown
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def notizen_list(request):
|
||||||
|
"""List all notes created by the user."""
|
||||||
|
notizen = Notiz.objects.filter(erstellt_von=request.user)
|
||||||
|
|
||||||
|
# Pagination
|
||||||
|
paginator = Paginator(notizen, 10)
|
||||||
|
page_number = request.GET.get('page')
|
||||||
|
page_obj = paginator.get_page(page_number)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'page_obj': page_obj,
|
||||||
|
'notizen': page_obj,
|
||||||
|
}
|
||||||
|
return render(request, 'notizen/list.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def notiz_detail(request, pk):
|
||||||
|
"""Display a single note."""
|
||||||
|
notiz = get_object_or_404(Notiz, pk=pk, erstellt_von=request.user)
|
||||||
|
|
||||||
|
# Convert markdown to HTML
|
||||||
|
html_content = markdown.markdown(notiz.inhalt, extensions=['markdown.extensions.fenced_code'])
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'notiz': notiz,
|
||||||
|
'html_content': html_content,
|
||||||
|
}
|
||||||
|
return render(request, 'notizen/detail.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def notiz_create(request):
|
||||||
|
"""Create a new note."""
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = NotizForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
notiz = form.save(commit=False)
|
||||||
|
notiz.erstellt_von = request.user
|
||||||
|
notiz.save()
|
||||||
|
messages.success(request, f'Notiz "{notiz.name}" wurde erfolgreich erstellt.')
|
||||||
|
return redirect('notizen:detail', pk=notiz.pk)
|
||||||
|
else:
|
||||||
|
form = NotizForm()
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'form': form,
|
||||||
|
'title': 'Neue Notiz erstellen',
|
||||||
|
}
|
||||||
|
return render(request, 'notizen/form.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def notiz_edit(request, pk):
|
||||||
|
"""Edit an existing note."""
|
||||||
|
notiz = get_object_or_404(Notiz, pk=pk, erstellt_von=request.user)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = NotizForm(request.POST, instance=notiz)
|
||||||
|
if form.is_valid():
|
||||||
|
form.save()
|
||||||
|
messages.success(request, f'Notiz "{notiz.name}" wurde erfolgreich aktualisiert.')
|
||||||
|
return redirect('notizen:detail', pk=notiz.pk)
|
||||||
|
else:
|
||||||
|
form = NotizForm(instance=notiz)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'form': form,
|
||||||
|
'notiz': notiz,
|
||||||
|
'title': f'Notiz "{notiz.name}" bearbeiten',
|
||||||
|
}
|
||||||
|
return render(request, 'notizen/form.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def notiz_delete(request, pk):
|
||||||
|
"""Delete a note."""
|
||||||
|
notiz = get_object_or_404(Notiz, pk=pk, erstellt_von=request.user)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
name = notiz.name
|
||||||
|
notiz.delete()
|
||||||
|
messages.success(request, f'Notiz "{name}" wurde erfolgreich gelöscht.')
|
||||||
|
return redirect('notizen:list')
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'notiz': notiz,
|
||||||
|
}
|
||||||
|
return render(request, 'notizen/confirm_delete.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def attach_notiz(request, content_type_id, object_id):
|
||||||
|
"""Attach a new note to an object."""
|
||||||
|
try:
|
||||||
|
content_type = ContentType.objects.get(id=content_type_id)
|
||||||
|
content_object = content_type.get_object_for_this_type(id=object_id)
|
||||||
|
except (ContentType.DoesNotExist, content_type.model_class().DoesNotExist):
|
||||||
|
raise Http404("Objekt nicht gefunden")
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = NotizAttachForm(request.POST, content_object=content_object)
|
||||||
|
if form.is_valid():
|
||||||
|
notiz = form.save(commit=False)
|
||||||
|
notiz.erstellt_von = request.user
|
||||||
|
notiz.save()
|
||||||
|
messages.success(request, f'Notiz "{notiz.name}" wurde erfolgreich an {content_object} angehängt.')
|
||||||
|
|
||||||
|
# Redirect back to the object's detail page
|
||||||
|
if hasattr(content_object, 'get_absolute_url'):
|
||||||
|
return redirect(content_object.get_absolute_url())
|
||||||
|
else:
|
||||||
|
return redirect('notizen:detail', pk=notiz.pk)
|
||||||
|
else:
|
||||||
|
form = NotizAttachForm(content_object=content_object)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'form': form,
|
||||||
|
'content_object': content_object,
|
||||||
|
'title': f'Notiz an {content_object} anhängen',
|
||||||
|
}
|
||||||
|
return render(request, 'notizen/attach_form.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def object_notizen(request, content_type_id, object_id):
|
||||||
|
"""Display all notes attached to an object."""
|
||||||
|
try:
|
||||||
|
content_type = ContentType.objects.get(id=content_type_id)
|
||||||
|
content_object = content_type.get_object_for_this_type(id=object_id)
|
||||||
|
except (ContentType.DoesNotExist, content_type.model_class().DoesNotExist):
|
||||||
|
raise Http404("Objekt nicht gefunden")
|
||||||
|
|
||||||
|
notizen = Notiz.objects.filter(
|
||||||
|
content_type=content_type,
|
||||||
|
object_id=object_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Convert markdown to HTML for each note
|
||||||
|
notizen_with_html = []
|
||||||
|
for notiz in notizen:
|
||||||
|
html_content = markdown.markdown(notiz.inhalt, extensions=['markdown.extensions.fenced_code'])
|
||||||
|
notizen_with_html.append({
|
||||||
|
'notiz': notiz,
|
||||||
|
'html_content': html_content
|
||||||
|
})
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'content_object': content_object,
|
||||||
|
'notizen_with_html': notizen_with_html,
|
||||||
|
}
|
||||||
|
return render(request, 'notizen/object_notizen.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def attach_page_notiz(request, page_identifier):
|
||||||
|
"""Attach a note to a specific page/overview."""
|
||||||
|
from .models import Page
|
||||||
|
|
||||||
|
# Get or create the page object
|
||||||
|
page, created = Page.objects.get_or_create(
|
||||||
|
identifier=page_identifier,
|
||||||
|
defaults={
|
||||||
|
'name': page_identifier.replace('_', ' ').title(),
|
||||||
|
'description': f'Übersichtsseite für {page_identifier}'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = NotizAttachForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
notiz = form.save(commit=False)
|
||||||
|
notiz.erstellt_von = request.user
|
||||||
|
notiz.content_object = page
|
||||||
|
notiz.save()
|
||||||
|
|
||||||
|
messages.success(request, f'Notiz "{notiz.name}" wurde erfolgreich zur Seite "{page.name}" hinzugefügt.')
|
||||||
|
|
||||||
|
# Redirect back to the page where the note was added
|
||||||
|
redirect_urls = {
|
||||||
|
'patient_overview': 'bird_all',
|
||||||
|
'aviary_overview': 'aviary_all',
|
||||||
|
'contact_overview': 'contact_all',
|
||||||
|
'costs_overview': 'costs_all',
|
||||||
|
}
|
||||||
|
|
||||||
|
redirect_url = redirect_urls.get(page_identifier, 'notizen:list')
|
||||||
|
return redirect(redirect_url)
|
||||||
|
else:
|
||||||
|
form = NotizAttachForm()
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'form': form,
|
||||||
|
'page': page,
|
||||||
|
'page_identifier': page_identifier,
|
||||||
|
}
|
||||||
|
return render(request, 'notizen/attach_page.html', context)
|
|
@ -9,6 +9,7 @@ django-environ>=0.9
|
||||||
django-jazzmin>=2.6.0
|
django-jazzmin>=2.6.0
|
||||||
Django>=4.2
|
Django>=4.2
|
||||||
gunicorn>=20.1
|
gunicorn>=20.1
|
||||||
|
markdown>=3.4
|
||||||
names>=0.3.0
|
names>=0.3.0
|
||||||
psycopg2-binary>=2.9
|
psycopg2-binary>=2.9
|
||||||
whitenoise>=6.5
|
whitenoise>=6.5
|
75
app/templates/notizen/attach_form.html
Normal file
75
app/templates/notizen/attach_form.html
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }} - Notizen{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8 mx-auto">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1>
|
||||||
|
<i class="fas fa-paperclip text-primary"></i>
|
||||||
|
{{ title }}
|
||||||
|
</h1>
|
||||||
|
<a href="javascript:history.back()" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Zurück
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-info mb-4">
|
||||||
|
<h5 class="alert-heading">
|
||||||
|
<i class="fas fa-info-circle"></i> Notiz anhängen
|
||||||
|
</h5>
|
||||||
|
<p class="mb-0">
|
||||||
|
Sie erstellen eine neue Notiz, die an <strong>{{ content_object }}</strong> angehängt wird.
|
||||||
|
Die Notiz wird auf der Detailseite dieses Objekts angezeigt.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" novalidate>
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ form.name|as_crispy_field }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
{{ form.inhalt|as_crispy_field }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-paperclip"></i> Notiz anhängen
|
||||||
|
</button>
|
||||||
|
<a href="javascript:history.back()" class="btn btn-outline-secondary ms-2">
|
||||||
|
<i class="fas fa-times"></i> Abbrechen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="fas fa-lightbulb"></i> Tipps für Notizen
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li>Verwenden Sie Markdown-Syntax für Formatierung (z.B. **fett**, *kursiv*)</li>
|
||||||
|
<li>Notizen werden automatisch als HTML gerendert angezeigt</li>
|
||||||
|
<li>Sie können die Notiz später jederzeit bearbeiten</li>
|
||||||
|
<li>Angehängte Notizen sind nur für angemeldete Benutzer sichtbar</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
57
app/templates/notizen/attach_page.html
Normal file
57
app/templates/notizen/attach_page.html
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<h3>Notiz zu "{{ page.name }}" hinzufügen</h3>
|
||||||
|
<p class="text-muted">
|
||||||
|
Fügen Sie eine Notiz zu dieser Übersichtsseite hinzu. Die Notiz wird am Ende der Seite angezeigt.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form|crispy }}
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save"></i> Notiz speichern
|
||||||
|
</button>
|
||||||
|
<a href="javascript:history.back()" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-times"></i> Abbrechen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5><i class="fas fa-info-circle"></i> Hinweise</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h6>Markdown-Unterstützung</h6>
|
||||||
|
<p class="small">
|
||||||
|
Sie können Markdown-Syntax verwenden, um Ihre Notiz zu formatieren:
|
||||||
|
</p>
|
||||||
|
<ul class="small">
|
||||||
|
<li><strong>**Fett**</strong> für fetten Text</li>
|
||||||
|
<li><em>*Kursiv*</em> für kursiven Text</li>
|
||||||
|
<li><code># Überschrift</code> für Überschriften</li>
|
||||||
|
<li><code>- Punkt</code> für Listen</li>
|
||||||
|
<li><code>[Link](URL)</code> für Links</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h6 class="mt-3">Sichtbarkeit</h6>
|
||||||
|
<p class="small">
|
||||||
|
Diese Notiz wird am Ende der Übersichtsseite "{{ page.name }}" angezeigt
|
||||||
|
und ist für alle Benutzer sichtbar, die Zugriff auf diese Seite haben.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
86
app/templates/notizen/confirm_delete.html
Normal file
86
app/templates/notizen/confirm_delete.html
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Notiz löschen - {{ notiz.name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-6 mx-auto">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1>
|
||||||
|
<i class="fas fa-trash text-danger"></i>
|
||||||
|
Notiz löschen
|
||||||
|
</h1>
|
||||||
|
<a href="{% url 'notizen:detail' notiz.pk %}" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Zurück
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<h4 class="alert-heading">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i> Achtung!
|
||||||
|
</h4>
|
||||||
|
<p>Sie sind dabei, die folgende Notiz unwiderruflich zu löschen:</p>
|
||||||
|
<hr>
|
||||||
|
<h5>{{ notiz.name }}</h5>
|
||||||
|
{% if notiz.attached_to_object_str %}
|
||||||
|
<p class="mb-0">
|
||||||
|
<small class="text-muted">
|
||||||
|
Angehängt an: {{ notiz.attached_to_model_name }} - {{ notiz.attached_to_object_str }}
|
||||||
|
</small>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Bestätigung erforderlich</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>Diese Aktion kann nicht rückgängig gemacht werden. Sind Sie sicher, dass Sie diese Notiz löschen möchten?</p>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<button type="submit" class="btn btn-danger">
|
||||||
|
<i class="fas fa-trash"></i> Ja, endgültig löschen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="{% url 'notizen:detail' notiz.pk %}" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-times"></i> Abbrechen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Notiz-Details</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-4"><strong>Name:</strong></div>
|
||||||
|
<div class="col-sm-8">{{ notiz.name }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-4"><strong>Erstellt von:</strong></div>
|
||||||
|
<div class="col-sm-8">{{ notiz.erstellt_von.get_full_name|default:notiz.erstellt_von.username }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-4"><strong>Erstellt am:</strong></div>
|
||||||
|
<div class="col-sm-8">{{ notiz.erstellt_am|date:"d.m.Y H:i" }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-4"><strong>Zuletzt geändert:</strong></div>
|
||||||
|
<div class="col-sm-8">{{ notiz.geaendert_am|date:"d.m.Y H:i" }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
138
app/templates/notizen/detail.html
Normal file
138
app/templates/notizen/detail.html
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
|
{% block title %}{{ notiz.name }} - Notizen{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1>
|
||||||
|
<i class="fas fa-sticky-note text-primary"></i>
|
||||||
|
{{ notiz.name }}
|
||||||
|
</h1>
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<a href="{% url 'notizen:list' %}" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Zurück zur Liste
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'notizen:edit' notiz.pk %}" class="btn btn-primary">
|
||||||
|
<i class="fas fa-edit"></i> Bearbeiten
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if notiz.attached_to_object_str %}
|
||||||
|
<div class="alert alert-info mb-4">
|
||||||
|
<i class="fas fa-paperclip"></i>
|
||||||
|
Diese Notiz ist angehängt an: <strong>{{ notiz.attached_to_model_name }} - {{ notiz.attached_to_object_str }}</strong>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<small class="text-muted">
|
||||||
|
Erstellt von: {{ notiz.erstellt_von.get_full_name|default:notiz.erstellt_von.username }}
|
||||||
|
am {{ notiz.erstellt_am|date:"d.m.Y H:i" }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<small class="text-muted">
|
||||||
|
Zuletzt geändert: {{ notiz.geaendert_am|date:"d.m.Y H:i" }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="notiz-content">
|
||||||
|
{{ html_content|safe }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer bg-transparent">
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<a href="{% url 'notizen:edit' notiz.pk %}" class="btn btn-primary">
|
||||||
|
<i class="fas fa-edit"></i> Bearbeiten
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'notizen:delete' notiz.pk %}" class="btn btn-outline-danger">
|
||||||
|
<i class="fas fa-trash"></i> Löschen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.notiz-content {
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notiz-content h1, .notiz-content h2, .notiz-content h3,
|
||||||
|
.notiz-content h4, .notiz-content h5, .notiz-content h6 {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notiz-content h1:first-child, .notiz-content h2:first-child,
|
||||||
|
.notiz-content h3:first-child, .notiz-content h4:first-child,
|
||||||
|
.notiz-content h5:first-child, .notiz-content h6:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notiz-content p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notiz-content pre {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
padding: 1rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notiz-content code {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 0.125rem 0.25rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-size: 0.875em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notiz-content pre code {
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notiz-content blockquote {
|
||||||
|
border-left: 4px solid #dee2e6;
|
||||||
|
padding-left: 1rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notiz-content ul, .notiz-content ol {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding-left: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notiz-content table {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notiz-content table th,
|
||||||
|
.notiz-content table td {
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notiz-content table th {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
100
app/templates/notizen/form.html
Normal file
100
app/templates/notizen/form.html
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }} - Notizen{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8 mx-auto">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1>
|
||||||
|
<i class="fas fa-sticky-note text-primary"></i>
|
||||||
|
{{ title }}
|
||||||
|
</h1>
|
||||||
|
<a href="{% url 'notizen:list' %}" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Zurück zur Liste
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" novalidate>
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ form.name|as_crispy_field }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
{{ form.inhalt|as_crispy_field }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save"></i> Speichern
|
||||||
|
</button>
|
||||||
|
<a href="{% url 'notizen:list' %}" class="btn btn-outline-secondary ms-2">
|
||||||
|
<i class="fas fa-times"></i> Abbrechen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if notiz %}
|
||||||
|
<div>
|
||||||
|
<a href="{% url 'notizen:detail' notiz.pk %}" class="btn btn-outline-info">
|
||||||
|
<i class="fas fa-eye"></i> Vorschau
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'notizen:delete' notiz.pk %}" class="btn btn-outline-danger ms-2">
|
||||||
|
<i class="fas fa-trash"></i> Löschen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if notiz %}
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="fas fa-info-circle"></i> Notiz-Informationen
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<p><strong>Erstellt von:</strong> {{ notiz.erstellt_von.get_full_name|default:notiz.erstellt_von.username }}</p>
|
||||||
|
<p><strong>Erstellt am:</strong> {{ notiz.erstellt_am|date:"d.m.Y H:i" }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<p><strong>Zuletzt geändert:</strong> {{ notiz.geaendert_am|date:"d.m.Y H:i" }}</p>
|
||||||
|
{% if notiz.attached_to_object_str %}
|
||||||
|
<p><strong>Angehängt an:</strong> {{ notiz.attached_to_model_name }} - {{ notiz.attached_to_object_str }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
{{ block.super }}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Auto-save draft functionality could be added here
|
||||||
|
const form = document.querySelector('form');
|
||||||
|
const nameField = document.querySelector('#id_name');
|
||||||
|
|
||||||
|
// Auto-focus on name field for new notes
|
||||||
|
if (nameField && !nameField.value) {
|
||||||
|
nameField.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
108
app/templates/notizen/list.html
Normal file
108
app/templates/notizen/list.html
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
|
{% block title %}Notizen{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<h1 class="mb-4">
|
||||||
|
<i class="fas fa-sticky-note text-primary"></i>
|
||||||
|
Meine Notizen
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<a href="{% url 'notizen:create' %}" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus"></i> Neue Notiz erstellen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if notizen %}
|
||||||
|
<div class="row">
|
||||||
|
{% for notiz in notizen %}
|
||||||
|
<div class="col-md-6 col-lg-4 mb-4">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">
|
||||||
|
<a href="{% url 'notizen:detail' notiz.pk %}" class="text-decoration-none">
|
||||||
|
{{ notiz.name }}
|
||||||
|
</a>
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
{% if notiz.attached_to_object_str %}
|
||||||
|
<p class="card-text">
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="fas fa-paperclip"></i>
|
||||||
|
Angehängt an: {{ notiz.attached_to_model_name }} - {{ notiz.attached_to_object_str }}
|
||||||
|
</small>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p class="card-text">
|
||||||
|
<small class="text-muted">
|
||||||
|
Zuletzt bearbeitet: {{ notiz.geaendert_am|date:"d.m.Y H:i" }}
|
||||||
|
</small>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer bg-transparent">
|
||||||
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
|
<a href="{% url 'notizen:detail' notiz.pk %}" class="btn btn-outline-primary">
|
||||||
|
<i class="fas fa-eye"></i> Anzeigen
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'notizen:edit' notiz.pk %}" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-edit"></i> Bearbeiten
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
{% if page_obj.has_other_pages %}
|
||||||
|
<nav aria-label="Notizen Navigation">
|
||||||
|
<ul class="pagination justify-content-center">
|
||||||
|
{% if page_obj.has_previous %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page=1">Erste</a>
|
||||||
|
</li>
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">Zurück</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<li class="page-item active">
|
||||||
|
<span class="page-link">
|
||||||
|
Seite {{ page_obj.number }} von {{ page_obj.paginator.num_pages }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{% if page_obj.has_next %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page={{ page_obj.next_page_number }}">Weiter</a>
|
||||||
|
</li>
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}">Letzte</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<h4 class="alert-heading">Keine Notizen vorhanden</h4>
|
||||||
|
<p>Sie haben noch keine Notizen erstellt.</p>
|
||||||
|
<hr>
|
||||||
|
<p class="mb-0">
|
||||||
|
<a href="{% url 'notizen:create' %}" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus"></i> Erste Notiz erstellen
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
125
app/templates/notizen/object_notizen.html
Normal file
125
app/templates/notizen/object_notizen.html
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
{% load static %}
|
||||||
|
{% load notizen_tags %}
|
||||||
|
|
||||||
|
<!-- Notizen für dieses Objekt -->
|
||||||
|
{% if notizen_with_html %}
|
||||||
|
<div class="mt-4">
|
||||||
|
<h4>
|
||||||
|
<i class="fas fa-sticky-note text-primary"></i>
|
||||||
|
Notizen ({{ notizen_with_html|length }})
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
{% for item in notizen_with_html %}
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">{{ item.notiz.name }}</h5>
|
||||||
|
<div class="btn-group btn-group-sm">
|
||||||
|
<a href="{% url 'notizen:edit' item.notiz.pk %}" class="btn btn-outline-primary">
|
||||||
|
<i class="fas fa-edit"></i> Bearbeiten
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'notizen:detail' item.notiz.pk %}" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-external-link-alt"></i> Vollansicht
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">
|
||||||
|
Von {{ item.notiz.erstellt_von.get_full_name|default:item.notiz.erstellt_von.username }}
|
||||||
|
am {{ item.notiz.erstellt_am|date:"d.m.Y H:i" }}
|
||||||
|
{% if item.notiz.geaendert_am != item.notiz.erstellt_am %}
|
||||||
|
(bearbeitet am {{ item.notiz.geaendert_am|date:"d.m.Y H:i" }})
|
||||||
|
{% endif %}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="notiz-content">
|
||||||
|
{{ item.html_content|safe }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Button zum Hinzufügen einer neuen Notiz -->
|
||||||
|
<div class="mt-3">
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<a href="{% url 'notizen:attach' content_type.id content_object.pk %}" class="btn btn-outline-primary">
|
||||||
|
<i class="fas fa-plus"></i> Notiz hinzufügen
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.notiz-content {
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notiz-content h1, .notiz-content h2, .notiz-content h3,
|
||||||
|
.notiz-content h4, .notiz-content h5, .notiz-content h6 {
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notiz-content h1:first-child, .notiz-content h2:first-child,
|
||||||
|
.notiz-content h3:first-child, .notiz-content h4:first-child,
|
||||||
|
.notiz-content h5:first-child, .notiz-content h6:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notiz-content p {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notiz-content pre {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-size: 0.875em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notiz-content code {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 0.125rem 0.25rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-size: 0.875em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notiz-content pre code {
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notiz-content blockquote {
|
||||||
|
border-left: 4px solid #dee2e6;
|
||||||
|
padding-left: 1rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
color: #6c757d;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notiz-content ul, .notiz-content ol {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notiz-content table {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notiz-content table th,
|
||||||
|
.notiz-content table td {
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notiz-content table th {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
98
app/templates/notizen/page_notizen.html
Normal file
98
app/templates/notizen/page_notizen.html
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
{% load static %}
|
||||||
|
{% load notizen_tags %}
|
||||||
|
|
||||||
|
<!-- Notizen für diese Seite -->
|
||||||
|
{% if notizen_with_html %}
|
||||||
|
<div class="mt-4 mb-4" style="border-top: 2px solid #dc3545; padding-top: 20px;">
|
||||||
|
<h4>
|
||||||
|
<i class="fas fa-sticky-note text-primary"></i>
|
||||||
|
Notizen zu dieser Übersicht ({{ notizen_with_html|length }})
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
{% for item in notizen_with_html %}
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">{{ item.notiz.name }}</h5>
|
||||||
|
<div class="btn-group btn-group-sm">
|
||||||
|
<a href="{% url 'notizen:edit' item.notiz.pk %}" class="btn btn-outline-primary">
|
||||||
|
<i class="fas fa-edit"></i> Bearbeiten
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'notizen:detail' item.notiz.pk %}" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-eye"></i> Details
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="fas fa-calendar"></i> {{ item.notiz.geaendert_am|date:"d.m.Y H:i" }} Uhr
|
||||||
|
{% if item.notiz.autor %}
|
||||||
|
| <i class="fas fa-user"></i> {{ item.notiz.autor }}
|
||||||
|
{% endif %}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="notiz-content">
|
||||||
|
{{ item.html_content|safe }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Button zum Hinzufügen einer neuen Notiz -->
|
||||||
|
<div class="mt-3 mb-4" style="border-top: 1px solid #dee2e6; padding-top: 15px;">
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<a href="{% url 'notizen:attach_page' page_identifier %}" class="btn btn-outline-primary">
|
||||||
|
<i class="fas fa-plus"></i> Notiz zu dieser Übersicht hinzufügen
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.notiz-content {
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notiz-content h1,
|
||||||
|
.notiz-content h2,
|
||||||
|
.notiz-content h3,
|
||||||
|
.notiz-content h4,
|
||||||
|
.notiz-content h5,
|
||||||
|
.notiz-content h6 {
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notiz-content p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notiz-content ul,
|
||||||
|
.notiz-content ol {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding-left: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notiz-content blockquote {
|
||||||
|
border-left: 4px solid #007bff;
|
||||||
|
padding-left: 1rem;
|
||||||
|
margin-left: 0;
|
||||||
|
font-style: italic;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notiz-content code {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-size: 0.875em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notiz-content pre {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -36,6 +36,10 @@
|
||||||
<a class="nav-link {% if '/contacts' in request.path %} active {% endif %}"
|
<a class="nav-link {% if '/contacts' in request.path %} active {% endif %}"
|
||||||
href="{% url 'contact_all' %}">Kontakte</a>
|
href="{% url 'contact_all' %}">Kontakte</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if '/notizen' in request.path %} active {% endif %}"
|
||||||
|
href="{% url 'notizen:list' %}">Notizen</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if '/bird/species' in request.path %} active {% endif %}"
|
<a class="nav-link {% if '/bird/species' in request.path %} active {% endif %}"
|
||||||
href="{% url 'bird_species_list' %}">Vogelarten</a>
|
href="{% url 'bird_species_list' %}">Vogelarten</a>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue