Compare commits

...
12 Commits
Author SHA1 Message Date
KKlochko 2ccd9edc03 Add the detailed item for a Task model.
continuous-integration/drone/push Build is passing
2023-07-25 15:31:39 +03:00
KKlochko 4f648a1389 Add the Tasks model to manage tasks.
continuous-integration/drone/push Build is passing
2023-07-24 15:02:23 +03:00
KKlochko de611a5a14 Add the CI/CD configuration. 2023-07-24 14:50:16 +03:00
KKlochko bc585a1c60 Update the database to use Postgres. 2023-07-24 13:24:35 +03:00
KKlochko b1e9c7e713 Add the action to set the circular next status for Task items. 2023-07-23 19:20:57 +03:00
KKlochko 9f6934d076 Add the tests for circular next methods. 2023-07-23 19:15:33 +03:00
KKlochko f18912bed4 Add the circular next methods for Task. 2023-07-23 19:14:38 +03:00
KKlochko 31c8770439 Update the style of buttons. 2023-07-22 13:47:13 +03:00
KKlochko 1711ef1d50 Add right alignment for buttons in a task item. 2023-07-21 15:21:26 +03:00
KKlochko 4fed486a58 Add the cancel button for a task form. 2023-07-21 15:20:44 +03:00
KKlochko 5e202fb29c Add the update button for a task. 2023-07-20 19:28:44 +03:00
KKlochko a929846d13 Add the delete button for a task. 2023-07-17 19:25:47 +03:00
18 changed files with 361 additions and 14 deletions
+44
View File
@@ -0,0 +1,44 @@
kind: pipeline
type: docker
name: default
steps:
- name: install dependencies
image: python:3.10-slim
volumes:
- name: package_cache
path: /package_cache
commands:
- cp .env.example .env
- pip install -r dev_requirements.txt --cache-dir=/package_cache
- name: run migrations
image: python:3.10-slim
volumes:
- name: package_cache
path: /package_cache
commands:
- pip install -r dev_requirements.txt --cache-dir=/package_cache
- python manage.py migrate
- name: run tests
image: python:3.10-slim
volumes:
- name: package_cache
path: /package_cache
commands:
- pip install -r dev_requirements.txt --cache-dir=/package_cache
- python manage.py test
volumes:
- name: package_cache
temp: {}
services:
- name: postgres
image: postgres:15-alpine
environment:
POSTGRES_DB: postgres_dev
POSTGRES_USER: postgres
POSTGRES_PASSWORD: testpassword
-2
View File
@@ -1,2 +0,0 @@
DEBUG=True
SECRET_KEY='your-secure-key'
+9
View File
@@ -0,0 +1,9 @@
DEBUG=True
SECRET_KEY='your-secure-key'
DATABASE_NAME=postgres_dev
DATABASE_USERNAME=postgres
DATABASE_PASSWORD=testpassword
DATABASE_HOST=postgres
DATABASE_PORT=5432
+23
View File
@@ -15,4 +15,27 @@
Add the htmx (CDN version). Add the htmx (CDN version).
Add the task form. Add the task form.
Add logic to swap the form with the task via htmx. Add logic to swap the form with the task via htmx.
** 0.3.1 <2023-07-17 Mon>
Add the delete button for a task.
** 0.3.2 <2023-07-20 Thu>
Add the update button for a task.
** 0.3.3 <2023-07-21 Fri>
Add the cancel button for a task form.
** 0.3.4 <2023-07-21 Fri>
Add right alignment for buttons in a task item.
** 0.3.5 <2023-07-22 Sat>
Update the style of buttons.
** 0.3.6 <2023-07-23 Sun>
Add the circular next methods for Task.
Add the tests for circular next methods.
** 0.3.7 <2023-07-23 Sun>
Add the action to set the circular next status for Task items.
** 0.3.8 <2023-07-24 Mon>
Update the database to use Postgres.
** 0.3.9 <2023-07-24 Mon>
Add the CI/CD configuration.
** 0.3.10 <2023-07-24 Mon>
Add the Tasks model to manage tasks.
** 0.4.0 <2023-07-24 Mon>
Add the detailed item for a Task model.
+12
View File
@@ -0,0 +1,12 @@
asgiref==3.7.2
crispy-tailwind==0.5.0
Django==4.2.3
django-browser-reload==1.11.0
django-crispy-forms==2.0
django-tailwind==3.6.0
gunicorn==21.2.0
packaging==23.1
psycopg2-binary==2.9.6
python-dotenv==1.0.0
sqlparse==0.4.4
typing_extensions==4.7.1
+4
View File
@@ -1,8 +1,12 @@
asgiref==3.7.2 asgiref==3.7.2
crispy-tailwind==0.5.0 crispy-tailwind==0.5.0
Django==4.2.3 Django==4.2.3
django-browser-reload==1.11.0
django-crispy-forms==2.0 django-crispy-forms==2.0
django-tailwind==3.6.0 django-tailwind==3.6.0
gunicorn==21.2.0
packaging==23.1
psycopg2-binary==2.9.6
python-dotenv==1.0.0 python-dotenv==1.0.0
sqlparse==0.4.4 sqlparse==0.4.4
typing_extensions==4.7.1 typing_extensions==4.7.1
+6 -2
View File
@@ -75,8 +75,12 @@ WSGI_APPLICATION = "simple_todo_list.wsgi.application"
DATABASES = { DATABASES = {
"default": { "default": {
"ENGINE": "django.db.backends.sqlite3", "ENGINE": "django.db.backends.postgresql",
"NAME": BASE_DIR / "db.sqlite3", 'NAME': os.environ.get('DATABASE_NAME'),
'USER': os.environ.get('DATABASE_USERNAME'),
'PASSWORD': os.environ.get('DATABASE_PASSWORD'),
'HOST': os.environ.get('DATABASE_HOST'),
'PORT': os.environ.get('DATABASE_PORT'),
} }
} }
+25
View File
@@ -25,3 +25,28 @@ class Task(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
@staticmethod
def get_statuses():
return list(map(lambda _: _[1], Task.STATUSES))
def _get_status_index(self):
return Task.get_statuses().index(self.status)
def circular_next_status_index(self):
current_index = self._get_status_index()
next_index = (current_index + 1) % len(Task.STATUSES)
return next_index
def circular_next_status(self):
next_index = self.circular_next_status_index()
return Task.get_statuses()[next_index]
def set_circular_next_status(self):
self.status = self.circular_next_status()
self.save()
class Tasks:
@staticmethod
def get_count_by_status(status: str):
return Task.objects.filter(status=status).count()
-2
View File
@@ -1,2 +0,0 @@
from django.test import TestCase
View File
+53
View File
@@ -0,0 +1,53 @@
from django.test import TestCase
from tasks.models import Task
class TaskTestCase(TestCase):
def setUp(self):
statuses = ["TODO", "DOING", "DONE", "CANCELED"]
names = [f"Test Task {i}" for i in statuses]
description = ""
tasks = [
Task(name=name, description=description, status=status)
for name, status in zip(names, statuses)
]
Task.objects.bulk_create(tasks)
def test__get_status_index_with_existing(self):
task = Task.objects.filter(status="DONE").first()
index = task._get_status_index()
self.assertEqual(index, 2)
def test_circular_next_status_index_for_todo(self):
task = Task.objects.filter(status="TODO").first()
index = task.circular_next_status_index()
self.assertEqual(index, 1)
def test_circular_next_status_index_for_canceled(self):
task = Task.objects.filter(status="CANCELED").first()
next_index = task.circular_next_status_index()
self.assertEqual(next_index, 0)
def test_circular_next_status_for_todo(self):
task = Task.objects.filter(status="TODO").first()
next_status = task.circular_next_status()
self.assertEqual(next_status, "DOING")
def test_circular_next_status_for_canceled(self):
task = Task.objects.filter(status="CANCELED").first()
next_status = task.circular_next_status()
self.assertEqual(next_status, "TODO")
def test_set_circular_next_status_for_todo(self):
task = Task.objects.filter(status="TODO").first()
task.set_circular_next_status()
task.refresh_from_db()
self.assertEqual(task.status, "DOING")
def test_set_circular_next_status_for_canceled(self):
task = Task.objects.filter(status="CANCELED").first()
task.set_circular_next_status()
task.refresh_from_db()
self.assertEqual(task.status, "TODO")
+29
View File
@@ -0,0 +1,29 @@
from django.test import TestCase
from tasks.models import Task, Tasks
class TasksTestCase(TestCase):
todo_count = 2
doing_count = 3
done_count = 4
cancel_count = 1
def setUp(self):
names = [f"Test Task {i}" for i in range(10)]
description = ""
statuses = ["TODO"] * self.todo_count
statuses += ["DOING"] * self.doing_count
statuses += ["DONE"] * self.done_count
statuses += ["CANCELED"] * self.cancel_count
tasks = [
Task(name=name, description=description, status=status)
for name, status in zip(names, statuses)
]
Task.objects.bulk_create(tasks)
def test_done_count(self):
count = Tasks.get_count_by_status("DONE")
self.assertEqual(count, self.done_count)
+6
View File
@@ -9,5 +9,11 @@ urlpatterns = [
path('', views.index, name='index'), path('', views.index, name='index'),
path('htmx/create-task-form/', views.create_task_form, name='create-task-form'), path('htmx/create-task-form/', views.create_task_form, name='create-task-form'),
path('htmx/task-item/<id>/', views.task_item, name='task-item'), path('htmx/task-item/<id>/', views.task_item, name='task-item'),
path('htmx/task-item/<id>/detailed/', views.task_detailed, name='task-detailed'),
path('htmx/task-item/<id>/update/', views.task_update, name='task-update'),
path('htmx/task-item/<id>/set-circular-next-status/',
views.task_set_circular_next_status,
name='task-set-circular-next-status'),
path('htmx/task-item/<id>/delete/', views.task_delete, name='task-delete'),
] ]
+46
View File
@@ -1,4 +1,5 @@
from django.shortcuts import render, redirect, get_object_or_404 from django.shortcuts import render, redirect, get_object_or_404
from django.http.response import HttpResponse, HttpResponseNotAllowed
from .models import Task from .models import Task
from .forms import TaskForm from .forms import TaskForm
@@ -42,3 +43,48 @@ def task_item(request, id):
return render(request, "partials/task_item.html", context) return render(request, "partials/task_item.html", context)
def task_detailed(request, id):
task = get_object_or_404(Task, id=id)
context = {
"task": task
}
return render(request, "partials/task_detailed.html", context)
def task_update(request, id):
task = get_object_or_404(Task, id=id)
task_form = TaskForm(request.POST or None, instance=task)
if request.method == "POST":
if task_form.is_valid():
task_form.save()
return redirect("tasks:task-item", id=task.id)
context = {
'task': task,
'task_form': task_form,
}
return render(request, "partials/task_form.html", context)
def task_set_circular_next_status(request, id):
task = get_object_or_404(Task, id=id)
if request.method == "POST":
task.set_circular_next_status()
context = {
'task': task,
}
return render(request, "partials/task_item.html", context)
def task_delete(request, id):
task = get_object_or_404(Task, id=id)
if request.method == "POST":
task.delete()
return HttpResponse("")
return HttpResponseNotAllowed([ "POST" ])
+57
View File
@@ -0,0 +1,57 @@
<div hx-target="this" class="p-2 border rounded-lg inline-flex gap-2 my-2 bg-gray-200">
<div class="grid grid-cols-3 gap-2 justify-center w-full max-w-2xl mx-auto">
<div class="col-span-3">
<dl class="row flex flex-inline">
<dt class="col-sm-3 font-bold mr-2">Status:</dt>
{% if task.status == "TODO" %}
<dd class="font-semibold text-green-500"
{% elif task.status == "DOING" %}
<dd class="font-semibold text-yellow-500"
{% elif task.status == "DONE" or task.status == "CANCELED" %}
<dd class="font-semibold text-gray-500"
{% endif %}
>
{{ task.status }}
</dd>
</dl>
<dl class="row flex flex-inline flex-wrap">
<dt class="col-sm-3 font-bold mr-2">Name:</dt>
<dd class="col-sm-9 whitespace-pre-line">{{ task.name }}</dd>
</dl>
<dl class="flex flex-col flex-inline">
<dt class="col-sm-3 font-bold mr-2">Description:</dt>
<dd class="col-sm-9 whitespace-pre-line">{{ task.description }}</dd>
</dl>
</div>
<button type="button"
hx-post="{% url 'tasks:task-item' task.id %}"
hx-swap="outerHTML"
class="inline-block px-5 py-1.5 bg-green-600 text-white font-medium text-xs leading-tight
uppercase rounded shadow-md hover:bg-green-700 hover:shadow-lg focus:bg-green-700
focus:shadow-lg focus:outline-none focus:ring-0 active:bg-green-800 active:shadow-lg
transition duration-150 ease-in-out">
Back
</button>
<button type="button"
hx-post="{% url 'tasks:task-update' task.id %}"
hx-swap="outerHTML"
class="inline-block px-5 py-1.5 bg-blue-600 text-white font-medium text-xs leading-tight
uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg
transition duration-150 ease-in-out">
Update
</button>
<button type="button"
hx-post="{% url 'tasks:task-delete' task.id %}"
hx-swap="outerHTML"
class="inline-block px-5 py-1.5 bg-red-600 text-white font-medium text-xs leading-tight
uppercase rounded shadow-md hover:bg-red-700 hover:shadow-lg focus:bg-red-700
focus:shadow-lg focus:outline-none focus:ring-0 active:bg-red-800 active:shadow-lg
transition duration-150 ease-in-out">
Delete
</button>
</div>
</div>
+6 -1
View File
@@ -4,6 +4,11 @@
<form method="POST"> <form method="POST">
{% csrf_token %} {% csrf_token %}
{{ task_form|crispy }} {{ task_form|crispy }}
<button type="submit" hx-post=".">Submit</button> {% if task %}
<button type="submit" hx-post="{% url 'tasks:task-update' task.id %}">Submit</button>
<button hx-post="{% url 'tasks:task-item' task.id %}">Cancel</button>
{% else %}
<button type="submit" hx-post=".">Submit</button>
{% endif %}
</form> </form>
</div> </div>
+33 -6
View File
@@ -1,13 +1,40 @@
<div class="p-2 border rounded-lg inline-flex gap-2 my-2 bg-gray-200"> <div hx-target="this" class="p-2 border rounded-lg inline-flex gap-2 my-2 bg-gray-200">
{% if task.status == "TODO" %} {% if task.status == "TODO" %}
<p class="font-semibold text-green-500"> <p class="font-semibold text-green-500"
{% elif task.status == "DOING" %} {% elif task.status == "DOING" %}
<p class="font-semibold text-yellow-500"> <p class="font-semibold text-yellow-500"
{% elif task.status == "DONE" or task.status == "CANCELED" %} {% elif task.status == "DONE" or task.status == "CANCELED" %}
<p class="font-semibold text-gray-500"> <p class="font-semibold text-gray-500"
{% endif %} {% endif %}
hx-post="{% url 'tasks:task-set-circular-next-status' task.id %}"
hx-swap="outerHTML">
{{ task.status }} {{ task.status }}
</p> </p>
<p class="font-semibold">{{ task.name }}</p>
<p>{{ task.description | truncatewords:10 }}</p> <p class="font-semibold"
hx-post="{% url 'tasks:task-detailed' task.id %}"
hx-swap="outerHTML">
{{ task.name }}
</p>
<p>{{ task.description | truncatechars:40 }}</p>
<button type="button"
hx-post="{% url 'tasks:task-update' task.id %}"
hx-swap="outerHTML"
class="inline-block px-5 py-1.5 bg-blue-600 text-white font-medium text-xs leading-tight
uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg
transition duration-150 ease-in-out ml-auto">
Update
</button>
<button type="button"
hx-post="{% url 'tasks:task-delete' task.id %}"
hx-swap="outerHTML"
class="inline-block px-5 py-1.5 bg-red-600 text-white font-medium text-xs leading-tight
uppercase rounded shadow-md hover:bg-red-700 hover:shadow-lg focus:bg-red-700
focus:shadow-lg focus:outline-none focus:ring-0 active:bg-red-800 active:shadow-lg
transition duration-150 ease-in-out">
Delete
</button>
</div> </div>
+8 -1
View File
@@ -4,7 +4,14 @@
{% block content %} {% block content %}
<h1 class="text-3xl font-bold text-center my-2">Tasks</h1> <h1 class="text-3xl font-bold text-center my-2">Tasks</h1>
<button type="button" hx-get="{% url 'tasks:create-task-form' %}" hx-target="#task-items" hx-swap="beforeend"> <button type="button"
hx-get="{% url 'tasks:create-task-form' %}"
hx-target="#task-items"
hx-swap="beforeend"
class="inline-block px-14 py-2.5 bg-green-600 text-white font-medium text-xs leading-tight
uppercase rounded shadow-md hover:bg-green-700 hover:shadow-lg focus:bg-green-700
focus:shadow-lg focus:outline-none focus:ring-0 active:bg-green-800 active:shadow-lg
transition duration-150 ease-in-out mx-auto">
Add a new task Add a new task
</button> </button>