add notes app

This commit is contained in:
Java-Fish 2025-06-10 14:49:08 +02:00
parent acb398be1c
commit a29376b3c5
38 changed files with 1720 additions and 45 deletions

0
app/notizen/__init__.py Normal file
View file

22
app/notizen/admin.py Normal file
View 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
View 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
View 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

View 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'],
},
),
]

View 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'),
),
]

View 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'],
},
),
]

View file

116
app/notizen/models.py Normal file
View 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

View file

@ -0,0 +1 @@
# Empty file to make this directory a Python package

View 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
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

20
app/notizen/urls.py Normal file
View 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
View 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)