Add workspace due indicators and todo assignment

This commit is contained in:
rucki
2026-09-11 12:19:38 +02:00
parent 291c8db471
commit d7472554df
7 changed files with 135 additions and 7 deletions
+34 -4
View File
@@ -7,7 +7,7 @@ from html import unescape
from urllib.request import Request, urlopen
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import login
from django.contrib.auth import get_user_model, login
from django.contrib.auth.decorators import login_required
from django.db import transaction
from django.db.models import Case, Count, IntegerField, ProtectedError, Q, Value, When
@@ -32,6 +32,7 @@ FIRST_LINE_URL_RE = re.compile(r'^(https?://\S+)[ \t]*(?:\r?\n|$)')
TITLE_RE = re.compile(r'<title[^>]*>(.*?)</title>', re.I | re.S)
DATE_COMMAND_RE = re.compile(r'/datum\s+(\d{1,2}\.\d{1,2}\.(?:\d{2}|\d{4}))(?:\s+(\d{1,2}:\d{2}))?\b', re.I)
RELATIVE_DATE_COMMAND_RE = re.compile(r'/(morgen|übermorgen|uebermorgen|nächstewoche|naechstewoche)\b', re.I)
MENTION_RE = re.compile(r'(?<!\w)@([\w.@+-]+)')
CODE_FENCE_RE = re.compile(r'^\s*(`{3,}|~{3,})')
INLINE_CODE_RE = re.compile(r'(`+).*?\1', re.S)
@@ -228,7 +229,7 @@ def user_team_ids(user, workspace=None):
def visible_items(user, api_key=None, workspace=None):
workspace = current_workspace(user, api_key, workspace)
qs = Item.objects.select_related('owner', 'team', 'workspace').prefetch_related('tags', 'attachments', 'linked_items')
qs = Item.objects.select_related('owner', 'assignee', 'team', 'workspace').prefetch_related('tags', 'attachments', 'linked_items')
if workspace:
qs = qs.filter(workspace=workspace)
elif not user.is_authenticated:
@@ -240,12 +241,12 @@ def visible_items(user, api_key=None, workspace=None):
qs = qs.filter(tags__in=allowed_tags).distinct()
return qs
if user.is_authenticated:
return qs.filter(Q(owner=user) | Q(team_id__in=user_team_ids(user, workspace)) | Q(visibility=Item.Visibility.PUBLIC)).distinct()
return qs.filter(Q(owner=user) | Q(assignee=user) | Q(team_id__in=user_team_ids(user, workspace)) | Q(visibility=Item.Visibility.PUBLIC)).distinct()
return qs.none()
def user_can_edit_item(user, item):
return item.owner_id == user.id or (item.team_id and TeamMembership.objects.filter(team=item.team, user=user).exists())
return item.owner_id == user.id or item.assignee_id == user.id or (item.team_id and TeamMembership.objects.filter(team=item.team, user=user).exists())
def visible_tags(user, workspace=None):
@@ -282,6 +283,26 @@ def item_scope_fits_collections(item, visibility, team):
) for section in sections)
def assign_item_from_mentions(item):
if item.kind != Item.Kind.TODO:
if item.assignee_id:
item.assignee = None
item.save(update_fields=['assignee', 'updated_at'])
return item
usernames = [match.group(1).lower() for match in MENTION_RE.finditer(without_markdown_code(item.content))]
assignee = None
if usernames:
members = WorkspaceMembership.objects.filter(workspace=item.workspace, user__username__in=usernames).select_related('user')
if item.team_id:
members = members.filter(user__team_memberships__team=item.team)
by_username = {membership.user.username.lower(): membership.user for membership in members}
assignee = next((by_username[name] for name in usernames if name in by_username), None)
if item.assignee_id != (assignee.id if assignee else None):
item.assignee = assignee
item.save(update_fields=['assignee', 'updated_at'])
return item
def apply_team_from_tags(item, user, force_private=False):
if force_private:
item.team = None
@@ -517,6 +538,7 @@ def index(request):
attach_files_and_replace_tokens(item, request.FILES.getlist('files'))
item.sync_metadata()
apply_team_from_tags(item, request.user, force_private=bool(re.search(r'/private\b', raw, re.I)))
assign_item_from_mentions(item)
if request.headers.get('HX-Request'):
active_tags = [t.strip().lower() for t in request.GET.getlist('tag') if t.strip()]
response = render(request, 'core/_create_response.html', {'item': item, 'tags': index_filter_tags(request, request.user, prefs, active_tags, workspace=active_workspace(request)), 'active_tag': active_tags[0] if active_tags else None, 'active_tags': active_tags})
@@ -612,8 +634,10 @@ def new_item(request):
attach_files_and_replace_tokens(item, request.FILES.getlist('files'))
item.sync_metadata()
apply_team_from_tags(item, request.user, force_private=bool(re.search(r'/private\b', raw, re.I)))
assign_item_from_mentions(item)
if collection:
item = adapt_item_for_collection(item, collection)
assign_item_from_mentions(item)
add_collection_section(collection, item)
return redirect(next_url)
return render(request, 'core/new_item.html', {
@@ -635,6 +659,7 @@ def journal(request):
attach_files_and_replace_tokens(item, request.FILES.getlist('files'))
item.sync_metadata()
apply_team_from_tags(item, request.user, force_private=bool(re.search(r'/private\b', raw, re.I)))
assign_item_from_mentions(item)
if request.headers.get('HX-Request'):
return render(request, 'core/_item.html', {'item': item, 'overview_lines': request.user.preferences.overview_lines if hasattr(request.user, 'preferences') else 4})
return redirect('journal')
@@ -1290,6 +1315,7 @@ def edit_item(request, pk):
attach_files_and_replace_tokens(item, request.FILES.getlist('files'))
item.sync_metadata()
apply_team_from_tags(item, request.user, force_private=item.visibility == Item.Visibility.PRIVATE)
assign_item_from_mentions(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
@@ -1320,6 +1346,7 @@ def item_editor(request, pk):
attach_files_and_replace_tokens(item, request.FILES.getlist('files'))
item.sync_metadata()
apply_team_from_tags(item, request.user, force_private=item.visibility == Item.Visibility.PRIVATE)
assign_item_from_mentions(item)
return redirect(next_url)
return render(request, 'core/item_editor.html', {'item': item, 'tags': visible_tags(request.user, workspace=active_workspace(request)), 'next_url': next_url})
@@ -1464,6 +1491,7 @@ def serialize_item(item):
'comment': item.comment,
'visibility': item.visibility,
'team': item.team.slug if item.team else None,
'assignee': item.assignee.username if item.assignee else None,
'is_done': item.is_done,
'completed_at': item.completed_at.isoformat() if item.completed_at else None,
'due_at': item.due_at.isoformat() if item.due_at else None,
@@ -1493,6 +1521,7 @@ def api_items(request):
item = Item.objects.create(workspace=current_workspace(user, request.api_key), owner=user, kind=data.get('kind') or kind, visibility=data.get('visibility') or visibility, content=content, url=data.get('url') or url, due_at=due_at)
item.sync_metadata()
apply_team_from_tags(item, user, force_private=bool(re.search(r'/private\b', raw, re.I)))
assign_item_from_mentions(item)
return JsonResponse(serialize_item(item), status=201)
@@ -1532,4 +1561,5 @@ def api_item_detail(request, pk):
item.save()
item.sync_metadata()
apply_team_from_tags(item, user, force_private=item.visibility == Item.Visibility.PRIVATE)
assign_item_from_mentions(item)
return JsonResponse(serialize_item(item))