├── 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 | 
22 |
23 | ### Adding a new TaskList
24 |
25 | 
26 |
27 | ### The newly created TaskList
28 |
29 | 
30 |
31 | ### Adding Task instances
32 |
33 | 
34 |
35 | ### Editing existing Task instances
36 |
37 | 
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 |
26 | |
27 |
28 |
--------------------------------------------------------------------------------
/tasker/tasker/tasks/templates/tasks/task_detail.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
27 | |
28 |
--------------------------------------------------------------------------------
/tasker/tasker/tasks/templates/tasks/task_edit_form.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
26 | |
27 |
28 |
--------------------------------------------------------------------------------
/tasker/tasker/tasks/templates/tasks/tasklist_create_form.html:
--------------------------------------------------------------------------------
1 |
21 |
--------------------------------------------------------------------------------
/tasker/tasker/tasks/templates/tasks/tasklist_detail.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block content %}
4 |
5 |
6 |
7 |
8 | {{ tasklist.name }} |
9 |
10 |
11 |
12 | {% include "tasks/tasklist_tasks.html" with tasklist=tasklist %}
13 |
14 |
15 |
16 |
20 |
21 |
22 | {% endblock %}
23 |
--------------------------------------------------------------------------------
/tasker/tasker/tasks/templates/tasks/tasklist_filter.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Name |
6 | Incomplete Tasks |
7 | Complete Tasks |
8 | Delete |
9 |
10 |
11 |
12 | {% for tasklist in object_list %}
13 |
14 |
15 |
16 | {{ tasklist.name }}
17 |
18 | |
19 | {{ tasklist.incomplete_tasks.count }} |
20 | {{ tasklist.complete_tasks.count }} |
21 |
22 |
28 | |
29 |
30 | {% endfor %}
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/tasker/tasker/tasks/templates/tasks/tasklist_list.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 |
4 | {% block content %}
5 |
6 |
31 |
32 |
33 |
34 |
38 |
39 |
40 |
41 |
42 |
43 |
49 |
50 | {% include "tasks/tasklist_create_form.html" %}
51 |
52 |
53 |
54 |
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 |
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 |
16 | |
17 |
18 |
--------------------------------------------------------------------------------
/tasker2/tasker2/tasks/templates/tasks/task_detail.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
27 | |
28 |
29 |
--------------------------------------------------------------------------------
/tasker2/tasker2/tasks/templates/tasks/task_edit_form.html:
--------------------------------------------------------------------------------
1 | {% load crispy_forms_tags %}
2 |
3 |
4 |
5 |
16 | |
17 |
18 |
--------------------------------------------------------------------------------
/tasker2/tasker2/tasks/templates/tasks/tasklist_create_form.html:
--------------------------------------------------------------------------------
1 | {% load crispy_forms_tags %}
2 |
3 |
23 |
--------------------------------------------------------------------------------
/tasker2/tasker2/tasks/templates/tasks/tasklist_detail.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block content %}
4 |
5 |
6 |
7 |
8 | {{ tasklist.name }} |
9 |
10 |
11 |
12 | {% include "tasks/tasklist_tasks.html" with tasklist=tasklist %}
13 |
14 |
15 |
16 |
20 |
21 |
22 | {% endblock %}
23 |
--------------------------------------------------------------------------------
/tasker2/tasker2/tasks/templates/tasks/tasklist_filter.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Name |
6 | Incomplete Tasks |
7 | Complete Tasks |
8 | Delete |
9 |
10 |
11 |
12 | {% for tasklist in object_list %}
13 |
14 |
15 |
16 | {{ tasklist.name }}
17 |
18 | |
19 | {{ tasklist.incomplete_tasks.count }} |
20 | {{ tasklist.complete_tasks.count }} |
21 |
22 |
28 | |
29 |
30 | {% endfor %}
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/tasker2/tasker2/tasks/templates/tasks/tasklist_list.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 |
4 | {% block content %}
5 |
6 |
31 |
32 |
33 |
34 |
38 |
39 |
40 |
41 |
42 |
43 |
49 |
50 | {% include "tasks/tasklist_create_form.html" %}
51 |
52 |
53 |
54 |
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 |
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 |
--------------------------------------------------------------------------------