Compare commits

..
10 Commits
16 changed files with 248 additions and 12 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
+19
View File
@@ -15,4 +15,23 @@
Add the htmx (CDN version).
Add the task form.
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.
+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
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
+6 -2
View File
@@ -75,8 +75,12 @@ WSGI_APPLICATION = "simple_todo_list.wsgi.application"
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
"ENGINE": "django.db.backends.postgresql",
'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'),
}
}
+20
View File
@@ -25,3 +25,23 @@ class Task(models.Model):
def __str__(self):
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()
-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, Tasks
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")
+5
View File
@@ -9,5 +9,10 @@ urlpatterns = [
path('', views.index, name='index'),
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>/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'),
]
+37
View File
@@ -1,4 +1,5 @@
from django.shortcuts import render, redirect, get_object_or_404
from django.http.response import HttpResponse, HttpResponseNotAllowed
from .models import Task
from .forms import TaskForm
@@ -42,3 +43,39 @@ def task_item(request, id):
return render(request, "partials/task_item.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" ])
+6 -1
View File
@@ -4,6 +4,11 @@
<form method="POST">
{% csrf_token %}
{{ 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>
</div>
+25 -4
View File
@@ -1,13 +1,34 @@
<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" %}
<p class="font-semibold text-green-500">
<p class="font-semibold text-green-500"
{% 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" %}
<p class="font-semibold text-gray-500">
<p class="font-semibold text-gray-500"
{% endif %}
hx-post="{% url 'tasks:task-set-circular-next-status' task.id %}"
hx-swap="outerHTML">
{{ task.status }}
</p>
<p class="font-semibold">{{ task.name }}</p>
<p>{{ task.description | truncatewords:10 }}</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>
+8 -1
View File
@@ -4,7 +4,14 @@
{% block content %}
<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
</button>