Add automatic tag collections

This commit is contained in:
rucki
2026-08-27 16:48:56 +02:00
parent 2bcd9d901a
commit 682f5f7442
11 changed files with 197 additions and 56 deletions
+38 -13
View File
@@ -536,7 +536,7 @@ def team_accept_invite(request, token):
def visible_collections(user):
qs = Collection.objects.select_related('owner', 'team').prefetch_related('tags', 'sections__item')
qs = Collection.objects.select_related('owner', 'team').prefetch_related('filter_tags', 'sections__item')
if user.is_authenticated:
return qs.filter(Q(owner=user) | Q(team_id__in=user_team_ids(user)) | Q(visibility=Item.Visibility.PUBLIC)).distinct()
return qs.filter(visibility=Item.Visibility.PUBLIC)
@@ -569,14 +569,25 @@ def collection_item_is_compatible(collection, item):
def collection_target_from_form(form, user):
tags = form.cleaned_data.get('tags')
tag_names = list(tags.values_list('name', flat=True)) if tags is not None else []
membership = TeamMembership.objects.select_related('team').filter(user=user, team__slug__in=tag_names).first()
if membership:
return Item.Visibility.TEAM, membership.team
team = form.cleaned_data.get('team')
if team:
return Item.Visibility.TEAM, team
return form.cleaned_data['visibility'], None
def collection_tag_items(collection):
items = Item.objects.filter(kind=Item.Kind.NOTE).select_related('owner', 'team').prefetch_related('tags', 'attachments')
if collection.visibility == Item.Visibility.PUBLIC:
items = items.filter(visibility=Item.Visibility.PUBLIC)
elif collection.visibility == Item.Visibility.TEAM:
items = items.filter(Q(visibility=Item.Visibility.PUBLIC) | Q(visibility=Item.Visibility.TEAM, team=collection.team))
else:
items = items.filter(Q(visibility=Item.Visibility.PUBLIC) | Q(visibility=Item.Visibility.PRIVATE, owner=collection.owner))
for tag in collection.filter_tags.all():
items = items.filter(tags=tag)
return items.distinct().order_by('created_at', 'id')
def item_can_change_for_collection(user, item, collection):
if not user_can_edit_item(user, item):
return False
@@ -659,8 +670,11 @@ def collection_detail_context(request, collection, pending_item=None):
@login_required
def collection_list(request):
collections = visible_collections(request.user)
return render(request, 'core/collection_list.html', {'collections': collections})
entries = []
for collection in visible_collections(request.user).prefetch_related('filter_tags'):
count = collection_tag_items(collection).count() if collection.mode == Collection.Mode.TAGS else collection.sections.count()
entries.append({'collection': collection, 'section_count': count})
return render(request, 'core/collection_list.html', {'entries': entries})
@login_required
@@ -670,7 +684,7 @@ def collection_create(request):
if form.is_valid():
visibility, team = collection_target_from_form(form, request.user)
if visibility == Item.Visibility.TEAM and not team:
form.add_error('visibility', _('Select a team tag for a team collection.'))
form.add_error('team', _('Select a team for a team collection.'))
else:
collection = form.save(commit=False)
collection.owner = request.user
@@ -678,6 +692,8 @@ def collection_create(request):
collection.team = team
collection.save()
form.save_m2m()
if collection.mode == Collection.Mode.MANUAL:
collection.filter_tags.clear()
messages.success(request, _('Collection created.'))
return redirect(collection)
else:
@@ -694,20 +710,26 @@ def collection_edit(request, pk):
form = CollectionForm(request.POST, instance=collection, user=request.user)
if form.is_valid():
visibility, team = collection_target_from_form(form, request.user)
incompatible = any(not collection_scope_allows_item(
target_mode = form.cleaned_data['mode']
has_manual_sections = collection.mode == Collection.Mode.MANUAL and collection.sections.exists()
incompatible = target_mode == Collection.Mode.MANUAL and any(not collection_scope_allows_item(
visibility, team.id if team else None, collection.owner_id,
section.item.visibility, section.item.team_id, section.item.owner_id,
) for section in collection.sections.select_related('item'))
if incompatible:
if target_mode == Collection.Mode.TAGS and has_manual_sections:
form.add_error('mode', _('Remove all manual sections before changing to a tag collection.'))
elif incompatible:
form.add_error(None, _('The collection visibility cannot change while it contains incompatible notes.'))
elif visibility == Item.Visibility.TEAM and not team:
form.add_error('visibility', _('Select a team tag for a team collection.'))
form.add_error('team', _('Select a team for a team collection.'))
else:
collection = form.save(commit=False)
collection.visibility = visibility
collection.team = team
collection.save()
form.save_m2m()
if collection.mode == Collection.Mode.MANUAL:
collection.filter_tags.clear()
messages.success(request, _('Collection saved.'))
return redirect(collection)
else:
@@ -719,7 +741,8 @@ def collection_detail(request, pk):
collection = get_object_or_404(visible_collections(request.user), pk=pk)
return render(request, 'core/collection_detail.html', {
'collection': collection,
'sections': collection.sections.select_related('item').prefetch_related('item__tags', 'item__attachments'),
'sections': collection.sections.select_related('item').prefetch_related('item__tags', 'item__attachments') if collection.mode == Collection.Mode.MANUAL else [],
'tag_items': collection_tag_items(collection) if collection.mode == Collection.Mode.TAGS else [],
'can_edit': user_can_edit_collection(request.user, collection),
})
@@ -729,6 +752,8 @@ def collection_manage(request, pk):
collection = get_object_or_404(visible_collections(request.user), pk=pk)
if not user_can_edit_collection(request.user, collection):
return JsonResponse({'error': 'forbidden'}, status=403)
if collection.mode == Collection.Mode.TAGS:
return redirect('collection_edit', pk=collection.pk)
manage_url = reverse('collection_manage', args=[collection.pk])
if request.method == 'POST':
action = request.POST.get('action')