Add journal, editor, and overview improvements
This commit is contained in:
+122
-11
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user