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 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.
+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'),
} }
} }
+20
View File
@@ -25,3 +25,23 @@ 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()
-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('', 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>/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.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,39 @@ def task_item(request, id):
return render(request, "partials/task_item.html", context) 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"> <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>
+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" %} {% 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 class="font-semibold">{{ task.name }}</p>
<p>{{ task.description | truncatewords:10 }}</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> </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>