Allow nested collections
This commit is contained in:
+57
-9
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user