Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de611a5a14 | ||
|
|
bc585a1c60 | ||
|
|
b1e9c7e713 | ||
|
|
9f6934d076 | ||
|
|
f18912bed4 | ||
|
|
31c8770439 | ||
|
|
1711ef1d50 | ||
|
|
4fed486a58 | ||
|
|
5e202fb29c | ||
|
|
a929846d13 |
+44
@@ -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
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
DEBUG=True
|
||||
SECRET_KEY='your-secure-key'
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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'),
|
||||
]
|
||||
|
||||
|
||||
@@ -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" ])
|
||||
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
{{ task_form|crispy }}
|
||||
{% 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user