Compare commits
10
Commits
c66712d0f4
...
de611a5a14
| 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 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,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
|
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
|
||||||
|
|||||||
@@ -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,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()
|
||||||
|
|
||||||
|
|||||||
@@ -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('', 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'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -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" ])
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user