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__/** **/__pycache__/**
**/migrations/** **/migrations/**
node_modules/
package-lock.json

View File

@ -2,6 +2,11 @@
FROM python:3.13-bookworm FROM python:3.13-bookworm
RUN useradd -m django 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 WORKDIR /app
@ -12,4 +17,4 @@ COPY --chown=django . .
USER django: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 = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [], 'DIRS': [ BASE_DIR / 'templates' ],
'APP_DIRS': True, 'APP_DIRS': True,
'OPTIONS': { 'OPTIONS': {
'context_processors': [ 'context_processors': [
@ -121,6 +121,9 @@ USE_TZ = True
# https://docs.djangoproject.com/en/5.2/howto/static-files/ # https://docs.djangoproject.com/en/5.2/howto/static-files/
STATIC_URL = 'static/' STATIC_URL = 'static/'
STATICFILES_DIRS = [
BASE_DIR / "node_modules",
]
# Default primary key field type # Default primary key field type
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field # 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.contrib import admin
from django.urls import path, include from django.urls import path, include
from django.conf.urls.static import static
from django.conf import settings
urlpatterns = [ urlpatterns = [
path("todolist/", include("todolist.urls")), path("", include("todolist.urls")),
path('admin/', admin.site.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 django.contrib import admin
from .models import Todo
# Register your models here. # 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 from django.db import models
# Create your models here. # 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 django.urls import path
from . import views from .views import TodoListView, TodoCreateView, TodoEditView, TodoDeleteView
urlpatterns = [ 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: volumes:
- db-data:/var/lib/postgresql - db-data:/var/lib/postgresql
adminer:
image: adminer
restart: always
ports:
- 8080:8080
volumes: volumes:
db-data: db-data: