Make task merging optional
This commit is contained in:
parent
24119b91ac
commit
c9ec890658
11 changed files with 125 additions and 72 deletions
|
@ -1,8 +1,15 @@
|
||||||
from django.conf import settings
|
|
||||||
from django.core.checks import Error, register
|
from django.core.checks import Error, register
|
||||||
|
|
||||||
|
# the sole purpose of this warning is to prevent people who have
|
||||||
|
# django-autocomplete-light installed but not configured to start the app
|
||||||
@register()
|
@register()
|
||||||
def dal_check(app_configs, **kwargs):
|
def dal_check(app_configs, **kwargs):
|
||||||
|
from django.conf import settings
|
||||||
|
from todo.features import HAS_AUTOCOMPLETE
|
||||||
|
|
||||||
|
if not HAS_AUTOCOMPLETE:
|
||||||
|
return
|
||||||
|
|
||||||
errors = []
|
errors = []
|
||||||
missing_apps = {'dal', 'dal_select2'} - set(settings.INSTALLED_APPS)
|
missing_apps = {'dal', 'dal_select2'} - set(settings.INSTALLED_APPS)
|
||||||
for missing_app in missing_apps:
|
for missing_app in missing_apps:
|
||||||
|
|
11
todo/features.py
Normal file
11
todo/features.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
HAS_AUTOCOMPLETE = True
|
||||||
|
try:
|
||||||
|
import dal
|
||||||
|
except ImportError:
|
||||||
|
HAS_AUTOCOMPLETE = False
|
||||||
|
|
||||||
|
HAS_TASK_MERGE = False
|
||||||
|
if HAS_AUTOCOMPLETE:
|
||||||
|
import dal.autocomplete
|
||||||
|
if getattr(dal.autocomplete, 'Select2QuerySetView', None) is not None:
|
||||||
|
HAS_TASK_MERGE = True
|
|
@ -18,3 +18,4 @@ def _declare_backend(backend_path):
|
||||||
|
|
||||||
smtp_backend = _declare_backend('django.core.mail.backends.smtp.EmailBackend')
|
smtp_backend = _declare_backend('django.core.mail.backends.smtp.EmailBackend')
|
||||||
console_backend = _declare_backend('django.core.mail.backends.console.EmailBackend')
|
console_backend = _declare_backend('django.core.mail.backends.console.EmailBackend')
|
||||||
|
locmem_backend = _declare_backend('django.core.mail.backends.locmem.EmailBackend')
|
||||||
|
|
|
@ -2,8 +2,7 @@
|
||||||
|
|
||||||
<form action="" name="add_task" method="post">
|
<form action="" name="add_task" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
<div class="mt-3">
|
||||||
<div id="AddEditTask" class="mt-3">
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="id_title" name="title">Task</label>
|
<label for="id_title" name="title">Task</label>
|
||||||
<input type="text" class="form-control" id="id_title" name="title" required placeholder="Task title"
|
<input type="text" class="form-control" id="id_title" name="title" required placeholder="Task title"
|
||||||
|
|
|
@ -10,7 +10,9 @@
|
||||||
data-toggle="collapse" data-target="#AddEditTask">Add Task</button>
|
data-toggle="collapse" data-target="#AddEditTask">Add Task</button>
|
||||||
|
|
||||||
{# Task edit / new task form #}
|
{# Task edit / new task form #}
|
||||||
{% include 'todo/include/task_edit.html' %}
|
<div id="AddEditTask" class="collapse">
|
||||||
|
{% include 'todo/include/task_edit.html' %}
|
||||||
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
|
@ -93,6 +93,7 @@
|
||||||
<div id="TaskEdit" class="collapse">
|
<div id="TaskEdit" class="collapse">
|
||||||
{# Task edit / new task form #}
|
{# Task edit / new task form #}
|
||||||
{% include 'todo/include/task_edit.html' %}
|
{% include 'todo/include/task_edit.html' %}
|
||||||
|
{% if merge_form is not None %}
|
||||||
<form action="" method="post">
|
<form action="" method="post">
|
||||||
<div class="card border-danger">
|
<div class="card border-danger">
|
||||||
<div class="card-header">Merge task</div>
|
<div class="card-header">Merge task</div>
|
||||||
|
@ -111,6 +112,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
|
|
22
todo/urls.py
22
todo/urls.py
|
@ -1,11 +1,12 @@
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from todo import views
|
from todo import views
|
||||||
|
from todo.features import HAS_TASK_MERGE
|
||||||
app_name = 'todo'
|
app_name = 'todo'
|
||||||
|
|
||||||
urlpatterns = [
|
from django.conf import settings
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
path(
|
path(
|
||||||
'',
|
'',
|
||||||
views.list_lists,
|
views.list_lists,
|
||||||
|
@ -55,12 +56,19 @@ urlpatterns = [
|
||||||
'task/<int:task_id>/',
|
'task/<int:task_id>/',
|
||||||
views.task_detail,
|
views.task_detail,
|
||||||
name='task_detail'),
|
name='task_detail'),
|
||||||
|
]
|
||||||
|
|
||||||
path(
|
if HAS_TASK_MERGE:
|
||||||
'task/<int:task_id>/autocomplete/',
|
# ensure autocomplete is optional
|
||||||
views.TaskAutocomplete.as_view(),
|
from todo.views.task_autocomplete import TaskAutocomplete
|
||||||
name='task_autocomplete'),
|
urlpatterns.append(
|
||||||
|
path(
|
||||||
|
'task/<int:task_id>/autocomplete/',
|
||||||
|
TaskAutocomplete.as_view(),
|
||||||
|
name='task_autocomplete')
|
||||||
|
)
|
||||||
|
|
||||||
|
urlpatterns.extend([
|
||||||
path(
|
path(
|
||||||
'toggle_done/<int:task_id>/',
|
'toggle_done/<int:task_id>/',
|
||||||
views.toggle_done,
|
views.toggle_done,
|
||||||
|
@ -75,4 +83,4 @@ urlpatterns = [
|
||||||
'search/',
|
'search/',
|
||||||
views.search,
|
views.search,
|
||||||
name="search"),
|
name="search"),
|
||||||
]
|
])
|
||||||
|
|
|
@ -23,6 +23,10 @@ def staff_check(user):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def user_can_read_task(task, user):
|
||||||
|
return task.task_list.group in user.groups.all() or user.is_staff
|
||||||
|
|
||||||
|
|
||||||
def todo_get_backend(task):
|
def todo_get_backend(task):
|
||||||
'''returns a mail backend for some task'''
|
'''returns a mail backend for some task'''
|
||||||
mail_backends = getattr(settings, "TODO_MAIL_BACKENDS", None)
|
mail_backends = getattr(settings, "TODO_MAIL_BACKENDS", None)
|
||||||
|
@ -148,8 +152,9 @@ def send_email_to_thread_participants(task, msg_body, user, subject=None):
|
||||||
for ca in commenters
|
for ca in commenters
|
||||||
if ca.author is not None
|
if ca.author is not None
|
||||||
)
|
)
|
||||||
for user_email in (task.created_by.email, task.assigned_to.email):
|
for related_user in (task.created_by, task.assigned_to):
|
||||||
recip_list.add(user_email)
|
if related_user is not None:
|
||||||
|
recip_list.add(related_user.email)
|
||||||
recip_list = list(m for m in recip_list if m)
|
recip_list = list(m for m in recip_list if m)
|
||||||
|
|
||||||
todo_send_mail(user, task, email_subject, email_body, recip_list)
|
todo_send_mail(user, task, email_subject, email_body, recip_list)
|
||||||
|
|
|
@ -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, TaskAutocomplete # noqa: F401
|
from todo.views.task_detail import task_detail # noqa: F401
|
||||||
from todo.views.toggle_done import toggle_done # noqa: F401
|
from todo.views.toggle_done import toggle_done # noqa: F401
|
||||||
|
|
29
todo/views/task_autocomplete.py
Normal file
29
todo/views/task_autocomplete.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
from dal import autocomplete
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from todo.models import Task
|
||||||
|
from todo.utils import user_can_read_task
|
||||||
|
|
||||||
|
|
||||||
|
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
|
|
@ -12,35 +12,35 @@ 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, user_can_read_task
|
||||||
from todo.utils import send_email_to_thread_participants, toggle_task_completed
|
from todo.features import HAS_TASK_MERGE
|
||||||
from dal import autocomplete
|
|
||||||
|
|
||||||
|
|
||||||
def user_can_read_task(task, user):
|
if HAS_TASK_MERGE:
|
||||||
return task.task_list.group in user.groups.all() or user.is_staff
|
from dal import autocomplete
|
||||||
|
from todo.views.task_autocomplete import TaskAutocomplete
|
||||||
|
|
||||||
|
|
||||||
class TaskAutocomplete(autocomplete.Select2QuerySetView):
|
def handle_add_comment(request, task):
|
||||||
@method_decorator(login_required)
|
if not request.POST.get("add_comment"):
|
||||||
def dispatch(self, request, task_id, *args, **kwargs):
|
return
|
||||||
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)
|
Comment.objects.create(
|
||||||
|
author=request.user,
|
||||||
|
task=task,
|
||||||
|
body=bleach.clean(request.POST["comment-body"], strip=True),
|
||||||
|
)
|
||||||
|
|
||||||
def get_queryset(self):
|
send_email_to_thread_participants(
|
||||||
# Don't forget to filter out results depending on the visitor !
|
task,
|
||||||
if not self.request.user.is_authenticated:
|
request.POST["comment-body"],
|
||||||
return Task.objects.none()
|
request.user,
|
||||||
|
subject='New comment posted on task "{}"'.format(task.title),
|
||||||
|
)
|
||||||
|
|
||||||
qs = Task.objects.filter(task_list=self.task.task_list).exclude(pk=self.task.pk)
|
messages.success(
|
||||||
|
request, "Comment posted. Notification email sent to thread participants."
|
||||||
if self.q:
|
)
|
||||||
qs = qs.filter(title__istartswith=self.q)
|
|
||||||
|
|
||||||
return qs
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@ -50,26 +50,32 @@ def task_detail(request, task_id: int) -> HttpResponse:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
task = get_object_or_404(Task, pk=task_id)
|
task = get_object_or_404(Task, pk=task_id)
|
||||||
comment_list = Comment.objects.filter(task=task_id)
|
comment_list = Comment.objects.filter(task=task_id).order_by('-date')
|
||||||
|
|
||||||
# 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 not user_can_read_task(task, request.user):
|
if not user_can_read_task(task, request.user):
|
||||||
raise PermissionDenied
|
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
|
# Handle task merging
|
||||||
if request.POST.get("merge_task_into"):
|
if not HAS_TASK_MERGE:
|
||||||
merge_form = MergeForm(request.POST)
|
merge_form = None
|
||||||
if merge_form.is_valid():
|
else:
|
||||||
merge_target = merge_form.cleaned_data["merge_target"]
|
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 not request.POST.get("merge_task_into"):
|
||||||
|
merge_form = MergeForm()
|
||||||
|
else:
|
||||||
|
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):
|
if not user_can_read_task(merge_target, request.user):
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
|
|
||||||
|
@ -78,29 +84,16 @@ def task_detail(request, task_id: int) -> HttpResponse:
|
||||||
"todo:task_detail",
|
"todo:task_detail",
|
||||||
kwargs={"task_id": merge_target.pk}
|
kwargs={"task_id": merge_target.pk}
|
||||||
))
|
))
|
||||||
else:
|
|
||||||
merge_form = MergeForm()
|
|
||||||
|
|
||||||
# Save submitted comments
|
# Save submitted comments
|
||||||
if request.POST.get("add_comment"):
|
handle_add_comment(request, task)
|
||||||
Comment.objects.create(
|
|
||||||
author=request.user,
|
|
||||||
task=task,
|
|
||||||
body=bleach.clean(request.POST["comment-body"], strip=True),
|
|
||||||
)
|
|
||||||
|
|
||||||
send_email_to_thread_participants(
|
|
||||||
task,
|
|
||||||
request.POST["comment-body"],
|
|
||||||
request.user,
|
|
||||||
subject='New comment posted on task "{}"'.format(task.title),
|
|
||||||
)
|
|
||||||
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 not request.POST.get("add_edit_task"):
|
||||||
|
form = AddEditTaskForm(
|
||||||
|
request.user, instance=task, initial={"task_list": task.task_list}
|
||||||
|
)
|
||||||
|
else:
|
||||||
form = AddEditTaskForm(
|
form = AddEditTaskForm(
|
||||||
request.user,
|
request.user,
|
||||||
request.POST,
|
request.POST,
|
||||||
|
@ -118,10 +111,6 @@ def task_detail(request, task_id: int) -> HttpResponse:
|
||||||
list_id=task.task_list.id,
|
list_id=task.task_list.id,
|
||||||
list_slug=task.task_list.slug,
|
list_slug=task.task_list.slug,
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
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"):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue