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
|
||||
|
||||
# the sole purpose of this warning is to prevent people who have
|
||||
# django-autocomplete-light installed but not configured to start the app
|
||||
@register()
|
||||
def dal_check(app_configs, **kwargs):
|
||||
from django.conf import settings
|
||||
from todo.features import HAS_AUTOCOMPLETE
|
||||
|
||||
if not HAS_AUTOCOMPLETE:
|
||||
return
|
||||
|
||||
errors = []
|
||||
missing_apps = {'dal', 'dal_select2'} - set(settings.INSTALLED_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')
|
||||
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">
|
||||
{% csrf_token %}
|
||||
|
||||
<div id="AddEditTask" class="mt-3">
|
||||
<div class="mt-3">
|
||||
<div class="form-group">
|
||||
<label for="id_title" name="title">Task</label>
|
||||
<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>
|
||||
|
||||
{# Task edit / new task form #}
|
||||
{% include 'todo/include/task_edit.html' %}
|
||||
<div id="AddEditTask" class="collapse">
|
||||
{% include 'todo/include/task_edit.html' %}
|
||||
</div>
|
||||
<hr />
|
||||
{% endif %}
|
||||
|
||||
|
|
|
@ -93,6 +93,7 @@
|
|||
<div id="TaskEdit" class="collapse">
|
||||
{# Task edit / new task form #}
|
||||
{% include 'todo/include/task_edit.html' %}
|
||||
{% if merge_form is not None %}
|
||||
<form action="" method="post">
|
||||
<div class="card border-danger">
|
||||
<div class="card-header">Merge task</div>
|
||||
|
@ -111,6 +112,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
|
|
22
todo/urls.py
22
todo/urls.py
|
@ -1,11 +1,12 @@
|
|||
from django.urls import path
|
||||
|
||||
from todo import views
|
||||
|
||||
from todo.features import HAS_TASK_MERGE
|
||||
app_name = 'todo'
|
||||
|
||||
urlpatterns = [
|
||||
from django.conf import settings
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
'',
|
||||
views.list_lists,
|
||||
|
@ -55,12 +56,19 @@ urlpatterns = [
|
|||
'task/<int:task_id>/',
|
||||
views.task_detail,
|
||||
name='task_detail'),
|
||||
]
|
||||
|
||||
path(
|
||||
'task/<int:task_id>/autocomplete/',
|
||||
views.TaskAutocomplete.as_view(),
|
||||
name='task_autocomplete'),
|
||||
if HAS_TASK_MERGE:
|
||||
# ensure autocomplete is optional
|
||||
from todo.views.task_autocomplete import TaskAutocomplete
|
||||
urlpatterns.append(
|
||||
path(
|
||||
'task/<int:task_id>/autocomplete/',
|
||||
TaskAutocomplete.as_view(),
|
||||
name='task_autocomplete')
|
||||
)
|
||||
|
||||
urlpatterns.extend([
|
||||
path(
|
||||
'toggle_done/<int:task_id>/',
|
||||
views.toggle_done,
|
||||
|
@ -75,4 +83,4 @@ urlpatterns = [
|
|||
'search/',
|
||||
views.search,
|
||||
name="search"),
|
||||
]
|
||||
])
|
||||
|
|
|
@ -23,6 +23,10 @@ def staff_check(user):
|
|||
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):
|
||||
'''returns a mail backend for some task'''
|
||||
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
|
||||
if ca.author is not None
|
||||
)
|
||||
for user_email in (task.created_by.email, task.assigned_to.email):
|
||||
recip_list.add(user_email)
|
||||
for related_user in (task.created_by, task.assigned_to):
|
||||
if related_user is not None:
|
||||
recip_list.add(related_user.email)
|
||||
recip_list = list(m for m in recip_list if m)
|
||||
|
||||
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.reorder_tasks import reorder_tasks # 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
|
||||
|
|
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.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
|
||||
from todo.utils import send_email_to_thread_participants, toggle_task_completed, staff_check, user_can_read_task
|
||||
from todo.features import HAS_TASK_MERGE
|
||||
|
||||
|
||||
def user_can_read_task(task, user):
|
||||
return task.task_list.group in user.groups.all() or user.is_staff
|
||||
if HAS_TASK_MERGE:
|
||||
from dal import autocomplete
|
||||
from todo.views.task_autocomplete import TaskAutocomplete
|
||||
|
||||
|
||||
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
|
||||
def handle_add_comment(request, task):
|
||||
if not request.POST.get("add_comment"):
|
||||
return
|
||||
|
||||
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):
|
||||
# Don't forget to filter out results depending on the visitor !
|
||||
if not self.request.user.is_authenticated:
|
||||
return Task.objects.none()
|
||||
send_email_to_thread_participants(
|
||||
task,
|
||||
request.POST["comment-body"],
|
||||
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)
|
||||
|
||||
if self.q:
|
||||
qs = qs.filter(title__istartswith=self.q)
|
||||
|
||||
return qs
|
||||
messages.success(
|
||||
request, "Comment posted. Notification email sent to thread participants."
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
|
@ -50,26 +50,32 @@ def task_detail(request, task_id: int) -> HttpResponse:
|
|||
"""
|
||||
|
||||
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.
|
||||
# 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):
|
||||
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.POST.get("merge_task_into"):
|
||||
merge_form = MergeForm(request.POST)
|
||||
if merge_form.is_valid():
|
||||
merge_target = merge_form.cleaned_data["merge_target"]
|
||||
if not HAS_TASK_MERGE:
|
||||
merge_form = None
|
||||
else:
|
||||
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):
|
||||
raise PermissionDenied
|
||||
|
||||
|
@ -78,29 +84,16 @@ def task_detail(request, task_id: int) -> HttpResponse:
|
|||
"todo:task_detail",
|
||||
kwargs={"task_id": merge_target.pk}
|
||||
))
|
||||
else:
|
||||
merge_form = MergeForm()
|
||||
|
||||
# Save submitted comments
|
||||
if request.POST.get("add_comment"):
|
||||
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."
|
||||
)
|
||||
handle_add_comment(request, task)
|
||||
|
||||
# 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(
|
||||
request.user,
|
||||
request.POST,
|
||||
|
@ -118,10 +111,6 @@ def task_detail(request, task_id: int) -> HttpResponse:
|
|||
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}
|
||||
)
|
||||
|
||||
# Mark complete
|
||||
if request.POST.get("toggle_done"):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue