diff --git a/.gitignore b/.gitignore index e3b2dd4..1abf015 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ venv/ .venv # Archivos de base de datos +db.sqlite3 # Archivos de migraciones (si quieres omitirlos) **/migrations/__pycache__/ @@ -21,4 +22,5 @@ venv/ # Archivos de Django estáticos staticfiles/ -**/*.sqlite3 \ No newline at end of file +# Archivos subidos por el usuario +media/ \ No newline at end of file diff --git "a/docs/Iteraci\303\263n 1/Informes de Seguimiento 1/03.11.25 Informe de seguimiento.pdf" "b/docs/Iteraci\303\263n 1/Informes de Seguimiento 1/03.11.25 Informe de seguimiento.pdf" new file mode 100644 index 0000000..9af2d44 Binary files /dev/null and "b/docs/Iteraci\303\263n 1/Informes de Seguimiento 1/03.11.25 Informe de seguimiento.pdf" differ diff --git "a/docs/Iteraci\303\263n 1/Informes de Seguimiento 1/04.11.25 Informe de seguimiento.pdf" "b/docs/Iteraci\303\263n 1/Informes de Seguimiento 1/04.11.25 Informe de seguimiento.pdf" new file mode 100644 index 0000000..0e9f2a2 Binary files /dev/null and "b/docs/Iteraci\303\263n 1/Informes de Seguimiento 1/04.11.25 Informe de seguimiento.pdf" differ diff --git "a/docs/Iteraci\303\263n 1/Informes de Seguimiento 1/05.11.25 Informe de seguimiento.pdf" "b/docs/Iteraci\303\263n 1/Informes de Seguimiento 1/05.11.25 Informe de seguimiento.pdf" new file mode 100644 index 0000000..6b0be90 Binary files /dev/null and "b/docs/Iteraci\303\263n 1/Informes de Seguimiento 1/05.11.25 Informe de seguimiento.pdf" differ diff --git "a/docs/Iteraci\303\263n 1/Informes de Seguimiento 1/06.11.25 Informe de seguimiento.pdf" "b/docs/Iteraci\303\263n 1/Informes de Seguimiento 1/06.11.25 Informe de seguimiento.pdf" new file mode 100644 index 0000000..f964b1b Binary files /dev/null and "b/docs/Iteraci\303\263n 1/Informes de Seguimiento 1/06.11.25 Informe de seguimiento.pdf" differ diff --git "a/docs/Iteraci\303\263n 1/Informes de Seguimiento 1/07.11.25 Informe de seguimiento.pdf" "b/docs/Iteraci\303\263n 1/Informes de Seguimiento 1/07.11.25 Informe de seguimiento.pdf" new file mode 100644 index 0000000..996820b Binary files /dev/null and "b/docs/Iteraci\303\263n 1/Informes de Seguimiento 1/07.11.25 Informe de seguimiento.pdf" differ diff --git "a/docs/Iteraci\303\263n 1/Informes de Seguimiento 1/08.11.25 Informe de seguimiento.pdf" "b/docs/Iteraci\303\263n 1/Informes de Seguimiento 1/08.11.25 Informe de seguimiento.pdf" new file mode 100644 index 0000000..3b36990 Binary files /dev/null and "b/docs/Iteraci\303\263n 1/Informes de Seguimiento 1/08.11.25 Informe de seguimiento.pdf" differ diff --git "a/docs/Iteraci\303\263n 1/Informes de Seguimiento 1/09.11.25 Informe de seguimiento.pdf" "b/docs/Iteraci\303\263n 1/Informes de Seguimiento 1/09.11.25 Informe de seguimiento.pdf" new file mode 100644 index 0000000..5e7261d Binary files /dev/null and "b/docs/Iteraci\303\263n 1/Informes de Seguimiento 1/09.11.25 Informe de seguimiento.pdf" differ diff --git "a/docs/Iteraci\303\263n 2/Informes de Seguimiento 2/11.11.25 Informe de seguimiento.pdf" "b/docs/Iteraci\303\263n 2/Informes de Seguimiento 2/11.11.25 Informe de seguimiento.pdf" new file mode 100644 index 0000000..783410b Binary files /dev/null and "b/docs/Iteraci\303\263n 2/Informes de Seguimiento 2/11.11.25 Informe de seguimiento.pdf" differ diff --git "a/docs/Iteraci\303\263n 2/Informes de Seguimiento 2/12.11.25 Informe de seguimiento.pdf" "b/docs/Iteraci\303\263n 2/Informes de Seguimiento 2/12.11.25 Informe de seguimiento.pdf" new file mode 100644 index 0000000..9e68f45 Binary files /dev/null and "b/docs/Iteraci\303\263n 2/Informes de Seguimiento 2/12.11.25 Informe de seguimiento.pdf" differ diff --git "a/docs/Iteraci\303\263n 2/Informes de Seguimiento 2/13.11.25 Informe de seguimiento.pdf" "b/docs/Iteraci\303\263n 2/Informes de Seguimiento 2/13.11.25 Informe de seguimiento.pdf" new file mode 100644 index 0000000..58edad6 Binary files /dev/null and "b/docs/Iteraci\303\263n 2/Informes de Seguimiento 2/13.11.25 Informe de seguimiento.pdf" differ diff --git "a/docs/Iteraci\303\263n 2/Informes de Seguimiento 2/14.11.25 Informe de seguimiento.pdf" "b/docs/Iteraci\303\263n 2/Informes de Seguimiento 2/14.11.25 Informe de seguimiento.pdf" new file mode 100644 index 0000000..f9ec6f9 Binary files /dev/null and "b/docs/Iteraci\303\263n 2/Informes de Seguimiento 2/14.11.25 Informe de seguimiento.pdf" differ diff --git "a/docs/Iteraci\303\263n 2/Informes de Seguimiento 2/15.11.25 Informe de seguimiento.pdf" "b/docs/Iteraci\303\263n 2/Informes de Seguimiento 2/15.11.25 Informe de seguimiento.pdf" new file mode 100644 index 0000000..fc87c34 Binary files /dev/null and "b/docs/Iteraci\303\263n 2/Informes de Seguimiento 2/15.11.25 Informe de seguimiento.pdf" differ diff --git "a/docs/Iteraci\303\263n 2/Informes de Seguimiento 2/16.11.25 Informe de seguimiento.pdf" "b/docs/Iteraci\303\263n 2/Informes de Seguimiento 2/16.11.25 Informe de seguimiento.pdf" new file mode 100644 index 0000000..8708c1e Binary files /dev/null and "b/docs/Iteraci\303\263n 2/Informes de Seguimiento 2/16.11.25 Informe de seguimiento.pdf" differ diff --git "a/docs/Iteraci\303\263n 2/REGISTRO DE DECISIONES_v2.0.pdf" "b/docs/Iteraci\303\263n 2/REGISTRO DE DECISIONES_v2.0.pdf" new file mode 100644 index 0000000..25bded6 Binary files /dev/null and "b/docs/Iteraci\303\263n 2/REGISTRO DE DECISIONES_v2.0.pdf" differ diff --git "a/docs/Iteraci\303\263n 2/REGISTRO DE INCIDENCIAS_v2.0.pdf" "b/docs/Iteraci\303\263n 2/REGISTRO DE INCIDENCIAS_v2.0.pdf" new file mode 100644 index 0000000..e326b64 Binary files /dev/null and "b/docs/Iteraci\303\263n 2/REGISTRO DE INCIDENCIAS_v2.0.pdf" differ diff --git "a/docs/Iteraci\303\263n 2/SPRINT RETROSPECTIVE 2.pdf" "b/docs/Iteraci\303\263n 2/SPRINT RETROSPECTIVE 2.pdf" new file mode 100644 index 0000000..67fdbda Binary files /dev/null and "b/docs/Iteraci\303\263n 2/SPRINT RETROSPECTIVE 2.pdf" differ diff --git "a/docs/Iteraci\303\263n 2/SPRINT REVIEW 2.pdf" "b/docs/Iteraci\303\263n 2/SPRINT REVIEW 2.pdf" new file mode 100644 index 0000000..68c00e2 Binary files /dev/null and "b/docs/Iteraci\303\263n 2/SPRINT REVIEW 2.pdf" differ diff --git "a/docs/Iteraci\303\263n 2/Sprint Backlog 2.pdf" "b/docs/Iteraci\303\263n 2/Sprint Backlog 2.pdf" new file mode 100644 index 0000000..6b8cc7d Binary files /dev/null and "b/docs/Iteraci\303\263n 2/Sprint Backlog 2.pdf" differ diff --git a/essenza/_sample_assets/products/aceite_capilar.jpg b/essenza/_sample_assets/products/aceite_capilar.jpg new file mode 100644 index 0000000..6a85d21 Binary files /dev/null and b/essenza/_sample_assets/products/aceite_capilar.jpg differ diff --git a/essenza/_sample_assets/products/crema_antiedad.jpg b/essenza/_sample_assets/products/crema_antiedad.jpg new file mode 100644 index 0000000..91c7067 Binary files /dev/null and b/essenza/_sample_assets/products/crema_antiedad.jpg differ diff --git a/essenza/_sample_assets/products/crema_hidratante.jpg b/essenza/_sample_assets/products/crema_hidratante.jpg new file mode 100644 index 0000000..f2bb003 Binary files /dev/null and b/essenza/_sample_assets/products/crema_hidratante.jpg differ diff --git a/essenza/_sample_assets/products/crema_pies.jpg b/essenza/_sample_assets/products/crema_pies.jpg new file mode 100644 index 0000000..2b095d5 Binary files /dev/null and b/essenza/_sample_assets/products/crema_pies.jpg differ diff --git a/essenza/_sample_assets/products/crema_solar.jpg b/essenza/_sample_assets/products/crema_solar.jpg new file mode 100644 index 0000000..8951692 Binary files /dev/null and b/essenza/_sample_assets/products/crema_solar.jpg differ diff --git a/essenza/_sample_assets/products/desodorante.jpg b/essenza/_sample_assets/products/desodorante.jpg new file mode 100644 index 0000000..a329289 Binary files /dev/null and b/essenza/_sample_assets/products/desodorante.jpg differ diff --git a/essenza/_sample_assets/products/gel_antibacterial.jpg b/essenza/_sample_assets/products/gel_antibacterial.jpg new file mode 100644 index 0000000..f035fcc Binary files /dev/null and b/essenza/_sample_assets/products/gel_antibacterial.jpg differ diff --git a/essenza/_sample_assets/products/laca_pelo.jpg b/essenza/_sample_assets/products/laca_pelo.jpg new file mode 100644 index 0000000..ae4e7ec Binary files /dev/null and b/essenza/_sample_assets/products/laca_pelo.jpg differ diff --git a/essenza/_sample_assets/products/limpieza_facial.jpg b/essenza/_sample_assets/products/limpieza_facial.jpg new file mode 100644 index 0000000..c988ce3 Binary files /dev/null and b/essenza/_sample_assets/products/limpieza_facial.jpg differ diff --git a/essenza/_sample_assets/products/maquillaje_base.jpg b/essenza/_sample_assets/products/maquillaje_base.jpg new file mode 100644 index 0000000..683a2ea Binary files /dev/null and b/essenza/_sample_assets/products/maquillaje_base.jpg differ diff --git a/essenza/_sample_assets/products/mascarilla_facial.jpg b/essenza/_sample_assets/products/mascarilla_facial.jpg new file mode 100644 index 0000000..517de1e Binary files /dev/null and b/essenza/_sample_assets/products/mascarilla_facial.jpg differ diff --git a/essenza/_sample_assets/products/perfume_floral.jpg b/essenza/_sample_assets/products/perfume_floral.jpg new file mode 100644 index 0000000..cd2dd8a Binary files /dev/null and b/essenza/_sample_assets/products/perfume_floral.jpg differ diff --git a/essenza/_sample_assets/products/pincel_maquillaje.jpg b/essenza/_sample_assets/products/pincel_maquillaje.jpg new file mode 100644 index 0000000..413bc33 Binary files /dev/null and b/essenza/_sample_assets/products/pincel_maquillaje.jpg differ diff --git a/essenza/_sample_assets/products/rizador_pelo.jpg b/essenza/_sample_assets/products/rizador_pelo.jpg new file mode 100644 index 0000000..1056977 Binary files /dev/null and b/essenza/_sample_assets/products/rizador_pelo.jpg differ diff --git a/essenza/_sample_assets/products/secador_pelo.jpg b/essenza/_sample_assets/products/secador_pelo.jpg new file mode 100644 index 0000000..02d5e8f Binary files /dev/null and b/essenza/_sample_assets/products/secador_pelo.jpg differ diff --git a/essenza/_sample_assets/products/shampoo_anticaspa.jpg b/essenza/_sample_assets/products/shampoo_anticaspa.jpg new file mode 100644 index 0000000..e59a462 Binary files /dev/null and b/essenza/_sample_assets/products/shampoo_anticaspa.jpg differ diff --git a/essenza/_sample_assets/products/shampoo_reconstructivo.jpg b/essenza/_sample_assets/products/shampoo_reconstructivo.jpg new file mode 100644 index 0000000..261d54b Binary files /dev/null and b/essenza/_sample_assets/products/shampoo_reconstructivo.jpg differ diff --git a/essenza/_sample_assets/products/shampoo_voluminizador.jpg b/essenza/_sample_assets/products/shampoo_voluminizador.jpg new file mode 100644 index 0000000..fcf6a33 Binary files /dev/null and b/essenza/_sample_assets/products/shampoo_voluminizador.jpg differ diff --git a/essenza/_sample_assets/products/tinte_cabello.jpg b/essenza/_sample_assets/products/tinte_cabello.jpg new file mode 100644 index 0000000..0331b64 Binary files /dev/null and b/essenza/_sample_assets/products/tinte_cabello.jpg differ diff --git a/essenza/_sample_assets/products/toallitas_desmaquillantes.jpg b/essenza/_sample_assets/products/toallitas_desmaquillantes.jpg new file mode 100644 index 0000000..b35cc92 Binary files /dev/null and b/essenza/_sample_assets/products/toallitas_desmaquillantes.jpg differ diff --git a/essenza/_sample_assets/profile_pics/admin.jpg b/essenza/_sample_assets/profile_pics/admin.jpg new file mode 100644 index 0000000..dff3fbd Binary files /dev/null and b/essenza/_sample_assets/profile_pics/admin.jpg differ diff --git a/essenza/profile_pics/user2.avif b/essenza/_sample_assets/profile_pics/user1.jpg similarity index 100% rename from essenza/profile_pics/user2.avif rename to essenza/_sample_assets/profile_pics/user1.jpg diff --git a/essenza/profile_pics/user1.avif b/essenza/_sample_assets/profile_pics/user2.jpg similarity index 100% rename from essenza/profile_pics/user1.avif rename to essenza/_sample_assets/profile_pics/user2.jpg diff --git a/essenza/profile_pics/user3.avif b/essenza/_sample_assets/profile_pics/user3.jpg similarity index 100% rename from essenza/profile_pics/user3.avif rename to essenza/_sample_assets/profile_pics/user3.jpg diff --git a/essenza/db.sqlite3 b/essenza/db.sqlite3 deleted file mode 100644 index 836a999..0000000 Binary files a/essenza/db.sqlite3 and /dev/null differ diff --git a/essenza/deploy.sh b/essenza/deploy.sh new file mode 100644 index 0000000..7d64b16 --- /dev/null +++ b/essenza/deploy.sh @@ -0,0 +1,7 @@ +pip install -r requirements.txt && \ +python manage.py flush --noinput && \ +python manage.py migrate --noinput && \ +# python manage.py collectstatic --noinput && \ +python manage.py loaddata user/sample/sample.json && \ +python manage.py loaddata product/sample/sample.json && \ +python manage.py loaddata order/sample/sample.json \ No newline at end of file diff --git a/essenza/essenza/settings.py b/essenza/essenza/settings.py index 662ee0e..73abfdc 100644 --- a/essenza/essenza/settings.py +++ b/essenza/essenza/settings.py @@ -20,7 +20,7 @@ # See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-7+c*kj699pt34%5ub-x04i3%nlbhc@y+7sdew3+7!z5h-z1k_v' +SECRET_KEY = "django-insecure-7+c*kj699pt34%5ub-x04i3%nlbhc@y+7sdew3+7!z5h-z1k_v" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -31,57 +31,58 @@ # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'user', - 'product', - 'order', - 'info', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "django.contrib.humanize", + "user", + "product", + "order", + "info", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.locale.LocaleMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.locale.LocaleMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = 'essenza.urls' +ROOT_URLCONF = "essenza.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [ BASE_DIR / 'templates' ], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.request', - 'django.template.context_processors.i18n', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [BASE_DIR / "templates"], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.request", + "django.template.context_processors.i18n", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'essenza.wsgi.application' +WSGI_APPLICATION = "essenza.wsgi.application" # Database # https://docs.djangoproject.com/en/5.2/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", } } @@ -91,16 +92,16 @@ AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] @@ -108,9 +109,9 @@ # Internationalization # https://docs.djangoproject.com/en/5.2/topics/i18n/ -LANGUAGE_CODE = 'es' +LANGUAGE_CODE = "es" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -120,16 +121,21 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.2/howto/static-files/ -STATIC_URL = 'static/' -STATICFILES_DIRS = [BASE_DIR / 'static'] +STATIC_URL = "static/" +STATICFILES_DIRS = [BASE_DIR / "static"] +STATIC_ROOT = BASE_DIR / "staticfiles" + +MEDIA_URL = "/media/" +MEDIA_ROOT = BASE_DIR / "media" + # Default primary key field type # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" # ----------------------------------------------------------------- # SOLUCIÓN AL ERROR E304 -# Especifica que nuestro modelo 'Usuario' en la app 'user' +# Especifica que nuestro modelo 'Usuario' en la app 'user' # es el modelo de autenticación oficial. # ----------------------------------------------------------------- -AUTH_USER_MODEL = 'user.Usuario' \ No newline at end of file +AUTH_USER_MODEL = "user.Usuario" diff --git a/essenza/essenza/urls.py b/essenza/essenza/urls.py index e08e7df..2390f45 100644 --- a/essenza/essenza/urls.py +++ b/essenza/essenza/urls.py @@ -1,106 +1,20 @@ +from django.conf import settings +from django.conf.urls.static import static from django.contrib import admin -from django.urls import path, include -from django.http import HttpResponse +from django.urls import include, path from info.views import info_view -from product.views import EscaparateView -import user - -def home(request): - html = """ - -
-Tu espacio online de cosmética natural, belleza y cuidado personal.
-Explora nuestros productos, descubre nuevas fragancias y disfruta de la experiencia Essenza 🌸
- - - - - """ - return HttpResponse(html) +from product.views import DashboardView +from product.views import CatalogView, CatalogDetailView urlpatterns = [ - path('', home, name='home'), - path('info/', info_view, name='info-home'), - path("user/", include("user.urls")), - path('admin/', admin.site.urls), - path('escaparate/', EscaparateView.as_view(), name='escaparate') -] \ No newline at end of file + path("info/", info_view, name="info-home"), + path("user/", include("user.urls")), + path("admin/", admin.site.urls), + path("product/", include("product.urls")), + path("", DashboardView.as_view(), name="dashboard"), + path("catalogo/", CatalogView.as_view(), name="catalog"), + path("catalogo/Essenza S.L. es la denominación social y el nombre comercial de la tienda online dedicada a la venta de productos de cosmética y cuidado personal, rigiéndose su actividad por la legislación española vigente. En cumplimiento del deber de información recogido en la Ley 34/2002 de Servicios de la Sociedad de la Información y Comercio Electrónico (LSSI-CE) y en el Real Decreto 85/2018 sobre productos cosméticos, a continuación se detallan los datos de identificación del titular de este sitio web:
-Estas condiciones regulan la relación contractual de compraventa entre Essenza y usted desde el momento en que realiza un pedido en nuestra web. La formalización de un pedido implica la lectura, comprensión y aceptación expresa de estas Condiciones Generales de Venta en su totalidad, siendo de obligado cumplimiento para ambas partes.
- -Essenza garantiza que todos sus productos han pasado rigurosos controles de calidad y cumplen con los requisitos de seguridad establecidos por la normativa europea (Reglamento CE 1223/2009). La información detallada de ingredientes, modo de uso, precauciones y el Periodo Después de la Apertura (PAO) se encuentra de forma clara y accesible en la ficha de cada producto y en su etiquetado. Mantenemos un estricto control de trazabilidad para garantizar la seguridad de todos los artículos de cosmética que comercializamos.
- -El proceso de compra se considera finalizado y vinculante una vez que el pago ha sido confirmado. Todos los precios mostrados en el sitio web están expresados en euros (€) e incluyen el Impuesto sobre el Valor Añadido (I.V.A.) legalmente aplicable. Los gastos de envío serán calculados en base al peso y la dirección de entrega, siendo detallados y aceptados por el Cliente antes de la confirmación final de la compra.
- -De acuerdo con la Ley General para la Defensa de los Consumidores y Usuarios, el Cliente dispone de 14 días naturales desde la recepción del pedido para ejercer su derecho de desistimiento. Condición Específica para Cosmética: Por estrictas razones de higiene, seguridad y protección de la salud, no se admitirá la devolución de productos cosméticos que hayan sido abiertos, desprecintados o usados. En caso de desistimiento válido, los costes directos de la devolución (envío de vuelta) correrán a cargo del cliente, salvo si la causa es un producto defectuoso o un error de Essenza.
- -En Essenza, su privacidad es nuestra prioridad. Los datos personales recabados (nombre, dirección, email, datos de pago) a través de la web son tratados bajo la legitimación de la ejecución de un contrato (para gestionar su pedido) o el consentimiento (para el newsletter). Nos comprometemos a no ceder sus datos a terceros, salvo obligación legal. Usted puede ejercer en todo momento sus derechos de Acceso, Rectificación, Supresión, Limitación, Portabilidad y Oposición (ARSLOP) enviando una solicitud a info@essenza.com.
- - - - - - """ - return HttpResponse(html) \ No newline at end of file + return render(request, "info/info.html") \ No newline at end of file diff --git a/essenza/load_samples.bat b/essenza/load_samples.bat new file mode 100644 index 0000000..8374cf9 --- /dev/null +++ b/essenza/load_samples.bat @@ -0,0 +1,48 @@ +@echo off +REM --------------------------------------------------------- +REM IMPORTANTE: Este archivo borra todos los datos de tu BD local (y la crea con los datos de sampleo). +REM Las imágenes de sampleo se copian a la carpeta 'media/'. +REM También instala las dependencias necesarias definidas en 'requirements.txt' (si aun no lo están). +REM --------------------------------------------------------- + +echo --- Instalando dependencias (pip)... +pip install -r requirements.txt && ( + + echo --- Borrando TODOS los datos de la BD... + python manage.py flush --noinput && ( + + echo. + echo --- Aplicando migraciones... + python manage.py migrate --noinput && ( + + echo. + echo --- Copiando imagenes de sampleo a 'media/'... + REM XCOPY [origen] [destino] /E /I /Y + REM /E = Copia subdirectorios (incluso vacíos) + REM /I = Si el destino no existe, asume que es un directorio + REM /Y = Suprime la pregunta de "sobreescribir archivo" + XCOPY _sample_assets media /E /I /Y && ( + + echo. + echo --- Cargando datos de USER... + python manage.py loaddata user/sample/sample.json && ( + + echo. + echo --- Cargando datos de PRODUCT... + python manage.py loaddata product/sample/sample.json && ( + + echo. + echo --- Cargando datos de ORDER... + python manage.py loaddata order/sample/sample.json && ( + + echo. + echo --- !Proceso completado! La base de datos esta lista. --- + ) + ) + ) + ) + ) + ) +) + +@echo on \ No newline at end of file diff --git a/essenza/order/admin.py b/essenza/order/admin.py index 54957e8..031b084 100644 --- a/essenza/order/admin.py +++ b/essenza/order/admin.py @@ -1,5 +1,7 @@ from django.contrib import admin -from .models import Order, OrderProduct, Status + +from .models import Order, OrderProduct + # Register your models here. admin.site.register(Order) diff --git a/essenza/order/apps.py b/essenza/order/apps.py index 42888e4..b2f0796 100644 --- a/essenza/order/apps.py +++ b/essenza/order/apps.py @@ -2,5 +2,5 @@ class OrderConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'order' + default_auto_field = "django.db.models.BigAutoField" + name = "order" diff --git a/essenza/order/models.py b/essenza/order/models.py index d623635..2857975 100644 --- a/essenza/order/models.py +++ b/essenza/order/models.py @@ -1,27 +1,43 @@ from django.db import models +from django.utils import timezone + # Create your models here. class Status(models.TextChoices): - PENDING = 'pending', 'Pending' - PAID = 'paid', 'Paid' - SHIPPED = 'shipped', 'Shipped' + PENDING = "pending", "Pending" + PAID = "paid", "Paid" + SHIPPED = "shipped", "Shipped" + class Order(models.Model): - user = models.ForeignKey('user.Usuario', on_delete=models.CASCADE, related_name='orders') - adress = models.CharField(max_length=255) - placed_at = models.DateTimeField(auto_now=True) - total_price = models.DecimalField(max_digits=10, decimal_places=2) - status = models.CharField(max_length=10, choices=Status.choices, default=Status.PENDING) + user = models.ForeignKey( + "user.Usuario", on_delete=models.CASCADE, related_name="orders" + ) + address = models.CharField(max_length=255) + placed_at = models.DateTimeField(default=timezone.now) + status = models.CharField( + max_length=10, choices=Status.choices, default=Status.PENDING + ) + + @property + def total_price(self): + total = 0 + for product in self.order_products.all(): + total += product.quantity * product.product.price + return total def __str__(self): return f"Order {self.id} by {self.user.email}" class OrderProduct(models.Model): - order = models.ForeignKey('order.Order', on_delete=models.CASCADE, related_name='order_products') - product = models.ForeignKey('product.Product', on_delete=models.CASCADE, related_name='product_orders') + order = models.ForeignKey( + "order.Order", on_delete=models.CASCADE, related_name="order_products" + ) + product = models.ForeignKey( + "product.Product", on_delete=models.CASCADE, related_name="product_orders" + ) quantity = models.IntegerField() - unity_price = models.DecimalField(max_digits=10, decimal_places=2) def __str__(self): return f"{self.quantity} of {self.product.name} in order {self.order.id}" diff --git a/essenza/order/sample/sample.json b/essenza/order/sample/sample.json new file mode 100644 index 0000000..fc7017d --- /dev/null +++ b/essenza/order/sample/sample.json @@ -0,0 +1,372 @@ +[ + { + "model": "order.order", + "pk": 1, + "fields": { + "user": 1, + "address": "Calle Gran Vía, 23, Madrid, 28013", + "placed_at": "2025-11-12T10:30:00Z", + "status": "pending" + } + }, + { + "model": "order.order", + "pk": 2, + "fields": { + "user": 2, + "address": "Avenida de la Constitución, 8, Sevilla, 41001", + "placed_at": "2025-11-11T15:10:00Z", + "status": "paid" + } + }, + { + "model": "order.order", + "pk": 3, + "fields": { + "user": 3, + "address": "Carrer de Pau Claris, 60, Barcelona, 08010", + "placed_at": "2025-11-10T19:25:00Z", + "status": "shipped" + } + }, + { + "model": "order.order", + "pk": 4, + "fields": { + "user": 1, + "address": "Calle Alcalá, 120, Madrid, 28009", + "placed_at": "2025-11-09T09:00:00Z", + "status": "pending" + } + }, + { + "model": "order.order", + "pk": 5, + "fields": { + "user": 2, + "address": "Plaza Nueva, 10, Bilbao, 48001", + "placed_at": "2025-11-08T12:15:00Z", + "status": "shipped" + } + }, + { + "model": "order.order", + "pk": 6, + "fields": { + "user": 3, + "address": "Calle Larios, 5, Málaga, 29001", + "placed_at": "2025-11-07T14:00:00Z", + "status": "paid" + } + }, + { + "model": "order.order", + "pk": 7, + "fields": { + "user": 1, + "address": "Paseo de Gracia, 92, Barcelona, 08008", + "placed_at": "2025-11-06T18:45:00Z", + "status": "pending" + } + }, + { + "model": "order.order", + "pk": 8, + "fields": { + "user": 2, + "address": "Calle de la Paz, 1, Valencia, 46003", + "placed_at": "2025-11-06T10:00:00Z", + "status": "shipped" + } + }, + { + "model": "order.order", + "pk": 9, + "fields": { + "user": 3, + "address": "Calle Mayor, 30, Zaragoza, 50001", + "placed_at": "2025-10-15T11:00:00Z", + "status": "shipped" + } + }, + { + "model": "order.order", + "pk": 10, + "fields": { + "user": 1, + "address": "Rúa do Vilar, 50, Santiago de Compostela, 15705", + "placed_at": "2025-10-28T08:30:00Z", + "status": "paid" + } + }, + { + "model": "order.orderproduct", + "pk": 1, + "fields": { + "order": 1, + "product": 1, + "quantity": 1 + } + }, + { + "model": "order.orderproduct", + "pk": 2, + "fields": { + "order": 1, + "product": 18, + "quantity": 2 + } + }, + { + "model": "order.orderproduct", + "pk": 3, + "fields": { + "order": 2, + "product": 2, + "quantity": 2 + } + }, + { + "model": "order.orderproduct", + "pk": 4, + "fields": { + "order": 3, + "product": 5, + "quantity": 1 + } + }, + { + "model": "order.orderproduct", + "pk": 5, + "fields": { + "order": 3, + "product": 11, + "quantity": 1 + } + }, + { + "model": "order.orderproduct", + "pk": 6, + "fields": { + "order": 3, + "product": 15, + "quantity": 1 + } + }, + { + "model": "order.orderproduct", + "pk": 7, + "fields": { + "order": 4, + "product": 4, + "quantity": 1 + } + }, + { + "model": "order.orderproduct", + "pk": 8, + "fields": { + "order": 5, + "product": 8, + "quantity": 1 + } + }, + { + "model": "order.orderproduct", + "pk": 9, + "fields": { + "order": 6, + "product": 3, + "quantity": 1 + } + }, + { + "model": "order.orderproduct", + "pk": 10, + "fields": { + "order": 6, + "product": 6, + "quantity": 1 + } + }, + { + "model": "order.orderproduct", + "pk": 11, + "fields": { + "order": 7, + "product": 10, + "quantity": 2 + } + }, + { + "model": "order.orderproduct", + "pk": 12, + "fields": { + "order": 8, + "product": 20, + "quantity": 1 + } + }, + { + "model": "order.orderproduct", + "pk": 13, + "fields": { + "order": 8, + "product": 17, + "quantity": 3 + } + }, + { + "model": "order.orderproduct", + "pk": 14, + "fields": { + "order": 9, + "product": 9, + "quantity": 1 + } + }, + { + "model": "order.orderproduct", + "pk": 15, + "fields": { + "order": 9, + "product": 12, + "quantity": 1 + } + }, + { + "model": "order.orderproduct", + "pk": 16, + "fields": { + "order": 10, + "product": 7, + "quantity": 5 + } + }, + { + "model": "order.orderproduct", + "pk": 17, + "fields": { + "order": 10, + "product": 16, + "quantity": 2 + } + }, + { + "model": "order.orderproduct", + "pk": 18, + "fields": { + "order": 1, + "product": 10, + "quantity": 1 + } + }, + { + "model": "order.orderproduct", + "pk": 19, + "fields": { + "order": 2, + "product": 7, + "quantity": 1 + } + }, + { + "model": "order.orderproduct", + "pk": 20, + "fields": { + "order": 4, + "product": 2, + "quantity": 3 + } + }, + { + "model": "order.orderproduct", + "pk": 21, + "fields": { + "order": 5, + "product": 19, + "quantity": 1 + } + }, + { + "model": "order.orderproduct", + "pk": 22, + "fields": { + "order": 7, + "product": 1, + "quantity": 1 + } + }, + { + "model": "order.orderproduct", + "pk": 23, + "fields": { + "order": 7, + "product": 18, + "quantity": 1 + } + }, + { + "model": "order.orderproduct", + "pk": 24, + "fields": { + "order": 9, + "product": 5, + "quantity": 2 + } + }, + { + "model": "order.orderproduct", + "pk": 25, + "fields": { + "order": 9, + "product": 14, + "quantity": 1 + } + }, + { + "model": "order.orderproduct", + "pk": 26, + "fields": { + "order": 10, + "product": 8, + "quantity": 2 + } + }, + { + "model": "order.orderproduct", + "pk": 27, + "fields": { + "order": 1, + "product": 4, + "quantity": 1 + } + }, + { + "model": "order.orderproduct", + "pk": 28, + "fields": { + "order": 3, + "product": 6, + "quantity": 1 + } + }, + { + "model": "order.orderproduct", + "pk": 29, + "fields": { + "order": 8, + "product": 13, + "quantity": 1 + } + }, + { + "model": "order.orderproduct", + "pk": 30, + "fields": { + "order": 2, + "product": 3, + "quantity": 1 + } + } +] \ No newline at end of file diff --git a/essenza/order/tests.py b/essenza/order/tests.py index 7ce503c..a39b155 100644 --- a/essenza/order/tests.py +++ b/essenza/order/tests.py @@ -1,3 +1 @@ -from django.test import TestCase - # Create your tests here. diff --git a/essenza/product/admin.py b/essenza/product/admin.py index 3617350..60f93f5 100644 --- a/essenza/product/admin.py +++ b/essenza/product/admin.py @@ -1,5 +1,6 @@ from django.contrib import admin -from .models import Category, Product + +from .models import Product # Register your models here. admin.site.register(Product) diff --git a/essenza/product/apps.py b/essenza/product/apps.py index 235a333..c86dab3 100644 --- a/essenza/product/apps.py +++ b/essenza/product/apps.py @@ -2,5 +2,5 @@ class ProductConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'product' + default_auto_field = "django.db.models.BigAutoField" + name = "product" diff --git a/essenza/product/forms.py b/essenza/product/forms.py new file mode 100644 index 0000000..c9369a9 --- /dev/null +++ b/essenza/product/forms.py @@ -0,0 +1,18 @@ +from django import forms + +from .models import Product + + +class ProductForm(forms.ModelForm): + class Meta: + model = Product + fields = [ + "name", + "description", + "category", + "brand", + "price", + "photo", + "stock", + "is_active", + ] diff --git a/essenza/product/models.py b/essenza/product/models.py index b2cfaaf..cb526a4 100644 --- a/essenza/product/models.py +++ b/essenza/product/models.py @@ -1,22 +1,23 @@ from django.db import models + # Create your models here. class Category(models.TextChoices): - MAQUILLAJE = 'maquillaje', 'Maquillaje' - TRATAMIENTO = 'tratamiento', 'Tratamiento' - CABELLO = 'cabello', 'Cabello' - PERFUME = 'perfume', 'Perfume' + MAQUILLAJE = "maquillaje", "Maquillaje" + TRATAMIENTO = "tratamiento", "Tratamiento" + CABELLO = "cabello", "Cabello" + PERFUME = "perfume", "Perfume" + class Product(models.Model): name = models.CharField(max_length=255) description = models.TextField() - categoria = models.CharField(max_length=20, choices=Category.choices) + category = models.CharField(max_length=20, choices=Category.choices) brand = models.CharField(max_length=255) price = models.DecimalField(max_digits=10, decimal_places=2) - foto = models.ImageField(upload_to='profile_pics/', null=True, blank=True) + photo = models.ImageField(upload_to="products/", null=True, blank=True) stock = models.IntegerField() is_active = models.BooleanField(default=False) def __str__(self): return self.name - diff --git a/essenza/product/sample/sample.json b/essenza/product/sample/sample.json new file mode 100644 index 0000000..260c486 --- /dev/null +++ b/essenza/product/sample/sample.json @@ -0,0 +1,282 @@ +[ + { + "model": "product.product", + "pk": 1, + "fields": { + "name": "Maquillaje Base", + "description": "Base líquida para rostro, de acabado mate, 30ml.", + "category": "maquillaje", + "brand": "L'Oréal", + "price": 19.99, + "photo": "products/maquillaje_base.jpg", + "stock": 50, + "is_active": true + } + }, + { + "model": "product.product", + "pk": 2, + "fields": { + "name": "Shampoo Reconstructivo", + "description": "Shampoo nutritivo para cabellos dañados, 500ml.", + "category": "tratamiento", + "brand": "Pantene", + "price": 6.99, + "photo": "products/shampoo_reconstructivo.jpg", + "stock": 100, + "is_active": true + } + }, + { + "model": "product.product", + "pk": 3, + "fields": { + "name": "Secador de Pelo", + "description": "Secador de pelo con 3 niveles de temperatura, 2000W.", + "category": "cabello", + "brand": "Braun", + "price": 45.99, + "photo": "products/secador_pelo.jpg", + "stock": 30, + "is_active": true + } + }, + { + "model": "product.product", + "pk": 4, + "fields": { + "name": "Perfume Floral", + "description": "Perfume con notas de jazmín y rosa, 100ml.", + "category": "perfume", + "brand": "Chanel", + "price": 79.99, + "photo": "products/perfume_floral.jpg", + "stock": 20, + "is_active": true + } + }, + { + "model": "product.product", + "pk": 5, + "fields": { + "name": "Crema Hidratante", + "description": "Crema hidratante para piel seca, 50ml.", + "category": "tratamiento", + "brand": "Nivea", + "price": 12.99, + "photo": "products/crema_hidratante.jpg", + "stock": 150, + "is_active": true + } + }, + { + "model": "product.product", + "pk": 6, + "fields": { + "name": "Rizador de Pelo", + "description": "Rizador de pelo con control de temperatura, 25mm.", + "category": "cabello", + "brand": "Remington", + "price": 29.99, + "photo": "products/rizador_pelo.jpg", + "stock": 40, + "is_active": true + } + }, + { + "model": "product.product", + "pk": 7, + "fields": { + "name": "Gel Antibacterial", + "description": "Gel antibacterial para manos, 250ml.", + "category": "tratamiento", + "brand": "Dettol", + "price": 4.99, + "photo": "products/gel_antibacterial.jpg", + "stock": 200, + "is_active": true + } + }, + { + "model": "product.product", + "pk": 8, + "fields": { + "name": "Shampoo Anticaspa", + "description": "Shampoo anticaspa para cuero cabelludo sensible, 400ml.", + "category": "tratamiento", + "brand": "Head & Shoulders", + "price": 7.99, + "photo": "products/shampoo_anticaspa.jpg", + "stock": 90, + "is_active": true + } + }, + { + "model": "product.product", + "pk": 9, + "fields": { + "name": "Aceite Capilar", + "description": "Aceite nutritivo para el cabello, 150ml.", + "category": "tratamiento", + "brand": "Argan Oil", + "price": 15.99, + "photo": "products/aceite_capilar.jpg", + "stock": 60, + "is_active": true + } + }, + { + "model": "product.product", + "pk": 10, + "fields": { + "name": "Tinte de Cabello", + "description": "Tinte permanente para cabello, color castaño claro.", + "category": "cabello", + "brand": "Garnier", + "price": 8.99, + "photo": "products/tinte_cabello.jpg", + "stock": 110, + "is_active": true + } + }, + { + "model": "product.product", + "pk": 11, + "fields": { + "name": "Mascarilla Facial", + "description": "Mascarilla hidratante para todo tipo de piel, 100ml.", + "category": "tratamiento", + "brand": "L'Oréal", + "price": 18.99, + "photo": "products/mascarilla_facial.jpg", + "stock": 80, + "is_active": true + } + }, + { + "model": "product.product", + "pk": 12, + "fields": { + "name": "Shampoo Voluminizador", + "description": "Shampoo voluminizador para cabellos finos, 300ml.", + "category": "tratamiento", + "brand": "TRESemmé", + "price": 5.99, + "photo": "products/shampoo_voluminizador.jpg", + "stock": 120, + "is_active": true + } + }, + { + "model": "product.product", + "pk": 13, + "fields": { + "name": "Laca de Pelo", + "description": "Laca fijadora para todo el día, 400ml.", + "category": "cabello", + "brand": "Schwarzkopf", + "price": 10.99, + "photo": "products/laca_pelo.jpg", + "stock": 70, + "is_active": true + } + }, + { + "model": "product.product", + "pk": 14, + "fields": { + "name": "Crema Solar", + "description": "Protección solar SPF 50+, 200ml.", + "category": "tratamiento", + "brand": "Hawaiian Tropic", + "price": 14.99, + "photo": "products/crema_solar.jpg", + "stock": 40, + "is_active": true + } + }, + { + "model": "product.product", + "pk": 15, + "fields": { + "name": "Crema Antiedad", + "description": "Crema antiarrugas para el rostro, 50ml.", + "category": "tratamiento", + "brand": "Olay", + "price": 25.99, + "photo": "products/crema_antiedad.jpg", + "stock": 30, + "is_active": true + } + }, + { + "model": "product.product", + "pk": 16, + "fields": { + "name": "Desodorante", + "description": "Desodorante en barra, 75g.", + "category": "tratamiento", + "brand": "Dove", + "price": 3.99, + "photo": "products/desodorante.jpg", + "stock": 150, + "is_active": true + } + }, + { + "model": "product.product", + "pk": 17, + "fields": { + "name": "Toallitas Desmaquillantes", + "description": "Toallitas para desmaquillar, 25 unidades.", + "category": "tratamiento", + "brand": "Neutrogena", + "price": 4.49, + "photo": "products/toallitas_desmaquillantes.jpg", + "stock": 90, + "is_active": true + } + }, + { + "model": "product.product", + "pk": 18, + "fields": { + "name": "Pincel de Maquillaje", + "description": "Pincel para base líquida, cerdas suaves.", + "category": "maquillaje", + "brand": "Real Techniques", + "price": 12.99, + "photo": "products/pincel_maquillaje.jpg", + "stock": 110, + "is_active": true + } + }, + { + "model": "product.product", + "pk": 19, + "fields": { + "name": "Crema para Pies", + "description": "Crema reparadora para pies agrietados, 100ml.", + "category": "tratamiento", + "brand": "Eucerin", + "price": 9.99, + "photo": "products/crema_pies.jpg", + "stock": 80, + "is_active": true + } + }, + { + "model": "product.product", + "pk": 20, + "fields": { + "name": "Limpieza Facial", + "description": "Gel limpiador facial suave, 200ml.", + "category": "tratamiento", + "brand": "Neutrogena", + "price": 7.49, + "photo": "products/limpieza_facial.jpg", + "stock": 130, + "is_active": true + } + } +] diff --git a/essenza/product/tests.py b/essenza/product/tests.py index 7ce503c..6bf1658 100644 --- a/essenza/product/tests.py +++ b/essenza/product/tests.py @@ -1,3 +1,634 @@ +from decimal import Decimal + +from django.contrib.auth import get_user_model +from django.contrib.messages import get_messages # Para probar mensajes from django.test import TestCase +from django.urls import reverse +from django.utils import timezone +from order.models import Order, OrderProduct + +from .models import Category, Product + +User = get_user_model() + + +class DashboardViewLogicTests(TestCase): + def setUp(self): + self.dashboard_url = reverse("dashboard") + self.login_url = reverse("login") + self.now = timezone.now() + + # --- Usuarios (Usando tus roles 'admin' y 'user') --- + self.admin_user = User.objects.create_user( + username="admin", email="admin@test.com", password="pass", role="admin" + ) + self.regular_user = User.objects.create_user( + username="user", email="user@test.com", password="pass", role="user" + ) + + # --- Productos --- + self.p_30_day = Product.objects.create( + name="Producto 30 Días", + description="Test Desc", + category=Category.MAQUILLAJE, + brand="TestBrand", + price=10.00, + stock=10, + is_active=True, + ) + self.p_1_year = Product.objects.create( + name="Producto 1 Año", + description="Test Desc", + category=Category.CABELLO, + brand="TestBrand", + price=20.00, + stock=20, + is_active=True, + ) + self.p_stock = Product.objects.create( + name="Producto Stock Alto", + description="Test Desc", + category=Category.PERFUME, + brand="TestBrand", + price=30.00, + stock=999, + is_active=True, + ) + self.p_stock_low = Product.objects.create( + name="Producto Stock Bajo", + description="Test Desc", + category=Category.TRATAMIENTO, + brand="TestBrand", + price=40.00, + stock=1, + is_active=True, + ) + self.p_inactive = Product.objects.create( + name="Producto Inactivo", + description="Test Desc", + category=Category.MAQUILLAJE, + brand="TestBrand", + price=50.00, + stock=1000, + is_active=False, + ) + + # --- 1. Tests de Permisos (test_func) --- + + def test_anonymous_user_gets_200(self): + """Prueba que los anónimos pueden ver la vista.""" + resp = self.client.get(self.dashboard_url) + self.assertEqual(resp.status_code, 200) + self.assertTemplateUsed(resp, "product/dashboard.html") + + def test_regular_user_gets_200(self): + """Prueba que los usuarios 'user' pueden ver la vista.""" + self.client.login(email="user@test.com", password="pass") + resp = self.client.get(self.dashboard_url) + self.assertEqual(resp.status_code, 200) + self.assertTemplateUsed(resp, "product/dashboard.html") + + def test_admin_user_is_forbidden(self): + """Prueba que los 'admin' son bloqueados.""" + self.client.login(email="admin@test.com", password="pass") + resp = self.client.get(self.dashboard_url) + self.assertEqual(resp.status_code, 403) + + # --- 2. Tests de Lógica de Negocio (método get) --- + + def test_logic_branch_1_shows_30_day_products(self): + """ + Prueba el primer 'if': Muestra productos de 30 días. + Ignora ventas más antiguas aunque sean mayores. + """ + # 1. Crear ventas recientes (hace 10 días) + order_recent = Order.objects.create( + user=self.regular_user, + address="Test Address 1", + placed_at=self.now + - timezone.timedelta(days=10), # <-- Controlamos la fecha + ) + OrderProduct.objects.create( + order=order_recent, product=self.p_30_day, quantity=100 + ) + + # 2. Crear ventas antiguas (hace 100 días) + order_old = Order.objects.create( + user=self.regular_user, + address="Test Address 2", + placed_at=self.now - timezone.timedelta(days=100), + ) + OrderProduct.objects.create( + order=order_old, + product=self.p_1_year, + quantity=500, # Más ventas, pero antiguo + ) + + resp = self.client.get(self.dashboard_url) + products_in_context = list(resp.context["products"]) + + # ASERCIÓN: Solo debe aparecer el producto de 30 días + self.assertEqual(len(products_in_context), 1) + self.assertEqual(products_in_context[0], self.p_30_day) + self.assertEqual(products_in_context[0].total_quantity, 100) + + def test_logic_branch_2_falls_back_to_1_year_products(self): + """ + Prueba el segundo 'if': Falla 30 días, muestra 1 año. + Ignora ventas de hace más de 1 año. + """ + # 1. NO crear ventas recientes + + # 2. Crear ventas antiguas (hace 100 días) + order_old = Order.objects.create( + user=self.regular_user, + address="Test Address 1", + placed_at=self.now - timezone.timedelta(days=100), + ) + OrderProduct.objects.create( + order=order_old, product=self.p_1_year, quantity=500 + ) + + # 3. Crear ventas MUY antiguas (hace 400 días) - debe ignorarse + order_ancient = Order.objects.create( + user=self.regular_user, + address="Test Address 2", + placed_at=self.now - timezone.timedelta(days=400), + ) + OrderProduct.objects.create( + order=order_ancient, product=self.p_30_day, quantity=999 + ) + + resp = self.client.get(self.dashboard_url) + products_in_context = list(resp.context["products"]) + + # ASERCIÓN: Solo debe aparecer el producto de 1 año + self.assertEqual(len(products_in_context), 1) + self.assertEqual(products_in_context[0], self.p_1_year) + self.assertEqual(products_in_context[0].total_quantity, 500) + + def test_logic_branch_3_falls_back_to_stock_products(self): + """ + Prueba el tercer 'if': Falla 1 año, muestra por stock. + Ignora ventas de productos inactivos. + """ + # 1. NO crear ventas recientes + # 2. NO crear ventas en el último año + + # 3. Crear ventas MUY antiguas (hace 400 días) - para forzar el fallback + order_ancient = Order.objects.create( + user=self.regular_user, + address="Test Address 1", + placed_at=self.now - timezone.timedelta(days=400), + ) + OrderProduct.objects.create( + order=order_ancient, product=self.p_30_day, quantity=999 + ) + + # 4. Crear ventas de productos INACTIVOS (deben ignorarse siempre) + order_inactive = Order.objects.create( + user=self.regular_user, + address="Test Address 2", + placed_at=self.now - timezone.timedelta(days=10), # Reciente, pero inactivo + ) + OrderProduct.objects.create( + order=order_inactive, product=self.p_inactive, quantity=5000 + ) + + resp = self.client.get(self.dashboard_url) + products_in_context = list(resp.context["products"]) + + # ASERCIÓN: Debe mostrar los productos activos por stock descendente + # p_stock (999) > p_1_year (20) > p_30_day (10) > p_stock_low (1) + # p_inactive (1000) debe ser ignorado. + + self.assertIn(self.p_stock, products_in_context) + self.assertIn(self.p_1_year, products_in_context) + self.assertNotIn(self.p_inactive, products_in_context) # Clave + + # Comprobar el orden por stock + self.assertEqual(products_in_context[0], self.p_stock) # stock 999 + self.assertEqual(products_in_context[1], self.p_1_year) # stock 20 + self.assertEqual(products_in_context[2], self.p_30_day) # stock 10 + self.assertEqual(products_in_context[3], self.p_stock_low) # stock 1 + + # Comprobar que no hay 'total_quantity' (viene de la consulta de stock) + self.assertFalse(hasattr(products_in_context[0], "total_quantity")) + + def test_logic_branch_4_handles_empty_database(self): + """ + Prueba el caso límite final: No hay productos activos en la BBDD. + La vista debe devolver una lista vacía, no romperse. + """ + + # 1. Borrar TODOS los productos creados en el setUp + # (Esto deja la BBDD sin productos activos) + Product.objects.all().delete() + + # Cargar la vista + resp = self.client.get(self.dashboard_url) + self.assertEqual(resp.status_code, 200) + + # ASERCIÓN: + # El contexto 'products' debe existir, pero estar vacío. + self.assertIn("products", resp.context) + products_in_context = list(resp.context["products"]) + + self.assertEqual(len(products_in_context), 0) + + +class ProductCRUDTests(TestCase): + def setUp(self): + self.user = User.objects.create_user( + username="user", + email="user@example.com", + password="pass1234", + role="user", + ) + # Crear usuario admin + self.admin = User.objects.create_user( + username="admin", + email="admin@example.com", + password="pass1234", + role="admin", + ) + # Crear producto inicial + self.product = Product.objects.create( + name="Producto Test", + description="Descripción", + brand="Marca X", + price=10, + photo=None, + stock=5, + category="maquillaje", + is_active=True, + ) + # URLs + self.list_url = reverse("product_list") + self.detail_url = reverse("product_detail", args=[self.product.pk]) + self.create_url = reverse("product_create") + self.update_url = reverse("product_update", args=[self.product.pk]) + self.delete_url = reverse("product_delete", args=[self.product.pk]) + + # --------------------------------------------------- + # TESTS PARA USUARIO NO AUTENTICADO + # --------------------------------------------------- + + def test_list_requires_login(self): + resp = self.client.get(self.list_url) + self.assertEqual(resp.status_code, 302) + self.assertIn("/login", resp.url) + + def test_detail_requires_login(self): + resp = self.client.get(self.detail_url) + self.assertEqual(resp.status_code, 302) + self.assertIn("/login", resp.url) + + def test_create_requires_login(self): + resp = self.client.get(self.create_url) + self.assertEqual(resp.status_code, 302) + + # --------------------------------------------------- + # TESTS PARA USUARIO AUTENTICADO PERO NO ADMIN + # --------------------------------------------------- + + def test_user_cannot_access_list(self): + self.client.login(username="user", password="pass1234") + resp = self.client.get(self.list_url) + self.assertEqual(resp.status_code, 302) # Redirigido por permisos + + def test_user_cannot_access_detail(self): + self.client.login(username="user", password="pass1234") + resp = self.client.get(self.detail_url) + self.assertEqual(resp.status_code, 302) + + def test_user_cannot_access_create(self): + self.client.login(username="user", password="pass1234") + resp = self.client.get(self.create_url) + self.assertEqual(resp.status_code, 302) + + # --------------------------------------------------- + # TESTS PARA USUARIO ADMIN (PERMISO TOTAL) + # --------------------------------------------------- + + def test_admin_can_access_list(self): + self.client.force_login(self.admin) + url = reverse("product_list") + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + self.assertContains(resp, "Producto Test") + + def test_admin_can_access_detail(self): + self.client.force_login(self.admin) + url = reverse("product_detail", args=[self.product.pk]) + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + self.assertContains(resp, self.product.name) + + def test_admin_can_create_product(self): + """Prueba que un admin puede crear un nuevo producto (POST).""" + self.client.force_login(self.admin) + + initial_count = Product.objects.count() + + data = { + "name": "Nuevo Producto Creado", + "description": "Creado por el test de admin", + "category": "perfume", + "brand": "NewBrand", + "price": "99.99", + "stock": 100, + "is_active": True, + } + resp = self.client.post(self.create_url, data) + self.assertEqual(resp.status_code, 302) + self.assertEqual(Product.objects.count(), initial_count + 1) + self.assertTrue(Product.objects.filter(name="Nuevo Producto Creado").exists()) + + def test_admin_can_update_product(self): + """Prueba que un admin puede actualizar un producto existente (POST).""" + self.client.force_login(self.admin) + + updated_name = "Nombre Actualizado Admin" + updated_price = "15.50" + + data = { + "name": updated_name, + "description": "Descripción actualizada", + "category": "tratamiento", + "brand": self.product.brand, + "price": updated_price, + "stock": 50, + "is_active": False, + } + resp = self.client.post(self.update_url, data) + self.assertEqual(resp.status_code, 302) + + self.assertRedirects(resp, reverse("product_list")) + + self.product.refresh_from_db() + self.assertEqual(self.product.name, updated_name) + self.assertEqual(self.product.price, Decimal(updated_price)) + self.assertFalse(self.product.is_active) + self.assertEqual(self.product.stock, 50) + + def test_admin_can_delete_product(self): + self.client.force_login(self.admin) + url = reverse("product_delete", args=[self.product.pk]) + + # GET renderiza el confirm delete + resp_get = self.client.get(url) + self.assertEqual(resp_get.status_code, 200) + + # POST borra el producto + resp_post = self.client.post(url) + self.assertEqual(resp_post.status_code, 302) + + self.assertFalse(Product.objects.filter(pk=self.product.pk).exists()) + + +class CatalogViewTests(TestCase): + @classmethod + def setUpTestData(cls): + # Producto visible en el catálogo (is_active = True) + cls.active_product = Product.objects.create( + name="Producto Activo", + description="Descripción producto activo", + category=Category.MAQUILLAJE, + brand="Marca A", + price=Decimal("19.99"), + stock=10, + is_active=True, + ) + + # Producto NO visible en el catálogo (is_active = False) + cls.inactive_product = Product.objects.create( + name="Producto Inactivo", + description="Descripción producto inactivo", + category=Category.TRATAMIENTO, + brand="Marca B", + price=Decimal("9.99"), + stock=5, + is_active=False, + ) + + def test_catalog_url_status_code(self): + """La URL del catálogo responde con 200.""" + url = reverse("catalog") + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + def test_catalog_uses_correct_template(self): + """El catálogo usa la plantilla correcta.""" + url = reverse("catalog") + response = self.client.get(url) + self.assertTemplateUsed(response, "product/catalog.html") + + def test_catalog_shows_only_active_products(self): + """ + En el catálogo solo aparecen productos activos + (is_active=True). + """ + url = reverse("catalog") + response = self.client.get(url) + + products = response.context["products"] + + self.assertIn(self.active_product, products) + self.assertNotIn(self.inactive_product, products) + + +class CatalogDetailViewTests(TestCase): + @classmethod + def setUpTestData(cls): + cls.active_product = Product.objects.create( + name="Detalle Activo", + description="Descripción detalle activo", + category=Category.CABELLO, + brand="Marca C", + price=Decimal("29.99"), + stock=20, + is_active=True, + ) + + cls.inactive_product = Product.objects.create( + name="Detalle Inactivo", + description="Descripción detalle inactivo", + category=Category.PERFUME, + brand="Marca D", + price=Decimal("39.99"), + stock=0, + is_active=False, + ) + + def test_catalog_detail_status_code_and_template(self): + """ + El detalle de un producto activo devuelve 200 y usa + la plantilla de detalle para usuario. + """ + url = reverse("catalog_detail", args=[self.active_product.pk]) + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "product/detail_user.html") + self.assertContains(response, self.active_product.name) + + def test_catalog_detail_returns_404_for_inactive_product(self): + """ + Si el producto está inactivo, el detalle del catálogo + debe devolver 404. + """ + url = reverse("catalog_detail", args=[self.inactive_product.pk]) + response = self.client.get(url) + self.assertEqual(response.status_code, 404) + + def test_catalog_detail_returns_404_for_nonexistent_product(self): + """Si el producto no existe, también 404.""" + url = reverse("catalog_detail", args=[9999]) + response = self.client.get(url) + self.assertEqual(response.status_code, 404) + + +class StockTests(TestCase): + @classmethod + def setUpTestData(cls): + cls.user = User.objects.create_user( + username="user", + email="user@example.com", + password="pass1234", + role="user", + ) + cls.admin = User.objects.create_user( + username="admin", + email="admin@example.com", + password="pass1234", + role="admin", + ) + + cls.product_high = Product.objects.create( + name="Producto Alto", stock=20, price=10 + ) + cls.product_low = Product.objects.create( + name="Producto Bajo", stock=5, price=10 + ) + cls.product_out = Product.objects.create( + name="Producto Agotado", stock=0, price=10 + ) + + cls.stock_url = reverse("stock") + cls.login_url = reverse("login") + cls.dashboard_url = reverse("dashboard") + + # --- TESTS DE ACCESO --- + + def test_anonymous_user_redirects_to_dashboard(self): + resp = self.client.get(self.stock_url) + self.assertEqual(resp.status_code, 302) + self.assertRedirects(resp, self.dashboard_url) + + def test_non_admin_user_redirects_to_dashboard(self): + self.client.login(email=self.user.email, password="pass1234") + resp = self.client.get(self.stock_url) + self.assertEqual(resp.status_code, 302) + self.assertRedirects(resp, self.dashboard_url) + + def test_admin_user_succeeds_get(self): + self.client.login(email=self.admin.email, password="pass1234") + resp = self.client.get(self.stock_url) + self.assertEqual(resp.status_code, 200) + self.assertTemplateUsed(resp, "product/stock.html") + + # --- TESTS DE FUNCIONALIDAD --- + + def test_stock_page_shows_all_products(self): + self.client.login(email=self.admin.email, password="pass1234") + resp = self.client.get(self.stock_url) + + # Comprobamos que aparecen los 3 productos + self.assertEqual(len(resp.context["products"]), 3) + + # Comprobamos el HTML + self.assertContains(resp, "Producto Alto") + self.assertContains( + resp, 'En Stock: 20', html=True + ) + + self.assertContains(resp, "Producto Bajo") + self.assertContains( + resp, 'Stock Bajo: 5', html=True + ) + + self.assertContains(resp, "Producto Agotado") + self.assertContains( + resp, 'Agotado (0)', html=True + ) + + def test_post_admin_updates_stock_successfully(self): + """5. CORRECTO: Un admin puede actualizar el stock (Test 7 en tu código).""" + self.client.login(email=self.admin.email, password="pass1234") + + self.assertEqual(self.product_high.stock, 20) # Stock inicial + + data = {"product_id": self.product_high.pk, "stock": 15} + resp = self.client.post( + self.stock_url, data, follow=True + ) # Se hace una petición POST para actualizar el stock a 15 + + # Comprobamos que volvemos a la página de stock + self.assertEqual(resp.status_code, 200) + self.assertTemplateUsed(resp, "product/stock.html") + + # Comprobamos que la base de datos se actualizó correctamente + self.product_high.refresh_from_db() + self.assertEqual(self.product_high.stock, 15) + + # Comprobamos el mensaje de éxito + messages = list(get_messages(resp.context["request"])) + self.assertEqual(len(messages), 1) + self.assertEqual(str(messages[0]), "Stock de 'Producto Alto' actualizado a 15.") + + def test_post_admin_invalid_product_returns_404(self): + self.client.login(email=self.admin.email, password="pass1234") + data = {"product_id": 999, "stock": 15} # ID 999 no existe + + resp = self.client.post(self.stock_url, data) + self.assertEqual(resp.status_code, 404) + + def test_post_admin_invalid_stock_value_shows_error(self): + self.client.login(email=self.admin.email, password="pass1234") + + # Enviamos un valor de stock no numérico + data = {"product_id": self.product_high.pk, "stock": "abc"} + resp = self.client.post(self.stock_url, data, follow=True) + + # Comprobamos que volvemos a la página de stock + self.assertEqual(resp.status_code, 200) + + # Comprobamos que el stock NO se actualizó + self.product_high.refresh_from_db() + self.assertEqual(self.product_high.stock, 20) + + # Comprobamos el mensaje de error + messages = list(get_messages(resp.context["request"])) + self.assertEqual(len(messages), 1) + self.assertEqual( + str(messages[0]), "El valor de stock 'abc' no es un número válido." + ) + + def test_post_admin_negative_stock_value_shows_error(self): + self.client.login(email=self.admin.email, password="pass1234") + + # Enviamos un valor de stock negativo y comprobamos el error + data = {"product_id": self.product_high.pk, "stock": "-5"} + resp = self.client.post(self.stock_url, data, follow=True) + + self.assertEqual(resp.status_code, 200) + + self.product_high.refresh_from_db() + self.assertEqual(self.product_high.stock, 20) # No cambia -# Create your tests here. + messages = list(get_messages(resp.context["request"])) + self.assertEqual(len(messages), 1) + self.assertEqual( + str(messages[0]), "El valor de stock '-5' no es un número válido." + ) diff --git a/essenza/product/urls.py b/essenza/product/urls.py new file mode 100644 index 0000000..dfa067d --- /dev/null +++ b/essenza/product/urls.py @@ -0,0 +1,17 @@ +from django.conf import settings +from django.conf.urls.static import static +from django.urls import path + +import product.views as views + +urlpatterns = [ + path("stock/", views.StockView.as_view(), name="stock"), + path("", views.ProductListView.as_view(), name="product_list"), + path("create/", views.ProductCreateView.as_view(), name="product_create"), + path("+ Essenza S.L. es la denominación social y el nombre comercial de la tienda + online dedicada a la venta de productos de cosmética y cuidado personal, + rigiéndose su actividad por la legislación española vigente. En + cumplimiento del deber de información recogido en la Ley 34/2002 de + Servicios de la Sociedad de la Información y Comercio Electrónico + (LSSI-CE) y en el Real Decreto 85/2018 sobre productos cosméticos, a + continuación se detallan los datos de identificación del titular de este + sitio web: +
++ Estas condiciones regulan la relación contractual de compraventa entre + Essenza y usted desde el momento en que realiza un pedido en nuestra web. + La formalización de un pedido implica la lectura, comprensión y aceptación + expresa de estas Condiciones Generales de Venta en su totalidad, siendo de + obligado cumplimiento para ambas partes. +
+ ++ Essenza garantiza que todos sus productos han pasado rigurosos controles + de calidad y cumplen con los requisitos de seguridad establecidos por la + normativa europea (Reglamento CE 1223/2009). La información detallada de + ingredientes, modo de uso, precauciones y el Periodo Después de la + Apertura (PAO) se encuentra de forma clara y accesible en la ficha de cada + producto y en su etiquetado. Mantenemos un estricto control de + trazabilidad para garantizar la seguridad de todos los artículos de + cosmética que comercializamos. +
+ ++ El proceso de compra se considera finalizado y vinculante una vez que el + pago ha sido confirmado. Todos los precios mostrados en el sitio web están + expresados en euros (€) e incluyen el Impuesto sobre el Valor Añadido + (I.V.A.) legalmente aplicable. Los gastos de envío serán calculados en + base al peso y la dirección de entrega, siendo detallados y aceptados por + el Cliente antes de la confirmación final de la compra. +
+ ++ De acuerdo con la Ley General para la Defensa de los Consumidores y + Usuarios, el Cliente dispone de 14 días naturales desde la recepción del + pedido para ejercer su derecho de desistimiento. Condición Específica para + Cosmética: Por estrictas razones de higiene, seguridad y protección de la + salud, no se admitirá la devolución de productos cosméticos que hayan sido + abiertos, desprecintados o usados. En caso de desistimiento válido, los + costes directos de la devolución (envío de vuelta) correrán a cargo del + cliente, salvo si la causa es un producto defectuoso o un error de + Essenza. +
+ ++ En Essenza, su privacidad es nuestra prioridad. Los datos personales + recabados (nombre, dirección, email, datos de pago) a través de la web son + tratados bajo la legitimación de la ejecución de un contrato (para + gestionar su pedido) o el consentimiento (para el newsletter). Nos + comprometemos a no ceder sus datos a terceros, salvo obligación legal. + Usted puede ejercer en todo momento sus derechos de Acceso, Rectificación, + Supresión, Limitación, Portabilidad y Oposición (ARSLOP) enviando una + solicitud a info@essenza.com. +
+ + + + diff --git a/essenza/templates/product/catalog.html b/essenza/templates/product/catalog.html new file mode 100644 index 0000000..5675991 --- /dev/null +++ b/essenza/templates/product/catalog.html @@ -0,0 +1,191 @@ +{% extends "base.html" %} + + + +{% load static %} {% load humanize %} + +{% block title %}Catálogo · Essenza{% endblock %} + +{% block content %} + + + + + + +Explora nuestra selección de productos mejor valorados
+
+ {% endif %}
+
+ {{ product.price }} €
+ {{ product.get_category_display }} +