From 369b6764c91b9b2c43d7d100e683a0cf6f65edb5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 05:36:42 +0000 Subject: [PATCH 01/47] Initial plan From a45830cf25ecfbabd8aec52ca0f1a1956e07edda Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 05:42:24 +0000 Subject: [PATCH 02/47] fix: keep mobile header title aligned when navbar menu expands Agent-Logs-Url: https://github.com/dsaub/proyecto-final/sessions/8f4a8d58-4e90-48ad-8195-23b90d8b22d4 Co-authored-by: dsaub <54474838+dsaub@users.noreply.github.com> --- tienda/static/css/custom.css | 3 ++- tienda/tests.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/tienda/static/css/custom.css b/tienda/static/css/custom.css index 5a77645..eaac49c 100644 --- a/tienda/static/css/custom.css +++ b/tienda/static/css/custom.css @@ -63,8 +63,9 @@ p.price { .navbar.header .site-title-mobile { color: #FFF; position: absolute; + top: calc(var(--bs-navbar-padding-y) + 20px); left: 50%; - transform: translateX(-50%); + transform: translate(-50%, -50%); margin: 0; max-width: calc(100% - 9rem); overflow: hidden; diff --git a/tienda/tests.py b/tienda/tests.py index 2c0b210..c7606ba 100644 --- a/tienda/tests.py +++ b/tienda/tests.py @@ -1,4 +1,5 @@ import json +from pathlib import Path from unittest.mock import MagicMock, patch from django.test import TestCase, override_settings @@ -1371,6 +1372,20 @@ class EndpointViewTests(TestCase): self.assertEqual(response.status_code, 200) self.assertContains(response, 'site-title-mobile d-md-none') self.assertContains(response, 'site-title-desktop') + + def test_mobile_site_title_css_keeps_title_pinned_to_header_row(self): + css_path = Path(__file__).resolve().parent / "static" / "css" / "custom.css" + css_content = css_path.read_text(encoding="utf-8") + + rule_start = css_content.find(".navbar.header .site-title-mobile") + self.assertNotEqual(rule_start, -1) + + rule_end = css_content.find("}", rule_start) + self.assertNotEqual(rule_end, -1) + rule_block = css_content[rule_start:rule_end] + + self.assertIn("top: calc(var(--bs-navbar-padding-y) + 20px);", rule_block) + self.assertIn("transform: translate(-50%, -50%);", rule_block) def test_home_mobile_welcome_title_centered(self): response = self.client.get(reverse("home")) html = response.content.decode() From e1e175f18fa910646c9abeef626bc5eaf491d44a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 05:45:19 +0000 Subject: [PATCH 03/47] test: make mobile navbar CSS regression assertion order-independent Agent-Logs-Url: https://github.com/dsaub/proyecto-final/sessions/8f4a8d58-4e90-48ad-8195-23b90d8b22d4 Co-authored-by: dsaub <54474838+dsaub@users.noreply.github.com> --- tienda/tests.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/tienda/tests.py b/tienda/tests.py index c7606ba..284ddc2 100644 --- a/tienda/tests.py +++ b/tienda/tests.py @@ -1,5 +1,6 @@ import json from pathlib import Path +import re from unittest.mock import MagicMock, patch from django.test import TestCase, override_settings @@ -1376,16 +1377,12 @@ class EndpointViewTests(TestCase): def test_mobile_site_title_css_keeps_title_pinned_to_header_row(self): css_path = Path(__file__).resolve().parent / "static" / "css" / "custom.css" css_content = css_path.read_text(encoding="utf-8") + selector_match = re.search(r"\.navbar\.header \.site-title-mobile\s*\{(?P[^}]*)\}", css_content, re.DOTALL) + self.assertIsNotNone(selector_match) - rule_start = css_content.find(".navbar.header .site-title-mobile") - self.assertNotEqual(rule_start, -1) - - rule_end = css_content.find("}", rule_start) - self.assertNotEqual(rule_end, -1) - rule_block = css_content[rule_start:rule_end] - - self.assertIn("top: calc(var(--bs-navbar-padding-y) + 20px);", rule_block) - self.assertIn("transform: translate(-50%, -50%);", rule_block) + rule_block = selector_match.group("body") + self.assertRegex(rule_block, r"top:\s*calc\(var\(--bs-navbar-padding-y\)\s*\+\s*20px\);") + self.assertRegex(rule_block, r"transform:\s*translate\(-50%,\s*-50%\);") def test_home_mobile_welcome_title_centered(self): response = self.client.get(reverse("home")) html = response.content.decode() From 540b3fdc439a586d549b04cafed689f0419b6b2b Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 22 Apr 2026 09:15:19 +0200 Subject: [PATCH 04/47] chore: add django-ninja to requirements --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4b6da18..db7b304 100644 --- a/requirements.txt +++ b/requirements.txt @@ -41,4 +41,5 @@ vine==5.1.0 wcwidth==0.6.0 whitenoise==6.12.0 fpdf2==2.8.7 -psycopg2-binary==2.9.11 \ No newline at end of file +psycopg2-binary==2.9.11 +django-ninja==1.6.2 \ No newline at end of file From 60cd29ee300afcf03994fd638047f39555b43692 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 22 Apr 2026 09:15:24 +0200 Subject: [PATCH 05/47] feat: add API URL routing to urlpatterns --- proyecto/urls.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/proyecto/urls.py b/proyecto/urls.py index ca32905..57c06ad 100644 --- a/proyecto/urls.py +++ b/proyecto/urls.py @@ -19,11 +19,13 @@ from django.conf.urls.static import static from django.contrib import admin from django.urls import include, path from tienda import views as tienda_views +from tienda.api import api urlpatterns = [ path('', tienda_views.home, name='home'), path('admin/', admin.site.urls), - path('tienda/', include('tienda.urls')) + path('tienda/', include('tienda.urls')), + path('api/', api.urls) ] if settings.DEBUG: From dc967c114fa726b1d2289fee8ca4a84083fb24d4 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 22 Apr 2026 09:15:28 +0200 Subject: [PATCH 06/47] feat: implement initial API endpoints for product retrieval --- tienda/api.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 tienda/api.py diff --git a/tienda/api.py b/tienda/api.py new file mode 100644 index 0000000..03155af --- /dev/null +++ b/tienda/api.py @@ -0,0 +1,12 @@ +from ninja import NinjaAPI +from .models import Product +api = NinjaAPI() + +@api.get("/hola") +def hola(request): + return {"mensaje": "¡Hola Mundo!"} + +@api.get("/products") +def products(request): + productos = Product.objects.all() + return [dict(producto) for producto in productos] \ No newline at end of file From 3cbca38c3274d4a59472608221d2b35e1a99e82e Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 22 Apr 2026 09:15:32 +0200 Subject: [PATCH 07/47] feat: add __dict__ method to models for JSON serialization --- tienda/models.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tienda/models.py b/tienda/models.py index 56d986e..665a88e 100644 --- a/tienda/models.py +++ b/tienda/models.py @@ -25,6 +25,11 @@ class User(AbstractUser): choices = RegisterStatus.choices, default = RegisterStatus.CONFIRMATION_REQUIRED ) + def __dict__(self): + return { + "username": self.username, + "fullname": self.get_full_name() + } class VerificationCode(models.Model): class VerificationModes(models.TextChoices): @@ -55,6 +60,11 @@ class Category(models.Model): def __str__(self): return self.name + + def __dict__(self): + return { + "name": self.name + } class Image(models.Model): name = models.CharField(max_length=200, default="") @@ -63,6 +73,13 @@ class Image(models.Model): def __str__(self): return self.name + + def __dict__(self): + return { + "name": self.name, + "image": self.image.url, + "alt": self.alt + } class Product(models.Model): name = models.CharField(max_length=200, default="") @@ -85,6 +102,19 @@ class Product(models.Model): def get_vat_amount(self): """Retorna la cantidad de IVA""" return round(self.price * VAT_RATE, 2) + + def __dict__(self): + return { + "name": self.name, + "description": self.description, + "briefdesc": self.briefdesc, + "price": self.price, + "stock": self.stock, + "category": dict(self.category), + "primary_image": self.primary_image, + "secondary_images": [dict(secondary_image) for secondary_image in self.secondary_images], + "creator": dict(self.creator) + } class StockReservation(models.Model): From 7d3cff0bd9f67a6146b1015131fd337f36933a25 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 22 Apr 2026 09:22:20 +0200 Subject: [PATCH 08/47] refactor: rename __dict__ methods to to_dict for consistency in models --- tienda/models.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tienda/models.py b/tienda/models.py index 665a88e..91cb225 100644 --- a/tienda/models.py +++ b/tienda/models.py @@ -25,7 +25,7 @@ class User(AbstractUser): choices = RegisterStatus.choices, default = RegisterStatus.CONFIRMATION_REQUIRED ) - def __dict__(self): + def to_dict(self): return { "username": self.username, "fullname": self.get_full_name() @@ -61,7 +61,7 @@ class Category(models.Model): def __str__(self): return self.name - def __dict__(self): + def to_dict(self): return { "name": self.name } @@ -74,7 +74,7 @@ class Image(models.Model): def __str__(self): return self.name - def __dict__(self): + def to_dict(self): return { "name": self.name, "image": self.image.url, @@ -103,17 +103,17 @@ class Product(models.Model): """Retorna la cantidad de IVA""" return round(self.price * VAT_RATE, 2) - def __dict__(self): + def to_dict(self): return { "name": self.name, "description": self.description, "briefdesc": self.briefdesc, "price": self.price, "stock": self.stock, - "category": dict(self.category), + "category": self.category.to_dict(), "primary_image": self.primary_image, - "secondary_images": [dict(secondary_image) for secondary_image in self.secondary_images], - "creator": dict(self.creator) + "secondary_images": [secondary_image.to_dict() for secondary_image in self.secondary_images], + "creator": self.creator.to_dict() } From 501d7aade5fd9a6819199396385635c70ca74503 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 22 Apr 2026 09:52:15 +0200 Subject: [PATCH 09/47] chore: add media directory to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index afb4ca3..1fe2ba1 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ __pycache__/ *.pyc tienda/__pycache__/ proyecto/__pycache__/ +media \ No newline at end of file From d9d9e5b1a6cfd283e32c697d2a878d02e26d7bae Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 22 Apr 2026 09:52:20 +0200 Subject: [PATCH 10/47] refactor: update products endpoint to use to_dict method for consistency --- tienda/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tienda/api.py b/tienda/api.py index 03155af..27f43dc 100644 --- a/tienda/api.py +++ b/tienda/api.py @@ -9,4 +9,4 @@ def hola(request): @api.get("/products") def products(request): productos = Product.objects.all() - return [dict(producto) for producto in productos] \ No newline at end of file + return [producto.to_dict() for producto in productos] \ No newline at end of file From d8f6838f0c700ee48055a4ced480671a5fdb076c Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 22 Apr 2026 09:52:30 +0200 Subject: [PATCH 11/47] refactor: update Product model to use to_dict for image serialization --- tienda/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tienda/models.py b/tienda/models.py index 91cb225..1cab708 100644 --- a/tienda/models.py +++ b/tienda/models.py @@ -111,8 +111,8 @@ class Product(models.Model): "price": self.price, "stock": self.stock, "category": self.category.to_dict(), - "primary_image": self.primary_image, - "secondary_images": [secondary_image.to_dict() for secondary_image in self.secondary_images], + "primary_image": self.primary_image.to_dict(), + "secondary_images": [secondary_image.to_dict() for secondary_image in self.secondary_images.all()], "creator": self.creator.to_dict() } From edda5aca50a9d9e9c3d271752e05aeb090f65ff5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 06:50:48 +0000 Subject: [PATCH 12/47] Initial plan From ea6c9c49a0e834f9fac768476ccd7f82880526dd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 06:56:38 +0000 Subject: [PATCH 13/47] fix: add aria-label and visible text to search button for accessibility Agent-Logs-Url: https://github.com/dsaub/proyecto-final/sessions/d1c1f40a-b3a3-4c18-98f9-be267a4a043b Co-authored-by: dsaub <54474838+dsaub@users.noreply.github.com> --- tienda/templates/tienda/base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tienda/templates/tienda/base.html b/tienda/templates/tienda/base.html index f4d6708..f58a554 100644 --- a/tienda/templates/tienda/base.html +++ b/tienda/templates/tienda/base.html @@ -108,7 +108,7 @@ From dd49a6a7d68d18b921cf40b02dd62448fc6d6dc9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 07:05:22 +0000 Subject: [PATCH 14/47] Initial plan From 71cbf6825eac78eefa2a06bc27c543816ef95169 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 07:05:32 +0000 Subject: [PATCH 15/47] Initial plan From 0eaaa8d19d67783d2deb339e98c20300293ed8aa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 07:05:42 +0000 Subject: [PATCH 16/47] Initial plan From 8a5edce758909a4960f49b47a0f1b6443e0c4d27 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 07:05:57 +0000 Subject: [PATCH 17/47] Initial plan From 3eb963fadf906632325b12d40955c7a24d1388ee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 07:06:05 +0000 Subject: [PATCH 18/47] Initial plan From 0a9b9138bccaf29356bf4b4e40ed967533bf7f5b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 07:06:16 +0000 Subject: [PATCH 19/47] Initial plan From 183685519ad7b48189536665e906b9542857e926 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 07:06:25 +0000 Subject: [PATCH 20/47] Initial plan From bdae5b073c03d777010b206a384c7ce35f9c8b31 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 07:06:32 +0000 Subject: [PATCH 21/47] Initial plan From d55026b69d3e6b5ee30f57a5d141361d9e0307f5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 07:06:40 +0000 Subject: [PATCH 22/47] Initial plan From fe61b3a212cd080b7e36717a79429975d57e986b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 07:07:12 +0000 Subject: [PATCH 23/47] fix: wrap saved-card radios in fieldset/legend for accessibility Agent-Logs-Url: https://github.com/dsaub/proyecto-final/sessions/bddffd0c-804e-448e-9954-98917149de3c Co-authored-by: dsaub <54474838+dsaub@users.noreply.github.com> --- tienda/templates/tienda/checkout.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tienda/templates/tienda/checkout.html b/tienda/templates/tienda/checkout.html index 1b88ee9..c6e6d2c 100644 --- a/tienda/templates/tienda/checkout.html +++ b/tienda/templates/tienda/checkout.html @@ -141,8 +141,8 @@
{% if saved_cards %} -
-

Tarjetas guardadas:

+
+ Selección de tarjeta {% for card in saved_cards %}
@@ -156,7 +156,7 @@
-
+ {% endif %}
From 63df5cf73fdd091cfe12aa72563b365f3c6157e5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 07:08:35 +0000 Subject: [PATCH 24/47] Add skip link 'Saltar al contenido' for keyboard/screen reader accessibility Agent-Logs-Url: https://github.com/dsaub/proyecto-final/sessions/6f9c00f2-c1ee-4dc2-80fb-2596645e9221 Co-authored-by: dsaub <54474838+dsaub@users.noreply.github.com> --- tienda/static/css/custom.css | 17 +++++++++++++++++ tienda/templates/tienda/base.html | 3 ++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/tienda/static/css/custom.css b/tienda/static/css/custom.css index eaac49c..efd1115 100644 --- a/tienda/static/css/custom.css +++ b/tienda/static/css/custom.css @@ -1,3 +1,20 @@ +.skip-link { + position: absolute; + top: -40px; + left: 0; + background: #513CB0; + color: #fff; + padding: 8px 16px; + font-weight: 600; + z-index: 10000; + text-decoration: none; + border-radius: 0 0 4px 0; + transition: top 0.2s; +} +.skip-link:focus { + top: 0; +} + @media (min-width: 1250px) { .grid { display: grid; diff --git a/tienda/templates/tienda/base.html b/tienda/templates/tienda/base.html index f58a554..1094df8 100644 --- a/tienda/templates/tienda/base.html +++ b/tienda/templates/tienda/base.html @@ -78,6 +78,7 @@ {% block head %}{% endblock %} + {% cache 500 sidebar request.user.username %} {% endcache %} -
+
{% if messages %}
From 17935c6160f673a6b6841d8998a4805b01c7098f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 07:09:31 +0000 Subject: [PATCH 25/47] Add :focus-visible to skip link for better keyboard navigation UX Agent-Logs-Url: https://github.com/dsaub/proyecto-final/sessions/6f9c00f2-c1ee-4dc2-80fb-2596645e9221 Co-authored-by: dsaub <54474838+dsaub@users.noreply.github.com> --- tienda/static/css/custom.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tienda/static/css/custom.css b/tienda/static/css/custom.css index efd1115..e80914d 100644 --- a/tienda/static/css/custom.css +++ b/tienda/static/css/custom.css @@ -11,7 +11,8 @@ border-radius: 0 0 4px 0; transition: top 0.2s; } -.skip-link:focus { +.skip-link:focus, +.skip-link:focus-visible { top: 0; } From cb31784097bbf4207e9e0ec24c1808bfae59f8cb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 07:09:44 +0000 Subject: [PATCH 26/47] Implement ARIA combobox/listbox pattern for search suggestions Agent-Logs-Url: https://github.com/dsaub/proyecto-final/sessions/86ca48b3-a56a-4392-9295-0f45ed4f752f Co-authored-by: dsaub <54474838+dsaub@users.noreply.github.com> --- tienda/templates/tienda/base.html | 87 ++++++++++++++++++++++++++----- 1 file changed, 74 insertions(+), 13 deletions(-) diff --git a/tienda/templates/tienda/base.html b/tienda/templates/tienda/base.html index f58a554..3ab8d2d 100644 --- a/tienda/templates/tienda/base.html +++ b/tienda/templates/tienda/base.html @@ -50,8 +50,11 @@ transition: background-color 0.2s; } - .search-suggestion-item:hover { + .search-suggestion-item:hover, + .search-suggestion-item.active { background-color: #f8f9fa; + outline: 2px solid #0d6efd; + outline-offset: -2px; } .search-suggestion-item:last-child { @@ -107,10 +110,10 @@
IVA (21%) - {{ cart.get_vat_amount|format_price }} € + {{ cart.get_vat_amount|format_price }} €
Envío diff --git a/tienda/templates/tienda/producto.html b/tienda/templates/tienda/producto.html index fca8dd8..dace4c9 100644 --- a/tienda/templates/tienda/producto.html +++ b/tienda/templates/tienda/producto.html @@ -34,16 +34,16 @@
Precio total (IVA incluido):
€{{ product.get_price_with_vat|format_price }} -
IVA: €{{ product.get_vat_amount|format_price }}
+
IVA incluido: €{{ product.get_vat_amount|format_price }}
{{ product.briefdesc }}
{% if product.stock > 0 %} - Stock disponible: {{ product.stock }} + Stock disponible: {{ product.stock }} {% else %} - Sin stock + Sin stock {% endif %}
From ad9fa741e5ce6c95c99d05ba9893387af2e8f7ac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 07:11:20 +0000 Subject: [PATCH 28/47] fix: add role=status to stock badge indicators for better screen reader support Agent-Logs-Url: https://github.com/dsaub/proyecto-final/sessions/b6a3a32a-ff80-4431-9ba0-769cbd08b939 Co-authored-by: dsaub <54474838+dsaub@users.noreply.github.com> --- tienda/templates/tienda/cart.html | 2 +- tienda/templates/tienda/producto.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tienda/templates/tienda/cart.html b/tienda/templates/tienda/cart.html index 1199d01..821fd15 100644 --- a/tienda/templates/tienda/cart.html +++ b/tienda/templates/tienda/cart.html @@ -59,7 +59,7 @@ {% if item.product.stock > 0 %} {{ item.product.stock }} {% else %} - Sin stock + Sin stock {% endif %} {{ item.get_subtotal_with_vat|format_price }} € diff --git a/tienda/templates/tienda/producto.html b/tienda/templates/tienda/producto.html index dace4c9..117aa9f 100644 --- a/tienda/templates/tienda/producto.html +++ b/tienda/templates/tienda/producto.html @@ -41,9 +41,9 @@
{% if product.stock > 0 %} - Stock disponible: {{ product.stock }} + Stock disponible: {{ product.stock }} {% else %} - Sin stock + Sin stock {% endif %}
From 6828074dd1c9f6611faffa71601978ada9da1e5b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 07:11:24 +0000 Subject: [PATCH 29/47] fix: complete WAI-ARIA tabs pattern in checkout.html (aria-selected, aria-controls, tabpanel, keyboard nav) Agent-Logs-Url: https://github.com/dsaub/proyecto-final/sessions/73a76f50-8c55-4285-81cf-931b63290b81 Co-authored-by: dsaub <54474838+dsaub@users.noreply.github.com> --- tienda/templates/tienda/checkout.html | 54 ++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/tienda/templates/tienda/checkout.html b/tienda/templates/tienda/checkout.html index 1b88ee9..d12f674 100644 --- a/tienda/templates/tienda/checkout.html +++ b/tienda/templates/tienda/checkout.html @@ -127,19 +127,22 @@ -
+
{% if saved_cards %}

Tarjetas guardadas:

@@ -183,7 +186,8 @@
-
+
{% if saved_paypal %}
Cuenta PayPal guardada: @@ -221,12 +225,42 @@ const HAS_STOCK_ISSUES = {{ stock_issues|yesno:"true,false" }}; const HAS_ADDRESS = {{ addresses|yesno:"true,false" }}; // ---- Tab switching ---- -document.querySelectorAll('#paymentTabs .nav-link').forEach(btn => { - btn.addEventListener('click', () => { - document.querySelectorAll('#paymentTabs .nav-link').forEach(b => b.classList.remove('active')); - document.querySelectorAll('.payment-tab-content').forEach(p => p.classList.remove('active')); - btn.classList.add('active'); - document.getElementById(btn.dataset.tab).classList.add('active'); +const paymentTabs = Array.from(document.querySelectorAll('#paymentTabs .nav-link[role="tab"]')); + +function activateTab(tab) { + paymentTabs.forEach(b => { + const isSelected = b === tab; + b.classList.toggle('active', isSelected); + b.setAttribute('aria-selected', isSelected ? 'true' : 'false'); + b.setAttribute('tabindex', isSelected ? '0' : '-1'); + }); + document.querySelectorAll('.payment-tab-content').forEach(p => p.classList.remove('active')); + document.getElementById(tab.dataset.tab).classList.add('active'); +} + +paymentTabs.forEach(btn => { + btn.addEventListener('click', () => activateTab(btn)); + btn.addEventListener('keydown', e => { + const idx = paymentTabs.indexOf(e.currentTarget); + if (e.key === 'ArrowRight') { + e.preventDefault(); + const next = paymentTabs[(idx + 1) % paymentTabs.length]; + activateTab(next); + next.focus(); + } else if (e.key === 'ArrowLeft') { + e.preventDefault(); + const prev = paymentTabs[(idx - 1 + paymentTabs.length) % paymentTabs.length]; + activateTab(prev); + prev.focus(); + } else if (e.key === 'Home') { + e.preventDefault(); + activateTab(paymentTabs[0]); + paymentTabs[0].focus(); + } else if (e.key === 'End') { + e.preventDefault(); + activateTab(paymentTabs[paymentTabs.length - 1]); + paymentTabs[paymentTabs.length - 1].focus(); + } }); }); From 3f521d81b4c8d9072dbe5acf19d2866d91347826 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 07:13:11 +0000 Subject: [PATCH 30/47] fix(a11y): add aria-label to cart quantity input for each product Agent-Logs-Url: https://github.com/dsaub/proyecto-final/sessions/36168486-a2a4-41f3-b3a3-8adf781b354a Co-authored-by: dsaub <54474838+dsaub@users.noreply.github.com> --- tienda/templates/tienda/cart.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tienda/templates/tienda/cart.html b/tienda/templates/tienda/cart.html index bcde235..63c0257 100644 --- a/tienda/templates/tienda/cart.html +++ b/tienda/templates/tienda/cart.html @@ -51,7 +51,7 @@
{% csrf_token %} - +
From d849e7d3e6b8af088ab133c351d6044fe2156751 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 07:14:49 +0000 Subject: [PATCH 31/47] Replace alert() payment errors with inline role=alert containers in checkout.html Agent-Logs-Url: https://github.com/dsaub/proyecto-final/sessions/e4ef062a-c246-4ec3-9424-987f29891c30 Co-authored-by: dsaub <54474838+dsaub@users.noreply.github.com> --- tienda/templates/tienda/checkout.html | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tienda/templates/tienda/checkout.html b/tienda/templates/tienda/checkout.html index 1b88ee9..8747769 100644 --- a/tienda/templates/tienda/checkout.html +++ b/tienda/templates/tienda/checkout.html @@ -196,6 +196,7 @@ Guardar esta cuenta de PayPal para futuras compras
+
{% if not addresses or stock_issues %}
Selecciona una dirección de envío válida para activar el pago.
@@ -254,10 +255,12 @@ document.getElementById('pay-card-btn').addEventListener('click', async () => { const addressId = document.getElementById('shipping-address').value; if (!addressId) { - alert('Selecciona una dirección de envío para continuar.'); + const cardErrors = document.getElementById('card-errors'); + cardErrors.textContent = 'Selecciona una dirección de envío para continuar.'; return; } + document.getElementById('card-errors').textContent = ''; const btn = document.getElementById('pay-card-btn'); const spinner = document.getElementById('card-spinner'); btn.disabled = true; @@ -335,9 +338,12 @@ paypal.Buttons({ createOrder: async () => { const addressId = document.getElementById('shipping-address').value; if (!addressId) { - alert('Selecciona una dirección de envío para continuar.'); + const paypalErrors = document.getElementById('paypal-errors'); + paypalErrors.textContent = 'Selecciona una dirección de envío para continuar.'; + paypalErrors.classList.remove('d-none'); return Promise.reject(new Error('Sin dirección')); } + document.getElementById('paypal-errors').classList.add('d-none'); const resp = await fetch('{% url "crear_orden_paypal" %}', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': CSRF_TOKEN }, @@ -359,7 +365,9 @@ paypal.Buttons({ showSuccess(result.transaction_code); }, onError: (err) => { - alert('Error en el pago con PayPal: ' + err); + const paypalErrors = document.getElementById('paypal-errors'); + paypalErrors.textContent = 'Error en el pago con PayPal: ' + err; + paypalErrors.classList.remove('d-none'); }, }).render('#paypal-button-container'); {% endif %} From 25c6fc7315bec31c11cb6a653779d652d4a937d6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 07:16:26 +0000 Subject: [PATCH 32/47] Add null guards for error container DOM lookups Agent-Logs-Url: https://github.com/dsaub/proyecto-final/sessions/e4ef062a-c246-4ec3-9424-987f29891c30 Co-authored-by: dsaub <54474838+dsaub@users.noreply.github.com> --- tienda/templates/tienda/checkout.html | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/tienda/templates/tienda/checkout.html b/tienda/templates/tienda/checkout.html index 8747769..04fafd9 100644 --- a/tienda/templates/tienda/checkout.html +++ b/tienda/templates/tienda/checkout.html @@ -256,11 +256,12 @@ document.getElementById('pay-card-btn').addEventListener('click', async () => { const addressId = document.getElementById('shipping-address').value; if (!addressId) { const cardErrors = document.getElementById('card-errors'); - cardErrors.textContent = 'Selecciona una dirección de envío para continuar.'; + if (cardErrors) cardErrors.textContent = 'Selecciona una dirección de envío para continuar.'; return; } - document.getElementById('card-errors').textContent = ''; + const cardErrorsEl = document.getElementById('card-errors'); + if (cardErrorsEl) cardErrorsEl.textContent = ''; const btn = document.getElementById('pay-card-btn'); const spinner = document.getElementById('card-spinner'); btn.disabled = true; @@ -339,11 +340,14 @@ paypal.Buttons({ const addressId = document.getElementById('shipping-address').value; if (!addressId) { const paypalErrors = document.getElementById('paypal-errors'); - paypalErrors.textContent = 'Selecciona una dirección de envío para continuar.'; - paypalErrors.classList.remove('d-none'); + if (paypalErrors) { + paypalErrors.textContent = 'Selecciona una dirección de envío para continuar.'; + paypalErrors.classList.remove('d-none'); + } return Promise.reject(new Error('Sin dirección')); } - document.getElementById('paypal-errors').classList.add('d-none'); + const paypalErrorsEl = document.getElementById('paypal-errors'); + if (paypalErrorsEl) paypalErrorsEl.classList.add('d-none'); const resp = await fetch('{% url "crear_orden_paypal" %}', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': CSRF_TOKEN }, @@ -366,8 +370,10 @@ paypal.Buttons({ }, onError: (err) => { const paypalErrors = document.getElementById('paypal-errors'); - paypalErrors.textContent = 'Error en el pago con PayPal: ' + err; - paypalErrors.classList.remove('d-none'); + if (paypalErrors) { + paypalErrors.textContent = 'Error en el pago con PayPal: ' + err; + paypalErrors.classList.remove('d-none'); + } }, }).render('#paypal-button-container'); {% endif %} From a94c256ad53606bf0abee6826f0abe7e7d03a42d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 07:18:04 +0000 Subject: [PATCH 33/47] Changes before error encountered Agent-Logs-Url: https://github.com/dsaub/proyecto-final/sessions/f8687aac-de86-402f-b36d-ea422d24ed8e Co-authored-by: dsaub <54474838+dsaub@users.noreply.github.com> --- tienda/templates/tienda/home.html | 8 +++++--- tienda/templates/tienda/search.html | 8 +++++--- tienda/views.py | 2 ++ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/tienda/templates/tienda/home.html b/tienda/templates/tienda/home.html index 4465721..3e28fb3 100644 --- a/tienda/templates/tienda/home.html +++ b/tienda/templates/tienda/home.html @@ -196,9 +196,11 @@ Ver detalles - - 🛒 - +
+ {% csrf_token %} + + +
diff --git a/tienda/templates/tienda/search.html b/tienda/templates/tienda/search.html index 3641bdd..7fc8385 100644 --- a/tienda/templates/tienda/search.html +++ b/tienda/templates/tienda/search.html @@ -79,9 +79,11 @@ Ver detalles - - 🛒 - +
+ {% csrf_token %} + + +
diff --git a/tienda/views.py b/tienda/views.py index a44db6b..b5af383 100644 --- a/tienda/views.py +++ b/tienda/views.py @@ -16,6 +16,7 @@ from .vars import ( ) from django.conf import settings from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.http import require_POST from django.urls import reverse from django.utils import timezone from decimal import Decimal, ROUND_HALF_UP @@ -704,6 +705,7 @@ def create_order_from_cart(request, payment_method, payment_reference="", shippi return order, "" +@require_POST def add_to_cart(request: HttpRequest, product_id: int): """Agrega un producto al carrito""" try: From 84d8a0e3b6b0cb4d9521f0281f9911e53009b5a5 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 28 Apr 2026 21:19:32 +0200 Subject: [PATCH 34/47] Add S3 Storage... --- .env.example | 12 +++++++++ .github/copilot-instructions.md | 1 + nginx.conf | 6 +++-- proyecto/settings.py | 44 ++++++++++++++++++++++++++++++++- proyecto/urls.py | 2 +- requirements.txt | 2 ++ tienda/storage_backends.py | 23 +++++++++++++++++ 7 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 tienda/storage_backends.py diff --git a/.env.example b/.env.example index 9a50945..7cdfeed 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,7 @@ SECRET_KEY=django-insecure-change-me DEBUG=True ALLOWED_HOSTS=localhost,127.0.0.1 +S3_ENABLE=False # PostgreSQL (por defecto habilitado; si POSTGRES_ENABLED=False se usa SQLite) POSTGRES_ENABLED=True @@ -14,6 +15,17 @@ POSTGRES_PORT=5432 # Redis REDIS_URL=redis://127.0.0.1:6379/1 +# S3 (activar con S3_ENABLE=True) +AWS_STORAGE_BUCKET_NAME= +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_S3_REGION_NAME= +AWS_S3_ENDPOINT_URL= +AWS_S3_CUSTOM_DOMAIN= +AWS_S3_USE_SSL=True +AWS_QUERYSTRING_AUTH=False +AWS_DEFAULT_ACL=public-read + # Stripe STRIPE_PUBLISHABLE_KEY= STRIPE_SECRET_KEY= diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index b3470d7..b9cdc65 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -35,6 +35,7 @@ Templates use Django's inheritance pattern: - **Image uploads**: Organized in `tienda/static/media/images/` via `upload_to='images/'` in ImageField - **Access**: Media files served automatically in development via Django's static file handler - **Image model**: Located in [tienda/models.py](tienda/models.py) with `ImageField(upload_to='images/')` +- **S3 mode**: if `S3_ENABLE=True` (case-insensitive), static and media switch to S3 storages instead of the local filesystem; Nginx should proxy the app only and the browser should load asset URLs from the bucket or CDN ## Shipping Restrictions - **Zona de envío**: Solo se vende/envía dentro de la provincia de Almería diff --git a/nginx.conf b/nginx.conf index a2a3e58..7e100bf 100644 --- a/nginx.conf +++ b/nginx.conf @@ -34,7 +34,9 @@ http { listen 80; server_name _; - # Archivos estáticos generados por collectstatic. + # Modo local: sirve static/media desde volúmenes montados. + # Si S3_ENABLE=True, estos bloques no se usan y el navegador debe + # cargar los assets directamente desde el bucket o CDN. location /static/ { alias /static/; expires 30d; @@ -42,7 +44,7 @@ http { access_log off; } - # Archivos subidos por usuarios. + # Archivos subidos por usuarios en modo local. location /media/ { alias /media/; expires 7d; diff --git a/proyecto/settings.py b/proyecto/settings.py index 2732df4..933895a 100644 --- a/proyecto/settings.py +++ b/proyecto/settings.py @@ -53,6 +53,21 @@ def env_int(name: str, default: int) -> int: return default return int(value) + +def env_str(name: str, default: str = '') -> str: + value = os.getenv(name) + if value is None: + return default + return value.strip() + + +def env_optional_str(name: str) -> str | None: + value = os.getenv(name) + if value is None: + return None + value = value.strip() + return value or None + # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent load_dotenv(BASE_DIR / '.env') @@ -66,6 +81,7 @@ SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-#g((q@lvnkt(j6)2(gvtn0px)r # SECURITY WARNING: don't run with debug turned on in production! DEBUG = env_bool('DEBUG', True) +S3_ENABLE = env_bool('S3_ENABLE', False) ALLOWED_HOSTS = env_list('ALLOWED_HOSTS', [ '192.168.1.142', @@ -87,9 +103,11 @@ INSTALLED_APPS = [ 'compressor', ] +if S3_ENABLE: + INSTALLED_APPS.append('storages') + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', - 'whitenoise.middleware.WhiteNoiseMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', @@ -98,6 +116,9 @@ MIDDLEWARE = [ 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] +if not S3_ENABLE: + MIDDLEWARE.insert(1, 'whitenoise.middleware.WhiteNoiseMiddleware') + ROOT_URLCONF = 'proyecto.urls' TEMPLATES = [ @@ -211,6 +232,27 @@ STORAGES = { }, } +if S3_ENABLE: + AWS_STORAGE_BUCKET_NAME = env_str('AWS_STORAGE_BUCKET_NAME') or None + AWS_ACCESS_KEY_ID = env_optional_str('AWS_ACCESS_KEY_ID') + AWS_SECRET_ACCESS_KEY = env_optional_str('AWS_SECRET_ACCESS_KEY') + AWS_S3_REGION_NAME = env_optional_str('AWS_S3_REGION_NAME') + AWS_S3_ENDPOINT_URL = env_optional_str('AWS_S3_ENDPOINT_URL') + AWS_S3_CUSTOM_DOMAIN = env_optional_str('AWS_S3_CUSTOM_DOMAIN') + AWS_S3_USE_SSL = env_bool('AWS_S3_USE_SSL', True) + AWS_QUERYSTRING_AUTH = env_bool('AWS_QUERYSTRING_AUTH', False) + AWS_DEFAULT_ACL = env_str('AWS_DEFAULT_ACL', 'public-read') or None + AWS_S3_OBJECT_PARAMETERS = {} + + STORAGES = { + 'default': { + 'BACKEND': 'tienda.storage_backends.MediaStorage', + }, + 'staticfiles': { + 'BACKEND': 'tienda.storage_backends.StaticStorage', + }, + } + STATICFILES_FINDERS = [ 'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder', diff --git a/proyecto/urls.py b/proyecto/urls.py index 57c06ad..ed79491 100644 --- a/proyecto/urls.py +++ b/proyecto/urls.py @@ -28,5 +28,5 @@ urlpatterns = [ path('api/', api.urls) ] -if settings.DEBUG: +if settings.DEBUG and not settings.S3_ENABLE: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/requirements.txt b/requirements.txt index db7b304..0e56595 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,6 +14,7 @@ Django==6.0.4 django-appconf==1.2.0 django-redis==5.4.0 django_compressor==4.6.0 +django-storages[boto3]==1.14.6 gunicorn==25.1.0 idna==3.11 Jinja2==3.1.6 @@ -22,6 +23,7 @@ MarkupSafe==3.0.3 packaging==26.0 paypalrestsdk==1.13.3 pillow==12.2.0 +boto3==1.42.97 prompt_toolkit==3.0.52 pycparser==3.0 pyOpenSSL==26.0.0 diff --git a/tienda/storage_backends.py b/tienda/storage_backends.py new file mode 100644 index 0000000..1b040ac --- /dev/null +++ b/tienda/storage_backends.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from storages.backends.s3 import S3ManifestStaticStorage, S3Storage + + +class StaticStorage(S3ManifestStaticStorage): + location = 'static' + default_acl = 'public-read' + querystring_auth = False + file_overwrite = True + object_parameters = { + 'CacheControl': 'public, max-age=31536000, immutable', + } + + +class MediaStorage(S3Storage): + location = 'media' + default_acl = 'public-read' + querystring_auth = False + file_overwrite = False + object_parameters = { + 'CacheControl': 'public, max-age=604800', + } \ No newline at end of file From 30f260c9bf90134e0cdcf1aaa31df5be78b9fd80 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 29 Apr 2026 08:12:57 +0200 Subject: [PATCH 35/47] feat: add support for local asset URLs in S3 storage backends Co-authored-by: Copilot --- proyecto/settings.py | 5 ++--- tienda/storage_backends.py | 23 ++++++++++++++++++++++- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/proyecto/settings.py b/proyecto/settings.py index 933895a..6e66468 100644 --- a/proyecto/settings.py +++ b/proyecto/settings.py @@ -82,6 +82,7 @@ SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-#g((q@lvnkt(j6)2(gvtn0px)r # SECURITY WARNING: don't run with debug turned on in production! DEBUG = env_bool('DEBUG', True) S3_ENABLE = env_bool('S3_ENABLE', False) +S3_USE_LOCAL_URLS = env_bool('S3_USE_LOCAL_URLS', False) ALLOWED_HOSTS = env_list('ALLOWED_HOSTS', [ '192.168.1.142', @@ -426,6 +427,4 @@ CELERY_RESULT_SERIALIZER = 'json' SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") USE_X_FORWARDED_HOST = True -SECURE_REFERER_POLICY = "strict-origin-when-cross-origin" - -print(f"DEBUG: ALLOWED_HOSTS is {ALLOWED_HOSTS}") \ No newline at end of file +SECURE_REFERER_POLICY = "strict-origin-when-cross-origin" \ No newline at end of file diff --git a/tienda/storage_backends.py b/tienda/storage_backends.py index 1b040ac..54a4508 100644 --- a/tienda/storage_backends.py +++ b/tienda/storage_backends.py @@ -1,8 +1,19 @@ from __future__ import annotations +import os + +from django.utils.encoding import iri_to_uri from storages.backends.s3 import S3ManifestStaticStorage, S3Storage +def _use_local_asset_urls() -> bool: + return os.getenv('S3_USE_LOCAL_URLS', '').strip().lower() in {'1', 'true', 'yes', 'on'} + + +def _local_asset_url(prefix: str, name: str) -> str: + return iri_to_uri(f'/{prefix}/{name.lstrip("/")}') + + class StaticStorage(S3ManifestStaticStorage): location = 'static' default_acl = 'public-read' @@ -12,6 +23,11 @@ class StaticStorage(S3ManifestStaticStorage): 'CacheControl': 'public, max-age=31536000, immutable', } + def url(self, name: str) -> str: + if _use_local_asset_urls(): + return _local_asset_url('static', name) + return super().url(name) + class MediaStorage(S3Storage): location = 'media' @@ -20,4 +36,9 @@ class MediaStorage(S3Storage): file_overwrite = False object_parameters = { 'CacheControl': 'public, max-age=604800', - } \ No newline at end of file + } + + def url(self, name: str) -> str: + if _use_local_asset_urls(): + return _local_asset_url('media', name) + return super().url(name) \ No newline at end of file From e78a936b21ace2508238323cd4f21f050eac78d1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Apr 2026 07:24:53 +0000 Subject: [PATCH 36/47] Fix terms link in register.html to point to terminos view Agent-Logs-Url: https://github.com/dsaub/proyecto-final/sessions/50c087d4-a283-4c38-bda2-5599d42d382f Co-authored-by: dsaub <54474838+dsaub@users.noreply.github.com> --- tienda/templates/tienda/register.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tienda/templates/tienda/register.html b/tienda/templates/tienda/register.html index ce794d8..c648827 100644 --- a/tienda/templates/tienda/register.html +++ b/tienda/templates/tienda/register.html @@ -36,7 +36,7 @@ From a2e6e5ad97a9b8ffb5c1e4e117f22f7e4cb14c99 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 29 Apr 2026 09:30:09 +0200 Subject: [PATCH 37/47] refactor: change StaticStorage to inherit from S3Storage instead of S3ManifestStaticStorage Co-authored-by: Copilot --- tienda/storage_backends.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tienda/storage_backends.py b/tienda/storage_backends.py index 54a4508..bad7897 100644 --- a/tienda/storage_backends.py +++ b/tienda/storage_backends.py @@ -3,7 +3,7 @@ from __future__ import annotations import os from django.utils.encoding import iri_to_uri -from storages.backends.s3 import S3ManifestStaticStorage, S3Storage +from storages.backends.s3 import S3Storage def _use_local_asset_urls() -> bool: @@ -14,7 +14,7 @@ def _local_asset_url(prefix: str, name: str) -> str: return iri_to_uri(f'/{prefix}/{name.lstrip("/")}') -class StaticStorage(S3ManifestStaticStorage): +class StaticStorage(S3Storage): location = 'static' default_acl = 'public-read' querystring_auth = False From ba75a0ab2e4db4df5afe522c53652582410fc3f0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Apr 2026 07:38:20 +0000 Subject: [PATCH 38/47] Style skip link to visually integrate with navbar header Agent-Logs-Url: https://github.com/dsaub/proyecto-final/sessions/a04a8e28-dcc3-4338-8ee9-49c7494bf486 Co-authored-by: dsaub <54474838+dsaub@users.noreply.github.com> --- tienda/static/css/custom.css | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/tienda/static/css/custom.css b/tienda/static/css/custom.css index e80914d..56a7bce 100644 --- a/tienda/static/css/custom.css +++ b/tienda/static/css/custom.css @@ -1,15 +1,22 @@ .skip-link { - position: absolute; - top: -40px; - left: 0; - background: #513CB0; - color: #fff; - padding: 8px 16px; - font-weight: 600; - z-index: 10000; + position: fixed; + top: -100%; + left: 50%; + transform: translateX(-50%); + background: #fff; + color: #513CB0; + padding: 8px 24px; + font-weight: 700; + font-size: 0.9rem; + z-index: 10001; text-decoration: none; - border-radius: 0 0 4px 0; - transition: top 0.2s; + border-radius: 0 0 8px 8px; + border: 2px solid #513CB0; + border-top: none; + box-shadow: 0 4px 12px rgba(81, 60, 176, 0.25); + transition: top 0.2s ease; + outline: none; + white-space: nowrap; } .skip-link:focus, .skip-link:focus-visible { From 81d369421057fa7d592f5c1e3144489767028fd1 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 30 Apr 2026 07:35:28 +0200 Subject: [PATCH 39/47] Solving issue #57 Auth 500 bug --- tienda/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tienda/views.py b/tienda/views.py index b5af383..3e003d3 100644 --- a/tienda/views.py +++ b/tienda/views.py @@ -240,6 +240,9 @@ def login(request: HttpRequest): # Autenticar usuario user = authenticate(request, username=username, password=password) + if user is None: # Bug de error 500 en caso de fallar la contra + messages.error(request, "Correo electrónico o contraseña incorrectos.") + return render(request, "tienda/login.html") user = User.objects.get(username=user.username) if user.registration_status == "CR": audit_logger.info( From 830966f3eeae37abddb8c36891639e4a10c2c600 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 30 Apr 2026 07:37:13 +0200 Subject: [PATCH 40/47] Fix issue #58 not deleting verification code. --- tienda/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tienda/views.py b/tienda/views.py index 3e003d3..400dded 100644 --- a/tienda/views.py +++ b/tienda/views.py @@ -2317,6 +2317,7 @@ def reset_password_phase2(request: HttpRequest, code: str): user = ver_code.user user.set_password(password) user.save() + ver_code.delete() # Delete Verification code after changing password messages.success(request, "Se ha cambiado la contraseña!") return redirect(reverse("index")) From 297b319a2096c5d326b625926a9fbc0d194ee24e Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 30 Apr 2026 07:38:17 +0200 Subject: [PATCH 41/47] Fix issue #59 duplicate reset_password --- tienda/views.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tienda/views.py b/tienda/views.py index 400dded..001dfa8 100644 --- a/tienda/views.py +++ b/tienda/views.py @@ -2258,13 +2258,6 @@ def verify(request: HttpRequest, code: str): return HttpResponse("

Error

No existe el codigo de verificación

") -def reset_password(request: HttpRequest): - if request.user.is_authenticated: - return redirect("index") - - - return render(request, "tienda/reset_password", {}) - def rgpd(request: HttpRequest): return render(request, "tienda/rgpd.html", {}) From 033c52a3659ff877bc64c445235174bacd0a786d Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 30 Apr 2026 07:39:14 +0200 Subject: [PATCH 42/47] Fix issue #60 verification code generation --- tienda/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tienda/models.py b/tienda/models.py index 1cab708..f0b9775 100644 --- a/tienda/models.py +++ b/tienda/models.py @@ -46,7 +46,7 @@ class VerificationCode(models.Model): def generate(user: User, code_mode: str) -> VerificationCode: while True: - code = "".join(random.choices(string.ascii_letters+string.digits+string.punctuation)) + code = "".join(random.choices(string.ascii_letters+string.digits+string.punctuation, k=64)) if not VerificationCode.objects.filter(code=code).exists(): return VerificationCode.objects.create( code = code, From 756f1ad36b3d2ff538c5a08f5718756069b4041b Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 30 Apr 2026 07:43:18 +0200 Subject: [PATCH 43/47] Remove entire api for issue #61 --- proyecto/urls.py | 4 +--- requirements.txt | 3 +-- tienda/api.py | 12 ------------ 3 files changed, 2 insertions(+), 17 deletions(-) delete mode 100644 tienda/api.py diff --git a/proyecto/urls.py b/proyecto/urls.py index ed79491..bce3227 100644 --- a/proyecto/urls.py +++ b/proyecto/urls.py @@ -19,13 +19,11 @@ from django.conf.urls.static import static from django.contrib import admin from django.urls import include, path from tienda import views as tienda_views -from tienda.api import api urlpatterns = [ path('', tienda_views.home, name='home'), path('admin/', admin.site.urls), - path('tienda/', include('tienda.urls')), - path('api/', api.urls) + path('tienda/', include('tienda.urls')) ] if settings.DEBUG and not settings.S3_ENABLE: diff --git a/requirements.txt b/requirements.txt index 0e56595..b1b1161 100644 --- a/requirements.txt +++ b/requirements.txt @@ -43,5 +43,4 @@ vine==5.1.0 wcwidth==0.6.0 whitenoise==6.12.0 fpdf2==2.8.7 -psycopg2-binary==2.9.11 -django-ninja==1.6.2 \ No newline at end of file +psycopg2-binary==2.9.11 \ No newline at end of file diff --git a/tienda/api.py b/tienda/api.py deleted file mode 100644 index 27f43dc..0000000 --- a/tienda/api.py +++ /dev/null @@ -1,12 +0,0 @@ -from ninja import NinjaAPI -from .models import Product -api = NinjaAPI() - -@api.get("/hola") -def hola(request): - return {"mensaje": "¡Hola Mundo!"} - -@api.get("/products") -def products(request): - productos = Product.objects.all() - return [producto.to_dict() for producto in productos] \ No newline at end of file From c190a65e57ed1d9ca5515d9c3ff36b9725c60b3e Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 4 May 2026 12:25:21 +0200 Subject: [PATCH 44/47] Opencore files --- .github/workflows/opencode.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/opencode.yml diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml new file mode 100644 index 0000000..515af89 --- /dev/null +++ b/.github/workflows/opencode.yml @@ -0,0 +1,33 @@ +name: opencode + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + +jobs: + opencode: + if: | + contains(github.event.comment.body, ' /oc') || + startsWith(github.event.comment.body, '/oc') || + contains(github.event.comment.body, ' /opencode') || + startsWith(github.event.comment.body, '/opencode') + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + pull-requests: read + issues: read + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Run opencode + uses: anomalyco/opencode/github@latest + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + with: + model: openai/gpt-5.3-codex \ No newline at end of file From 6ed4fb19544af2bd4482d6c704838af6d156309f Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 4 May 2026 12:30:09 +0200 Subject: [PATCH 45/47] Remove punctuation Signs so we generate 'url-safe' codes --- tienda/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tienda/models.py b/tienda/models.py index f0b9775..553afb6 100644 --- a/tienda/models.py +++ b/tienda/models.py @@ -46,7 +46,7 @@ class VerificationCode(models.Model): def generate(user: User, code_mode: str) -> VerificationCode: while True: - code = "".join(random.choices(string.ascii_letters+string.digits+string.punctuation, k=64)) + code = "".join(random.choices(string.ascii_letters+string.digits, k=64)) if not VerificationCode.objects.filter(code=code).exists(): return VerificationCode.objects.create( code = code, From d75165e31a9e38053fd9de7f570dc7ec16e22235 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 4 May 2026 12:31:49 +0200 Subject: [PATCH 46/47] Arreglar el bug de posiblemente creator y primary_image este en None... --- tienda/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tienda/models.py b/tienda/models.py index 553afb6..f15a28f 100644 --- a/tienda/models.py +++ b/tienda/models.py @@ -111,9 +111,9 @@ class Product(models.Model): "price": self.price, "stock": self.stock, "category": self.category.to_dict(), - "primary_image": self.primary_image.to_dict(), + "primary_image": self.primary_image.to_dict() if self.primary_image else None, "secondary_images": [secondary_image.to_dict() for secondary_image in self.secondary_images.all()], - "creator": self.creator.to_dict() + "creator": self.creator.to_dict() if self.creator else None } From 191f8823d4566be0e84d2d520f87ca69988f43ca Mon Sep 17 00:00:00 2001 From: "Daniel (elordenador)" <54474838+dsaub@users.noreply.github.com> Date: Mon, 4 May 2026 12:32:16 +0200 Subject: [PATCH 47/47] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- proyecto/urls.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/proyecto/urls.py b/proyecto/urls.py index bce3227..d7813f5 100644 --- a/proyecto/urls.py +++ b/proyecto/urls.py @@ -26,5 +26,7 @@ urlpatterns = [ path('tienda/', include('tienda.urls')) ] -if settings.DEBUG and not settings.S3_ENABLE: +if settings.DEBUG and ( + not settings.S3_ENABLE or getattr(settings, 'S3_USE_LOCAL_URLS', False) +): urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)