Add journal, editor, and overview improvements

This commit is contained in:
rucki
2026-08-22 15:19:39 +02:00
parent 4e542c2b8e
commit 28f100c6df
18 changed files with 405 additions and 56 deletions
+122 -11
View File
@@ -1,6 +1,8 @@
import json
import re
from functools import wraps
from calendar import Calendar
from datetime import date
from html import unescape
from urllib.request import Request, urlopen
from django.contrib import messages
@@ -10,10 +12,10 @@ from django.http import HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods, require_POST
from .forms import ApiKeyForm, EditItemForm, QuickItemForm, UserSettingsForm
from .models import ApiKey, Attachment, Item, Tag
from .forms import ApiKeyForm, EditItemForm, QuickItemForm, UserPreferenceForm, UserSettingsForm
from .models import ApiKey, Attachment, Item, Tag, UserPreference
COMMAND_RE = re.compile(r'/(todo|note|link|public|private)\b', re.I)
COMMAND_RE = re.compile(r'/(todo|note|link|journal|public|private)\b', re.I)
URL_RE = re.compile(r'https?://\S+')
ONLY_URL_RE = re.compile(r'^https?://\S+$')
TITLE_RE = re.compile(r'<title[^>]*>(.*?)</title>', re.I | re.S)
@@ -37,7 +39,7 @@ def parse_quick_content(raw):
commands = {c.lower() for c in COMMAND_RE.findall(content)}
content = COMMAND_RE.sub('', content).strip()
only_url = bool(ONLY_URL_RE.match(content))
kind = Item.Kind.TODO if 'todo' in commands else Item.Kind.LINK if 'link' in commands or only_url else Item.Kind.NOTE
kind = Item.Kind.TODO if 'todo' in commands else Item.Kind.JOURNAL if 'journal' in commands else Item.Kind.LINK if 'link' in commands or only_url else Item.Kind.NOTE
visibility = Item.Visibility.PUBLIC if 'public' in commands else Item.Visibility.PRIVATE
url_match = URL_RE.search(content)
url = url_match.group(0) if kind == Item.Kind.LINK and url_match else ''
@@ -46,6 +48,23 @@ def parse_quick_content(raw):
return kind, visibility, content, url
def attach_files_and_replace_tokens(item, files):
replacements = {}
for file in files:
attachment = Attachment.objects.create(item=item, file=file)
replacements[f'![[paste:{file.name}]]'] = f'![[file:{attachment.id}]]'
if replacements:
content = item.content
comment = item.comment
for old, new in replacements.items():
content = content.replace(old, new)
comment = comment.replace(old, new)
if content != item.content or comment != item.comment:
item.content = content
item.comment = comment
item.save(update_fields=['content', 'comment', 'updated_at'])
def visible_items(user, api_key=None):
qs = Item.objects.select_related('owner').prefetch_related('tags', 'attachments', 'linked_items')
if api_key:
@@ -86,9 +105,8 @@ def index(request):
if active_tag and f'#{active_tag.lower()}' not in content.lower():
content = f'{content} #{active_tag}'
item = Item.objects.create(owner=request.user, kind=kind, visibility=visibility, content=content, url=url)
attach_files_and_replace_tokens(item, request.FILES.getlist('files'))
item.sync_metadata()
if request.FILES.get('files'):
Attachment.objects.create(item=item, file=request.FILES['files'])
if request.headers.get('HX-Request'):
response = render(request, 'core/_create_response.html', {'item': item, 'tags': Tag.objects.all(), 'active_tag': request.GET.get('tag')})
response['HX-Trigger'] = 'tagsChanged'
@@ -97,6 +115,7 @@ def index(request):
tag = request.GET.get('tag')
kind = request.GET.get('kind')
status = request.GET.get('status')
day = request.GET.get('day')
q = request.GET.get('q', '').strip()
if q.startswith('#') and len(q) > 1:
query = request.GET.copy()
@@ -104,20 +123,83 @@ def index(request):
query.pop('q', None)
return redirect(f'{request.path}?{query.urlencode()}')
items = visible_items(request.user)
applied_preferences = False
if tag:
items = items.filter(tags__name=tag)
if kind in dict(Item.Kind.choices):
items = items.filter(kind=kind)
elif not any([tag, status, q, day]):
applied_preferences = True
prefs, _ = UserPreference.objects.get_or_create(user=request.user)
type_filter = Q()
if prefs.show_notes:
type_filter |= Q(kind=Item.Kind.NOTE)
if prefs.show_links:
type_filter |= Q(kind=Item.Kind.LINK)
if prefs.show_journal:
type_filter |= Q(kind=Item.Kind.JOURNAL)
if prefs.show_open_todos:
type_filter |= Q(kind=Item.Kind.TODO, is_done=False)
if prefs.show_done_todos:
type_filter |= Q(kind=Item.Kind.TODO, is_done=True)
items = items.filter(type_filter) if type_filter else items.none()
if day:
items = items.filter(created_at__date=day)
if status == 'open':
items = items.filter(kind=Item.Kind.TODO, is_done=False)
elif status == 'archive':
items = items.filter(kind=Item.Kind.TODO, is_done=True)
else:
elif not applied_preferences:
items = items.exclude(kind=Item.Kind.TODO, is_done=True)
if q:
items = items.filter(Q(content__icontains=q) | Q(url__icontains=q) | Q(tags__name__icontains=q)).distinct()
prefs, _ = UserPreference.objects.get_or_create(user=request.user)
journal_days = []
if kind == Item.Kind.JOURNAL:
today = date.today()
year = int(request.GET.get('year', today.year))
month = int(request.GET.get('month', today.month))
journal_qs = visible_items(request.user).filter(kind=Item.Kind.JOURNAL, created_at__year=year, created_at__month=month)
days_with_entries = set(journal_qs.dates('created_at', 'day'))
journal_days = [
{'day': d, 'date': date(year, month, d).isoformat(), 'has_entries': date(year, month, d) in days_with_entries}
for week in Calendar(firstweekday=0).monthdayscalendar(year, month) for d in week if d
]
return render(request, 'core/index.html', {
'form': QuickItemForm(), 'items': items, 'tags': Tag.objects.all(), 'active_tag': tag, 'active_kind': kind, 'active_status': status, 'q': q,
'form': QuickItemForm(), 'items': items, 'tags': Tag.objects.all(), 'active_tag': tag, 'active_kind': kind, 'active_status': status, 'active_day': day, 'q': q, 'journal_days': journal_days, 'overview_lines': prefs.overview_lines,
})
@login_required
def new_item(request):
if request.method == 'POST':
form = QuickItemForm(request.POST, request.FILES)
if form.is_valid():
kind, visibility, content, url = parse_quick_content(form.cleaned_data['content'])
item = Item.objects.create(owner=request.user, kind=kind, visibility=visibility, content=content, url=url)
attach_files_and_replace_tokens(item, request.FILES.getlist('files'))
item.sync_metadata()
return redirect(item.get_absolute_url())
return render(request, 'core/new_item.html', {'form': QuickItemForm(), 'tags': Tag.objects.all()})
@login_required
def journal(request):
today = date.today()
year = int(request.GET.get('year', today.year))
month = int(request.GET.get('month', today.month))
entries = visible_items(request.user).filter(kind=Item.Kind.JOURNAL, created_at__year=year, created_at__month=month)
by_day = {}
for item in entries:
by_day.setdefault(item.created_at.day, []).append(item)
weeks = []
for week in Calendar(firstweekday=0).monthdayscalendar(year, month):
weeks.append([(day, by_day.get(day, [])) for day in week])
prev_month, prev_year = (12, year - 1) if month == 1 else (month - 1, year)
next_month, next_year = (1, year + 1) if month == 12 else (month + 1, year)
return render(request, 'core/journal.html', {
'weeks': weeks, 'year': year, 'month': month,
'prev_year': prev_year, 'prev_month': prev_month, 'next_year': next_year, 'next_month': next_month,
})
@@ -130,6 +212,13 @@ def settings_view(request):
form.save()
messages.success(request, 'Einstellungen gespeichert.')
return redirect('settings')
elif request.POST.get('action') == 'preferences':
prefs, _ = UserPreference.objects.get_or_create(user=request.user)
pref_form = UserPreferenceForm(request.POST, instance=prefs)
if pref_form.is_valid():
pref_form.save()
messages.success(request, 'Standardansicht gespeichert.')
return redirect('settings')
elif request.POST.get('action') == 'apikey':
key_form = ApiKeyForm(request.POST)
if key_form.is_valid():
@@ -139,8 +228,10 @@ def settings_view(request):
key_form.save_m2m()
messages.success(request, f'API Key erzeugt: {api_key.token}')
return redirect('settings')
prefs, _ = UserPreference.objects.get_or_create(user=request.user)
return render(request, 'core/settings.html', {
'profile_form': UserSettingsForm(instance=request.user),
'preference_form': UserPreferenceForm(instance=prefs),
'key_form': ApiKeyForm(),
'api_keys': request.user.api_keys.prefetch_related('tags'),
})
@@ -184,15 +275,28 @@ def edit_item(request, pk):
form = EditItemForm(request.POST, request.FILES, instance=item)
if form.is_valid():
item = form.save()
attach_files_and_replace_tokens(item, request.FILES.getlist('files'))
item.sync_metadata()
if request.FILES.get('files'):
Attachment.objects.create(item=item, file=request.FILES['files'])
response = render(request, 'core/_item.html', {'item': item})
response = render(request, 'core/_item.html', {'item': item, 'overview_lines': request.user.preferences.overview_lines if hasattr(request.user, 'preferences') else 4})
response['HX-Trigger'] = 'tagsChanged'
return response
return render(request, 'core/_edit_form.html', {'item': item, 'tags': Tag.objects.all()})
@login_required
def item_editor(request, pk):
item = get_object_or_404(Item, pk=pk, owner=request.user)
next_url = request.GET.get('next') or item.get_absolute_url()
if request.method == 'POST':
form = EditItemForm(request.POST, request.FILES, instance=item)
if form.is_valid():
item = form.save()
attach_files_and_replace_tokens(item, request.FILES.getlist('files'))
item.sync_metadata()
return redirect(next_url)
return render(request, 'core/item_editor.html', {'item': item, 'tags': Tag.objects.all(), 'next_url': next_url})
@login_required
@require_POST
def delete_item(request, pk):
@@ -211,6 +315,13 @@ def api_tags(request):
return JsonResponse({'tags': list(Tag.objects.order_by('name').values_list('name', flat=True))})
@login_required
def api_attachment_suggestions(request):
item = get_object_or_404(Item, pk=request.GET.get('item'), owner=request.user)
data = [{'id': a.id, 'name': a.file.name.split('/')[-1], 'url': a.file.url} for a in item.attachments.all()]
return JsonResponse({'attachments': data})
@login_required
def api_item_suggestions(request):
q = request.GET.get('q', '').strip()