Improve task filters and tag collections
This commit is contained in:
+14
-3
@@ -156,19 +156,26 @@ class CollectionForm(forms.ModelForm):
|
||||
|
||||
def clean(self):
|
||||
cleaned = super().clean()
|
||||
if cleaned.get('mode') == Collection.Mode.TAGS and not cleaned.get('filter_tags'):
|
||||
self.add_error('filter_tags', _('Select at least one tag for a tag collection.'))
|
||||
if cleaned.get('mode') == Collection.Mode.TAGS:
|
||||
if not cleaned.get('filter_tags'):
|
||||
self.add_error('filter_tags', _('Select at least one tag for a tag collection.'))
|
||||
if not any(cleaned.get(field) for field in ['include_notes', 'include_todos', 'include_links', 'include_journal']):
|
||||
self.add_error('include_notes', _('Select at least one entry type for a tag collection.'))
|
||||
return cleaned
|
||||
|
||||
class Meta:
|
||||
model = Collection
|
||||
fields = ['title', 'description', 'visibility', 'mode', 'team', 'filter_tags']
|
||||
fields = ['title', 'description', 'visibility', 'mode', 'team', 'filter_tags', 'include_notes', 'include_todos', 'include_links', 'include_journal']
|
||||
labels = {
|
||||
'title': _('Title'),
|
||||
'description': _('Description'),
|
||||
'visibility': _('Visibility'),
|
||||
'mode': _('Collection type'),
|
||||
'filter_tags': _('Content tags'),
|
||||
'include_notes': _('Notes'),
|
||||
'include_todos': _('Tasks'),
|
||||
'include_links': _('Links'),
|
||||
'include_journal': _('Journal'),
|
||||
}
|
||||
widgets = {
|
||||
'title': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
@@ -176,6 +183,10 @@ class CollectionForm(forms.ModelForm):
|
||||
'visibility': forms.Select(attrs={'class': 'form-select'}),
|
||||
'mode': forms.Select(attrs={'class': 'form-select', 'x-model': 'mode'}),
|
||||
'filter_tags': forms.CheckboxSelectMultiple(),
|
||||
'include_notes': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
'include_todos': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
'include_links': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
'include_journal': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# Generated by Pi coding agent
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0020_collection_tutorial_modes'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='collection',
|
||||
name='include_notes',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='collection',
|
||||
name='include_todos',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='collection',
|
||||
name='include_links',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='collection',
|
||||
name='include_journal',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -215,6 +215,10 @@ class Collection(models.Model):
|
||||
visibility = models.CharField(max_length=10, choices=Item.Visibility.choices, default=Item.Visibility.PRIVATE)
|
||||
mode = models.CharField(max_length=10, choices=Mode.choices, default=Mode.MANUAL)
|
||||
filter_tags = models.ManyToManyField(Tag, blank=True, related_name='tag_collections')
|
||||
include_notes = models.BooleanField(default=True)
|
||||
include_todos = models.BooleanField(default=False)
|
||||
include_links = models.BooleanField(default=False)
|
||||
include_journal = models.BooleanField(default=False)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
@@ -227,6 +231,18 @@ class Collection(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return reverse('collection_detail', args=[self.pk])
|
||||
|
||||
def included_item_kinds(self):
|
||||
kinds = []
|
||||
if self.include_notes:
|
||||
kinds.append(Item.Kind.NOTE)
|
||||
if self.include_todos:
|
||||
kinds.append(Item.Kind.TODO)
|
||||
if self.include_links:
|
||||
kinds.append(Item.Kind.LINK)
|
||||
if self.include_journal:
|
||||
kinds.append(Item.Kind.JOURNAL)
|
||||
return kinds or [Item.Kind.NOTE]
|
||||
|
||||
|
||||
class CollectionSection(models.Model):
|
||||
collection = models.ForeignKey(Collection, on_delete=models.CASCADE, related_name='sections')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% load i18n %}
|
||||
<form id="item-{{ item.id }}" class="card item-card shadow-sm position-relative" method="post" enctype="multipart/form-data" hx-post="{% url 'edit_item' item.id %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" hx-target="#item-{{ item.id }}" hx-swap="outerHTML" x-data="quickComposer([{% for tag in tags %}'{{ tag.name|escapejs }}'{% if not forloop.last %},{% endif %}{% endfor %}], {{ item.id }})" x-init="text=$el.querySelector('textarea').value">
|
||||
<form id="item-{{ item.id }}" class="card item-card shadow-sm position-relative inline-edit-form" method="post" enctype="multipart/form-data" hx-post="{% url 'edit_item' item.id %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" hx-target="#item-{{ item.id }}" hx-swap="outerHTML" x-data="quickComposer([{% for tag in tags %}'{{ tag.name|escapejs }}'{% if not forloop.last %},{% endif %}{% endfor %}], {{ item.id }})" x-init="text=$el.querySelector('textarea').value">
|
||||
{% csrf_token %}
|
||||
<div class="card-body vstack gap-2">
|
||||
{% if form.errors %}<div class="alert alert-danger mb-0">{{ form.errors }}</div>{% endif %}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
<div class="collection-flow content">
|
||||
{% if collection.mode == 'tags' %}
|
||||
{% for item in tag_items %}<section id="item-{{ item.id }}" class="collection-flow-section">{{ item|collection_item_markdown }}{% if item.comment %}<div class="text-muted border-start ps-3 mt-3">{{ item.comment|markdown }}</div>{% endif %}</section>{% empty %}<div class="text-center text-muted py-5">{% trans "No notes match the selected tags yet." %}</div>{% endfor %}
|
||||
{% for item in tag_items %}<section id="item-{{ item.id }}" class="collection-flow-section">{{ item|collection_item_markdown }}{% if item.comment %}<div class="text-muted border-start ps-3 mt-3">{{ item.comment|markdown }}</div>{% endif %}</section>{% empty %}<div class="text-center text-muted py-5">{% trans "No entries match the selected tags and types yet." %}</div>{% endfor %}
|
||||
{% else %}
|
||||
{% for section in sections %}{% include 'core/_collection_section_read.html' with section=section %}{% empty %}<div class="text-center text-muted py-5">{% trans "This collection has no sections yet." %}</div>{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
@@ -13,7 +13,15 @@
|
||||
<div><label class="form-label">{{ form.mode.label }}</label>{{ form.mode }}{{ form.mode.errors }}<div class="form-text">{% trans "Tag collections update automatically and are sorted by note creation time." %}</div></div>
|
||||
<div><label class="form-label">{{ form.visibility.label }}</label>{{ form.visibility }}{{ form.visibility.errors }}</div>
|
||||
<div><label class="form-label">{{ form.team.label }}</label>{{ form.team }}{{ form.team.errors }}<div class="form-text">{% trans "Optional. Selecting a team makes the collection visible and editable for that team." %}</div></div>
|
||||
<div x-show="mode === 'tags'"><label class="form-label">{{ form.filter_tags.label }}</label><div class="border rounded p-3 collection-tags">{% if form.filter_tags.field.queryset.exists %}{{ form.filter_tags }}{% else %}<span class="text-muted small">{% trans "No content tags are available yet." %}</span>{% endif %}</div>{{ form.filter_tags.errors }}<div class="form-text">{% trans "Notes must contain all selected tags. Team tags are not content filters." %}</div></div>
|
||||
<div x-show="mode === 'tags'" class="vstack gap-3">
|
||||
<div><label class="form-label">{{ form.filter_tags.label }}</label><div class="border rounded p-3 collection-tags">{% if form.filter_tags.field.queryset.exists %}{{ form.filter_tags }}{% else %}<span class="text-muted small">{% trans "No content tags are available yet." %}</span>{% endif %}</div>{{ form.filter_tags.errors }}<div class="form-text">{% trans "Entries must contain all selected tags. Team tags are not content filters." %}</div></div>
|
||||
<div><label class="form-label">{% trans "Entry types" %}</label><div class="border rounded p-3 d-flex flex-wrap gap-3">
|
||||
<label class="form-check mb-0">{{ form.include_notes }} <span class="form-check-label">{{ form.include_notes.label }}</span></label>
|
||||
<label class="form-check mb-0">{{ form.include_todos }} <span class="form-check-label">{{ form.include_todos.label }}</span></label>
|
||||
<label class="form-check mb-0">{{ form.include_links }} <span class="form-check-label">{{ form.include_links.label }}</span></label>
|
||||
<label class="form-check mb-0">{{ form.include_journal }} <span class="form-check-label">{{ form.include_journal.label }}</span></label>
|
||||
</div>{{ form.include_notes.errors }}</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2"><button class="btn btn-dark">{% trans "Save" %}</button><a class="btn btn-outline-secondary" href="{% if collection %}{{ collection.get_absolute_url }}{% else %}{% url 'collection_list' %}{% endif %}">{% trans "Cancel" %}</a></div>
|
||||
</div></form>
|
||||
</div>
|
||||
|
||||
@@ -111,6 +111,8 @@
|
||||
<section class="col-lg-9">
|
||||
<form class="desktop-quick-create position-relative mb-4" method="post" enctype="multipart/form-data" hx-post="{{ request.get_full_path }}" hx-target="#items" hx-swap="afterbegin" x-data="quickComposer([{% for tag in tags %}'{{ tag.name|escapejs }}'{% if not forloop.last %},{% endif %}{% endfor %}])" x-on:htmx:after-request="if($event.detail.successful){text='';open=false;$el.reset()}">
|
||||
{% csrf_token %}
|
||||
<fieldset :disabled="editingItem" :class="{'opacity-50': editingItem}">
|
||||
<div class="small text-muted mb-2" x-show="editingItem" x-cloak>{% trans "Inline editor is open; quick entry is disabled." %}</div>
|
||||
<div class="mobile-create-buttons gap-2 mb-2">
|
||||
<button type="button" class="btn btn-sm btn-primary flex-fill" @click="insert('/todo')">{% trans "Task" %}</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary flex-fill" @click="$el.querySelector('textarea').focus()">{% trans "Note" %}</button>
|
||||
@@ -130,6 +132,7 @@
|
||||
<button class="btn btn-dark px-4">{% trans "Save" %}</button>
|
||||
</div>
|
||||
<template x-if="preview"><div class="mt-2 small"><span class="text-muted">{% trans "Image from clipboard:" %}</span><br><img :src="preview" class="img-thumbnail mt-1" style="max-height:160px"></div></template>
|
||||
</fieldset>
|
||||
</form>
|
||||
<div id="items" class="vstack gap-3">
|
||||
{% for item in items %}{% include 'core/_item.html' %}{% empty %}<div class="text-center text-muted py-5">{% trans "Nothing here yet." %}</div>{% endfor %}
|
||||
@@ -150,12 +153,16 @@ function quickComposer(tags, itemId=null){
|
||||
linkSuggestions:[],
|
||||
attachmentSuggestions:[],
|
||||
pendingFiles:[],
|
||||
editingItem:false,
|
||||
itemId:itemId,
|
||||
tags:tags||[],
|
||||
init(){
|
||||
document.body.addEventListener('htmx:afterRequest', (event) => {
|
||||
if(event.detail.successful && event.target === this.$el){ this.preview=null; }
|
||||
});
|
||||
document.body.addEventListener('htmx:afterSwap', () => {
|
||||
this.editingItem=!!document.querySelector('.inline-edit-form');
|
||||
});
|
||||
document.body.addEventListener('tagsChanged', () => {
|
||||
fetch('{% url "api_tags" %}').then(r=>r.json()).then(data=>{this.tags=data.tags||[]});
|
||||
});
|
||||
|
||||
@@ -151,6 +151,31 @@ class MarkdownHeadingTests(TestCase):
|
||||
self.assertEqual(markdown_title('## Ziel von my2dos\n\nText'), 'Ziel von my2dos')
|
||||
|
||||
|
||||
class QuickCreateFilterTests(TestCase):
|
||||
def setUp(self):
|
||||
self.user = get_user_model().objects.create_user(username='quick-filter', password='secret')
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_active_tag_is_added_only_when_content_has_no_tags(self):
|
||||
self.client.post(f'{reverse("index")}?tag=work', {'content': 'Task without tag'})
|
||||
first = Item.objects.get(content='Task without tag #work')
|
||||
self.assertEqual(list(first.tags.values_list('name', flat=True)), ['work'])
|
||||
|
||||
self.client.post(f'{reverse("index")}?tag=work', {'content': 'Task with own #other'})
|
||||
second = Item.objects.get(content='Task with own #other')
|
||||
self.assertEqual(list(second.tags.values_list('name', flat=True)), ['other'])
|
||||
|
||||
def test_hx_create_response_keeps_active_tag_marked(self):
|
||||
response = self.client.post(
|
||||
f'{reverse("index")}?tag=work', {'content': 'Task without tag'}, HTTP_HX_REQUEST='true',
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, 'hx-swap-oob="true"')
|
||||
self.assertContains(response, '#work ×')
|
||||
self.assertContains(response, 'text-bg-dark')
|
||||
|
||||
|
||||
class FormattingToolbarTests(TestCase):
|
||||
def test_toolbar_and_shortcuts_are_available(self):
|
||||
user = get_user_model().objects.create_user(username='formatting', password='secret')
|
||||
@@ -189,6 +214,31 @@ class NavigationAndLayoutTests(TestCase):
|
||||
self.assertLess(menu_position, toolbar_position)
|
||||
self.assertLess(toolbar_position, file_position)
|
||||
|
||||
def test_quick_create_is_disabled_while_inline_editor_is_open(self):
|
||||
response = self.client.get(reverse('index'))
|
||||
self.assertContains(response, ':disabled="editingItem"')
|
||||
self.assertContains(response, 'inline-edit-form')
|
||||
item = Item.objects.create(owner=self.user, content='Editable')
|
||||
response = self.client.get(reverse('edit_item', args=[item.id]))
|
||||
self.assertContains(response, 'inline-edit-form')
|
||||
|
||||
def test_tag_filter_list_depends_on_current_filters(self):
|
||||
open_todo = Item.objects.create(owner=self.user, kind=Item.Kind.TODO, content='Open #openonly')
|
||||
open_todo.sync_metadata()
|
||||
done_todo = Item.objects.create(owner=self.user, kind=Item.Kind.TODO, content='Done #archiveonly', is_done=True)
|
||||
done_todo.sync_metadata()
|
||||
note = Item.objects.create(owner=self.user, kind=Item.Kind.NOTE, content='Note #noteonly')
|
||||
note.sync_metadata()
|
||||
|
||||
response = self.client.get(f'{reverse("index")}?status=archive')
|
||||
self.assertContains(response, '#archiveonly')
|
||||
self.assertNotContains(response, '#openonly')
|
||||
self.assertNotContains(response, '#noteonly')
|
||||
|
||||
response = self.client.get(f'{reverse("tag_list")}?status=archive')
|
||||
self.assertContains(response, '#archiveonly')
|
||||
self.assertNotContains(response, '#openonly')
|
||||
|
||||
|
||||
class AttachmentEditTests(TestCase):
|
||||
def setUp(self):
|
||||
@@ -500,6 +550,34 @@ class CollectionTests(TestCase):
|
||||
manage_response = self.client.get(reverse('collection_manage', args=[collection.id]))
|
||||
self.assertRedirects(manage_response, reverse('collection_edit', args=[collection.id]))
|
||||
|
||||
def test_tag_collection_can_filter_included_item_types(self):
|
||||
note = Item.objects.create(owner=self.user, kind=Item.Kind.NOTE, content='Matching note #project')
|
||||
note.sync_metadata()
|
||||
todo = Item.objects.create(owner=self.user, kind=Item.Kind.TODO, content='Matching todo #project')
|
||||
todo.sync_metadata()
|
||||
link = Item.objects.create(owner=self.user, kind=Item.Kind.LINK, content='Matching link #project')
|
||||
link.sync_metadata()
|
||||
collection = Collection.objects.create(
|
||||
owner=self.user, title='Typed tags', mode=Collection.Mode.TAGS,
|
||||
include_notes=False, include_todos=True, include_links=False, include_journal=False,
|
||||
)
|
||||
collection.filter_tags.add(Tag.objects.get(name='project'))
|
||||
|
||||
response = self.client.get(collection.get_absolute_url())
|
||||
|
||||
self.assertContains(response, 'Matching todo')
|
||||
self.assertNotContains(response, 'Matching note')
|
||||
self.assertNotContains(response, 'Matching link')
|
||||
|
||||
def test_tag_collection_form_offers_item_type_selection(self):
|
||||
response = self.client.get(reverse('collection_create'))
|
||||
|
||||
self.assertContains(response, 'Entry types')
|
||||
self.assertContains(response, 'name="include_notes"')
|
||||
self.assertContains(response, 'name="include_todos"')
|
||||
self.assertContains(response, 'name="include_links"')
|
||||
self.assertContains(response, 'name="include_journal"')
|
||||
|
||||
def test_collection_opens_as_continuous_reading_view(self):
|
||||
collection = Collection.objects.create(owner=self.user, title='Reading view')
|
||||
first = Item.objects.create(owner=self.user, kind=Item.Kind.NOTE, content='First paragraph')
|
||||
|
||||
+49
-7
@@ -20,7 +20,7 @@ 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, CollectionForm, EditItemForm, InviteRegistrationForm, KanbanForm, QuickItemForm, TeamForm, TeamInviteForm, UserInviteForm, UserPreferenceForm, UserSettingsForm
|
||||
from .models import ApiKey, Attachment, Collection, CollectionSection, Item, Kanban, Tag, Team, TeamInvite, TeamMembership, UserInvite, UserPreference, without_markdown_code
|
||||
from .models import ApiKey, Attachment, Collection, CollectionSection, Item, Kanban, Tag, Team, TeamInvite, TeamMembership, UserInvite, UserPreference, TAG_RE, without_markdown_code
|
||||
|
||||
TYPE_COMMAND_RE = re.compile(r'^/(todo|note|link|journal)\b', re.I)
|
||||
VISIBILITY_COMMAND_RE = re.compile(r'/(public|private)\b', re.I)
|
||||
@@ -276,6 +276,46 @@ def api_auth_required(view):
|
||||
return wrapper
|
||||
|
||||
|
||||
def index_filter_tags(request, user, prefs, active_tags=None):
|
||||
active_tags = active_tags or []
|
||||
kind = request.GET.get('kind')
|
||||
status = request.GET.get('status')
|
||||
day = request.GET.get('day')
|
||||
q = request.GET.get('q', '').strip()
|
||||
items = visible_items(user)
|
||||
applied_preferences = False
|
||||
if kind in dict(Item.Kind.choices):
|
||||
items = items.filter(kind=kind)
|
||||
elif not any([active_tags, status, q, day]):
|
||||
applied_preferences = True
|
||||
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 == 'due':
|
||||
today_end = timezone.make_aware(datetime.combine(timezone.localdate(), time(23, 59, 59)))
|
||||
items = items.filter(kind=Item.Kind.TODO, is_done=False, due_at__lte=today_end)
|
||||
elif status == 'archive':
|
||||
items = items.filter(kind=Item.Kind.TODO, is_done=True)
|
||||
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()
|
||||
return Tag.objects.filter(Q(items__in=items) | Q(name__in=active_tags)).distinct().order_by('name')
|
||||
|
||||
|
||||
def index(request):
|
||||
if not request.user.is_authenticated:
|
||||
if request.method == 'POST':
|
||||
@@ -289,14 +329,15 @@ def index(request):
|
||||
raw, due_at, _ = parse_due_command(raw)
|
||||
kind, visibility, content, url = parse_quick_content(raw, prefs.default_item_kind)
|
||||
active_tag = request.GET.get('tag')
|
||||
if active_tag and f'#{active_tag.lower()}' not in content.lower():
|
||||
content = f'{content} #{active_tag}'
|
||||
if active_tag and not TAG_RE.search(without_markdown_code(content)):
|
||||
content = f'{content} #{active_tag.strip().lower()}'
|
||||
item = Item.objects.create(owner=request.user, kind=kind, visibility=visibility, content=content, url=url, due_at=due_at)
|
||||
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)))
|
||||
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')})
|
||||
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), 'active_tag': active_tags[0] if active_tags else None, 'active_tags': active_tags})
|
||||
response['HX-Trigger'] = 'tagsChanged'
|
||||
return response
|
||||
return redirect('index')
|
||||
@@ -357,7 +398,7 @@ def index(request):
|
||||
default=Value(1), output_field=IntegerField(),
|
||||
)).order_by('due_rank', 'due_at', '-created_at')
|
||||
return render(request, 'core/index.html', {
|
||||
'form': QuickItemForm(), 'items': items, 'tags': Tag.objects.all(), 'active_tag': tag, 'active_tags': active_tags, 'active_tag_mode': active_tag_mode, 'active_kind': kind, 'active_status': status, 'active_day': day, 'q': q, 'due_count': due_count, 'overview_lines': prefs.overview_lines,
|
||||
'form': QuickItemForm(), 'items': items, 'tags': index_filter_tags(request, request.user, prefs, active_tags), 'active_tag': tag, 'active_tags': active_tags, 'active_tag_mode': active_tag_mode, 'active_kind': kind, 'active_status': status, 'active_day': day, 'q': q, 'due_count': due_count, 'overview_lines': prefs.overview_lines,
|
||||
})
|
||||
|
||||
|
||||
@@ -661,7 +702,7 @@ def collection_target_from_form(form, user):
|
||||
|
||||
|
||||
def collection_tag_items(collection):
|
||||
items = Item.objects.filter(kind=Item.Kind.NOTE).select_related('owner', 'team').prefetch_related('tags', 'attachments')
|
||||
items = Item.objects.filter(kind__in=collection.included_item_kinds()).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:
|
||||
@@ -1120,9 +1161,10 @@ def kanban_move(request, pk, item_pk):
|
||||
|
||||
@login_required
|
||||
def tag_list(request):
|
||||
prefs, created = UserPreference.objects.get_or_create(user=request.user)
|
||||
active_tags = [t.strip().lower() for t in request.GET.getlist('tag') if t.strip()]
|
||||
active_tag_mode = 'all' if request.GET.get('tag_mode') == 'all' else 'any'
|
||||
return render(request, 'core/_tags.html', {'tags': Tag.objects.all(), 'active_tag': active_tags[0] if active_tags else None, 'active_tags': active_tags, 'active_tag_mode': active_tag_mode})
|
||||
return render(request, 'core/_tags.html', {'tags': index_filter_tags(request, request.user, prefs, active_tags), 'active_tag': active_tags[0] if active_tags else None, 'active_tags': active_tags, 'active_tag_mode': active_tag_mode})
|
||||
|
||||
|
||||
@login_required
|
||||
|
||||
Reference in New Issue
Block a user