Autocomplete task names

This commit is contained in:
Victor "multun" Collod 2019-02-08 16:24:53 +01:00
parent 3ddc8d250e
commit 69c782fd59
8 changed files with 128 additions and 25 deletions

View file

@ -8,3 +8,5 @@ __email__ = 'shacker@birdhouse.org'
__url__ = 'https://github.com/shacker/django-todo' __url__ = 'https://github.com/shacker/django-todo'
__license__ = 'BSD License' __license__ = 'BSD License'
from . import check

12
todo/check.py Normal file
View file

@ -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

View file

@ -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 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... caution, since it has impacts on performance, for obvious reasons...
""" """
def __init__(self, *models, using=None, savepoint=None): def __init__(self, *models, using=None, savepoint=None):
if using is None: if using is None:
using = DEFAULT_DB_ALIAS using = DEFAULT_DB_ALIAS
@ -29,14 +30,15 @@ class LockedAtomicTransaction(Atomic):
super(LockedAtomicTransaction, self).__enter__() super(LockedAtomicTransaction, self).__enter__()
# Make sure not to lock, when sqlite is used, or you'll run into problems while running tests!!! # 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 cursor = None
try: try:
cursor = get_connection(self.using).cursor() cursor = get_connection(self.using).cursor()
for model in self.models: for model in self.models:
cursor.execute( cursor.execute(
'LOCK TABLE {table_name}'.format( "LOCK TABLE {table_name}".format(
table_name=model._meta.db_table) table_name=model._meta.db_table
)
) )
finally: finally:
if cursor and not cursor.closed: if cursor and not cursor.closed:
@ -102,6 +104,9 @@ class Task(models.Model):
super(Task, self).save() super(Task, self).save()
def merge_into(self, merge_target): 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 # lock the comments to avoid concurrent additions of comments after the
# update request. these comments would be irremediably lost because of # update request. these comments would be irremediably lost because of
# the cascade clause # the cascade clause

View file

@ -2,6 +2,22 @@
{% block title %}Task:{{ task.title }}{% endblock %} {% block title %}Task:{{ task.title }}{% endblock %}
{% block extrahead %}
<style>
.select2 {
width: 100% !important;
}
.select2-container {
min-width: 0 !important;
}
</style>
{{ form.media }}
{{ merge_form.media }}
{% endblock %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col-sm-8"> <div class="col-sm-8">
@ -74,12 +90,14 @@
</a> </a>
</li> </li>
<li class="list-group-item"> <li class="list-group-item">
<h5>Merge task into</h5>
<form action="" method="post"> <form action="" method="post">
{% csrf_token %} {% csrf_token %}
<div class="form-group"> {% for field in merge_form.visible_fields %}
<textarea class="form-control" name="merge-target" rows="3"></textarea> <p>
</div> {{ field.errors }}
{{ field }}
</p>
{% endfor %}
<input class="btn btn-sm btn-primary" type="submit" name="merge_task_into" value="Merge task"> <input class="btn btn-sm btn-primary" type="submit" name="merge_task_into" value="Merge task">
</form> </form>
</li> </li>

View file

@ -56,6 +56,11 @@ urlpatterns = [
views.task_detail, views.task_detail,
name='task_detail'), name='task_detail'),
path(
'task/<int:task_id>/autocomplete/',
views.TaskAutocomplete.as_view(),
name='task_autocomplete'),
path( path(
'toggle_done/<int:task_id>/', 'toggle_done/<int:task_id>/',
views.toggle_done, views.toggle_done,

View file

@ -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.list_lists import list_lists # noqa: F401
from todo.views.reorder_tasks import reorder_tasks # noqa: F401 from todo.views.reorder_tasks import reorder_tasks # noqa: F401
from todo.views.search import search # 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 from todo.views.toggle_done import toggle_done # noqa: F401

View file

@ -1,15 +1,46 @@
import bleach import bleach
import datetime import datetime
from django import forms
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required, user_passes_test from django.contrib.auth.decorators import login_required, user_passes_test
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.http import HttpResponse 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.forms import AddEditTaskForm
from todo.models import Comment, Task 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, 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 @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. # 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. # 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
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 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) task.merge_into(merge_target)
return redirect("todo:task_detail", task_id=merge_target.id) return redirect(reverse("todo:task_detail", kwargs={"task_id": task_id}))
else:
merge_form = MergeForm()
# Save submitted comments # Save submitted comments
if request.POST.get("add_comment"): if request.POST.get("add_comment"):
@ -46,12 +92,17 @@ def task_detail(request, task_id: int) -> HttpResponse:
request.user, request.user,
subject='New comment posted on task "{}"'.format(task.title), 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 # Save task edits
if request.POST.get("add_edit_task"): if request.POST.get("add_edit_task"):
form = AddEditTaskForm( 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(): if form.is_valid():
@ -60,10 +111,14 @@ def task_detail(request, task_id: int) -> HttpResponse:
item.save() item.save()
messages.success(request, "The task has been edited.") messages.success(request, "The task has been edited.")
return redirect( 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: 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 # Mark complete
if request.POST.get("toggle_done"): if request.POST.get("toggle_done"):
@ -78,6 +133,12 @@ def task_detail(request, task_id: int) -> HttpResponse:
else: else:
thedate = datetime.datetime.now() 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) return render(request, "todo/task_detail.html", context)