Compare commits

..

44 Commits

Author SHA1 Message Date
Daniel (elordenador) 191f8823d4 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-04 12:32:16 +02:00
elordenador d75165e31a Arreglar el bug de posiblemente creator y primary_image este en None... 2026-05-04 12:31:49 +02:00
elordenador 6ed4fb1954 Remove punctuation Signs so we generate 'url-safe' codes 2026-05-04 12:30:09 +02:00
elordenador c190a65e57 Opencore files 2026-05-04 12:25:21 +02:00
elordenador 756f1ad36b Remove entire api for issue #61 2026-04-30 07:43:18 +02:00
elordenador 033c52a365 Fix issue #60 verification code generation 2026-04-30 07:39:14 +02:00
elordenador 297b319a20 Fix issue #59 duplicate reset_password 2026-04-30 07:38:17 +02:00
elordenador 830966f3ee Fix issue #58 not deleting verification code. 2026-04-30 07:37:13 +02:00
elordenador 81d3694210 Solving issue #57 Auth 500 bug 2026-04-30 07:35:28 +02:00
Daniel (elordenador) dce0937511 Merge pull request #56 from dsaub/rama-usabilidad
Agregado parche de usabilidad
2026-04-29 17:02:39 +02:00
Daniel (elordenador) 7f8f70bc42 Merge pull request #55 from dsaub/copilot/unify-add-to-cart-post
[WIP] Fix inconsistency in add to cart action using POST
2026-04-29 11:18:42 +02:00
Daniel (elordenador) 7203a07350 Merge pull request #48 from dsaub/copilot/add-skip-link-to-body
Add "Saltar al contenido" skip link for keyboard/screen reader accessibility
2026-04-29 11:15:44 +02:00
copilot-swe-agent[bot] ba75a0ab2e 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>
2026-04-29 07:38:20 +00:00
Daniel (elordenador) 1f7db2db3a Merge pull request #54 from dsaub/copilot/fix-terms-link-destination
[WIP] Fix terms link without real destination
2026-04-29 09:30:50 +02:00
copilot-swe-agent[bot] e78a936b21 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>
2026-04-29 07:24:53 +00:00
Daniel (elordenador) 68dbbcad07 Merge pull request #52 from dsaub/copilot/fix-payment-errors-modal
[WIP] Fix payment errors with inline error container
2026-04-28 09:19:53 +02:00
copilot-swe-agent[bot] a94c256ad5 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>
2026-04-28 07:18:04 +00:00
Daniel (elordenador) 0ff70589b9 Merge pull request #47 from dsaub/copilot/implement-aria-combobox-pattern
Implement ARIA combobox/listbox pattern for search suggestions
2026-04-28 09:17:11 +02:00
copilot-swe-agent[bot] 25c6fc7315 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>
2026-04-28 07:16:26 +00:00
Daniel (elordenador) 9f598f56fe Merge pull request #53 from dsaub/copilot/add-hidden-label-for-quantity
fix(a11y): add unique aria-label to cart quantity inputs
2026-04-28 09:16:22 +02:00
Daniel (elordenador) b905ef435a Merge pull request #49 from dsaub/copilot/improve-stock-iva-color-dependence
fix: replace color-only stock/IVA indicators with explicit text and icons (WCAG Perceptible)
2026-04-28 09:15:22 +02:00
copilot-swe-agent[bot] d849e7d3e6 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>
2026-04-28 07:14:49 +00:00
Daniel (elordenador) 5fa127ddf7 Merge pull request #51 from dsaub/copilot/complete-aria-support-for-tabs
fix: complete WAI-ARIA tabs pattern in checkout payment tabs
2026-04-28 09:14:47 +02:00
copilot-swe-agent[bot] 3f521d81b4 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>
2026-04-28 07:13:11 +00:00
copilot-swe-agent[bot] 6828074dd1 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>
2026-04-28 07:11:24 +00:00
copilot-swe-agent[bot] ad9fa741e5 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>
2026-04-28 07:11:20 +00:00
Daniel (elordenador) 07486bb5ec Merge pull request #50 from dsaub/copilot/add-fieldset-legend-to-radio-group
fix(a11y): wrap saved-card radio group in fieldset/legend
2026-04-28 09:11:19 +02:00
copilot-swe-agent[bot] a36740b02d fix: add explicit text and icons for stock/IVA accessibility (WCAG Perceptible)
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>
2026-04-28 07:10:02 +00:00
copilot-swe-agent[bot] cb31784097 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>
2026-04-28 07:09:44 +00:00
copilot-swe-agent[bot] 17935c6160 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>
2026-04-28 07:09:31 +00:00
copilot-swe-agent[bot] 63df5cf73f 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>
2026-04-28 07:08:35 +00:00
copilot-swe-agent[bot] fe61b3a212 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>
2026-04-28 07:07:12 +00:00
copilot-swe-agent[bot] d55026b69d Initial plan 2026-04-28 07:06:40 +00:00
copilot-swe-agent[bot] bdae5b073c Initial plan 2026-04-28 07:06:32 +00:00
copilot-swe-agent[bot] 183685519a Initial plan 2026-04-28 07:06:25 +00:00
copilot-swe-agent[bot] 0a9b9138bc Initial plan 2026-04-28 07:06:16 +00:00
copilot-swe-agent[bot] 3eb963fadf Initial plan 2026-04-28 07:06:05 +00:00
copilot-swe-agent[bot] 8a5edce758 Initial plan 2026-04-28 07:05:57 +00:00
copilot-swe-agent[bot] 0eaaa8d19d Initial plan 2026-04-28 07:05:42 +00:00
copilot-swe-agent[bot] 71cbf6825e Initial plan 2026-04-28 07:05:32 +00:00
copilot-swe-agent[bot] dd49a6a7d6 Initial plan 2026-04-28 07:05:22 +00:00
Daniel (elordenador) f785b1862f Merge pull request #46 from dsaub/copilot/fix-search-button-icon
fix(a11y): add visible text and aria-label to search button
2026-04-28 09:04:47 +02:00
copilot-swe-agent[bot] ea6c9c49a0 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>
2026-04-28 06:56:38 +00:00
copilot-swe-agent[bot] edda5aca50 Initial plan 2026-04-28 06:50:48 +00:00
14 changed files with 230 additions and 72 deletions
+33
View File
@@ -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
+4 -4
View File
@@ -19,14 +19,14 @@ 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:
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)
-1
View File
@@ -44,4 +44,3 @@ wcwidth==0.6.0
whitenoise==6.12.0
fpdf2==2.8.7
psycopg2-binary==2.9.11
django-ninja==1.6.2
-12
View File
@@ -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]
+3 -3
View File
@@ -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, k=64))
if not VerificationCode.objects.filter(code=code).exists():
return VerificationCode.objects.create(
code = code,
@@ -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
}
+25
View File
@@ -1,3 +1,28 @@
.skip-link {
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 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 {
top: 0;
}
@media (min-width: 1250px) {
.grid {
display: grid;
+77 -15
View File
@@ -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 {
@@ -78,6 +81,7 @@
{% block head %}{% endblock %}
</head>
<body class="d-flex flex-column min-vh-100">
<a href="#main-content" class="skip-link">Saltar al contenido</a>
{% cache 500 sidebar request.user.username %}
<nav class="navbar navbar-expand-md header" role="banner">
<div class="container-fluid">
@@ -107,10 +111,10 @@
<!-- Barra de búsqueda con sugerencias -->
<form class="search-suggestions-container" method="GET" action="{% url 'search' %}" role="search" id="searchForm">
<div class="input-group">
<input class="form-control" type="search" name="q" id="searchInput" placeholder="Buscar productos..." aria-label="Buscar" autocomplete="off">
<button class="btn btn-outline-primary" type="submit">🔍</button>
<input class="form-control" type="search" name="q" id="searchInput" placeholder="Buscar productos..." aria-label="Buscar" autocomplete="off" role="combobox" aria-expanded="false" aria-autocomplete="list" aria-controls="searchSuggestions" aria-activedescendant="" aria-haspopup="listbox">
<button class="btn btn-outline-primary" type="submit" aria-label="Buscar productos">🔍 Buscar</button>
</div>
<div class="search-suggestions" id="searchSuggestions"></div>
<div class="search-suggestions" id="searchSuggestions" role="listbox" aria-label="Sugerencias de búsqueda"></div>
</form>
<div class="navbar-nav ms-auto d-flex align-items-md-center gap-2 flex-wrap" role="navigation">
@@ -137,7 +141,7 @@
</nav>
{% endcache %}
<div class="container-fluid flex-grow-1 d-flex flex-column" role="main">
<div id="main-content" class="container-fluid flex-grow-1 d-flex flex-column" role="main">
<!-- Mensajes -->
{% if messages %}
<div class="row mt-3">
@@ -201,6 +205,35 @@
const searchSuggestions = document.getElementById('searchSuggestions');
const searchForm = document.getElementById('searchForm');
let searchTimeout;
let currentFocusIndex = -1;
// Helpers para gestionar el estado ARIA del combobox
function openSuggestions() {
searchSuggestions.classList.add('show');
searchInput.setAttribute('aria-expanded', 'true');
}
function closeSuggestions() {
searchSuggestions.classList.remove('show');
searchInput.setAttribute('aria-expanded', 'false');
searchInput.setAttribute('aria-activedescendant', '');
currentFocusIndex = -1;
}
function updateFocus(options) {
options.forEach((option, index) => {
const active = index === currentFocusIndex;
option.classList.toggle('active', active);
option.setAttribute('aria-selected', active ? 'true' : 'false');
});
if (currentFocusIndex >= 0) {
const activeOption = options[currentFocusIndex];
searchInput.setAttribute('aria-activedescendant', activeOption.id);
activeOption.scrollIntoView({ block: 'nearest' });
} else {
searchInput.setAttribute('aria-activedescendant', '');
}
}
// Escuchar cambios en el input
searchInput.addEventListener('input', function() {
@@ -208,7 +241,7 @@
const query = this.value.trim();
if (query.length < 2) {
searchSuggestions.classList.remove('show');
closeSuggestions();
return;
}
@@ -218,6 +251,31 @@
}, 300);
});
// Navegación por teclado (ArrowDown/ArrowUp/Enter/Escape)
searchInput.addEventListener('keydown', function(event) {
const options = searchSuggestions.querySelectorAll('[role="option"]');
if (!options.length || !searchSuggestions.classList.contains('show')) {
return;
}
if (event.key === 'ArrowDown') {
event.preventDefault();
currentFocusIndex = Math.min(currentFocusIndex + 1, options.length - 1);
updateFocus(options);
} else if (event.key === 'ArrowUp') {
event.preventDefault();
currentFocusIndex = Math.max(currentFocusIndex - 1, -1);
updateFocus(options);
} else if (event.key === 'Enter' && currentFocusIndex >= 0) {
event.preventDefault();
const selected = options[currentFocusIndex];
window.location.href = selected.dataset.href;
} else if (event.key === 'Escape') {
closeSuggestions();
searchInput.focus();
}
});
// Función para obtener sugerencias del servidor
function fetchSuggestions(query) {
fetch(`{% url 'search_suggestions' %}?q=${encodeURIComponent(query)}`)
@@ -227,33 +285,37 @@
})
.catch(error => {
console.error('Error fetching suggestions:', error);
searchSuggestions.classList.remove('show');
closeSuggestions();
});
}
// Función para mostrar las sugerencias
function displaySuggestions(suggestions, query) {
currentFocusIndex = -1;
if (suggestions.length === 0) {
searchSuggestions.innerHTML = '<div class="search-suggestion-item text-muted">No se encontraron productos</div>';
searchSuggestions.classList.add('show');
openSuggestions();
return;
}
let html = '';
suggestions.forEach(suggestion => {
suggestions.forEach((suggestion, index) => {
// Resaltar la coincidencia en el nombre
const highlightedName = highlightMatch(suggestion.name, query);
const priceWithVAT = (suggestion.price * 1.21).toFixed(2);
html += `
<a href="/tienda/producto/${suggestion.id}" class="search-suggestion-item text-decoration-none">
<div class="search-suggestion-item" role="option" id="search-option-${index}"
aria-selected="false" tabindex="-1"
data-href="/tienda/producto/${suggestion.id}"
onclick="window.location.href=this.dataset.href">
<span class="suggestion-name">${highlightedName}</span>
<span class="suggestion-price">€${priceWithVAT}</span>
</a>
</div>
`;
});
searchSuggestions.innerHTML = html;
searchSuggestions.classList.add('show');
openSuggestions();
}
// Función para resaltar el texto que coincide
@@ -265,19 +327,19 @@
// Cerrar sugerencias cuando se hace clic fuera
document.addEventListener('click', function(event) {
if (!searchForm.contains(event.target)) {
searchSuggestions.classList.remove('show');
closeSuggestions();
}
});
// Cerrar sugerencias al enviar el formulario
searchForm.addEventListener('submit', function() {
searchSuggestions.classList.remove('show');
closeSuggestions();
});
// Mostrar sugerencias al hacer clic en el input (si hay texto)
searchInput.addEventListener('focus', function() {
if (this.value.trim().length >= 2 && searchSuggestions.innerHTML) {
searchSuggestions.classList.add('show');
openSuggestions();
}
});
</script>
+3 -3
View File
@@ -51,7 +51,7 @@
<td>
<form method="post" action="{% url 'update_cart_item' item.id %}" class="d-flex align-items-center" style="max-width: 150px;">
{% csrf_token %}
<input type="number" name="quantity" value="{{ item.quantity }}" min="1" max="{{ item.product.stock }}" class="form-control form-control-sm me-2" style="width: 70px;">
<input type="number" name="quantity" value="{{ item.quantity }}" min="1" max="{{ item.product.stock }}" class="form-control form-control-sm me-2" style="width: 70px;" aria-label="Cantidad para {{ item.product.name }}">
<button type="submit" class="btn btn-sm btn-primary">Actualizar</button>
</form>
</td>
@@ -59,7 +59,7 @@
{% if item.product.stock > 0 %}
{{ item.product.stock }}
{% else %}
<span class="text-danger">0</span>
<span class="text-danger" role="status"><span aria-hidden="true"></span> Sin stock</span>
{% endif %}
</td>
<td class="price">{{ item.get_subtotal_with_vat|format_price }} €</td>
@@ -89,7 +89,7 @@
</div>
<div class="d-flex justify-content-between mb-2">
<span>IVA (21%)</span>
<span class="price text-success">{{ cart.get_vat_amount|format_price }} €</span>
<span class="price text-success"><span aria-hidden="true"></span> {{ cart.get_vat_amount|format_price }} €</span>
</div>
<div class="d-flex justify-content-between mb-2">
<span>Envío</span>
+64 -16
View File
@@ -127,22 +127,25 @@
<!-- Tabs -->
<ul class="nav nav-tabs mb-3" id="paymentTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="tab-card" data-tab="pane-card" type="button" role="tab">
<button class="nav-link active" id="tab-card" data-tab="pane-card" type="button"
role="tab" aria-selected="true" aria-controls="pane-card" tabindex="0">
💳 Tarjeta
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="tab-paypal" data-tab="pane-paypal" type="button" role="tab">
<button class="nav-link" id="tab-paypal" data-tab="pane-paypal" type="button"
role="tab" aria-selected="false" aria-controls="pane-paypal" tabindex="-1">
🅿️ PayPal
</button>
</li>
</ul>
<!-- Tarjeta tab -->
<div id="pane-card" class="payment-tab-content active">
<div id="pane-card" class="payment-tab-content active"
role="tabpanel" aria-labelledby="tab-card" tabindex="0">
{% if saved_cards %}
<div class="mb-3">
<p class="fw-semibold">Tarjetas guardadas:</p>
<fieldset class="mb-3">
<legend class="fw-semibold fs-6 mb-2">Selección de tarjeta</legend>
{% for card in saved_cards %}
<div class="form-check mb-2">
<input class="form-check-input" type="radio" name="saved_card_choice" id="card-{{ card.id }}" value="{{ card.id }}" data-pm-id="{{ card.stripe_payment_method_id }}" {% if card.is_default %}checked{% endif %}>
@@ -156,7 +159,7 @@
<input class="form-check-input" type="radio" name="saved_card_choice" id="card-new" value="new">
<label class="form-check-label" for="card-new">Usar nueva tarjeta</label>
</div>
</div>
</fieldset>
{% endif %}
<div id="new-card-section" {% if saved_cards %}style="display:none;"{% endif %}>
@@ -183,7 +186,8 @@
</div>
<!-- PayPal tab -->
<div id="pane-paypal" class="payment-tab-content">
<div id="pane-paypal" class="payment-tab-content"
role="tabpanel" aria-labelledby="tab-paypal" tabindex="0">
{% if saved_paypal %}
<div class="alert alert-light border mb-3">
<small class="text-muted">Cuenta PayPal guardada:</small>
@@ -196,6 +200,7 @@
Guardar esta cuenta de PayPal para futuras compras
</label>
</div>
<div id="paypal-errors" class="alert alert-danger d-none mt-2" role="alert"></div>
<div id="paypal-button-container"></div>
{% if not addresses or stock_issues %}
<div class="alert alert-warning mt-2">Selecciona una dirección de envío válida para activar el pago.</div>
@@ -221,12 +226,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();
}
});
});
@@ -254,10 +289,13 @@ 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');
if (cardErrors) cardErrors.textContent = 'Selecciona una dirección de envío para continuar.';
return;
}
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;
@@ -335,9 +373,15 @@ 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');
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'));
}
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 },
@@ -359,7 +403,11 @@ paypal.Buttons({
showSuccess(result.transaction_code);
},
onError: (err) => {
alert('Error en el pago con PayPal: ' + err);
const paypalErrors = document.getElementById('paypal-errors');
if (paypalErrors) {
paypalErrors.textContent = 'Error en el pago con PayPal: ' + err;
paypalErrors.classList.remove('d-none');
}
},
}).render('#paypal-button-container');
{% endif %}
+5 -3
View File
@@ -196,9 +196,11 @@
<a href="{% url 'producto' product.id %}" class="btn btn-primary btn-sm flex-grow-1">
Ver detalles
</a>
<a href="{% url 'add_to_cart' product.id %}" class="btn btn-outline-primary btn-sm">
🛒
</a>
<form method="post" action="{% url 'add_to_cart' product.id %}" style="display:inline;">
{% csrf_token %}
<input type="hidden" name="quantity" value="1">
<button type="submit" class="btn btn-outline-primary btn-sm">🛒</button>
</form>
</div>
</div>
</div>
+3 -3
View File
@@ -34,16 +34,16 @@
<div class="small text-primary font-weight-bold mb-2">Precio total (IVA incluido):</div>
<span class="price" style="font-size: 2rem; color: #28a745;">€{{ product.get_price_with_vat|format_price }}</span>
<div class="small text-success mt-2">IVA: €{{ product.get_vat_amount|format_price }}</div>
<div class="small text-success mt-2"><span aria-hidden="true"></span> IVA incluido: €{{ product.get_vat_amount|format_price }}</div>
</div>
<div id="descripcion" class="texto-ajustado">
{{ product.briefdesc }}
</div>
<div class="mt-3">
{% if product.stock > 0 %}
<span class="badge bg-success">Stock disponible: {{ product.stock }}</span>
<span class="badge bg-success" role="status"><span aria-hidden="true"></span> Stock disponible: {{ product.stock }}</span>
{% else %}
<span class="badge bg-danger">Sin stock</span>
<span class="badge bg-danger" role="status"><span aria-hidden="true"></span> Sin stock</span>
{% endif %}
</div>
+1 -1
View File
@@ -36,7 +36,7 @@
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="acceptTerms" name="terms" required>
<label class="form-check-label" for="acceptTerms">
Acepto los <a href="#" target="_blank">términos y condiciones</a>
Acepto los <a href="{% url 'terminos' %}" target="_blank">términos y condiciones</a>
</label>
</div>
+5 -3
View File
@@ -79,9 +79,11 @@
<a href="{% url 'producto' product.id %}" class="btn btn-primary btn-sm flex-grow-1">
Ver detalles
</a>
<a href="{% url 'add_to_cart' product.id %}" class="btn btn-outline-primary btn-sm">
🛒
</a>
<form method="post" action="{% url 'add_to_cart' product.id %}" style="display:inline;">
{% csrf_token %}
<input type="hidden" name="quantity" value="1">
<button type="submit" class="btn btn-outline-primary btn-sm">🛒</button>
</form>
</div>
</div>
</div>
+6 -7
View File
@@ -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
@@ -239,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(
@@ -704,6 +708,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:
@@ -2253,13 +2258,6 @@ def verify(request: HttpRequest, code: str):
return HttpResponse("<h1>Error</h1><p>No existe el codigo de verificación</p>")
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", {})
@@ -2312,6 +2310,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"))