├── README.md ├── images ├── 0_initial_look.png ├── 1_add_tasklist.png ├── 2_new_tasklist.png ├── 3_adding_tasks.png └── 4_editing_tasks.png ├── tasker ├── manage.py ├── requirements.txt └── tasker │ ├── __init__.py │ ├── settings.py │ ├── static │ └── favicon.ico │ ├── tasks │ ├── __init__.py │ ├── admin.py │ ├── filters.py │ ├── forms.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── templates │ │ └── tasks │ │ │ ├── task_create_form.html │ │ │ ├── task_detail.html │ │ │ ├── task_edit_form.html │ │ │ ├── tasklist_create_form.html │ │ │ ├── tasklist_detail.html │ │ │ ├── tasklist_filter.html │ │ │ ├── tasklist_list.html │ │ │ └── tasklist_tasks.html │ ├── urls.py │ └── views.py │ ├── templates │ └── base.html │ ├── urls.py │ └── wsgi.py └── tasker2 ├── manage.py ├── requirements.txt └── tasker2 ├── __init__.py ├── settings.py ├── static └── favicon.ico ├── tasks ├── __init__.py ├── admin.py ├── filters.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── templates │ └── tasks │ │ ├── task_create_form.html │ │ ├── task_detail.html │ │ ├── task_edit_form.html │ │ ├── tasklist_create_form.html │ │ ├── tasklist_detail.html │ │ ├── tasklist_filter.html │ │ ├── tasklist_list.html │ │ └── tasklist_tasks.html ├── urls.py └── views.py ├── templates └── base.html ├── urls.py └── wsgi.py /README.md: -------------------------------------------------------------------------------- 1 | # django-htmx-todo-list 2 | 3 | Quick example of a todo list application using [Django](https://www.djangoproject.com/) and [HTMX](https://htmx.org/) 4 | 5 | ## Background 6 | 7 | Modified & expanded from https://github.com/jaredlockhart/django-htmx-todo/ 8 | 9 | This project lets you build todo lists. It demonstrates functionality with django and HTMX, including use of modal forms, adding multiple forms to a list (an alternative to traditional django formsets), and deleting items from a list (or an entire list). 10 | 11 | The original project used class-based Django views. That has been improved to use function-based views (see [Django Views — The Right Way](https://spookylukey.github.io/django-views-the-right-way/) to read why FBV is often the better approach!) 12 | 13 | There are actually two example projects here which are the same in all respects except that `tasker` uses hard-coded html forms, and `tasker2` uses django forms with [django-crispy-forms](https://github.com/django-crispy-forms/django-crispy-forms) for formatting. 14 | 15 | This project is very basic. It does not make use of authorization or other common important concerns. The focus is 100% on demonstrating some Django & HTMX concepts. 16 | 17 | ## Images 18 | 19 | ### Intitial Look at the app 20 | 21 | ![Intitial Look at the app](/images/0_initial_look.png) 22 | 23 | ### Adding a new TaskList 24 | 25 | ![Adding a new TaskList](/images/1_add_tasklist.png) 26 | 27 | ### The newly created TaskList 28 | 29 | ![The newly created TaskList](/images/2_new_tasklist.png) 30 | 31 | ### Adding Task instances 32 | 33 | ![Adding Task instances](/images/3_adding_tasks.png) 34 | 35 | ### Editing existing Task instances 36 | 37 | ![Editing existing Task instances](/images/4_editing_tasks.png) 38 | -------------------------------------------------------------------------------- /images/0_initial_look.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacklinke/django-htmx-todo-list/fa269e262fa50b78cc780b2ab691571780e7674b/images/0_initial_look.png -------------------------------------------------------------------------------- /images/1_add_tasklist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacklinke/django-htmx-todo-list/fa269e262fa50b78cc780b2ab691571780e7674b/images/1_add_tasklist.png -------------------------------------------------------------------------------- /images/2_new_tasklist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacklinke/django-htmx-todo-list/fa269e262fa50b78cc780b2ab691571780e7674b/images/2_new_tasklist.png -------------------------------------------------------------------------------- /images/3_adding_tasks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacklinke/django-htmx-todo-list/fa269e262fa50b78cc780b2ab691571780e7674b/images/3_adding_tasks.png -------------------------------------------------------------------------------- /images/4_editing_tasks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacklinke/django-htmx-todo-list/fa269e262fa50b78cc780b2ab691571780e7674b/images/4_editing_tasks.png -------------------------------------------------------------------------------- /tasker/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | ALLOWED_HOSTS = ["*"] 7 | 8 | 9 | def main(): 10 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tasker.settings") 11 | try: 12 | from django.core.management import execute_from_command_line 13 | except ImportError as exc: 14 | raise ImportError( 15 | "Couldn't import Django. Are you sure it's installed and " 16 | "available on your PYTHONPATH environment variable? Did you " 17 | "forget to activate a virtual environment?" 18 | ) from exc 19 | execute_from_command_line(sys.argv) 20 | 21 | 22 | if __name__ == "__main__": 23 | main() 24 | -------------------------------------------------------------------------------- /tasker/requirements.txt: -------------------------------------------------------------------------------- 1 | django 2 | django-filter 3 | -------------------------------------------------------------------------------- /tasker/tasker/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacklinke/django-htmx-todo-list/fa269e262fa50b78cc780b2ab691571780e7674b/tasker/tasker/__init__.py -------------------------------------------------------------------------------- /tasker/tasker/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for django-htmx-todo-list. 3 | """ 4 | import os 5 | from pathlib import Path 6 | from typing import List 7 | 8 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 9 | BASE_DIR = Path(__file__).resolve().parent.parent 10 | 11 | # SECURITY WARNING: keep the secret key used in production secret! 12 | SECRET_KEY = "SECRETKEY" 13 | 14 | # SECURITY WARNING: don't run with debug turned on in production! 15 | DEBUG = True 16 | 17 | ALLOWED_HOSTS = ["localhost", "*"] 18 | 19 | INSTALLED_APPS = [ 20 | "django.contrib.admin", 21 | "django.contrib.auth", 22 | "django.contrib.contenttypes", 23 | "django.contrib.sessions", 24 | "django.contrib.messages", 25 | "django.contrib.staticfiles", 26 | "django_filters", 27 | "tasker.tasks", 28 | ] 29 | 30 | MIDDLEWARE = [ 31 | "django.middleware.security.SecurityMiddleware", 32 | "django.contrib.sessions.middleware.SessionMiddleware", 33 | "django.middleware.common.CommonMiddleware", 34 | "django.middleware.csrf.CsrfViewMiddleware", 35 | "django.contrib.auth.middleware.AuthenticationMiddleware", 36 | "django.contrib.messages.middleware.MessageMiddleware", 37 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 38 | ] 39 | 40 | ROOT_URLCONF = "tasker.urls" 41 | 42 | TEMPLATES = [ 43 | { 44 | "BACKEND": "django.template.backends.django.DjangoTemplates", 45 | "DIRS": [os.path.join(BASE_DIR, "tasker/templates")], 46 | "APP_DIRS": True, 47 | "OPTIONS": { 48 | "context_processors": [ 49 | "django.template.context_processors.debug", 50 | "django.template.context_processors.request", 51 | "django.contrib.auth.context_processors.auth", 52 | "django.contrib.messages.context_processors.messages", 53 | ], 54 | }, 55 | }, 56 | ] 57 | 58 | WSGI_APPLICATION = "tasker.wsgi.application" 59 | 60 | 61 | # Database 62 | # https://docs.djangoproject.com/en/3.1/ref/settings/#databases 63 | 64 | DATABASES = { 65 | "default": { 66 | "ENGINE": "django.db.backends.sqlite3", 67 | "NAME": "todo", 68 | "USER": "todo", 69 | "PASSWORD": "todo", 70 | "HOST": "db", 71 | "PORT": "5432", 72 | } 73 | } 74 | 75 | 76 | # Password validation 77 | # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators 78 | 79 | AUTH_PASSWORD_VALIDATORS = [ 80 | { 81 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 82 | }, 83 | { 84 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 85 | }, 86 | { 87 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 88 | }, 89 | { 90 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 91 | }, 92 | ] 93 | 94 | 95 | LANGUAGE_CODE = "en-us" 96 | 97 | TIME_ZONE = "UTC" 98 | 99 | USE_I18N = True 100 | 101 | USE_L10N = True 102 | 103 | USE_TZ = True 104 | 105 | 106 | STATIC_URL = "/static/" 107 | STATICFILES_DIRS = [ 108 | BASE_DIR / "tasker" / "static", 109 | ] 110 | -------------------------------------------------------------------------------- /tasker/tasker/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacklinke/django-htmx-todo-list/fa269e262fa50b78cc780b2ab691571780e7674b/tasker/tasker/static/favicon.ico -------------------------------------------------------------------------------- /tasker/tasker/tasks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacklinke/django-htmx-todo-list/fa269e262fa50b78cc780b2ab691571780e7674b/tasker/tasker/tasks/__init__.py -------------------------------------------------------------------------------- /tasker/tasker/tasks/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from tasker.tasks.models import Task, TaskList 4 | 5 | # Register your models here. 6 | admin.site.register(TaskList) 7 | admin.site.register(Task) 8 | -------------------------------------------------------------------------------- /tasker/tasker/tasks/filters.py: -------------------------------------------------------------------------------- 1 | import django_filters 2 | from django import forms 3 | from django.db import models 4 | from django.db.models.query import QuerySet 5 | 6 | from tasker.tasks.models import TaskList 7 | 8 | 9 | class CompletenessChoices(models.TextChoices): 10 | ALL = "all" 11 | COMPLETE = "complete" 12 | NOT_COMPLETE = "not_complete" 13 | 14 | 15 | class TaskListFilter(django_filters.FilterSet): 16 | name = django_filters.CharFilter(lookup_expr="icontains") 17 | completeness = django_filters.ChoiceFilter( 18 | choices=CompletenessChoices.choices, 19 | widget=forms.widgets.RadioSelect, 20 | empty_label=None, 21 | method="get_completeness", 22 | ) 23 | 24 | class Meta: 25 | model = TaskList 26 | fields = ["name", "completeness"] 27 | 28 | def get_completeness(self, queryset: QuerySet[TaskList], field_name: str, value: str) -> QuerySet[TaskList]: 29 | if value == CompletenessChoices.COMPLETE: 30 | return queryset.filter(id__in=[tasklist.id for tasklist in queryset if tasklist.is_complete]) 31 | elif value == CompletenessChoices.NOT_COMPLETE: 32 | return queryset.exclude(id__in=[tasklist.id for tasklist in queryset if tasklist.is_complete]) 33 | return queryset 34 | -------------------------------------------------------------------------------- /tasker/tasker/tasks/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.utils.text import slugify 3 | 4 | from tasker.tasks.models import Task, TaskList 5 | 6 | 7 | class TaskListCreateForm(forms.ModelForm): 8 | slug = forms.CharField(required=False, widget=forms.widgets.HiddenInput()) 9 | 10 | class Meta: 11 | model = TaskList 12 | fields = ("name", "slug") 13 | 14 | def clean_name(self) -> str: 15 | name: str = self.cleaned_data["name"] 16 | slug = slugify(name) 17 | if TaskList.objects.filter(slug=slug).exists(): 18 | raise forms.ValidationError(f"A Task List with the name {name} exists") 19 | return name 20 | 21 | def save(self, commit: bool = True) -> TaskList: 22 | task_list: TaskList = super().save(commit) 23 | task_list.slug = slugify(task_list.name) 24 | task_list.save() 25 | return task_list 26 | 27 | 28 | class TaskForm(forms.ModelForm): 29 | class Meta: 30 | model = Task 31 | fields = ("name", "is_done") 32 | 33 | def clean_name(self) -> str: 34 | name: str = self.cleaned_data["name"] 35 | if Task.objects.filter(name=name).exclude(id=self.instance.id).exists(): 36 | raise forms.ValidationError(f"A Task with the name {name} exists") 37 | return name 38 | 39 | def save(self, commit: bool = True) -> Task: 40 | task: Task = super().save(commit) 41 | task.save() 42 | return task 43 | -------------------------------------------------------------------------------- /tasker/tasker/tasks/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.2 on 2020-11-08 00:19 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name="TaskList", 16 | fields=[ 17 | ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), 18 | ("name", models.CharField(max_length=255)), 19 | ("slug", models.CharField(max_length=255)), 20 | ], 21 | options={ 22 | "verbose_name": "Task List", 23 | "verbose_name_plural": "Task Lists", 24 | }, 25 | ), 26 | migrations.CreateModel( 27 | name="Task", 28 | fields=[ 29 | ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), 30 | ("name", models.CharField(max_length=255)), 31 | ("is_done", models.BooleanField(default=False)), 32 | ( 33 | "task_list", 34 | models.ForeignKey( 35 | on_delete=django.db.models.deletion.CASCADE, related_name="tasks", to="tasks.tasklist" 36 | ), 37 | ), 38 | ], 39 | options={ 40 | "verbose_name": "Task Item", 41 | "verbose_name_plural": "Task Items", 42 | }, 43 | ), 44 | ] 45 | -------------------------------------------------------------------------------- /tasker/tasker/tasks/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacklinke/django-htmx-todo-list/fa269e262fa50b78cc780b2ab691571780e7674b/tasker/tasker/tasks/migrations/__init__.py -------------------------------------------------------------------------------- /tasker/tasker/tasks/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.db.models.query import QuerySet 3 | from django.urls import reverse 4 | 5 | 6 | class TaskList(models.Model): 7 | name = models.CharField(max_length=255) 8 | slug = models.CharField(max_length=255) 9 | 10 | tasks: QuerySet["Task"] 11 | 12 | class Meta: 13 | verbose_name = "Task List" 14 | verbose_name_plural = "Task Lists" 15 | 16 | def __str__(self) -> str: 17 | return self.name 18 | 19 | def get_absolute_url(self) -> str: 20 | return reverse("tasklist-detail", kwargs={"slug": self.slug}) 21 | 22 | @property 23 | def is_complete(self) -> bool: 24 | return not self.tasks.filter(is_done=False).exists() 25 | 26 | @property 27 | def complete_tasks(self) -> models.QuerySet["Task"]: 28 | return self.tasks.filter(is_done=True) 29 | 30 | @property 31 | def incomplete_tasks(self) -> models.QuerySet["Task"]: 32 | return self.tasks.filter(is_done=False) 33 | 34 | 35 | class Task(models.Model): 36 | task_list = models.ForeignKey( 37 | TaskList, 38 | related_name="tasks", 39 | on_delete=models.CASCADE, 40 | ) 41 | name = models.CharField(max_length=255) 42 | is_done = models.BooleanField(default=False) 43 | 44 | class Meta: 45 | verbose_name = "Task Item" 46 | verbose_name_plural = "Task Items" 47 | 48 | def __str__(self) -> str: 49 | return self.name 50 | -------------------------------------------------------------------------------- /tasker/tasker/tasks/templates/tasks/task_create_form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | {% csrf_token %} 5 |
6 |
7 | 10 |
11 |
12 | 15 | 18 |
19 |
20 | 21 |
22 |
23 | {% if form.name.errors %}{{ form.name.errors }}{% endif %} 24 | {% if form.is_done.errors %}{{ form.is_done.errors }}{% endif %} 25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /tasker/tasker/tasks/templates/tasks/task_detail.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 | Task name: {{ task.name }} 6 |
7 |
8 | 9 | 12 |
13 |
14 |
15 | 18 |
19 | {% csrf_token %} 20 | 23 |
24 |
25 |
26 |
27 | 28 | -------------------------------------------------------------------------------- /tasker/tasker/tasks/templates/tasks/task_edit_form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | {% csrf_token %} 5 |
6 |
7 | 10 |
11 |
12 | 15 | 18 |
19 |
20 | 21 |
22 |
23 | {% if form.name.errors %}{{ form.name.errors }}{% endif %} 24 | {% if form.is_done.errors %}{{ form.is_done.errors }}{% endif %} 25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /tasker/tasker/tasks/templates/tasks/tasklist_create_form.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {% csrf_token %} 4 |
5 |
6 | 8 |
9 | {{ form.name.errors }} 10 |
11 |
12 |
13 |
14 |
15 | 17 |
18 |
19 |
20 |
21 | -------------------------------------------------------------------------------- /tasker/tasker/tasks/templates/tasks/tasklist_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {% include "tasks/tasklist_tasks.html" with tasklist=tasklist %} 13 | 14 |
{{ tasklist.name }}
15 |
16 | 20 |
21 |
22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /tasker/tasker/tasks/templates/tasks/tasklist_filter.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {% for tasklist in object_list %} 13 | 14 | 19 | 20 | 21 | 29 | 30 | {% endfor %} 31 | 32 |
NameIncomplete TasksComplete TasksDelete
15 | 16 | {{ tasklist.name }} 17 | 18 | {{ tasklist.incomplete_tasks.count }}{{ tasklist.complete_tasks.count }} 22 |
23 | {% csrf_token %} 24 | 27 |
28 |
33 |
34 | -------------------------------------------------------------------------------- /tasker/tasker/tasks/templates/tasks/tasklist_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block content %} 5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 |
20 | {% for choice in filterset.form.completeness.field.choices %} 21 |
22 | 24 | 25 |
26 | {% endfor %} 27 |
28 |
29 |
30 |
31 | 32 |
33 | 34 | 38 | 39 | 40 | 55 |
56 |
57 | 58 |
59 | {% include "tasks/tasklist_filter.html" with object_list=object_list %} 60 |
61 | {% endblock %} 62 | 63 | {% block extrascripts %} 64 | 71 | {% endblock %} 72 | -------------------------------------------------------------------------------- /tasker/tasker/tasks/templates/tasks/tasklist_tasks.html: -------------------------------------------------------------------------------- 1 | {% for task in tasklist.tasks.all %} 2 | {% include "tasks/task_detail.html" with task=task %} 3 | {% endfor %} 4 | -------------------------------------------------------------------------------- /tasker/tasker/tasks/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from tasker.tasks.views import ( 4 | TaskListFilterView, 5 | task_create_view, 6 | task_delete_view, 7 | task_detail_view, 8 | task_edit_view, 9 | tasklist_add_task_view, 10 | tasklist_create_view, 11 | tasklist_delete_view, 12 | tasklist_detail_view, 13 | tasklist_list_view, 14 | ) 15 | 16 | urlpatterns = [ 17 | path("task//create/", task_create_view, name="task-create"), 18 | path("task//edit/", task_edit_view, name="task-edit"), 19 | path("task//delete/", task_delete_view, name="task-delete"), 20 | path("task//", task_detail_view, name="task-detail"), 21 | path("filter/", TaskListFilterView.as_view(), name="tasklist-filter"), 22 | path("create/", tasklist_create_view, name="tasklist-create"), 23 | path("/add_task/", tasklist_add_task_view, name="tasklist-add-task"), 24 | path("/delete/", tasklist_delete_view, name="tasklist-delete"), 25 | path("/", tasklist_detail_view, name="tasklist-detail"), 26 | path("", tasklist_list_view, name="tasklist-list"), 27 | ] 28 | -------------------------------------------------------------------------------- /tasker/tasker/tasks/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Any, Dict, List, cast 3 | 4 | from django.forms.models import BaseModelForm 5 | from django.http import HttpResponse, HttpResponseNotAllowed, HttpResponseRedirect 6 | from django.http.request import HttpRequest 7 | from django.shortcuts import get_object_or_404, render 8 | from django.urls import reverse 9 | from django.views.generic import CreateView, DetailView, ListView 10 | from django_filters.views import FilterView 11 | 12 | from tasker.tasks.filters import TaskListFilter 13 | from tasker.tasks.forms import TaskForm, TaskListCreateForm 14 | from tasker.tasks.models import Task, TaskList 15 | 16 | 17 | class TaskListFilterView(FilterView): 18 | filterset_class = TaskListFilter 19 | 20 | 21 | def tasklist_list_view(request): 22 | context = {} 23 | context["object_list"] = TaskList.objects.all() 24 | context["form"] = TaskListCreateForm() 25 | context["filterset"] = TaskListFilter 26 | 27 | return render(request, "tasks/tasklist_list.html", context) 28 | 29 | 30 | def tasklist_create_view(request): 31 | context = {} 32 | form = TaskListCreateForm(request.POST or None) 33 | 34 | if request.method == "POST": 35 | if form.is_valid(): 36 | task_list = form.save() 37 | response = HttpResponse() 38 | response["HX-Trigger"] = json.dumps({"redirect": {"url": task_list.get_absolute_url()}}) 39 | return response 40 | 41 | context["form"] = form 42 | return render(request, "tasks/tasklist_create_form.html", context) 43 | 44 | 45 | def tasklist_detail_view(request, slug): 46 | context = {} 47 | context["tasklist"] = TaskList.objects.get(slug=slug) 48 | 49 | return render(request, "tasks/tasklist_detail.html", context) 50 | 51 | 52 | def tasklist_add_task_view(request, slug): 53 | context = {} 54 | tasklist = TaskList.objects.get(slug=slug) 55 | 56 | if request.method == "POST": 57 | cast(TaskList, tasklist.tasks.create()) 58 | 59 | context["tasklist"] = tasklist 60 | return render(request, "tasks/tasklist_tasks.html", context) 61 | 62 | 63 | def tasklist_delete_view(request, slug): 64 | context = {} 65 | obj = get_object_or_404(TaskList, slug=slug) 66 | 67 | if request.method == "POST": 68 | obj.delete() 69 | return HttpResponse("") 70 | 71 | return HttpResponseNotAllowed( 72 | [ 73 | "POST", 74 | ] 75 | ) 76 | 77 | 78 | def task_create_view(request, id): 79 | context = {} 80 | task_list = get_object_or_404(TaskList, id=id) 81 | 82 | form = TaskForm(request.POST or None) 83 | if request.method == "POST": 84 | if form.is_valid(): 85 | form.instance.task_list = task_list 86 | form.save() 87 | return HttpResponseRedirect(reverse("task-detail", kwargs={"id": form.instance.id})) 88 | 89 | context["form"] = form 90 | context["task_list_id"] = id 91 | return render(request, "tasks/task_create_form.html", context) 92 | 93 | 94 | def task_edit_view(request, id): 95 | context = {} 96 | obj = get_object_or_404(Task, id=id) 97 | form = TaskForm(request.POST or None, instance=obj) 98 | 99 | # save the data from the form and redirect to detail_view 100 | if form.is_valid(): 101 | form.save() 102 | return HttpResponseRedirect(reverse("task-detail", kwargs={"id": obj.id})) 103 | 104 | context["form"] = form 105 | return render(request, "tasks/task_edit_form.html", context) 106 | 107 | 108 | def task_detail_view(request, id): 109 | context = {} 110 | obj = get_object_or_404(Task, id=id) 111 | context["task"] = obj 112 | 113 | return render(request, "tasks/task_detail.html", context) 114 | 115 | 116 | def task_delete_view(request, id): 117 | context = {} 118 | obj = get_object_or_404(Task, id=id) 119 | 120 | if request.method == "POST": 121 | obj.delete() 122 | return HttpResponse("") 123 | 124 | return HttpResponseNotAllowed( 125 | [ 126 | "POST", 127 | ] 128 | ) 129 | -------------------------------------------------------------------------------- /tasker/tasker/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | Task Lists 17 | 18 | 19 | 20 |
21 |
22 | 27 |
28 | {% block content %} 29 | {% endblock %} 30 |
31 | 32 | 33 | 36 | 39 | 42 | 43 | {% block extrascripts %} 44 | {% endblock %} 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /tasker/tasker/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import include, path 3 | 4 | urlpatterns = [path("admin/", admin.site.urls), path("", include("tasker.tasks.urls"))] 5 | -------------------------------------------------------------------------------- /tasker/tasker/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for tasker project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tasker.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /tasker2/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | ALLOWED_HOSTS = ["*"] 7 | 8 | 9 | def main(): 10 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tasker2.settings") 11 | try: 12 | from django.core.management import execute_from_command_line 13 | except ImportError as exc: 14 | raise ImportError( 15 | "Couldn't import Django. Are you sure it's installed and " 16 | "available on your PYTHONPATH environment variable? Did you " 17 | "forget to activate a virtual environment?" 18 | ) from exc 19 | execute_from_command_line(sys.argv) 20 | 21 | 22 | if __name__ == "__main__": 23 | main() 24 | -------------------------------------------------------------------------------- /tasker2/requirements.txt: -------------------------------------------------------------------------------- 1 | django 2 | django-filter 3 | django-crispy-forms 4 | -------------------------------------------------------------------------------- /tasker2/tasker2/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacklinke/django-htmx-todo-list/fa269e262fa50b78cc780b2ab691571780e7674b/tasker2/tasker2/__init__.py -------------------------------------------------------------------------------- /tasker2/tasker2/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for tasker2 project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.1.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.1/ref/settings/ 11 | """ 12 | import os 13 | from pathlib import Path 14 | from typing import List 15 | 16 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 17 | BASE_DIR = Path(__file__).resolve().parent.parent 18 | 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = "SECRETKEY" 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = True 28 | 29 | ALLOWED_HOSTS = ["localhost", "*"] 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | "django.contrib.admin", 35 | "django.contrib.auth", 36 | "django.contrib.contenttypes", 37 | "django.contrib.sessions", 38 | "django.contrib.messages", 39 | "django.contrib.staticfiles", 40 | "django_filters", 41 | "crispy_forms", 42 | "tasker2.tasks", 43 | ] 44 | 45 | MIDDLEWARE = [ 46 | "django.middleware.security.SecurityMiddleware", 47 | "django.contrib.sessions.middleware.SessionMiddleware", 48 | "django.middleware.common.CommonMiddleware", 49 | "django.middleware.csrf.CsrfViewMiddleware", 50 | "django.contrib.auth.middleware.AuthenticationMiddleware", 51 | "django.contrib.messages.middleware.MessageMiddleware", 52 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 53 | ] 54 | 55 | ROOT_URLCONF = "tasker2.urls" 56 | 57 | TEMPLATES = [ 58 | { 59 | "BACKEND": "django.template.backends.django.DjangoTemplates", 60 | "DIRS": [os.path.join(BASE_DIR, "tasker2/templates")], 61 | "APP_DIRS": True, 62 | "OPTIONS": { 63 | "context_processors": [ 64 | "django.template.context_processors.debug", 65 | "django.template.context_processors.request", 66 | "django.contrib.auth.context_processors.auth", 67 | "django.contrib.messages.context_processors.messages", 68 | ], 69 | }, 70 | }, 71 | ] 72 | 73 | WSGI_APPLICATION = "tasker2.wsgi.application" 74 | 75 | 76 | # Database 77 | # https://docs.djangoproject.com/en/3.1/ref/settings/#databases 78 | 79 | DATABASES = { 80 | "default": { 81 | "ENGINE": "django.db.backends.sqlite3", 82 | "NAME": "todo.sqlite", 83 | "USER": "todo", 84 | "PASSWORD": "todo", 85 | "HOST": "db", 86 | "PORT": "5432", 87 | } 88 | } 89 | 90 | 91 | # Password validation 92 | # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators 93 | 94 | AUTH_PASSWORD_VALIDATORS = [ 95 | { 96 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 97 | }, 98 | { 99 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 100 | }, 101 | { 102 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 103 | }, 104 | { 105 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 106 | }, 107 | ] 108 | 109 | 110 | # Internationalization 111 | # https://docs.djangoproject.com/en/3.1/topics/i18n/ 112 | 113 | LANGUAGE_CODE = "en-us" 114 | 115 | TIME_ZONE = "UTC" 116 | 117 | USE_I18N = True 118 | 119 | USE_L10N = True 120 | 121 | USE_TZ = True 122 | 123 | 124 | # Static files (CSS, JavaScript, Images) 125 | # https://docs.djangoproject.com/en/3.1/howto/static-files/ 126 | 127 | STATIC_URL = "/static/" 128 | STATICFILES_DIRS = [ 129 | BASE_DIR / "tasker2" / "static", 130 | ] 131 | 132 | CRISPY_TEMPLATE_PACK = "bootstrap4" 133 | 134 | 135 | DJANGO_LOG_LEVEL = DEBUG 136 | LOGGING = { 137 | "version": 1, 138 | "disable_existing_loggers": False, 139 | "formatters": { 140 | "verbose": { 141 | # 'format': '{levelname} {asctime} [{correlation_id}] {module} {process:d} {thread:d} {message}', 142 | "format": "[{levelname} {asctime} Logger: {name}, File: {filename}:{lineno}," 143 | "\n\t\t\t\t\t\t\t\t\tMessage: {message}", 144 | "style": "{", 145 | }, 146 | }, 147 | "handlers": { 148 | "console": { 149 | "level": "DEBUG", 150 | "class": "logging.StreamHandler", 151 | "formatter": "verbose", 152 | }, 153 | }, 154 | "root": { 155 | "level": "DEBUG", 156 | "handlers": ["console"], 157 | }, 158 | "loggers": { 159 | # "django.db.backends": { 160 | # "handlers": ["console"], 161 | # "level": "DEBUG", 162 | # "propagate": False, 163 | # }, 164 | "": { 165 | "level": "DEBUG", 166 | "handlers": [ 167 | "console", 168 | ], 169 | }, 170 | }, 171 | } 172 | -------------------------------------------------------------------------------- /tasker2/tasker2/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacklinke/django-htmx-todo-list/fa269e262fa50b78cc780b2ab691571780e7674b/tasker2/tasker2/static/favicon.ico -------------------------------------------------------------------------------- /tasker2/tasker2/tasks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacklinke/django-htmx-todo-list/fa269e262fa50b78cc780b2ab691571780e7674b/tasker2/tasker2/tasks/__init__.py -------------------------------------------------------------------------------- /tasker2/tasker2/tasks/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from tasker2.tasks.models import Task, TaskList 4 | 5 | 6 | @admin.register(TaskList) 7 | class TaskListAdmin(admin.ModelAdmin): 8 | list_display = ["id", "name", "slug"] 9 | 10 | 11 | @admin.register(Task) 12 | class TaskAdmin(admin.ModelAdmin): 13 | list_display = ["id", "name", "is_done"] 14 | -------------------------------------------------------------------------------- /tasker2/tasker2/tasks/filters.py: -------------------------------------------------------------------------------- 1 | import django_filters 2 | from django import forms 3 | from django.db import models 4 | from django.db.models.query import QuerySet 5 | 6 | from tasker2.tasks.models import TaskList 7 | 8 | 9 | class CompletenessChoices(models.TextChoices): 10 | ALL = "all" 11 | COMPLETE = "complete" 12 | NOT_COMPLETE = "not_complete" 13 | 14 | 15 | class TaskListFilter(django_filters.FilterSet): 16 | name = django_filters.CharFilter(lookup_expr="icontains") 17 | completeness = django_filters.ChoiceFilter( 18 | choices=CompletenessChoices.choices, 19 | widget=forms.widgets.RadioSelect, 20 | empty_label=None, 21 | method="get_completeness", 22 | ) 23 | 24 | class Meta: 25 | model = TaskList 26 | fields = ["name", "completeness"] 27 | 28 | def get_completeness(self, queryset: QuerySet[TaskList], field_name: str, value: str) -> QuerySet[TaskList]: 29 | if value == CompletenessChoices.COMPLETE: 30 | return queryset.filter(id__in=[tasklist.id for tasklist in queryset if tasklist.is_complete]) 31 | elif value == CompletenessChoices.NOT_COMPLETE: 32 | return queryset.exclude(id__in=[tasklist.id for tasklist in queryset if tasklist.is_complete]) 33 | return queryset 34 | -------------------------------------------------------------------------------- /tasker2/tasker2/tasks/forms.py: -------------------------------------------------------------------------------- 1 | from crispy_forms.helper import FormHelper 2 | from crispy_forms.layout import Column, Div, Field, Layout, Row, Submit 3 | from django import forms 4 | from django.utils.text import slugify 5 | 6 | from tasker2.tasks.models import Task, TaskList 7 | 8 | 9 | class TaskListCreateForm(forms.ModelForm): 10 | slug = forms.CharField(required=False, widget=forms.widgets.HiddenInput()) 11 | 12 | class Meta: 13 | model = TaskList 14 | fields = ("name", "slug") 15 | 16 | def clean_name(self) -> str: 17 | name: str = self.cleaned_data["name"] 18 | slug = slugify(name) 19 | if TaskList.objects.filter(slug=slug).exists(): 20 | raise forms.ValidationError(f"A Task List with the name {name} exists") 21 | return name 22 | 23 | def save(self, commit: bool = True) -> TaskList: 24 | task_list: TaskList = super().save(commit) 25 | task_list.slug = slugify(task_list.name) 26 | task_list.save() 27 | return task_list 28 | 29 | def __init__(self, *args, **kwargs): 30 | super().__init__(*args, **kwargs) 31 | self.helper = FormHelper() 32 | self.helper.layout = Layout( 33 | Field("name", css_class="col-sm-12", placeholder="Name"), 34 | ) 35 | self.helper.form_show_labels = False 36 | 37 | 38 | class TaskForm(forms.ModelForm): 39 | class Meta: 40 | model = Task 41 | fields = ("name", "is_done") 42 | 43 | def clean_name(self) -> str: 44 | name: str = self.cleaned_data["name"] 45 | if Task.objects.filter(name=name).exclude(id=self.instance.id).exists(): 46 | raise forms.ValidationError(f"A Task with the name {name} exists") 47 | return name 48 | 49 | def save(self, commit: bool = True) -> Task: 50 | task: Task = super().save(commit) 51 | task.save() 52 | return task 53 | 54 | def __init__(self, *args, **kwargs): 55 | super().__init__(*args, **kwargs) 56 | self.helper = FormHelper() 57 | self.helper.layout = Layout( 58 | Row(Column("name", css_class="col-sm-6"), Column("is_done", css_class="col-sm-6"), css_class="col-sm-8"), 59 | ) 60 | -------------------------------------------------------------------------------- /tasker2/tasker2/tasks/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.2 on 2020-11-08 00:19 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name="TaskList", 16 | fields=[ 17 | ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), 18 | ("name", models.CharField(max_length=255)), 19 | ("slug", models.CharField(max_length=255)), 20 | ], 21 | options={ 22 | "verbose_name": "Task List", 23 | "verbose_name_plural": "Task Lists", 24 | }, 25 | ), 26 | migrations.CreateModel( 27 | name="Task", 28 | fields=[ 29 | ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), 30 | ("name", models.CharField(max_length=255)), 31 | ("is_done", models.BooleanField(default=False)), 32 | ( 33 | "task_list", 34 | models.ForeignKey( 35 | on_delete=django.db.models.deletion.CASCADE, related_name="tasks", to="tasks.tasklist" 36 | ), 37 | ), 38 | ], 39 | options={ 40 | "verbose_name": "Task Item", 41 | "verbose_name_plural": "Task Items", 42 | }, 43 | ), 44 | ] 45 | -------------------------------------------------------------------------------- /tasker2/tasker2/tasks/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacklinke/django-htmx-todo-list/fa269e262fa50b78cc780b2ab691571780e7674b/tasker2/tasker2/tasks/migrations/__init__.py -------------------------------------------------------------------------------- /tasker2/tasker2/tasks/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.db.models.query import QuerySet 3 | from django.urls import reverse 4 | from django.utils.translation import ugettext_lazy as _ 5 | 6 | 7 | class TaskList(models.Model): 8 | name = models.CharField(_("Tasklist Name"), max_length=255) 9 | slug = models.CharField(_("Tasklist Slug"), max_length=255) 10 | 11 | tasks: QuerySet["Task"] 12 | 13 | class Meta: 14 | verbose_name = "Task List" 15 | verbose_name_plural = "Task Lists" 16 | 17 | def __str__(self) -> str: 18 | return self.name 19 | 20 | def get_absolute_url(self) -> str: 21 | return reverse("tasklist-detail", kwargs={"slug": self.slug}) 22 | 23 | @property 24 | def is_complete(self) -> bool: 25 | return not self.tasks.filter(is_done=False).exists() 26 | 27 | @property 28 | def complete_tasks(self) -> models.QuerySet["Task"]: 29 | return self.tasks.filter(is_done=True) 30 | 31 | @property 32 | def incomplete_tasks(self) -> models.QuerySet["Task"]: 33 | return self.tasks.filter(is_done=False) 34 | 35 | 36 | class Task(models.Model): 37 | task_list = models.ForeignKey( 38 | TaskList, 39 | related_name="tasks", 40 | on_delete=models.CASCADE, 41 | ) 42 | name = models.CharField(_("Task Name"), max_length=255) 43 | is_done = models.BooleanField(_("Task Is Done"), default=False) 44 | 45 | class Meta: 46 | verbose_name = "Task Item" 47 | verbose_name_plural = "Task Items" 48 | 49 | def __str__(self) -> str: 50 | return self.name 51 | -------------------------------------------------------------------------------- /tasker2/tasker2/tasks/templates/tasks/task_create_form.html: -------------------------------------------------------------------------------- 1 | {% load crispy_forms_tags %} 2 | 3 | 4 | 5 |
6 |
7 | {% crispy form %} 8 |
9 | 10 | 11 |
12 |
13 | {% if form.name.errors %}{{ form.name.errors }}{% endif %} 14 | {% if form.is_done.errors %}{{ form.is_done.errors }}{% endif %} 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /tasker2/tasker2/tasks/templates/tasks/task_detail.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 | Task name: {{ task.name }} 6 |
7 |
8 | 9 | 12 |
13 |
14 |
15 | 18 |
19 | {% csrf_token %} 20 | 23 |
24 |
25 |
26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /tasker2/tasker2/tasks/templates/tasks/task_edit_form.html: -------------------------------------------------------------------------------- 1 | {% load crispy_forms_tags %} 2 | 3 | 4 | 5 |
6 |
7 | {% crispy form %} 8 |
9 | 10 | 11 |
12 |
13 | {% if form.name.errors %}{{ form.name.errors }}{% endif %} 14 | {% if form.is_done.errors %}{{ form.is_done.errors }}{% endif %} 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /tasker2/tasker2/tasks/templates/tasks/tasklist_create_form.html: -------------------------------------------------------------------------------- 1 | {% load crispy_forms_tags %} 2 | 3 |
4 |
5 |
6 |
7 |
8 | {% crispy form %} 9 |
10 | {{ form.name.errors }} 11 |
12 |
13 |
14 |
15 |
16 | 18 |
19 |
20 |
21 |
22 |
23 | -------------------------------------------------------------------------------- /tasker2/tasker2/tasks/templates/tasks/tasklist_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {% include "tasks/tasklist_tasks.html" with tasklist=tasklist %} 13 | 14 |
{{ tasklist.name }}
15 |
16 | 20 |
21 |
22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /tasker2/tasker2/tasks/templates/tasks/tasklist_filter.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {% for tasklist in object_list %} 13 | 14 | 19 | 20 | 21 | 29 | 30 | {% endfor %} 31 | 32 |
NameIncomplete TasksComplete TasksDelete
15 | 16 | {{ tasklist.name }} 17 | 18 | {{ tasklist.incomplete_tasks.count }}{{ tasklist.complete_tasks.count }} 22 |
23 | {% csrf_token %} 24 | 27 |
28 |
33 |
34 | -------------------------------------------------------------------------------- /tasker2/tasker2/tasks/templates/tasks/tasklist_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block content %} 5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 |
20 | {% for choice in filterset.form.completeness.field.choices %} 21 |
22 | 24 | 25 |
26 | {% endfor %} 27 |
28 |
29 |
30 |
31 | 32 |
33 | 34 | 38 | 39 | 40 | 55 |
56 |
57 | 58 |
59 | {% include "tasks/tasklist_filter.html" with object_list=object_list %} 60 |
61 | {% endblock %} 62 | 63 | {% block extrascripts %} 64 | 71 | {% endblock %} 72 | -------------------------------------------------------------------------------- /tasker2/tasker2/tasks/templates/tasks/tasklist_tasks.html: -------------------------------------------------------------------------------- 1 | {% for task in tasklist.tasks.all %} 2 | {% include "tasks/task_detail.html" with task=task %} 3 | {% endfor %} 4 | -------------------------------------------------------------------------------- /tasker2/tasker2/tasks/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from tasker2.tasks.views import ( 4 | TaskListFilterView, 5 | task_create_view, 6 | task_delete_view, 7 | task_detail_view, 8 | task_edit_view, 9 | tasklist_add_task_view, 10 | tasklist_create_view, 11 | tasklist_delete_view, 12 | tasklist_detail_view, 13 | tasklist_list_view, 14 | ) 15 | 16 | urlpatterns = [ 17 | path("task//create/", task_create_view, name="task-create"), 18 | path("task//edit/", task_edit_view, name="task-edit"), 19 | path("task//delete/", task_delete_view, name="task-delete"), 20 | path("task//", task_detail_view, name="task-detail"), 21 | path("filter/", TaskListFilterView.as_view(), name="tasklist-filter"), 22 | path("create/", tasklist_create_view, name="tasklist-create"), 23 | path("/add_task/", tasklist_add_task_view, name="tasklist-add-task"), 24 | path("/delete/", tasklist_delete_view, name="tasklist-delete"), 25 | path("/", tasklist_detail_view, name="tasklist-detail"), 26 | path("", tasklist_list_view, name="tasklist-list"), 27 | ] 28 | -------------------------------------------------------------------------------- /tasker2/tasker2/tasks/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | from typing import Any, Dict, List, cast 4 | 5 | from django.forms.models import BaseModelForm 6 | from django.http import HttpResponse, HttpResponseNotAllowed, HttpResponseRedirect 7 | from django.http.request import HttpRequest 8 | from django.shortcuts import get_object_or_404, render 9 | from django.urls import reverse 10 | from django.views.generic import CreateView, DetailView, ListView 11 | from django_filters.views import FilterView 12 | 13 | from tasker2.tasks.filters import TaskListFilter 14 | from tasker2.tasks.forms import TaskForm, TaskListCreateForm 15 | from tasker2.tasks.models import Task, TaskList 16 | 17 | logger = logging.getLogger("tasker") 18 | 19 | 20 | class TaskListFilterView(FilterView): 21 | filterset_class = TaskListFilter 22 | 23 | 24 | def tasklist_list_view(request): 25 | context = {} 26 | context["object_list"] = TaskList.objects.all() 27 | context["form"] = TaskListCreateForm() 28 | context["filterset"] = TaskListFilter 29 | 30 | return render(request, "tasks/tasklist_list.html", context) 31 | 32 | 33 | def tasklist_create_view(request): 34 | context = {} 35 | form = TaskListCreateForm(request.POST or None) 36 | 37 | logger.debug(request.method) 38 | 39 | if request.method == "POST": 40 | logger.debug(request.POST) 41 | if form.is_valid(): 42 | task_list = form.save() 43 | response = HttpResponse() 44 | response["HX-Trigger"] = json.dumps({"redirect": {"url": task_list.get_absolute_url()}}) 45 | return response 46 | 47 | context["form"] = form 48 | return render(request, "tasks/tasklist_create_form.html", context) 49 | 50 | 51 | def tasklist_detail_view(request, slug): 52 | context = {} 53 | context["tasklist"] = TaskList.objects.get(slug=slug) 54 | 55 | return render(request, "tasks/tasklist_detail.html", context) 56 | 57 | 58 | def tasklist_add_task_view(request, slug): 59 | context = {} 60 | tasklist = TaskList.objects.get(slug=slug) 61 | 62 | if request.method == "POST": 63 | cast(TaskList, tasklist.tasks.create()) 64 | 65 | context["tasklist"] = tasklist 66 | return render(request, "tasks/tasklist_tasks.html", context) 67 | 68 | 69 | def tasklist_delete_view(request, slug): 70 | context = {} 71 | obj = get_object_or_404(TaskList, slug=slug) 72 | 73 | if request.method == "POST": 74 | obj.delete() 75 | return HttpResponse("") 76 | 77 | return HttpResponseNotAllowed( 78 | [ 79 | "POST", 80 | ] 81 | ) 82 | 83 | 84 | def task_create_view(request, id): 85 | context = {} 86 | task_list = get_object_or_404(TaskList, id=id) 87 | 88 | form = TaskForm(request.POST or None) 89 | if request.method == "POST": 90 | if form.is_valid(): 91 | form.instance.task_list = task_list 92 | form.save() 93 | return HttpResponseRedirect(reverse("task-detail", kwargs={"id": form.instance.id})) 94 | 95 | context["form"] = form 96 | context["task_list_id"] = id 97 | return render(request, "tasks/task_create_form.html", context) 98 | 99 | 100 | def task_edit_view(request, id): 101 | context = {} 102 | obj = get_object_or_404(Task, id=id) 103 | form = TaskForm(request.POST or None, instance=obj) 104 | 105 | # save the data from the form and redirect to detail_view 106 | if form.is_valid(): 107 | form.save() 108 | return HttpResponseRedirect(reverse("task-detail", kwargs={"id": obj.id})) 109 | 110 | context["form"] = form 111 | return render(request, "tasks/task_edit_form.html", context) 112 | 113 | 114 | def task_detail_view(request, id): 115 | context = {} 116 | obj = get_object_or_404(Task, id=id) 117 | context["task"] = obj 118 | 119 | return render(request, "tasks/task_detail.html", context) 120 | 121 | 122 | def task_delete_view(request, id): 123 | context = {} 124 | obj = get_object_or_404(Task, id=id) 125 | 126 | if request.method == "POST": 127 | obj.delete() 128 | return HttpResponse("") 129 | 130 | return HttpResponseNotAllowed( 131 | [ 132 | "POST", 133 | ] 134 | ) 135 | -------------------------------------------------------------------------------- /tasker2/tasker2/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | Task Lists 17 | 18 | 19 | 20 |
21 |
22 | 27 |
28 | {% block content %} 29 | {% endblock %} 30 |
31 | 32 | 33 | 34 | 37 | 40 | 43 | 44 | {% block extrascripts %} 45 | {% endblock %} 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /tasker2/tasker2/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import include, path 3 | 4 | urlpatterns = [path("admin/", admin.site.urls), path("", include("tasker2.tasks.urls"))] 5 | -------------------------------------------------------------------------------- /tasker2/tasker2/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for tasker project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tasker2.settings") 15 | 16 | application = get_wsgi_application() 17 | --------------------------------------------------------------------------------