finish todolist app and adjust docker envoirnment

This commit is contained in:
rafa 2025-10-22 13:15:42 +01:00
parent 039dcd98f5
commit 1b9fcf76b7
17 changed files with 352 additions and 9 deletions

3
backend/.gitignore vendored
View File

@ -4,3 +4,6 @@
**/__pycache__/**
**/migrations/**
node_modules/
package-lock.json

View File

@ -2,6 +2,11 @@
FROM python:3.13-bookworm
RUN useradd -m django
RUN apt update -y && apt upgrade -y
RUN wget https://nodejs.org/dist/v22.21.0/node-v22.21.0-linux-x64.tar.xz
RUN tar -xvf node-v22.21.0-linux-x64.tar.xz
RUN cp -r node-v22.21.0-linux-x64/* /usr/local
WORKDIR /app
@ -12,4 +17,4 @@ COPY --chown=django . .
USER django:django
CMD ["python", "manage.py", "runserver" , "0.0.0.0:8000"]
ENTRYPOINT ["/bin/bash", "entrypoint.sh"]

View File

@ -56,7 +56,7 @@ ROOT_URLCONF = 'backend.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'DIRS': [ BASE_DIR / 'templates' ],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
@ -121,6 +121,9 @@ USE_TZ = True
# https://docs.djangoproject.com/en/5.2/howto/static-files/
STATIC_URL = 'static/'
STATICFILES_DIRS = [
BASE_DIR / "node_modules",
]
# Default primary key field type
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field

View File

@ -16,8 +16,11 @@ Including another URLconf
"""
from django.contrib import admin
from django.urls import path, include
from django.conf.urls.static import static
from django.conf import settings
urlpatterns = [
path("todolist/", include("todolist.urls")),
path("", include("todolist.urls")),
path('admin/', admin.site.urls),
]
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

7
backend/entrypoint.sh Normal file
View File

@ -0,0 +1,7 @@
#!/bin/bash
npm install
python manage.py makemigrations todolist
python manage.py migrate
python manage.py runserver 0.0.0.0:8000

6
backend/package.json Normal file
View File

@ -0,0 +1,6 @@
{
"dependencies": {
"bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.1"
}
}

View File

@ -0,0 +1,41 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{% static 'bootstrap/dist/css/bootstrap.min.css'%}">
<link rel="stylesheet" href="{% static 'bootstrap-icons/font/bootstrap-icons.min.css'%}">
{% block extra_css %}{% endblock %}
<title>{% block title %}Todo List{% endblock %}</title>
</head>
<body>
<div id="content">
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container">
<a class="navbar-brand" href="/">Todo List</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse justify-content-end" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="{% url 'list' %}">List</a>
</li>
<li class="nav-item">
<a class="nav-link" aria-current="page" href="{% url 'create' %}">Create Todo</a>
</li>
</ul>
</div>
</div>
</nav>
{% block content %}{% endblock %}
</div>
<script src="{% static 'bootstrap/dist/js/bootstrap.min.js'%}"></script>
{% block extra_js %}{% endblock %}
</body>
</html>

View File

@ -1,3 +1,6 @@
from django.contrib import admin
from .models import Todo
# Register your models here.
admin.site.register(Todo)

13
backend/todolist/forms.py Normal file
View File

@ -0,0 +1,13 @@
from django.forms import ModelForm
from .models import Todo
class TodoForm(ModelForm):
class Meta:
model = Todo
fields = ['title']
class TodoEditForm(ModelForm):
class Meta:
model = Todo
fields = ['title', 'completed']

View File

@ -1,3 +1,11 @@
from django.db import models
# Create your models here.
class Todo(models.Model):
title = models.CharField(max_length=200)
completed = models.BooleanField(default=False)
def __str__(self):
return self.title

View File

@ -0,0 +1,59 @@
{% extends "base.html" %}
{% block title %}
Create Todo
{% endblock title %}
{% block content %}
<div class="container mt-5">
<h3>Create Todo</h3>
<form action="" method="post" class="needs-validation" novalidate>
{% csrf_token %}
<div class="mb-3 fieldWrapper">
<label for="{{ form.title.id_for_label }}" class="form-label">Title</label>
<input type="text" class="form-control" name="{{ form.title.name }}" id="{{ form.title.id_for_label }}" aria-describedby="titleHelp" required>
<div class="invalid-feedback">
This field is required
</div>
{% if form.title.errors %}
<div class="text-danger">
{{ form.title.errors }}
</div>
{% endif %}
</div>
<input class="btn btn-primary" type="submit" value="Create">
</form>
</div>
{% endblock %}
{% block extra_js %}
<script>
(() => {
'use strict'
const forms = document.querySelectorAll('.needs-validation')
// Loop over them and prevent submission
Array.from(forms).forEach(form => {
form.addEventListener('submit', event => {
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
}
form.classList.add('was-validated')
}, false)
})
})()
</script>
{% endblock %}

View File

@ -0,0 +1,67 @@
{% extends "base.html" %}
{% block title %}
Edit Todo
{% endblock title %}
{% block content %}
<div class="container mt-5">
<h3>Edit Todo</h3>
<form action="" method="post" class="needs-validation" novalidate>
{% csrf_token %}
<div class="mb-3 fieldWrapper">
<label for="{{ form.title.id_for_label }}" class="form-label">Title</label>
<input type="text" class="form-control" name="{{ form.title.name }}" id="{{ form.title.id_for_label }}" aria-describedby="titleHelp" value="{{ form.instance.title }}" required>
<div class="invalid-feedback">
This field is required
</div>
</div>
<div class="mb-3 fieldWrapper">
<label for="{{ form.completed.id_for_label }}" class="form-label">Completed</label>
<input type="checkbox" class="form-check-input" name="{{ form.completed.name }}" id="{{ form.completed.id_for_label }}" aria-describedby="completedHelp" {% if form.instance.completed %}checked{% endif %}>
<div class="invalid-feedback">
This field is required
</div>
{% if form.completed.errors %}
<div class="text-danger">
{{ form.completed.errors }}
</div>
{% endif %}
</div>
<input class="btn btn-primary" type="submit" value="Save">
</form>
</div>
{% endblock %}
{% block extra_js %}
<script>
(() => {
'use strict'
const forms = document.querySelectorAll('.needs-validation')
// Loop over them and prevent submission
Array.from(forms).forEach(form => {
form.addEventListener('submit', event => {
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
}
form.classList.add('was-validated')
}, false)
})
})()
</script>
{% endblock %}

View File

@ -0,0 +1,43 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-5">
<div class="row">
<div class="col">
<h3>Todo List</h3>
</div>
<div class="col text-end">
<a href="{% url 'create' %}" class="btn btn-success"><i class="bi bi-clipboard-plus"></i> Create Todo</a>
</div>
</div>
<ul class="list-group">
{% if not todos %}
<div class="alert alert-danger mt-5 text-center" role="alert">
There are no todos yet!
</div>
{% endif %}
<div class="mt-5">
{% for todo in todos %}
<li class="list-group-item d-flex justify-content-between">
<span>
<a href="{% url 'delete' todo.id %}"><i class="bi bi-trash3-fill text-danger"></i></i></a>
<a href="{% url 'edit' todo.id %}"><i class="bi bi-pencil-fill text-success"></i></a>
<span class="ms-3">
{{ todo.title }}
</span>
</span>
{% if todo.completed %}
<span class="badge bg-success">Completed</span>
{% else %}
<span class="badge bg-danger">Not Completed</span>
{% endif %}
</li>
{% endfor %}
</div>
</ul>
</div>
{% endblock %}

View File

@ -0,0 +1,22 @@
{% extends "base.html" %}
{% block title %}
Delete {{ object }}
{% endblock title %}
{% block content %}
<div class="container mt-5">
<form method="post">{% csrf_token %}
<div class="alert alert-warning" role="alert">
<h4 class="alert-heading">Are you sure ?</h4>
<p>Are you sure you want to delete "{{ object }}"?</p>
<hr>
{{ form }}
<input type="submit" class="btn btn-danger" value="Confirm">
<a href="{% url 'list' %}" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
{% endblock content %}

View File

@ -1,7 +1,10 @@
from django.urls import path
from . import views
from .views import TodoListView, TodoCreateView, TodoEditView, TodoDeleteView
urlpatterns = [
#path("", views.index, name="index"),
path("", TodoListView.as_view(), name="list"),
path("create", TodoCreateView.as_view(), name="create"),
path("edit/<int:id>/", TodoEditView.as_view(), name="edit"),
path("delete/<int:pk>/", TodoDeleteView.as_view(), name="delete"),
]

View File

@ -1,3 +1,53 @@
from django.shortcuts import render
from django.views.generic.base import TemplateView
from django.views.generic.edit import DeleteView
from .forms import TodoForm, TodoEditForm
from .models import Todo
from django.shortcuts import redirect, render
from django.urls import reverse_lazy
class TodoListView(TemplateView):
template_name = "todolist/list.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["todos"] = Todo.objects.all()[:5]
return context
class TodoCreateView(TemplateView):
template_name = "todolist/create.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["form"] = TodoForm()
return context
def post(self, request):
form = TodoForm(request.POST)
if form.is_valid():
form.save()
return redirect("list")
return render(request, "create.html", {"form": form})
class TodoEditView(TemplateView):
template_name = "todolist/edit.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
id = kwargs["id"]
todo = Todo.objects.get(id=id)
context["form"] = TodoEditForm(instance=todo)
return context
def post(self, request, id):
todo = Todo.objects.get(id=id)
form = TodoEditForm(request.POST, instance=todo)
if form.is_valid():
form.save()
return redirect("list")
return render(request, "edit.html", {"form": form})
class TodoDeleteView(DeleteView):
model = Todo
success_url = reverse_lazy("list")
# Create your views here.

View File

@ -22,5 +22,12 @@ services:
volumes:
- db-data:/var/lib/postgresql
adminer:
image: adminer
restart: always
ports:
- 8080:8080
volumes:
db-data: