├── todo ├── mail │ ├── __init__.py │ ├── producers │ │ ├── __init__.py │ │ └── imap.py │ ├── consumers │ │ ├── __init__.py │ │ └── tracker.py │ └── delivery.py ├── tests │ ├── __init__.py │ ├── data │ │ └── csv_import_data.csv │ ├── conftest.py │ ├── test_import.py │ ├── test_utils.py │ ├── test_tracker.py │ └── test_views.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── mail_worker.py │ │ ├── import_csv.py │ │ └── hopper.py ├── migrations │ ├── __init__.py │ ├── 0006_rename_item_model.py │ ├── 0007_auto_update_created_date.py │ ├── 0002_auto_20150614_2339.py │ ├── 0005_auto_20180212_2325.py │ ├── 0009_priority_optional.py │ ├── 0011_add_related_name_to_created_by.py │ ├── 0012_add_related_name_to_comments.py │ ├── 0003_assignee_optional.py │ ├── 0010_attachment.py │ ├── 0008_mail_tracker.py │ ├── 0004_rename_list_tasklist.py │ └── 0001_initial.py ├── operations │ ├── __init__.py │ └── csv_importer.py ├── static │ └── todo │ │ ├── css │ │ └── styles.css │ │ └── js │ │ └── jquery.tablednd_0_5.js ├── templates │ ├── base.html │ └── todo │ │ ├── email │ │ ├── assigned_subject.txt │ │ ├── newcomment_body.txt │ │ └── assigned_body.txt │ │ ├── base.html │ │ ├── include │ │ ├── toggle_delete.html │ │ └── task_edit.html │ │ ├── add_list.html │ │ ├── search_results.html │ │ ├── list_lists.html │ │ ├── del_list.html │ │ ├── add_task_external.html │ │ ├── import_csv.html │ │ ├── list_detail.html │ │ └── task_detail.html ├── __init__.py ├── data │ └── import_example.csv ├── features.py ├── check.py ├── views │ ├── __init__.py │ ├── task_autocomplete.py │ ├── import_csv.py │ ├── reorder_tasks.py │ ├── delete_task.py │ ├── remove_attachment.py │ ├── toggle_done.py │ ├── search.py │ ├── del_list.py │ ├── list_lists.py │ ├── add_list.py │ ├── list_detail.py │ ├── external_add.py │ └── task_detail.py ├── defaults.py ├── admin.py ├── urls.py ├── forms.py ├── utils.py └── models.py ├── setup.py ├── mkdocs.yml ├── pyproject.toml ├── pytest.ini ├── MANIFEST.in ├── SECURITY.md ├── .gitignore ├── base_urls.py ├── setup.cfg ├── LICENSE ├── test_settings.py ├── docs └── index.md └── README.md /todo/mail/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /todo/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /todo/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /todo/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /todo/operations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /todo/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: "readthedocs" 2 | theme: "readthedocs" 3 | -------------------------------------------------------------------------------- /todo/static/todo/css/styles.css: -------------------------------------------------------------------------------- 1 | label { 2 | display: block; 3 | font-weight: bold; 4 | } 5 | -------------------------------------------------------------------------------- /todo/templates/base.html: -------------------------------------------------------------------------------- 1 | This file not actually used by django-todo - here to satisfy the test runner. -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ['setuptools>=40.8.0', 'wheel'] 3 | build-backend = 'setuptools.build_meta:__legacy__' 4 | -------------------------------------------------------------------------------- /todo/templates/todo/email/assigned_subject.txt: -------------------------------------------------------------------------------- 1 | A new task has been assigned to you - {% autoescape off %}{{ task.title }}{% endautoescape %} -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE = test_settings 3 | # -- recommended but optional: 4 | python_files = tests.py test_*.py *_tests.py -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | recursive-include todo/data * 4 | recursive-include todo/static * 5 | recursive-include todo/templates * 6 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | If you find what you believe is a security issue with django-todo, please send a detailed report to django_todo_security_1213@birdhouse.org before publicizing. We thank you for your discretion. 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # tools, IDEs, build folders 2 | /.coverage/ 3 | /.eggs/ 4 | /.idea/ 5 | /build/ 6 | /dist/ 7 | /docs/build/ 8 | /*.egg-info/ 9 | settings.json 10 | 11 | # Django and Python 12 | *.py[cod] 13 | .pytest_cache/* 14 | .mypy_cache 15 | .venv 16 | -------------------------------------------------------------------------------- /todo/templates/todo/base.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | 4 | {% block extrahead %} 5 | 6 | 7 | {% endblock extrahead %} 8 | -------------------------------------------------------------------------------- /todo/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | try: 3 | # if django is not installed, 4 | # skips check because it blocks automated installs 5 | import django 6 | from . import check 7 | except ModuleNotFoundError: 8 | # this can happen during install time, if django is not installed yet! 9 | pass 10 | -------------------------------------------------------------------------------- /todo/tests/data/csv_import_data.csv: -------------------------------------------------------------------------------- 1 | Title,Group,Task List,Created By,Created Date,Due Date,Completed,Assigned To,Note,Priority 2 | Make dinner,Workgroup One,Zip,u1,,2019-06-14,No,u1,This is note one,3 3 | Bake bread,Workgroup One,Zip,u1,2012-03-14,,Yes,,, 4 | Bring dessert,Workgroup Two,Zap,u2,2015-06-248,,,,This is note two,77 -------------------------------------------------------------------------------- /todo/mail/producers/__init__.py: -------------------------------------------------------------------------------- 1 | def imap_producer(**kwargs): 2 | def imap_producer_factory(): 3 | # the import needs to be delayed until call to enable 4 | # using the wrapper in the django settings 5 | from .imap import imap_producer 6 | 7 | return imap_producer(**kwargs) 8 | 9 | return imap_producer_factory 10 | -------------------------------------------------------------------------------- /todo/mail/consumers/__init__.py: -------------------------------------------------------------------------------- 1 | def tracker_consumer(**kwargs): 2 | def tracker_factory(producer): 3 | # the import needs to be delayed until call to enable 4 | # using the wrapper in the django settings 5 | from .tracker import tracker_consumer 6 | 7 | return tracker_consumer(producer, **kwargs) 8 | 9 | return tracker_factory 10 | -------------------------------------------------------------------------------- /todo/data/import_example.csv: -------------------------------------------------------------------------------- 1 | Title,Group,Task List,Created By,Created Date,Due Date,Completed,Assigned To,Note,Priority 2 | Make dinner,Scuba Divers,Web project,shacker,,2019-06-14,No,,Please check with mgmt first,3 3 | Bake bread,Scuba Divers,Example List,mr_random,2012-03-14,,Yes,,, 4 | Bring dessert,Scuba Divers,Web project,user1,2015-06-248,,,user1,Every generation throws a hero up the pop charts,77 -------------------------------------------------------------------------------- /base_urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | 3 | """ 4 | This urlconf exists so we can run tests without an actual Django project 5 | (Django expects ROOT_URLCONF to exist.) This helps the tests remain isolated. 6 | For your project, ignore this file and add 7 | 8 | `path('lists/', include('todo.urls')),` 9 | 10 | to your site's urlconf. 11 | """ 12 | 13 | urlpatterns = [path("lists/", include("todo.urls"))] 14 | -------------------------------------------------------------------------------- /todo/migrations/0006_rename_item_model.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.3 on 2018-03-28 22:40 2 | 3 | from django.conf import settings 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | atomic = False 9 | dependencies = [ 10 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 11 | ("todo", "0005_auto_20180212_2325"), 12 | ] 13 | 14 | operations = [migrations.RenameModel(old_name="Item", new_name="Task")] 15 | -------------------------------------------------------------------------------- /todo/templates/todo/email/newcomment_body.txt: -------------------------------------------------------------------------------- 1 | A new task comment has been added. 2 | 3 | Task: {{ task.title }} 4 | Commenter: {{ user.first_name }} {{ user.last_name }} 5 | 6 | Comment: 7 | {% autoescape off %} 8 | {{ body }} 9 | {% endautoescape %} 10 | 11 | Task details/comments: 12 | https://{{ site }}{% url 'todo:task_detail' task.id %} 13 | 14 | List {{ task.task_list.name }}: 15 | https://{{ site }}{% url 'todo:list_detail' task.task_list.id task.task_list.slug %} 16 | 17 | -------------------------------------------------------------------------------- /todo/templates/todo/include/toggle_delete.html: -------------------------------------------------------------------------------- 1 | 2 | {% if list_slug != "mine" %} 3 | {% if view_completed %} 4 | View incomplete tasks 5 | 6 | {% else %} 7 | View completed tasks 8 | 9 | {% endif %} 10 | 11 | Delete this list 12 | {% endif %} 13 | -------------------------------------------------------------------------------- /todo/migrations/0007_auto_update_created_date.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.4 on 2018-04-05 00:24 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [("todo", "0006_rename_item_model")] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="task", 14 | name="created_date", 15 | field=models.DateField(blank=True, default=django.utils.timezone.now, null=True), 16 | ) 17 | ] 18 | -------------------------------------------------------------------------------- /todo/features.py: -------------------------------------------------------------------------------- 1 | # The integrated mail queue functionality can enable advanced functionality if 2 | # django-autocomplete-light is installed and configured. We can use this module 3 | # to check for other installed dependencies in the future. 4 | 5 | HAS_AUTOCOMPLETE = True 6 | try: 7 | import dal 8 | except ImportError: 9 | HAS_AUTOCOMPLETE = False 10 | 11 | HAS_TASK_MERGE = False 12 | if HAS_AUTOCOMPLETE: 13 | import dal.autocomplete 14 | 15 | if getattr(dal.autocomplete, "Select2QuerySetView", None) is not None: 16 | HAS_TASK_MERGE = True 17 | -------------------------------------------------------------------------------- /todo/templates/todo/email/assigned_body.txt: -------------------------------------------------------------------------------- 1 | {{ task.assigned_to.first_name }} - 2 | 3 | A new task on the list {{ task.task_list.name }} has been assigned to you by {{ task.created_by.get_full_name }}: 4 | 5 | {{ task.title }} 6 | 7 | {% if task.note %} 8 | {% autoescape off %} 9 | Note: {{ task.note }} 10 | {% endautoescape %} 11 | {% endif %} 12 | 13 | Task details/comments: 14 | http://{{ site }}{% url 'todo:task_detail' task.id %} 15 | 16 | List {{ task.task_list.name }}: 17 | http://{{ site }}{% url 'todo:list_detail' task.task_list.id task.task_list.slug %} 18 | -------------------------------------------------------------------------------- /todo/migrations/0002_auto_20150614_2339.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [("todo", "0001_initial")] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="item", name="created_date", field=models.DateField(auto_now=True) 14 | ), 15 | migrations.AlterField( 16 | model_name="item", name="priority", field=models.PositiveIntegerField() 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /todo/migrations/0005_auto_20180212_2325.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.2 on 2018-02-12 23:25 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [("todo", "0004_rename_list_tasklist")] 9 | 10 | operations = [ 11 | migrations.AlterModelOptions( 12 | name="tasklist", options={"ordering": ["name"], "verbose_name_plural": "Task Lists"} 13 | ), 14 | migrations.AlterField( 15 | model_name="item", name="completed", field=models.BooleanField(default=False) 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /todo/migrations/0009_priority_optional.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.7 on 2019-03-18 23:14 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [("todo", "0008_mail_tracker")] 9 | 10 | operations = [ 11 | migrations.AlterModelOptions( 12 | name="task", options={"ordering": ["priority", "created_date"]} 13 | ), 14 | migrations.AlterField( 15 | model_name="task", 16 | name="priority", 17 | field=models.PositiveIntegerField(blank=True, null=True), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /todo/check.py: -------------------------------------------------------------------------------- 1 | from django.core.checks import Error, register 2 | 3 | # the sole purpose of this warning is to prevent people who have 4 | # django-autocomplete-light installed but not configured to start the app 5 | @register() 6 | def dal_check(app_configs, **kwargs): 7 | from django.conf import settings 8 | from todo.features import HAS_AUTOCOMPLETE 9 | 10 | if not HAS_AUTOCOMPLETE: 11 | return [] 12 | 13 | errors = [] 14 | missing_apps = {"dal", "dal_select2"} - set(settings.INSTALLED_APPS) 15 | for missing_app in missing_apps: 16 | errors.append(Error("{} needs to be in INSTALLED_APPS".format(missing_app))) 17 | return errors 18 | -------------------------------------------------------------------------------- /todo/migrations/0011_add_related_name_to_created_by.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.8 on 2019-07-24 11:30 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('todo', '0010_attachment'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='task', 17 | name='created_by', 18 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='todo_created_by', to=settings.AUTH_USER_MODEL), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /todo/migrations/0012_add_related_name_to_comments.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.24 on 2021-09-22 22:05 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('todo', '0011_add_related_name_to_created_by'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='comment', 17 | name='author', 18 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='todo_comments', to=settings.AUTH_USER_MODEL), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /todo/views/__init__.py: -------------------------------------------------------------------------------- 1 | from todo.views.add_list import add_list # noqa: F401 2 | from todo.views.del_list import del_list # noqa: F401 3 | from todo.views.delete_task import delete_task # noqa: F401 4 | from todo.views.external_add import external_add # noqa: F401 5 | from todo.views.import_csv import import_csv # noqa: F401 6 | from todo.views.list_detail import list_detail # noqa: F401 7 | from todo.views.list_lists import list_lists # noqa: F401 8 | from todo.views.remove_attachment import remove_attachment # noqa: F401 9 | from todo.views.reorder_tasks import reorder_tasks # noqa: F401 10 | from todo.views.search import search # noqa: F401 11 | from todo.views.task_detail import task_detail # noqa: F401 12 | from todo.views.toggle_done import toggle_done # noqa: F401 13 | -------------------------------------------------------------------------------- /todo/templates/todo/add_list.html: -------------------------------------------------------------------------------- 1 | {% extends "todo/base.html" %} 2 | {% block page_heading %}{% endblock %} 3 | {% block title %}Add Todo List{% endblock %} 4 | {% block content %} 5 | 6 |
12 |
13 | {{ f.title }}
14 |
15 |
16 |
17 | In list:
18 |
19 | {{ f.task_list.name }}
20 |
21 |
Assigned to: {% if f.assigned_to %}{{ f.assigned_to }}{% else %}Anyone{% endif %}
22 |
Complete: {{ f.completed|yesno:"Yes,No" }}
23 |
24 |
{{ task_count }} tasks in {{ list_count }} list{{ list_count|pluralize }}
9 | 10 | {% regroup lists by group as section_list %} 11 | {% for group in section_list %} 12 |Category tally:
9 | 10 |... all of which will be irretrievably 19 | blown away. Are you sure you want to do that?
20 | 21 | 29 | 30 | 31 | {% else %} 32 |Sorry, you don't have permission to delete lists. Please contact your group administrator.
33 | {% endif %} {% endblock %} -------------------------------------------------------------------------------- /todo/views/import_csv.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.contrib.auth.decorators import login_required, user_passes_test 3 | from django.http import HttpResponse 4 | from django.shortcuts import redirect, render, reverse 5 | 6 | from todo.operations.csv_importer import CSVImporter 7 | from todo.utils import staff_check 8 | 9 | 10 | @login_required 11 | @user_passes_test(staff_check) 12 | def import_csv(request) -> HttpResponse: 13 | """Import a specifically formatted CSV into stored tasks. 14 | """ 15 | 16 | ctx = {"results": None} 17 | 18 | if request.method == "POST": 19 | filepath = request.FILES.get("csvfile") 20 | 21 | if not filepath: 22 | messages.error(request, "You must supply a CSV file to import.") 23 | return redirect(reverse("todo:import_csv")) 24 | 25 | importer = CSVImporter() 26 | results = importer.upsert(filepath) 27 | 28 | if results: 29 | ctx["results"] = results 30 | else: 31 | messages.error(request, "Could not parse provided CSV file.") 32 | return redirect(reverse("todo:import_csv")) 33 | 34 | return render(request, "todo/import_csv.html", context=ctx) 35 | -------------------------------------------------------------------------------- /todo/management/commands/mail_worker.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import socket 3 | import sys 4 | 5 | from django.core.management.base import BaseCommand 6 | from django.conf import settings 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | DEFAULT_IMAP_TIMEOUT = 20 12 | 13 | 14 | class Command(BaseCommand): 15 | help = "Starts a mail worker" 16 | 17 | def add_arguments(self, parser): 18 | parser.add_argument("--imap_timeout", type=int, default=30) 19 | parser.add_argument("worker_name") 20 | 21 | def handle(self, *args, **options): 22 | if not hasattr(settings, "TODO_MAIL_TRACKERS"): 23 | logger.error("missing TODO_MAIL_TRACKERS setting") 24 | sys.exit(1) 25 | 26 | worker_name = options["worker_name"] 27 | tracker = settings.TODO_MAIL_TRACKERS.get(worker_name, None) 28 | if tracker is None: 29 | logger.error("couldn't find configuration for %r in TODO_MAIL_TRACKERS", worker_name) 30 | sys.exit(1) 31 | 32 | # set the default socket timeout (imaplib doesn't enable configuring it) 33 | timeout = options["imap_timeout"] 34 | if timeout: 35 | socket.setdefaulttimeout(timeout) 36 | 37 | # run the mail polling loop 38 | producer = tracker["producer"] 39 | consumer = tracker["consumer"] 40 | 41 | consumer(producer()) 42 | -------------------------------------------------------------------------------- /todo/views/reorder_tasks.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.decorators import login_required, user_passes_test 2 | from django.http import HttpResponse 3 | from django.views.decorators.csrf import csrf_exempt 4 | 5 | from todo.models import Task 6 | from todo.utils import staff_check 7 | 8 | 9 | @csrf_exempt 10 | @login_required 11 | @user_passes_test(staff_check) 12 | def reorder_tasks(request) -> HttpResponse: 13 | """Handle task re-ordering (priorities) from JQuery drag/drop in list_detail.html 14 | """ 15 | newtasklist = request.POST.getlist("tasktable[]") 16 | if newtasklist: 17 | # First task in received list is always empty - remove it 18 | del newtasklist[0] 19 | 20 | # Re-prioritize each task in list 21 | i = 1 22 | for id in newtasklist: 23 | try: 24 | task = Task.objects.get(pk=id) 25 | task.priority = i 26 | task.save() 27 | i += 1 28 | except Task.DoesNotExist: 29 | # Can occur if task is deleted behind the scenes during re-ordering. 30 | # Not easy to remove it from the UI without page refresh, but prevent crash. 31 | pass 32 | 33 | # All views must return an httpresponse of some kind ... without this we get 34 | # error 500s in the log even though things look peachy in the browser. 35 | return HttpResponse(status=201) 36 | -------------------------------------------------------------------------------- /todo/views/delete_task.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.contrib.auth.decorators import login_required, user_passes_test 3 | from django.core.exceptions import PermissionDenied 4 | from django.http import HttpResponse 5 | from django.shortcuts import get_object_or_404, redirect 6 | from django.urls import reverse 7 | 8 | from todo.models import Task 9 | from todo.utils import staff_check 10 | 11 | 12 | @login_required 13 | @user_passes_test(staff_check) 14 | def delete_task(request, task_id: int) -> HttpResponse: 15 | """Delete specified task. 16 | Redirect to the list from which the task came. 17 | """ 18 | 19 | if request.method == "POST": 20 | task = get_object_or_404(Task, pk=task_id) 21 | 22 | redir_url = reverse( 23 | "todo:list_detail", 24 | kwargs={"list_id": task.task_list.id, "list_slug": task.task_list.slug}, 25 | ) 26 | 27 | # Permissions 28 | if not ( 29 | (task.created_by == request.user) 30 | or (request.user.is_superuser) 31 | or (task.assigned_to == request.user) 32 | or (task.task_list.group in request.user.groups.all()) 33 | ): 34 | raise PermissionDenied 35 | 36 | task.delete() 37 | 38 | messages.success(request, "Task '{}' has been deleted".format(task.title)) 39 | return redirect(redir_url) 40 | 41 | else: 42 | raise PermissionDenied 43 | -------------------------------------------------------------------------------- /todo/views/remove_attachment.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.contrib.auth.decorators import login_required 3 | from django.core.exceptions import PermissionDenied 4 | from django.http import HttpResponse 5 | from django.shortcuts import get_object_or_404, redirect 6 | from django.urls import reverse 7 | 8 | from todo.models import Attachment 9 | from todo.utils import remove_attachment_file 10 | 11 | 12 | @login_required 13 | def remove_attachment(request, attachment_id: int) -> HttpResponse: 14 | """Delete a previously posted attachment object and its corresponding file 15 | from the filesystem, permissions allowing. 16 | """ 17 | 18 | if request.method == "POST": 19 | attachment = get_object_or_404(Attachment, pk=attachment_id) 20 | 21 | redir_url = reverse("todo:task_detail", kwargs={"task_id": attachment.task.id}) 22 | 23 | # Permissions 24 | if not ( 25 | attachment.task.task_list.group in request.user.groups.all() 26 | or request.user.is_superuser 27 | ): 28 | raise PermissionDenied 29 | 30 | if remove_attachment_file(attachment.id): 31 | messages.success(request, f"Attachment {attachment.id} removed.") 32 | else: 33 | messages.error( 34 | request, f"Sorry, there was a problem deleting attachment {attachment.id}." 35 | ) 36 | 37 | return redirect(redir_url) 38 | 39 | else: 40 | raise PermissionDenied 41 | -------------------------------------------------------------------------------- /todo/views/toggle_done.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.contrib.auth.decorators import login_required, user_passes_test 3 | from django.core.exceptions import PermissionDenied 4 | from django.http import HttpResponse 5 | from django.shortcuts import get_object_or_404, redirect 6 | from django.urls import reverse 7 | 8 | from todo.models import Task 9 | from todo.utils import toggle_task_completed 10 | from todo.utils import staff_check 11 | 12 | 13 | @login_required 14 | @user_passes_test(staff_check) 15 | def toggle_done(request, task_id: int) -> HttpResponse: 16 | """Toggle the completed status of a task from done to undone, or vice versa. 17 | Redirect to the list from which the task came. 18 | """ 19 | 20 | if request.method == "POST": 21 | task = get_object_or_404(Task, pk=task_id) 22 | 23 | redir_url = reverse( 24 | "todo:list_detail", 25 | kwargs={"list_id": task.task_list.id, "list_slug": task.task_list.slug}, 26 | ) 27 | 28 | # Permissions 29 | if not ( 30 | (task.created_by == request.user) 31 | or (request.user.is_superuser) 32 | or (task.assigned_to == request.user) 33 | or (task.task_list.group in request.user.groups.all()) 34 | ): 35 | raise PermissionDenied 36 | 37 | toggle_task_completed(task.id) 38 | messages.success(request, "Task status changed for '{}'".format(task.title)) 39 | 40 | return redirect(redir_url) 41 | 42 | else: 43 | raise PermissionDenied 44 | -------------------------------------------------------------------------------- /todo/views/search.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.decorators import login_required, user_passes_test 2 | from django.db.models import Q 3 | from django.http import HttpResponse 4 | from django.shortcuts import render 5 | 6 | from todo.models import Task 7 | from todo.utils import staff_check 8 | 9 | 10 | @login_required 11 | @user_passes_test(staff_check) 12 | def search(request) -> HttpResponse: 13 | """Search for tasks user has permission to see. 14 | """ 15 | 16 | query_string = "" 17 | 18 | if request.GET: 19 | 20 | found_tasks = None 21 | if ("q" in request.GET) and request.GET["q"].strip(): 22 | query_string = request.GET["q"] 23 | 24 | found_tasks = Task.objects.filter( 25 | Q(title__icontains=query_string) | Q(note__icontains=query_string) 26 | ) 27 | else: 28 | # What if they selected the "completed" toggle but didn't enter a query string? 29 | # We still need found_tasks in a queryset so it can be "excluded" below. 30 | found_tasks = Task.objects.all() 31 | 32 | if "inc_complete" in request.GET: 33 | found_tasks = found_tasks.exclude(completed=True) 34 | 35 | else: 36 | found_tasks = None 37 | 38 | # Only include tasks that are in groups of which this user is a member: 39 | if not request.user.is_superuser: 40 | found_tasks = found_tasks.filter(task_list__group__in=request.user.groups.all()) 41 | 42 | context = {"query_string": query_string, "found_tasks": found_tasks} 43 | return render(request, "todo/search_results.html", context) 44 | -------------------------------------------------------------------------------- /todo/migrations/0010_attachment.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2019-04-06 16:28 2 | 3 | import datetime 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | import todo.models 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ("todo", "0009_priority_optional"), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name="Attachment", 20 | fields=[ 21 | ( 22 | "id", 23 | models.AutoField( 24 | auto_created=True, primary_key=True, serialize=False, verbose_name="ID" 25 | ), 26 | ), 27 | ("timestamp", models.DateTimeField(default=datetime.datetime.now)), 28 | ( 29 | "file", 30 | models.FileField( 31 | max_length=255, upload_to=todo.models.get_attachment_upload_dir 32 | ), 33 | ), 34 | ( 35 | "added_by", 36 | models.ForeignKey( 37 | on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL 38 | ), 39 | ), 40 | ( 41 | "task", 42 | models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="todo.Task"), 43 | ), 44 | ], 45 | ) 46 | ] 47 | -------------------------------------------------------------------------------- /todo/migrations/0008_mail_tracker.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.7 on 2019-03-24 22:50 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [("todo", "0007_auto_update_created_date")] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="comment", 15 | name="email_from", 16 | field=models.CharField(blank=True, max_length=320, null=True), 17 | ), 18 | migrations.AddField( 19 | model_name="comment", 20 | name="email_message_id", 21 | field=models.CharField(blank=True, max_length=255, null=True), 22 | ), 23 | migrations.AlterField( 24 | model_name="comment", 25 | name="author", 26 | field=models.ForeignKey( 27 | blank=True, 28 | null=True, 29 | on_delete=django.db.models.deletion.CASCADE, 30 | to=settings.AUTH_USER_MODEL, 31 | ), 32 | ), 33 | migrations.AlterField( 34 | model_name="task", 35 | name="created_by", 36 | field=models.ForeignKey( 37 | null=True, 38 | on_delete=django.db.models.deletion.CASCADE, 39 | related_name="todo_created_by", 40 | to=settings.AUTH_USER_MODEL, 41 | ), 42 | ), 43 | migrations.AlterUniqueTogether( 44 | name="comment", unique_together={("task", "email_message_id")} 45 | ), 46 | ] 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, Scot Hacker, Birdhouse Arts and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of Birdhouse Arts nor the names of its contributors 15 | may be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /todo/migrations/0004_rename_list_tasklist.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.2 on 2018-02-09 23:15 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("auth", "0009_alter_user_last_name_max_length"), 11 | ("todo", "0003_assignee_optional"), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name="TaskList", 17 | fields=[ 18 | ( 19 | "id", 20 | models.AutoField( 21 | auto_created=True, primary_key=True, serialize=False, verbose_name="ID" 22 | ), 23 | ), 24 | ("name", models.CharField(max_length=60)), 25 | ("slug", models.SlugField(default="")), 26 | ( 27 | "group", 28 | models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="auth.Group"), 29 | ), 30 | ], 31 | options={"verbose_name_plural": "Lists", "ordering": ["name"]}, 32 | ), 33 | migrations.AlterUniqueTogether(name="list", unique_together=set()), 34 | migrations.RemoveField(model_name="list", name="group"), 35 | migrations.RemoveField(model_name="item", name="list"), 36 | migrations.DeleteModel(name="List"), 37 | migrations.AddField( 38 | model_name="item", 39 | name="task_list", 40 | field=models.ForeignKey( 41 | null=True, on_delete=django.db.models.deletion.CASCADE, to="todo.TaskList" 42 | ), 43 | ), 44 | migrations.AlterUniqueTogether(name="tasklist", unique_together={("group", "slug")}), 45 | ] 46 | -------------------------------------------------------------------------------- /todo/templates/todo/add_task_external.html: -------------------------------------------------------------------------------- 1 | {% extends "todo/base.html" %} 2 | {% block page_heading %}{% endblock %} 3 | {% block title %}File Ticket{% endblock %} 4 | 5 | {% block content %} 6 | 7 |12 | Batch-import tasks by uploading a specifically-formatted CSV. 13 | See documentation for formatting rules. 14 | Successs and failures will be reported here. 15 |
16 | 17 | {% if results %} 18 |26 | Summary: 27 |
28 |37 | Upserts (tasks created or updated): 38 |
39 |48 | Errors (tasks NOT created or updated): 49 |
50 |In workgroup "{{ task_list.group }}" - drag rows to set priorities.
25 | {% endif %} 26 | 27 || Task | 30 |Created | 31 |Due on | 32 |Owner | 33 |Assigned | 34 |Mark | 35 |
|---|---|---|---|---|---|
| 40 | {{ task.title|truncatewords:10 }} 41 | | 42 |43 | {{ task.created_date|date:"m/d/Y" }} 44 | | 45 |46 | 47 | {{ task.due_date|date:"m/d/Y" }} 48 | 49 | | 50 |51 | {{ task.created_by }} 52 | | 53 |54 | {% if task.assigned_to %}{{ task.assigned_to }}{% else %}Anyone{% endif %} 55 | | 56 |57 | 67 | | 68 |
| File | 131 |Uploaded | 132 |By | 133 |Type | 134 |Remove | 135 |
|---|---|---|---|---|
| {{ attachment.filename }} | 141 |{{ attachment.timestamp }} | 142 |{{ attachment.added_by.get_full_name }} | 143 |{{ attachment.extension.lower }} | 144 |145 | 149 | | 150 |
Comments on this task
188 | {% for comment in comment_list %} 189 |No comments (yet).
208 | {% endif %} 209 |