diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
new file mode 100644
index 0000000..a34d4f8
--- /dev/null
+++ b/.github/workflows/docker.yml
@@ -0,0 +1,59 @@
+name: Build and Push Docker Image
+
+on:
+ push:
+ branches:
+ - '**' # Esto aplica para cualquier rama
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout del código
+ uses: actions/checkout@v6
+ - name: Configurar Python
+ uses: actions/setup-python@v6
+ with:
+ python-version: '3.14'
+ - name: Instalar dependencias
+ run: |
+ python -m pip install --upgrade pip
+ pip install -r requirements.txt
+ - name: Ejecutar tests
+ env:
+ DJANGO_SETTINGS_MODULE: proyecto.settings
+ run: |
+ python manage.py test
+ docker:
+ runs-on: ubuntu-latest
+ needs: test
+ permissions:
+ contents: read
+ packages: write # Necesario para subir a GHCR
+
+ steps:
+ - name: Checkout del código
+ uses: actions/checkout@v6
+
+ - name: Configurar Docker Buildx
+ uses: docker/setup-buildx-action@v4
+
+ - name: Login en GHCR
+ uses: docker/login-action@v4
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Preparar tag de imagen
+ run: |
+ TAG=$(echo "${{ github.ref_name }}" | sed 's/\//-/g')
+ echo "IMAGE_TAG=$TAG" >> $GITHUB_ENV
+
+ - name: Build y Push
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ push: true
+ # Sanitizamos el nombre de la rama (reemplazamos / por -)
+ tags: ghcr.io/dsaub/proyecto-mvc:${{ env.IMAGE_TAG }}
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index acf9d2d..9852d82 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,14 +4,9 @@ ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
-RUN adduser -D -S app
-
COPY requirements.txt /app/
RUN apk --no-cache update && apk --no-cache upgrade
-RUN apk add --no-cache --virtual .build-deps build-base mariadb-dev libffi-dev \
- && apk add --no-cache mariadb-connector-c \
- && pip install --no-cache-dir -r requirements.txt \
- && apk del .build-deps
+RUN pip install --no-cache-dir -r requirements.txt
COPY . /app/
RUN chmod +x /app/entrypoint.sh
@@ -21,8 +16,4 @@ EXPOSE 8000
RUN mkdir -pv /fonts
COPY tienda/static/fonts/ /fonts/
-RUN chown -R app: /app /fonts
-RUN chmod 770 /app/entrypoint.sh
-USER app
-
ENTRYPOINT ["/bin/sh", "/app/entrypoint.sh"]
\ No newline at end of file
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..076a0d0
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,6 @@
+docker:
+ docker build -t ghcr.io/dsaub/proyecto-mvc:development .
+ docker push ghcr.io/dsaub/proyecto-mvc:development
+
+test:
+ ./manage.py test
\ No newline at end of file
diff --git a/entrypoint.sh b/entrypoint.sh
index 7fdb705..c505069 100644
--- a/entrypoint.sh
+++ b/entrypoint.sh
@@ -1,11 +1,13 @@
#!/bin/sh
+set -eu
+
echo "Sleeping due to mysql..."
sleep 10
echo "Running DB migrations..."
python manage.py migrate
echo "Collecting STATIC..."
-python manage.py collectstatic --noinput
+python manage.py collectstatic --noinput --clear
echo "Running server!"
diff --git a/proyecto/settings.py b/proyecto/settings.py
index 9eac4f9..2732df4 100644
--- a/proyecto/settings.py
+++ b/proyecto/settings.py
@@ -203,7 +203,11 @@ STORAGES = {
'BACKEND': 'django.core.files.storage.FileSystemStorage',
},
'staticfiles': {
- 'BACKEND': 'whitenoise.storage.CompressedManifestStaticFilesStorage',
+ 'BACKEND': (
+ 'django.contrib.staticfiles.storage.StaticFilesStorage'
+ if DEBUG
+ else 'whitenoise.storage.CompressedManifestStaticFilesStorage'
+ ),
},
}
diff --git a/tienda/admin.py b/tienda/admin.py
index c31c77f..ab03a94 100644
--- a/tienda/admin.py
+++ b/tienda/admin.py
@@ -1,5 +1,5 @@
from django.contrib import admin
-from .models import Category, Image, Product, Cart, CartItem, Order, OrderItem, OrderMessage, StockReservation, StockReservationItem, User, VerificationCode
+from .models import Category, Image, Product, Cart, CartItem, Order, OrderItem, OrderMessage, StockReservation, StockReservationItem, User, VerificationCode, SavedPaymentMethod
# Register your models here.
admin.site.register(Category)
@@ -86,4 +86,11 @@ class StockReservationAdmin(admin.ModelAdmin):
list_display = ('id', 'user', 'session_key', 'status', 'payment_method', 'expires_at', 'created_at')
list_filter = ('status', 'payment_method', 'created_at')
search_fields = ('user__username', 'user__email', 'session_key')
- inlines = [StockReservationItemInline]
\ No newline at end of file
+ inlines = [StockReservationItemInline]
+
+
+@admin.register(SavedPaymentMethod)
+class SavedPaymentMethodAdmin(admin.ModelAdmin):
+ list_display = ('id', 'user', 'method_type', 'label', 'is_default', 'created_at')
+ list_filter = ('method_type', 'is_default', 'created_at')
+ search_fields = ('user__username', 'user__email', 'label', 'paypal_email')
\ No newline at end of file
diff --git a/tienda/migrations/0005_savedpaymentmethod.py b/tienda/migrations/0005_savedpaymentmethod.py
new file mode 100644
index 0000000..45617ca
--- /dev/null
+++ b/tienda/migrations/0005_savedpaymentmethod.py
@@ -0,0 +1,35 @@
+# Generated by Django 6.0.1 on 2026-04-10 06:09
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('tienda', '0004_product_stock_stockreservation_stockreservationitem'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='SavedPaymentMethod',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('method_type', models.CharField(choices=[('card', 'Tarjeta'), ('paypal', 'PayPal')], max_length=10)),
+ ('label', models.CharField(max_length=200, verbose_name='Etiqueta')),
+ ('stripe_customer_id', models.CharField(blank=True, default='', max_length=100)),
+ ('stripe_payment_method_id', models.CharField(blank=True, default='', max_length=100)),
+ ('paypal_email', models.CharField(blank=True, default='', max_length=254)),
+ ('paypal_payer_id', models.CharField(blank=True, default='', max_length=100)),
+ ('is_default', models.BooleanField(default=False, verbose_name='Predeterminado')),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payment_methods', to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'verbose_name': 'Método de pago guardado',
+ 'verbose_name_plural': 'Métodos de pago guardados',
+ 'ordering': ['-is_default', '-created_at'],
+ },
+ ),
+ ]
diff --git a/tienda/migrations/0006_alter_category_name.py b/tienda/migrations/0006_alter_category_name.py
new file mode 100644
index 0000000..83fbaeb
--- /dev/null
+++ b/tienda/migrations/0006_alter_category_name.py
@@ -0,0 +1,18 @@
+# Generated by Django 6.0.4 on 2026-04-17 07:05
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('tienda', '0005_savedpaymentmethod'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='category',
+ name='name',
+ field=models.CharField(max_length=200, unique=True),
+ ),
+ ]
diff --git a/tienda/migrations/__pycache__/0005_savedpaymentmethod.cpython-314.pyc b/tienda/migrations/__pycache__/0005_savedpaymentmethod.cpython-314.pyc
new file mode 100644
index 0000000..d1996ce
Binary files /dev/null and b/tienda/migrations/__pycache__/0005_savedpaymentmethod.cpython-314.pyc differ
diff --git a/tienda/migrations/__pycache__/0006_alter_category_name.cpython-314.pyc b/tienda/migrations/__pycache__/0006_alter_category_name.cpython-314.pyc
new file mode 100644
index 0000000..808b8e4
Binary files /dev/null and b/tienda/migrations/__pycache__/0006_alter_category_name.cpython-314.pyc differ
diff --git a/tienda/models.py b/tienda/models.py
index 6b74a54..56d986e 100644
--- a/tienda/models.py
+++ b/tienda/models.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
from django.db import models
from django.contrib.auth.models import User, AbstractUser
from django.utils.crypto import get_random_string
@@ -253,6 +255,41 @@ class OrderMessage(models.Model):
return f"Mensaje de {self.sender} - {self.created_at}"
+class SavedPaymentMethod(models.Model):
+ """Métodos de pago guardados por el usuario (tarjetas Stripe o cuentas PayPal)."""
+ TYPE_CARD = "card"
+ TYPE_PAYPAL = "paypal"
+ TYPE_CHOICES = [
+ (TYPE_CARD, "Tarjeta"),
+ (TYPE_PAYPAL, "PayPal"),
+ ]
+
+ user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="payment_methods")
+ method_type = models.CharField(max_length=10, choices=TYPE_CHOICES)
+ label = models.CharField(max_length=200, verbose_name="Etiqueta")
+ # Stripe fields
+ stripe_customer_id = models.CharField(max_length=100, blank=True, default="")
+ stripe_payment_method_id = models.CharField(max_length=100, blank=True, default="")
+ # PayPal fields
+ paypal_email = models.CharField(max_length=254, blank=True, default="")
+ paypal_payer_id = models.CharField(max_length=100, blank=True, default="")
+ is_default = models.BooleanField(default=False, verbose_name="Predeterminado")
+ created_at = models.DateTimeField(auto_now_add=True)
+
+ class Meta:
+ verbose_name = "Método de pago guardado"
+ verbose_name_plural = "Métodos de pago guardados"
+ ordering = ["-is_default", "-created_at"]
+
+ def __str__(self):
+ return f"{self.user.username} – {self.label}"
+
+ def save(self, *args, **kwargs):
+ if self.is_default:
+ SavedPaymentMethod.objects.filter(user=self.user, is_default=True).update(is_default=False)
+ super().save(*args, **kwargs)
+
+
class ShippingAddress(models.Model):
"""Direcciones de entrega de los usuarios"""
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='shipping_addresses')
@@ -279,4 +316,4 @@ class ShippingAddress(models.Model):
# Si se marca como predeterminada, desmarcar las demás del usuario
if self.is_default:
ShippingAddress.objects.filter(user=self.user, is_default=True).update(is_default=False)
- super().save(*args, **kwargs)
\ No newline at end of file
+ super().save(*args, **kwargs)
diff --git a/tienda/static/css/custom.css b/tienda/static/css/custom.css
index b4bd2e0..58dc78e 100644
--- a/tienda/static/css/custom.css
+++ b/tienda/static/css/custom.css
@@ -234,6 +234,46 @@ p.price {
object-position: center;
}
+/* Estilos para el footer */
+.footer-link {
+ color: inherit;
+ text-decoration: none;
+}
+.footer-link:hover {
+ text-decoration: underline;
+}
+
+/* Estilos para páginas legales / informativas */
+.legal-container {
+ max-width: 860px;
+ margin: 2rem auto;
+ padding: 0 1rem;
+}
+.legal-container h1 {
+ margin-bottom: 1.5rem;
+}
+
+.legal-section {
+ margin-bottom: 2rem;
+}
+.legal-section h2 {
+ font-size: 1.2rem;
+ margin-bottom: 0.75rem;
+ border-bottom: 1px solid #dee2e6;
+ padding-bottom: 0.25rem;
+}
+.legal-section h3 {
+ font-size: 1rem;
+ font-weight: 600;
+ margin-top: 1rem;
+ margin-bottom: 0.4rem;
+}
+
+.legal-footer {
+ margin-top: 2rem;
+ color: #6c757d;
+}
+
.texto-ajustado {
overflow-wrap: anywhere;
}
diff --git a/tienda/templates/tienda/agregar_paypal.html b/tienda/templates/tienda/agregar_paypal.html
new file mode 100644
index 0000000..56b57e2
--- /dev/null
+++ b/tienda/templates/tienda/agregar_paypal.html
@@ -0,0 +1,88 @@
+{% extends "tienda/base.html" %}
+{% load static %}
+
+{% block head %}
+
+{% endblock %}
+
+{% block content %}
+{% csrf_token %}
+
+
+
+
+
+
+
+ Se realizará un pequeño pago de verificación de 0,01 € para confirmar
+ tu cuenta de PayPal. Tu cuenta quedará guardada para futuras compras.
+
+
+
+
+
+
+
+
Cancelar
+
+
+
+
+
+
+{% endblock %}
diff --git a/tienda/templates/tienda/agregar_tarjeta.html b/tienda/templates/tienda/agregar_tarjeta.html
new file mode 100644
index 0000000..1b1b0a5
--- /dev/null
+++ b/tienda/templates/tienda/agregar_tarjeta.html
@@ -0,0 +1,130 @@
+{% extends "tienda/base.html" %}
+{% load static %}
+
+{% block head %}
+
+
+{% endblock %}
+
+{% block content %}
+{% csrf_token %}
+
+
+
+
+
+
+
+ Introduce los datos de tu tarjeta. No se realizará ningún cobro ahora; la tarjeta
+ se guardará de forma segura en Stripe para usar en tus próximas compras.
+
+
+
+
Datos de la tarjeta
+
+
+
+
+
+ 💳 Guardar tarjeta
+
+
+
+
+
Cancelar
+
+
+
+
+
+
+{% endblock %}
diff --git a/tienda/templates/tienda/aviso_legal.html b/tienda/templates/tienda/aviso_legal.html
new file mode 100644
index 0000000..a847648
--- /dev/null
+++ b/tienda/templates/tienda/aviso_legal.html
@@ -0,0 +1,49 @@
+{% extends "tienda/base.html" %}
+{% load static %}
+{% block content %}
+
+
+
+
+ Aviso Legal
+ El presente Aviso Legal regula el acceso y uso del sitio web Comercialmeria , de conformidad con la Ley 34/2002, de 11 de julio, de Servicios de la Sociedad de la Información y de Comercio Electrónico (LSSICE).
+
+
+ 1. Datos del Titular
+
+ Denominación: Comercialmeria
+ NIF/CIF: [00000000X]
+ Domicilio social: (Dirección de la empresa), Almería, España
+ Email de contacto: example@example.com
+ Inscripción registral: [Registro Mercantil de Almería]
+
+
+
+
+ 2. Objeto y Ámbito de Aplicación
+ Este sitio web ofrece servicios de comercio electrónico local dentro de la provincia de Almería. El acceso y uso de esta web implica la aceptación plena de las condiciones recogidas en este Aviso Legal.
+
+
+
+ 3. Propiedad Intelectual e Industrial
+ Todos los contenidos de esta web —textos, imágenes, logotipos, gráficos, código fuente y diseño— son propiedad de Comercialmeria o de sus respectivos titulares, y están protegidos por la legislación vigente en materia de propiedad intelectual e industrial. Queda expresamente prohibida su reproducción, distribución o modificación sin autorización previa por escrito.
+
+
+
+ 4. Exclusión de Responsabilidad
+ Comercialmeria no se responsabiliza de los daños derivados del uso incorrecto del sitio, de interrupciones en el servicio por causas ajenas a nuestra voluntad, ni del contenido de páginas externas enlazadas desde nuestra web.
+
+
+
+ 5. Legislación Aplicable y Jurisdicción
+ Las relaciones establecidas entre el usuario y Comercialmeria se regirán por la legislación española vigente. Para la resolución de cualquier conflicto, ambas partes se someten a los Juzgados y Tribunales de Almería, con renuncia expresa a cualquier otro fuero.
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/tienda/templates/tienda/ayuda.html b/tienda/templates/tienda/ayuda.html
new file mode 100644
index 0000000..0d336c5
--- /dev/null
+++ b/tienda/templates/tienda/ayuda.html
@@ -0,0 +1,82 @@
+{% extends "tienda/base.html" %}
+{% load static %}
+{% block content %}
+
+
+
+
+ Centro de Ayuda
+ ¿Tienes alguna duda? Aquí encontrarás respuesta a las preguntas más frecuentes sobre cómo funciona Comercialmeria .
+
+
+ 🛒 Compras
+
+ ¿Cómo realizo un pedido?
+ Busca el producto que deseas, añádelo al carrito y sigue los pasos del proceso de compra. Necesitarás una cuenta registrada y una dirección de envío en la provincia de Almería.
+
+ ¿Qué métodos de pago se aceptan?
+ Aceptamos tarjeta de crédito/débito a través de Stripe y PayPal . Todos los pagos están protegidos con cifrado SSL.
+
+ ¿Puedo modificar un pedido tras confirmarlo?
+ Una vez confirmado el pedido, no es posible modificarlo directamente. Si necesitas hacer cambios antes de que se envíe, contacta con nosotros lo antes posible en example@example.com .
+
+ ¿Cuándo recibiré mi pedido?
+ El plazo de entrega depende del vendedor. Puedes consultar el estado de tu pedido desde Mi cuenta → Mis compras .
+
+
+
+ 📦 Envíos
+
+ ¿A qué zonas se realizan envíos?
+ Comercialmeria envía únicamente dentro de la provincia de Almería (códigos postales que empiecen por 04).
+
+ ¿Cuánto cuesta el envío?
+ Los gastos de envío se calculan en el momento del checkout y dependen del vendedor y la dirección de entrega.
+
+
+
+ 🔄 Devoluciones
+
+ ¿Puedo devolver un producto?
+ Sí. Tienes 14 días naturales desde la recepción para solicitar una devolución. Consulta nuestra Política de Devoluciones para más detalles.
+
+ ¿Cuándo recibiré el reembolso?
+ Una vez aceptada la devolución, el reembolso se tramita en un máximo de 14 días al mismo método de pago utilizado.
+
+
+
+ 👤 Mi Cuenta
+
+ ¿Cómo creo una cuenta?
+ Haz clic en Registrarse en la parte superior de la página, rellena el formulario y verifica tu correo electrónico.
+
+ He olvidado mi contraseña, ¿qué hago?
+ Haz clic en ¿Olvidaste tu contraseña? en la pantalla de inicio de sesión. Recibirás un email con instrucciones para restablecerla.
+
+ ¿Cómo cambio mis datos personales?
+ Accede a Mi cuenta → Editar perfil para actualizar tu nombre, email o contraseña.
+
+
+
+ 🏪 Vender en Comercialmeria
+
+ ¿Cómo puedo vender mis productos?
+ Regístrate, accede al Panel Vendedor desde la barra de navegación y crea tu primer producto. ¡Es gratis y sencillo!
+
+ ¿Hay comisiones?
+ Actualmente no aplicamos comisiones sobre las ventas. Consulta nuestra sección de Términos y Condiciones para más información.
+
+
+
+ ¿No encuentras respuesta?
+ Escríbenos a example@example.com y te ayudaremos en el menor tiempo posible.
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/tienda/templates/tienda/base.html b/tienda/templates/tienda/base.html
index 7a8150c..28364dd 100644
--- a/tienda/templates/tienda/base.html
+++ b/tienda/templates/tienda/base.html
@@ -77,7 +77,7 @@
{% block head %}{% endblock %}
-
+
{% cache 500 sidebar request.user.username %}
@@ -135,7 +135,7 @@
{% endcache %}
-
+
{% if messages %}
@@ -155,26 +155,39 @@
{% cache 500 footer %}
-
{% cache 500 scripts %}
@@ -268,4 +281,4 @@
{% endcache %}
-
\ No newline at end of file
+