diff --git a/todo/check.py b/todo/check.py index 0a5def7..3b72496 100644 --- a/todo/check.py +++ b/todo/check.py @@ -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: diff --git a/todo/features.py b/todo/features.py new file mode 100644 index 0000000..b55a9ea --- /dev/null +++ b/todo/features.py @@ -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 diff --git a/todo/mail/delivery.py b/todo/mail/delivery.py index 42cd306..5b7f2d4 100644 --- a/todo/mail/delivery.py +++ b/todo/mail/delivery.py @@ -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') diff --git a/todo/templates/todo/include/task_edit.html b/todo/templates/todo/include/task_edit.html index 3713958..936d465 100644 --- a/todo/templates/todo/include/task_edit.html +++ b/todo/templates/todo/include/task_edit.html @@ -2,8 +2,7 @@
{% csrf_token %} - -
+
Add Task {# Task edit / new task form #} - {% include 'todo/include/task_edit.html' %} +
+ {% include 'todo/include/task_edit.html' %} +

{% endif %} diff --git a/todo/templates/todo/task_detail.html b/todo/templates/todo/task_detail.html index 05338aa..8452300 100644 --- a/todo/templates/todo/task_detail.html +++ b/todo/templates/todo/task_detail.html @@ -93,6 +93,7 @@
{# Task edit / new task form #} {% include 'todo/include/task_edit.html' %} + {% if merge_form is not None %}
Merge task
@@ -111,6 +112,7 @@
+ {% endif %}
diff --git a/todo/urls.py b/todo/urls.py index 5db2f3b..eccef35 100644 --- a/todo/urls.py +++ b/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//', views.task_detail, name='task_detail'), +] - path( - 'task//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//autocomplete/', + TaskAutocomplete.as_view(), + name='task_autocomplete') + ) +urlpatterns.extend([ path( 'toggle_done//', views.toggle_done, @@ -75,4 +83,4 @@ urlpatterns = [ 'search/', views.search, name="search"), -] +]) diff --git a/todo/utils.py b/todo/utils.py index ae1766c..8e682ea 100644 --- a/todo/utils.py +++ b/todo/utils.py @@ -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) diff --git a/todo/views/__init__.py b/todo/views/__init__.py index 63cdb5b..c36bb65 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, TaskAutocomplete # noqa: F401 +from todo.views.task_detail import task_detail # noqa: F401 from todo.views.toggle_done import toggle_done # noqa: F401 diff --git a/todo/views/task_autocomplete.py b/todo/views/task_autocomplete.py new file mode 100644 index 0000000..0a5667e --- /dev/null +++ b/todo/views/task_autocomplete.py @@ -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 diff --git a/todo/views/task_detail.py b/todo/views/task_detail.py index 436847a..64a201e 100644 --- a/todo/views/task_detail.py +++ b/todo/views/task_detail.py @@ -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"):