From 69c782fd599bb2a838736493722da796999c6544 Mon Sep 17 00:00:00 2001 From: "Victor \"multun\" Collod" Date: Fri, 8 Feb 2019 16:24:53 +0100 Subject: [PATCH] Autocomplete task names --- todo/__init__.py | 2 + todo/check.py | 12 ++++ todo/models.py | 11 +++- todo/static/todo/css/styles.css | 2 +- todo/templates/todo/task_detail.html | 34 ++++++++--- todo/urls.py | 5 ++ todo/views/__init__.py | 2 +- todo/views/task_detail.py | 85 ++++++++++++++++++++++++---- 8 files changed, 128 insertions(+), 25 deletions(-) create mode 100644 todo/check.py diff --git a/todo/__init__.py b/todo/__init__.py index 71f82f5..aeb0a7a 100644 --- a/todo/__init__.py +++ b/todo/__init__.py @@ -8,3 +8,5 @@ __email__ = 'shacker@birdhouse.org' __url__ = 'https://github.com/shacker/django-todo' __license__ = 'BSD License' + +from . import check diff --git a/todo/check.py b/todo/check.py new file mode 100644 index 0000000..0a5def7 --- /dev/null +++ b/todo/check.py @@ -0,0 +1,12 @@ +from django.conf import settings +from django.core.checks import Error, register + +@register() +def dal_check(app_configs, **kwargs): + errors = [] + missing_apps = {'dal', 'dal_select2'} - set(settings.INSTALLED_APPS) + for missing_app in missing_apps: + errors.append( + Error('{} needs to be in INSTALLED_APPS'.format(missing_app)) + ) + return errors diff --git a/todo/models.py b/todo/models.py index 4fc81fc..241ac69 100644 --- a/todo/models.py +++ b/todo/models.py @@ -19,6 +19,7 @@ class LockedAtomicTransaction(Atomic): transaction. Although this is the only way to avoid concurrency issues in certain situations, it should be used with caution, since it has impacts on performance, for obvious reasons... """ + def __init__(self, *models, using=None, savepoint=None): if using is None: using = DEFAULT_DB_ALIAS @@ -29,14 +30,15 @@ class LockedAtomicTransaction(Atomic): super(LockedAtomicTransaction, self).__enter__() # Make sure not to lock, when sqlite is used, or you'll run into problems while running tests!!! - if settings.DATABASES[self.using]['ENGINE'] != 'django.db.backends.sqlite3': + if settings.DATABASES[self.using]["ENGINE"] != "django.db.backends.sqlite3": cursor = None try: cursor = get_connection(self.using).cursor() for model in self.models: cursor.execute( - 'LOCK TABLE {table_name}'.format( - table_name=model._meta.db_table) + "LOCK TABLE {table_name}".format( + table_name=model._meta.db_table + ) ) finally: if cursor and not cursor.closed: @@ -102,6 +104,9 @@ class Task(models.Model): super(Task, self).save() def merge_into(self, merge_target): + if merge_target.pk == self.pk: + raise ValueError("can't merge a task with self") + # lock the comments to avoid concurrent additions of comments after the # update request. these comments would be irremediably lost because of # the cascade clause diff --git a/todo/static/todo/css/styles.css b/todo/static/todo/css/styles.css index 7ce2568..44b68e3 100644 --- a/todo/static/todo/css/styles.css +++ b/todo/static/todo/css/styles.css @@ -1,4 +1,4 @@ label { display: block; font-weight: bold; -} \ No newline at end of file +} diff --git a/todo/templates/todo/task_detail.html b/todo/templates/todo/task_detail.html index 8a98c01..cfde051 100644 --- a/todo/templates/todo/task_detail.html +++ b/todo/templates/todo/task_detail.html @@ -2,6 +2,22 @@ {% block title %}Task:{{ task.title }}{% endblock %} +{% block extrahead %} + +{{ form.media }} +{{ merge_form.media }} +{% endblock %} + + + {% block content %}
@@ -74,14 +90,16 @@
  • -
    Merge task into
    -
    - {% csrf_token %} -
    - -
    - -
    +
    + {% csrf_token %} + {% for field in merge_form.visible_fields %} +

    + {{ field.errors }} + {{ field }} +

    + {% endfor %} + +
  • diff --git a/todo/urls.py b/todo/urls.py index 2c97a8d..5db2f3b 100644 --- a/todo/urls.py +++ b/todo/urls.py @@ -56,6 +56,11 @@ urlpatterns = [ views.task_detail, name='task_detail'), + path( + 'task//autocomplete/', + views.TaskAutocomplete.as_view(), + name='task_autocomplete'), + path( 'toggle_done//', views.toggle_done, diff --git a/todo/views/__init__.py b/todo/views/__init__.py index c36bb65..63cdb5b 100644 --- a/todo/views/__init__.py +++ b/todo/views/__init__.py @@ -6,5 +6,5 @@ from todo.views.list_detail import list_detail # noqa: F401 from todo.views.list_lists import list_lists # noqa: F401 from todo.views.reorder_tasks import reorder_tasks # noqa: F401 from todo.views.search import search # noqa: F401 -from todo.views.task_detail import task_detail # noqa: F401 +from todo.views.task_detail import task_detail, TaskAutocomplete # noqa: F401 from todo.views.toggle_done import toggle_done # noqa: F401 diff --git a/todo/views/task_detail.py b/todo/views/task_detail.py index 32eb3b8..50746b3 100644 --- a/todo/views/task_detail.py +++ b/todo/views/task_detail.py @@ -1,15 +1,46 @@ import bleach import datetime +from django import forms from django.contrib import messages from django.contrib.auth.decorators import login_required, user_passes_test from django.core.exceptions import PermissionDenied from django.http import HttpResponse -from django.shortcuts import get_object_or_404, redirect, render +from django.shortcuts import get_object_or_404, redirect, render, redirect +from django.urls import reverse +from django.utils.decorators import method_decorator from todo.forms import AddEditTaskForm from todo.models import Comment, Task from todo.utils import send_email_to_thread_participants, toggle_task_completed, staff_check +from todo.utils import send_email_to_thread_participants, toggle_task_completed +from dal import autocomplete + + +def user_can_read_task(task, user): + return task.task_list.group in user.groups.all() or user.is_staff + + +class TaskAutocomplete(autocomplete.Select2QuerySetView): + @method_decorator(login_required) + def dispatch(self, request, task_id, *args, **kwargs): + self.task = get_object_or_404(Task, pk=task_id) + if not user_can_read_task(self.task, request.user): + raise PermissionDenied + + return super().dispatch(request, task_id, *args, **kwargs) + + def get_queryset(self): + # Don't forget to filter out results depending on the visitor ! + if not self.request.user.is_authenticated: + return Task.objects.none() + + qs = Task.objects.filter(task_list=self.task.task_list).exclude(pk=self.task.pk) + + if self.q: + qs = qs.filter(title__istartswith=self.q) + + return qs @login_required @@ -23,14 +54,29 @@ def task_detail(request, task_id: int) -> HttpResponse: # Ensure user has permission to view task. Admins can view all tasks. # Get the group this task belongs to, and check whether current user is a member of that group. - if task.task_list.group not in request.user.groups.all() and not request.user.is_staff: + if not user_can_read_task(task, request.user): raise PermissionDenied - if request.POST.get("merge_task_into"): - target_task_id = int(request.POST.get("merge-target")) - merge_target = Task.objects.get(pk=target_task_id) - task.merge_into(merge_target) - return redirect("todo:task_detail", task_id=merge_target.id) + class MergeForm(forms.Form): + merge_target = forms.ModelChoiceField( + queryset=Task.objects.all(), + widget=autocomplete.ModelSelect2( + url=reverse("todo:task_autocomplete", kwargs={"task_id": task_id}) + ), + ) + + # Handle task merging + if request.method == "POST": + merge_form = MergeForm(request.POST) + if merge_form.is_valid(): + merge_target = merge_form.cleaned_data["merge_target"] + if not user_can_read_task(merge_target, request.user): + raise PermissionDenied + + task.merge_into(merge_target) + return redirect(reverse("todo:task_detail", kwargs={"task_id": task_id})) + else: + merge_form = MergeForm() # Save submitted comments if request.POST.get("add_comment"): @@ -46,12 +92,17 @@ def task_detail(request, task_id: int) -> HttpResponse: request.user, subject='New comment posted on task "{}"'.format(task.title), ) - messages.success(request, "Comment posted. Notification email sent to thread participants.") + messages.success( + request, "Comment posted. Notification email sent to thread participants." + ) # Save task edits if request.POST.get("add_edit_task"): form = AddEditTaskForm( - request.user, request.POST, instance=task, initial={"task_list": task.task_list} + request.user, + request.POST, + instance=task, + initial={"task_list": task.task_list}, ) if form.is_valid(): @@ -60,10 +111,14 @@ def task_detail(request, task_id: int) -> HttpResponse: item.save() messages.success(request, "The task has been edited.") return redirect( - "todo:list_detail", list_id=task.task_list.id, list_slug=task.task_list.slug + "todo:list_detail", + list_id=task.task_list.id, + list_slug=task.task_list.slug, ) else: - form = AddEditTaskForm(request.user, instance=task, initial={"task_list": task.task_list}) + form = AddEditTaskForm( + request.user, instance=task, initial={"task_list": task.task_list} + ) # Mark complete if request.POST.get("toggle_done"): @@ -78,6 +133,12 @@ def task_detail(request, task_id: int) -> HttpResponse: else: thedate = datetime.datetime.now() - context = {"task": task, "comment_list": comment_list, "form": form, "thedate": thedate} + context = { + "task": task, + "comment_list": comment_list, + "form": form, + "merge_form": merge_form, + "thedate": thedate, + } return render(request, "todo/task_detail.html", context)