Allow nested collections

This commit is contained in:
rucki
2026-08-30 18:47:35 +02:00
parent d5917d411a
commit 5793283653
9 changed files with 202 additions and 25 deletions
+57 -9
View File
@@ -542,7 +542,7 @@ def team_accept_invite(request, token):
def visible_collections(user):
qs = Collection.objects.select_related('owner', 'team').prefetch_related('filter_tags', 'sections__item')
qs = Collection.objects.select_related('owner', 'team').prefetch_related('filter_tags', 'sections__item', 'sections__sub_collection')
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)
@@ -574,6 +574,22 @@ def collection_item_is_compatible(collection, item):
)
def collection_subcollection_is_compatible(collection, sub_collection):
return collection_scope_allows_item(
collection.visibility, collection.team_id, collection.owner_id,
sub_collection.visibility, sub_collection.team_id, sub_collection.owner_id,
)
def collection_contains_collection(collection, target):
if collection.pk == target.pk:
return True
for section in collection.sections.select_related('sub_collection'):
if section.sub_collection and collection_contains_collection(section.sub_collection, target):
return True
return False
def collection_target_from_form(form, user):
team = form.cleaned_data.get('team')
if team:
@@ -639,11 +655,22 @@ def copy_item_for_collection(item, collection, owner):
return adapt_item_for_collection(copied, collection)
def add_collection_section(collection, item):
def next_collection_section_position(collection):
last_position = collection.sections.order_by('-position').values_list('position', flat=True).first()
return (last_position + 1) if last_position is not None else 0
def add_collection_section(collection, item):
return CollectionSection.objects.get_or_create(
collection=collection, item=item,
defaults={'position': (last_position + 1) if last_position is not None else 0},
defaults={'position': next_collection_section_position(collection)},
)
def add_collection_subcollection(collection, sub_collection):
return CollectionSection.objects.get_or_create(
collection=collection, sub_collection=sub_collection,
defaults={'position': next_collection_section_position(collection)},
)
@@ -657,17 +684,23 @@ def collection_detail_context(request, collection, pending_item=None):
can_edit = user_can_edit_collection(request.user, collection)
q = request.GET.get('q', '').strip()
candidates = Item.objects.none()
collection_candidates = Collection.objects.none()
if can_edit:
candidates = editable_collection_notes(request.user).exclude(collection_sections__collection=collection)
collection_candidates = visible_collections(request.user).filter(mode=Collection.Mode.MANUAL).exclude(pk=collection.pk).exclude(containing_sections__collection=collection)
collection_candidates = [candidate for candidate in collection_candidates if collection_subcollection_is_compatible(collection, candidate) and not collection_contains_collection(candidate, collection)]
if q:
candidates = candidates.filter(Q(content__icontains=q) | Q(tags__name__icontains=q)).distinct()
collection_candidates = [candidate for candidate in collection_candidates if q.lower() in candidate.title.lower()]
candidates = candidates[:30]
collection_candidates = collection_candidates[:30]
return {
'collection': collection,
'sections': collection.sections.select_related('item').prefetch_related('item__tags', 'item__attachments'),
'sections': collection.sections.select_related('item', 'sub_collection').prefetch_related('item__tags', 'item__attachments', 'sub_collection__sections__item', 'sub_collection__sections__sub_collection'),
'can_edit': can_edit,
'can_delete': user_can_delete_collection(request.user, collection),
'candidates': candidates,
'collection_candidates': collection_candidates,
'q': q,
'pending_item': pending_item,
'pending_can_convert': pending_item and item_can_change_for_collection(request.user, pending_item, collection),
@@ -718,10 +751,16 @@ def collection_edit(request, pk):
visibility, team = collection_target_from_form(form, request.user)
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'))
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,
) if section.item else not collection_scope_allows_item(
visibility, team.id if team else None, collection.owner_id,
section.sub_collection.visibility, section.sub_collection.team_id, section.sub_collection.owner_id,
)
for section in collection.sections.select_related('item', 'sub_collection')
)
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:
@@ -747,7 +786,7 @@ 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') if collection.mode == Collection.Mode.MANUAL else [],
'sections': collection.sections.select_related('item', 'sub_collection').prefetch_related('item__tags', 'item__attachments', 'sub_collection__sections__item', 'sub_collection__sections__sub_collection') 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),
})
@@ -782,6 +821,15 @@ def collection_manage(request, pk):
return redirect(manage_url)
add_collection_section(collection, item)
return redirect(manage_url)
if action == 'add_collection':
sub_collection = get_object_or_404(visible_collections(request.user), pk=request.POST.get('collection_id'), mode=Collection.Mode.MANUAL)
if sub_collection.pk == collection.pk or collection_contains_collection(sub_collection, collection):
messages.error(request, _('This would create a collection loop.'))
elif not collection_subcollection_is_compatible(collection, sub_collection):
messages.error(request, _('This collection has an incompatible visibility.'))
else:
add_collection_subcollection(collection, sub_collection)
return redirect(manage_url)
section = get_object_or_404(CollectionSection, pk=request.POST.get('section_id'), collection=collection)
if action == 'remove':
section.delete()