Compare commits
21 Commits
a61664a46e
..
latest
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d319d8efa | |||
| 874f4e29db | |||
| 131fe8fecc | |||
| b154da09a5 | |||
| f47fd21deb | |||
| ac27137b77 | |||
| 7c445d4b66 | |||
| e4f0611ac5 | |||
| 33dee87cb2 | |||
| 3de6d37e03 | |||
| 5503bbe8f7 | |||
| dd5ecec3f6 | |||
| c778669a7a | |||
| 09f6f800de | |||
| 1ac17109a3 | |||
| 325e55417b | |||
| e363bfd6dd | |||
| 90308d2383 | |||
| de4f36a25c | |||
| 424ffcffaf | |||
| f0a638be2e |
@@ -28,7 +28,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
DJANGO_SETTINGS_MODULE: proyecto.settings
|
DJANGO_SETTINGS_MODULE: proyecto.settings
|
||||||
run: |
|
run: |
|
||||||
uv run python manage.py test
|
SECRET_KEY=testkeynotuseinproducto uv run python manage.py test
|
||||||
|
|
||||||
docker:
|
docker:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout del código
|
- name: Checkout del código
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
- name: Configurar Python
|
- name: Configurar Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||||
with:
|
with:
|
||||||
python-version: '3.14'
|
python-version: '3.14'
|
||||||
- name: Configurar uv
|
- name: Configurar uv
|
||||||
uses: astral-sh/setup-uv@v6
|
uses: astral-sh/setup-uv@d0d8abe699bfb85fec6de9f7adb5ae17292296ff # v6
|
||||||
- name: Instalar dependencias
|
- name: Instalar dependencias
|
||||||
run: |
|
run: |
|
||||||
uv sync --no-dev --no-install-project
|
uv sync --no-dev --no-install-project
|
||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
DJANGO_SETTINGS_MODULE: proyecto.settings
|
DJANGO_SETTINGS_MODULE: proyecto.settings
|
||||||
run: |
|
run: |
|
||||||
uv run python manage.py test
|
SECRET_KEY=donotusethisinproductionitisunsafe uv run python manage.py test
|
||||||
docker:
|
docker:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: test
|
needs: test
|
||||||
@@ -37,13 +37,13 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout del código
|
- name: Checkout del código
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- name: Configurar Docker Buildx
|
- name: Configurar Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
|
||||||
|
|
||||||
- name: Login en GHCR
|
- name: Login en GHCR
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
echo "IMAGE_TAG=$TAG" >> $GITHUB_ENV
|
echo "IMAGE_TAG=$TAG" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Build y Push
|
- name: Build y Push
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
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
|
|
||||||
+70
-100
@@ -11,83 +11,47 @@ https://docs.djangoproject.com/en/6.0/ref/settings/
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os, sys
|
import os
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import environ
|
||||||
|
|
||||||
|
|
||||||
DEV_ENV = len(sys.argv) > 1 and sys.argv[1] == 'runserver'
|
DEV_ENV = len(sys.argv) > 1 and sys.argv[1] == 'runserver'
|
||||||
|
|
||||||
RUNNING_TESTS = any(arg in {'test', 'pytest'} for arg in sys.argv) or 'PYTEST_CURRENT_TEST' in os.environ
|
RUNNING_TESTS = any(arg in {'test', 'pytest'} for arg in sys.argv) or 'PYTEST_CURRENT_TEST' in os.environ
|
||||||
|
|
||||||
|
|
||||||
def load_dotenv(dotenv_path: Path) -> None:
|
|
||||||
if not dotenv_path.exists():
|
|
||||||
return
|
|
||||||
|
|
||||||
for raw_line in dotenv_path.read_text(encoding='utf-8').splitlines():
|
|
||||||
line = raw_line.strip()
|
|
||||||
if not line or line.startswith('#') or '=' not in line:
|
|
||||||
continue
|
|
||||||
|
|
||||||
key, value = line.split('=', 1)
|
|
||||||
key = key.strip()
|
|
||||||
value = value.strip().strip('"').strip("'")
|
|
||||||
os.environ.setdefault(key, value)
|
|
||||||
|
|
||||||
|
|
||||||
def env_bool(name: str, default: bool = False) -> bool:
|
|
||||||
value = os.getenv(name)
|
|
||||||
if value is None:
|
|
||||||
return default
|
|
||||||
return value.strip().lower() in {'1', 'true', 'yes', 'on'}
|
|
||||||
|
|
||||||
|
|
||||||
def env_list(name: str, default: list[str] | None = None) -> list[str]:
|
|
||||||
value = os.getenv(name)
|
|
||||||
if value is None:
|
|
||||||
return default or []
|
|
||||||
return [item.strip() for item in value.split(',') if item.strip()]
|
|
||||||
|
|
||||||
|
|
||||||
def env_int(name: str, default: int) -> int:
|
|
||||||
value = os.getenv(name)
|
|
||||||
if value is None:
|
|
||||||
return default
|
|
||||||
return int(value)
|
|
||||||
|
|
||||||
|
|
||||||
def env_str(name: str, default: str = '') -> str:
|
|
||||||
value = os.getenv(name)
|
|
||||||
if value is None:
|
|
||||||
return default
|
|
||||||
return value.strip()
|
|
||||||
|
|
||||||
|
|
||||||
def env_optional_str(name: str) -> str | None:
|
|
||||||
value = os.getenv(name)
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
value = value.strip()
|
|
||||||
return value or None
|
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
load_dotenv(BASE_DIR / '.env')
|
env = environ.Env(
|
||||||
|
DEBUG=(bool, True),
|
||||||
|
S3_ENABLE=(bool, False),
|
||||||
|
S3_USE_LOCAL_URLS=(bool, False),
|
||||||
|
POSTGRES_ENABLED=(bool, True),
|
||||||
|
POSTGRES_PORT=(int, 5432),
|
||||||
|
SMTP_PORT=(int, 587),
|
||||||
|
AWS_S3_USE_SSL=(bool, True),
|
||||||
|
AWS_QUERYSTRING_AUTH=(bool, False),
|
||||||
|
)
|
||||||
|
env.read_env(BASE_DIR / '.env')
|
||||||
|
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
# Quick-start development settings - unsuitable for production
|
||||||
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = os.getenv('SECRET_KEY', '')
|
SECRET_KEY = env('SECRET_KEY', default='')
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = env_bool('DEBUG', True)
|
DEBUG = env.bool('DEBUG')
|
||||||
S3_ENABLE = env_bool('S3_ENABLE', False)
|
S3_ENABLE = env.bool('S3_ENABLE')
|
||||||
S3_USE_LOCAL_URLS = env_bool('S3_USE_LOCAL_URLS', False)
|
S3_USE_LOCAL_URLS = env.bool('S3_USE_LOCAL_URLS')
|
||||||
|
|
||||||
ALLOWED_HOSTS = env_list('ALLOWED_HOSTS', [
|
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=[
|
||||||
'localhost',
|
'localhost',
|
||||||
'127.0.0.1',
|
'127.0.0.1',
|
||||||
|
'zkqpv8r3-8000.uks1.devtunnels.ms'
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
@@ -147,7 +111,7 @@ WSGI_APPLICATION = 'proyecto.wsgi.application'
|
|||||||
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
|
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
|
||||||
# Usa PostgreSQL por defecto (POSTGRES_ENABLED=True); si no, SQLite.
|
# Usa PostgreSQL por defecto (POSTGRES_ENABLED=True); si no, SQLite.
|
||||||
|
|
||||||
if RUNNING_TESTS or not env_bool('POSTGRES_ENABLED', True):
|
if RUNNING_TESTS or not env.bool('POSTGRES_ENABLED'):
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
@@ -158,11 +122,11 @@ else:
|
|||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.postgresql',
|
'ENGINE': 'django.db.backends.postgresql',
|
||||||
'NAME': os.getenv('POSTGRES_DB', 'tienda'),
|
'NAME': env('POSTGRES_DB', default='tienda'),
|
||||||
'USER': os.getenv('POSTGRES_USER', 'postgres'),
|
'USER': env('POSTGRES_USER', default='postgres'),
|
||||||
'PASSWORD': os.getenv('POSTGRES_PASSWORD', ''),
|
'PASSWORD': env('POSTGRES_PASSWORD', default=''),
|
||||||
'HOST': os.getenv('POSTGRES_HOST', '127.0.0.1'),
|
'HOST': env('POSTGRES_HOST', default='127.0.0.1'),
|
||||||
'PORT': env_int('POSTGRES_PORT', 5432),
|
'PORT': env.int('POSTGRES_PORT'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,10 +164,10 @@ USE_TZ = True
|
|||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/6.0/howto/static-files/
|
# https://docs.djangoproject.com/en/6.0/howto/static-files/
|
||||||
|
|
||||||
STATIC_URL = 'static/'
|
|
||||||
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
||||||
COMPRESS_ROOT = STATIC_ROOT
|
COMPRESS_ROOT = STATIC_ROOT
|
||||||
COMPRESS_URL = STATIC_URL
|
|
||||||
STATICFILES_DIRS = [
|
STATICFILES_DIRS = [
|
||||||
BASE_DIR / 'tienda' / 'static',
|
BASE_DIR / 'tienda' / 'static',
|
||||||
]
|
]
|
||||||
@@ -222,15 +186,15 @@ STORAGES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if S3_ENABLE:
|
if S3_ENABLE:
|
||||||
AWS_STORAGE_BUCKET_NAME = env_str('AWS_STORAGE_BUCKET_NAME') or None
|
AWS_STORAGE_BUCKET_NAME = env('AWS_STORAGE_BUCKET_NAME', default='') or None
|
||||||
AWS_ACCESS_KEY_ID = env_optional_str('AWS_ACCESS_KEY_ID')
|
AWS_ACCESS_KEY_ID = env('AWS_ACCESS_KEY_ID', default=None)
|
||||||
AWS_SECRET_ACCESS_KEY = env_optional_str('AWS_SECRET_ACCESS_KEY')
|
AWS_SECRET_ACCESS_KEY = env('AWS_SECRET_ACCESS_KEY', default=None)
|
||||||
AWS_S3_REGION_NAME = env_optional_str('AWS_S3_REGION_NAME')
|
AWS_S3_REGION_NAME = env('AWS_S3_REGION_NAME', default=None)
|
||||||
AWS_S3_ENDPOINT_URL = env_optional_str('AWS_S3_ENDPOINT_URL')
|
AWS_S3_ENDPOINT_URL = env('AWS_S3_ENDPOINT_URL', default=None)
|
||||||
AWS_S3_CUSTOM_DOMAIN = env_optional_str('AWS_S3_CUSTOM_DOMAIN')
|
AWS_S3_CUSTOM_DOMAIN = env('AWS_S3_CUSTOM_DOMAIN', default=None)
|
||||||
AWS_S3_USE_SSL = env_bool('AWS_S3_USE_SSL', True)
|
AWS_S3_USE_SSL = env.bool('AWS_S3_USE_SSL')
|
||||||
AWS_QUERYSTRING_AUTH = env_bool('AWS_QUERYSTRING_AUTH', False)
|
AWS_QUERYSTRING_AUTH = env.bool('AWS_QUERYSTRING_AUTH')
|
||||||
AWS_DEFAULT_ACL = env_str('AWS_DEFAULT_ACL', 'public-read') or None
|
AWS_DEFAULT_ACL = env('AWS_DEFAULT_ACL', default='public-read') or None
|
||||||
AWS_S3_OBJECT_PARAMETERS = {}
|
AWS_S3_OBJECT_PARAMETERS = {}
|
||||||
|
|
||||||
STORAGES = {
|
STORAGES = {
|
||||||
@@ -242,6 +206,14 @@ if S3_ENABLE:
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if S3_ENABLE and AWS_S3_CUSTOM_DOMAIN:
|
||||||
|
STATIC_URL = f"https://{AWS_S3_CUSTOM_DOMAIN}/static/"
|
||||||
|
MEDIA_URL = f"https://{AWS_S3_CUSTOM_DOMAIN}/media/"
|
||||||
|
else:
|
||||||
|
STATIC_URL = env("STATIC_URL", default="static/")
|
||||||
|
MEDIA_URL = env("MEDIA_URL", default="media/")
|
||||||
|
|
||||||
|
COMPRESS_URL = STATIC_URL
|
||||||
STATICFILES_FINDERS = [
|
STATICFILES_FINDERS = [
|
||||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||||
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||||
@@ -250,15 +222,13 @@ STATICFILES_FINDERS = [
|
|||||||
|
|
||||||
COMPRESS_PRECOMPILERS = ()
|
COMPRESS_PRECOMPILERS = ()
|
||||||
|
|
||||||
# Media files (User uploads)
|
MEDIA_ROOT = Path(env('MEDIA_ROOT', default='/app/media'))
|
||||||
MEDIA_URL = 'media/'
|
|
||||||
MEDIA_ROOT = Path(os.getenv('MEDIA_ROOT', '/app/media'))
|
|
||||||
|
|
||||||
# Redis Configuration
|
# Redis Configuration
|
||||||
CACHES = {
|
CACHES = {
|
||||||
'default': {
|
'default': {
|
||||||
'BACKEND': 'django_redis.cache.RedisCache',
|
'BACKEND': 'django_redis.cache.RedisCache',
|
||||||
'LOCATION': os.getenv('REDIS_URL', 'redis://127.0.0.1:6379/1'),
|
'LOCATION': env('REDIS_URL', default='redis://127.0.0.1:6379/1'),
|
||||||
'OPTIONS': {
|
'OPTIONS': {
|
||||||
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
|
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
|
||||||
}
|
}
|
||||||
@@ -283,30 +253,30 @@ MESSAGE_TAGS = {
|
|||||||
# Login URL
|
# Login URL
|
||||||
LOGIN_URL = '/tienda/login/'
|
LOGIN_URL = '/tienda/login/'
|
||||||
|
|
||||||
STRIPE_PUBLISHABLE_KEY = os.getenv('STRIPE_PUBLISHABLE_KEY', '')
|
STRIPE_PUBLISHABLE_KEY = env('STRIPE_PUBLISHABLE_KEY', default='')
|
||||||
STRIPE_SECRET_KEY = os.getenv('STRIPE_SECRET_KEY', '')
|
STRIPE_SECRET_KEY = env('STRIPE_SECRET_KEY', default='')
|
||||||
|
|
||||||
# PayPal Configuration (Sandbox)
|
# PayPal Configuration (Sandbox)
|
||||||
# Para obtener credenciales: https://sandbox.paypal.com/
|
# Para obtener credenciales: https://sandbox.paypal.com/
|
||||||
PAYPAL_CLIENT_ID = os.getenv('PAYPAL_CLIENT_ID', '') # Reemplazar con tu Client ID de PayPal Sandbox
|
PAYPAL_CLIENT_ID = env('PAYPAL_CLIENT_ID', default='') # Reemplazar con tu Client ID de PayPal Sandbox
|
||||||
PAYPAL_CLIENT_SECRET = os.getenv('PAYPAL_CLIENT_SECRET', '') # Reemplazar con tu Client Secret de PayPal Sandbox
|
PAYPAL_CLIENT_SECRET = env('PAYPAL_CLIENT_SECRET', default='') # Reemplazar con tu Client Secret de PayPal Sandbox
|
||||||
PAYPAL_MODE = os.getenv('PAYPAL_MODE', 'sandbox') # Cambiar a 'live' en producción
|
PAYPAL_MODE = env('PAYPAL_MODE', default='sandbox') # Cambiar a 'live' en producción
|
||||||
|
|
||||||
|
|
||||||
SMTP_ENDPOINT = os.getenv('SMTP_ENDPOINT', 'smtp.email.eu-paris-1.oci.oraclecloud.com')
|
SMTP_ENDPOINT = env('SMTP_ENDPOINT', default='smtp.email.eu-paris-1.oci.oraclecloud.com')
|
||||||
SMTP_PORT = env_int('SMTP_PORT', 587)
|
SMTP_PORT = env.int('SMTP_PORT')
|
||||||
SECURITY = os.getenv('SECURITY', 'tls')
|
SECURITY = env('SECURITY', default='tls')
|
||||||
SMTP_USERNAME = os.getenv('SMTP_USERNAME', None)
|
SMTP_USERNAME = env('SMTP_USERNAME', default=None)
|
||||||
SMTP_PASSWORD = os.getenv('SMTP_PASSWORD', None)
|
SMTP_PASSWORD = env('SMTP_PASSWORD', default=None)
|
||||||
SMTP_EMAIL = os.getenv("SMTP_EMAIL", None)
|
SMTP_EMAIL = env('SMTP_EMAIL', default=None)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
AUTH_USER_MODEL = 'tienda.User'
|
AUTH_USER_MODEL = 'tienda.User'
|
||||||
|
|
||||||
|
|
||||||
DOMAIN = os.getenv("DOMAIN", "localhost")
|
DOMAIN = env('DOMAIN', default='localhost')
|
||||||
PROTOCOL = os.getenv("PROTOCOL", "http")
|
PROTOCOL = env('PROTOCOL', default='http')
|
||||||
|
|
||||||
default_csrf_trusted_origins = []
|
default_csrf_trusted_origins = []
|
||||||
if DOMAIN:
|
if DOMAIN:
|
||||||
@@ -316,16 +286,16 @@ for host in ALLOWED_HOSTS:
|
|||||||
if host and host != '*':
|
if host and host != '*':
|
||||||
default_csrf_trusted_origins.append(f"{PROTOCOL}://{host}")
|
default_csrf_trusted_origins.append(f"{PROTOCOL}://{host}")
|
||||||
|
|
||||||
CSRF_TRUSTED_ORIGINS = env_list(
|
CSRF_TRUSTED_ORIGINS = env.list(
|
||||||
'CSRF_TRUSTED_ORIGINS',
|
'CSRF_TRUSTED_ORIGINS',
|
||||||
list(dict.fromkeys(default_csrf_trusted_origins)),
|
default=list(dict.fromkeys(default_csrf_trusted_origins)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO').upper()
|
LOG_LEVEL = env('LOG_LEVEL', default='INFO').upper()
|
||||||
LOG_DIR = Path(os.getenv('LOG_DIR', BASE_DIR / 'logs'))
|
LOG_DIR = Path(env('LOG_DIR', default=str(BASE_DIR / 'logs')))
|
||||||
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
LOG_FILE = LOG_DIR / os.getenv('LOG_FILE', 'app.log')
|
LOG_FILE = LOG_DIR / env('LOG_FILE', default='app.log')
|
||||||
|
|
||||||
|
|
||||||
LOGGING = {
|
LOGGING = {
|
||||||
@@ -399,13 +369,13 @@ EMAIL_HOST_USER = SMTP_USERNAME
|
|||||||
EMAIL_HOST_PASSWORD = SMTP_PASSWORD
|
EMAIL_HOST_PASSWORD = SMTP_PASSWORD
|
||||||
|
|
||||||
# El correo que se usará como remitente por defecto
|
# El correo que se usará como remitente por defecto
|
||||||
DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL") or SMTP_EMAIL or "no-reply@localhost"
|
DEFAULT_FROM_EMAIL = env('DEFAULT_FROM_EMAIL', default='') or SMTP_EMAIL or 'no-reply@localhost'
|
||||||
|
|
||||||
# URL de Redis (asumiendo que corre en el puerto default 6379)
|
# URL de Redis (asumiendo que corre en el puerto default 6379)
|
||||||
CELERY_BROKER_URL = os.getenv("REDIS_URL", "redis://localhost:6379/0")
|
CELERY_BROKER_URL = env('REDIS_URL', default='redis://localhost:6379/0')
|
||||||
|
|
||||||
# Opcional: para guardar el resultado de las tareas
|
# Opcional: para guardar el resultado de las tareas
|
||||||
CELERY_RESULT_BACKEND = os.getenv("REDIS_URL", "redis://localhost:6379/0")
|
CELERY_RESULT_BACKEND = env('REDIS_URL', default='redis://localhost:6379/0')
|
||||||
|
|
||||||
# Configuraciones adicionales recomendadas
|
# Configuraciones adicionales recomendadas
|
||||||
CELERY_ACCEPT_CONTENT = ['json']
|
CELERY_ACCEPT_CONTENT = ['json']
|
||||||
|
|||||||
+2
-1
@@ -7,6 +7,7 @@ dependencies = [
|
|||||||
"celery==5.6.3",
|
"celery==5.6.3",
|
||||||
"Django==6.0.5",
|
"Django==6.0.5",
|
||||||
"django-compressor==4.6.0",
|
"django-compressor==4.6.0",
|
||||||
|
"django-environ>=0.13.0",
|
||||||
"django-ninja>=1.6.2",
|
"django-ninja>=1.6.2",
|
||||||
"django-redis==6.0.0",
|
"django-redis==6.0.0",
|
||||||
# S3 backend requerido por tienda/storage_backends.py cuando S3_ENABLE=True.
|
# S3 backend requerido por tienda/storage_backends.py cuando S3_ENABLE=True.
|
||||||
@@ -17,7 +18,7 @@ dependencies = [
|
|||||||
"pillow==12.2.0",
|
"pillow==12.2.0",
|
||||||
"psycopg2-binary==2.9.12",
|
"psycopg2-binary==2.9.12",
|
||||||
"requests==2.34.2",
|
"requests==2.34.2",
|
||||||
"stripe==15.1.0",
|
"stripe==15.2.0",
|
||||||
"whitenoise==6.12.0",
|
"whitenoise==6.12.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@ from django import forms
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import FileExtensionValidator, MinLengthValidator, MaxLengthValidator
|
from django.core.validators import FileExtensionValidator, MinLengthValidator, MaxLengthValidator
|
||||||
from .models import Category
|
from .models import Category
|
||||||
from .constants import *
|
from .constants import IMAGE_TYPE, EMAIL_FORMNAME, INCORRECT_PASSWORDS
|
||||||
ALLOWED_IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp']
|
ALLOWED_IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp']
|
||||||
ALLOWED_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
|
ALLOWED_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
|
||||||
|
|
||||||
|
|||||||
@@ -22,13 +22,16 @@
|
|||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="form-label">Puntuación</label>
|
<label class="form-label" for="rating-input">
|
||||||
|
Puntuación
|
||||||
<div class="star-rating d-flex gap-1" id="star-rating">
|
<div class="star-rating d-flex gap-1" id="star-rating">
|
||||||
{% for i in "12345" %}
|
{% for i in "12345" %}
|
||||||
<span class="star fs-2 {% if form.initial.rating|default:0 >= i|add:0 %}text-warning text-dark{% else %}text-secondary{% endif %}" data-value="{{ i }}" style="cursor: pointer; font-size: 2rem;">★</span>
|
<span class="star fs-2 {% if form.initial.rating|default:0 >= i|add:0 %}text-warning text-dark{% else %}text-secondary{% endif %}" data-value="{{ i }}" style="cursor: pointer; font-size: 2rem;">★</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" name="rating" id="rating-input" value="{{ form.initial.rating|default:1 }}">
|
<input type="hidden" name="rating" id="rating-input" value="{{ form.initial.rating|default:1 }}">
|
||||||
|
</label>
|
||||||
|
|
||||||
{% if form.rating.errors %}
|
{% if form.rating.errors %}
|
||||||
<div class="text-danger small">{{ form.rating.errors }}</div>
|
<div class="text-danger small">{{ form.rating.errors }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -83,12 +86,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
stars.forEach(star => {
|
stars.forEach(star => {
|
||||||
star.addEventListener('click', function() {
|
star.addEventListener('click', function() {
|
||||||
const value = parseInt(this.dataset.value);
|
const value = Number.parseInt(this.dataset.value);
|
||||||
updateStars(value);
|
updateStars(value);
|
||||||
});
|
});
|
||||||
|
|
||||||
star.addEventListener('mouseenter', function() {
|
star.addEventListener('mouseenter', function() {
|
||||||
const value = parseInt(this.dataset.value);
|
const value = Number.parseInt(this.dataset.value);
|
||||||
stars.forEach((s, index) => {
|
stars.forEach((s, index) => {
|
||||||
if (index < value) {
|
if (index < value) {
|
||||||
s.classList.remove('text-secondary');
|
s.classList.remove('text-secondary');
|
||||||
@@ -98,7 +101,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
star.addEventListener('mouseleave', function() {
|
star.addEventListener('mouseleave', function() {
|
||||||
updateStars(parseInt(ratingInput.value) || 1);
|
updateStars(Number.parseInt(ratingInput.value) || 1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -44,8 +44,8 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Datos de la tarjeta</label>
|
<label id="label-card-data" class="form-label">Datos de la tarjeta <input type="hidden"></label>
|
||||||
<div id="card-element"></div>
|
<div id="card-element" aria-labelledby="label-card-data"></div>
|
||||||
<div id="card-errors" role="alert"></div>
|
<div id="card-errors" role="alert"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Comercialmeria</title>
|
||||||
<meta name="description" content="Sitio web de comercio local Almeriense">
|
<meta name="description" content="Sitio web de comercio local Almeriense">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
|
||||||
<link rel="preload" href="{% static 'css/custom.css' %}" as="style" onload="this.onload=null;this.rel='stylesheet'">
|
<link rel="preload" href="{% static 'css/custom.css' %}" as="style" onload="this.onload=null;this.rel='stylesheet'">
|
||||||
@@ -105,8 +106,8 @@
|
|||||||
<!-- Barra de búsqueda con sugerencias -->
|
<!-- Barra de búsqueda con sugerencias -->
|
||||||
<form class="search-suggestions-container" method="GET" action="{% url 'search' %}" role="search" id="searchForm">
|
<form class="search-suggestions-container" method="GET" action="{% url 'search' %}" role="search" id="searchForm">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<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">
|
<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="searchbutton" aria-haspopup="listbox">
|
||||||
<button class="btn btn-outline-primary" type="submit" aria-label="Buscar productos">🔍 Buscar</button>
|
<button class="btn btn-outline-primary" type="submit" id="searchbutton" aria-label="Buscar productos">🔍 Buscar</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="search-suggestions" id="searchSuggestions" role="listbox" aria-label="Sugerencias de búsqueda"></div>
|
<div class="search-suggestions" id="searchSuggestions" role="listbox" aria-label="Sugerencias de búsqueda"></div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -84,11 +84,11 @@
|
|||||||
<table class="table table-striped align-middle">
|
<table class="table table-striped align-middle">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Producto</th>
|
<th scope="col">Producto</th>
|
||||||
<th class="text-end">Precio (sin IVA)</th>
|
<th scope="col" class="text-end">Precio (sin IVA)</th>
|
||||||
<th class="text-end">Cantidad</th>
|
<th scope="col" class="text-end">Cantidad</th>
|
||||||
<th class="text-end">Stock actual</th>
|
<th scope="col" class="text-end">Stock actual</th>
|
||||||
<th class="text-end">Subtotal (con IVA)</th>
|
<th scope="col" class="text-end">Subtotal (con IVA)</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -104,16 +104,16 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="4" class="text-end">Subtotal:</th>
|
<th colspan="4" scope="row" class="text-end">Subtotal:</th>
|
||||||
<th class="text-end">{{ cart.get_total|format_price }}€</th>
|
<td class="text-end">{{ cart.get_total|format_price }}€</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="4" class="text-end">IVA (21%):</th>
|
<th scope="row" colspan="4" class="text-end">IVA (21%):</th>
|
||||||
<th class="text-end text-success">+{{ cart.get_vat_amount|format_price }}€</th>
|
<td class="text-end text-success">+{{ cart.get_vat_amount|format_price }}€</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr style="background-color: #f8f9fa;">
|
<tr style="background-color: #f8f9fa;">
|
||||||
<th colspan="4" class="text-end" style="font-size: 1.1rem;">Total:</th>
|
<th scope="row" colspan="4" class="text-end" style="font-size: 1.1rem;">Total:</th>
|
||||||
<th class="text-end" style="font-size: 1.1rem;">{{ cart.get_total_with_vat|format_price }}€</th>
|
<td class="text-end" style="font-size: 1.1rem;">{{ cart.get_total_with_vat|format_price }}€</th>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
@@ -125,7 +125,7 @@
|
|||||||
<h5 class="card-title mb-3">2) Selecciona tu método de pago</h5>
|
<h5 class="card-title mb-3">2) Selecciona tu método de pago</h5>
|
||||||
|
|
||||||
<!-- Tabs -->
|
<!-- Tabs -->
|
||||||
<ul class="nav nav-tabs mb-3" id="paymentTabs" role="tablist">
|
<ul class="nav nav-tabs mb-3" id="paymentTabs">
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link active" id="tab-card" data-tab="pane-card" type="button"
|
<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">
|
role="tab" aria-selected="true" aria-controls="pane-card" tabindex="0">
|
||||||
@@ -142,7 +142,7 @@
|
|||||||
|
|
||||||
<!-- Tarjeta tab -->
|
<!-- 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">
|
role="tabpanel" aria-labelledby="tab-card">
|
||||||
{% if saved_cards %}
|
{% if saved_cards %}
|
||||||
<fieldset class="mb-3">
|
<fieldset class="mb-3">
|
||||||
<legend class="fw-semibold fs-6 mb-2">Selección de tarjeta</legend>
|
<legend class="fw-semibold fs-6 mb-2">Selección de tarjeta</legend>
|
||||||
@@ -164,8 +164,8 @@
|
|||||||
|
|
||||||
<div id="new-card-section" {% if saved_cards %}style="display:none;"{% endif %}>
|
<div id="new-card-section" {% if saved_cards %}style="display:none;"{% endif %}>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Número de tarjeta</label>
|
<label id="label-card-number" class="form-label">Número de tarjeta <input type="hidden"></label>
|
||||||
<div id="card-element"></div>
|
<div id="card-element" aria-labelledby="label-card-number"></div>
|
||||||
<div id="card-errors" role="alert"></div>
|
<div id="card-errors" role="alert"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check mb-3">
|
<div class="form-check mb-3">
|
||||||
@@ -187,7 +187,7 @@
|
|||||||
|
|
||||||
<!-- PayPal tab -->
|
<!-- 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">
|
role="tabpanel" aria-labelledby="tab-paypal">
|
||||||
{% if saved_paypal %}
|
{% if saved_paypal %}
|
||||||
<div class="alert alert-light border mb-3">
|
<div class="alert alert-light border mb-3">
|
||||||
<small class="text-muted">Cuenta PayPal guardada:</small>
|
<small class="text-muted">Cuenta PayPal guardada:</small>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="padding: 20px;">
|
<th align="center" style="padding: 20px;">
|
||||||
<table width="600" border="0" cellspacing="0" cellpadding="0" style="border: 1px solid #eeeeee; background-color: #ffffff;">
|
<table width="600" border="0" cellspacing="0" cellpadding="0" style="border: 1px solid #eeeeee; background-color: #ffffff;">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="background-color: #007bff; padding: 40px;">
|
<th scope="col" align="center" style="background-color: #007bff; padding: 40px;">
|
||||||
<h1 style="color: #ffffff; font-family: sans-serif; margin: 0;">Su cuenta ha sido bloqueada</h1>
|
<h1 style="color: #ffffff; font-family: sans-serif; margin: 0;">Su cuenta ha sido bloqueada</h1>
|
||||||
</td>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="padding: 40px">
|
<td align="center" style="padding: 40px">
|
||||||
@@ -22,6 +22,6 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="padding: 20px;">
|
<th scope="col" align="center" style="padding: 20px;">
|
||||||
<table width="600" border="0" cellspacing="0" cellpadding="0" style="border: 1px solid #eeeeee; background-color: #ffffff;">
|
<table width="600" border="0" cellspacing="0" cellpadding="0" style="border: 1px solid #eeeeee; background-color: #ffffff;">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="background-color: #007bff; padding: 40px;">
|
<th scope="col" align="center" style="background-color: #007bff; padding: 40px;">
|
||||||
<h1 style="color: #ffffff; font-family: sans-serif; margin: 0;">¡Hola {{ name }}!</h1>
|
<h1 style="color: #ffffff; font-family: sans-serif; margin: 0;">¡Hola {{ name }}!</h1>
|
||||||
</td>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding: 30px; font-family: sans-serif; line-height: 1.5; color: #444444;">
|
<td style="padding: 30px; font-family: sans-serif; line-height: 1.5; color: #444444;">
|
||||||
@@ -16,6 +16,6 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="padding: 20px;">
|
<th scope="col" align="center" style="padding: 20px;">
|
||||||
<table width="600" border="0" cellspacing="0" cellpadding="0" style="border: 1px solid #eeeeee; background-color: #ffffff;">
|
<table width="600" border="0" cellspacing="0" cellpadding="0" style="border: 1px solid #eeeeee; background-color: #ffffff;">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="background-color: #007bff; padding: 40px;">
|
<th scope="col" align="center" style="background-color: #007bff; padding: 40px;">
|
||||||
<h1 style="color: #ffffff; font-family: sans-serif; margin: 0;">¡Hola {{ name }}!</h1>
|
<h1 style="color: #ffffff; font-family: sans-serif; margin: 0;">¡Hola {{ name }}!</h1>
|
||||||
</td>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="padding: 40px">
|
<td align="center" style="padding: 40px">
|
||||||
@@ -22,6 +22,6 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
<table border="0" cellpadding="0" style="width: 100%;">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="padding: 20px;">
|
<th scope="col" align="center" style="padding: 20px;">
|
||||||
<table width="600" border="0" cellspacing="0" cellpadding="0" style="border: 1px solid #eeeeee; background-color: #ffffff;">
|
<table width="600" border="0" cellspacing="0" cellpadding="0" style="border: 1px solid #eeeeee; background-color: #ffffff;">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="background-color: #007bff; padding: 40px;">
|
<th scope="col" align="center" style="background-color: #007bff; padding: 40px;">
|
||||||
<h1 style="color: #ffffff; font-family: sans-serif; margin: 0;">Su cuenta ha sido desbloqueada</h1>
|
<h1 style="color: #ffffff; font-family: sans-serif; margin: 0;">Su cuenta ha sido desbloqueada</h1>
|
||||||
</td>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="padding: 40px">
|
<td align="center" style="padding: 40px">
|
||||||
@@ -22,6 +22,6 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="padding: 20px;">
|
<th scope="col" align="center" style="padding: 20px;">
|
||||||
<table width="600" border="0" cellspacing="0" cellpadding="0" style="border: 1px solid #eeeeee; background-color: #ffffff;">
|
<table width="600" border="0" cellspacing="0" cellpadding="0" style="border: 1px solid #eeeeee; background-color: #ffffff;">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="background-color: #007bff; padding: 40px;">
|
<th scope="col" align="center" style="background-color: #007bff; padding: 40px;">
|
||||||
<h1 style="color: #ffffff; font-family: sans-serif; margin: 0;">¡Hola {{ name }}!</h1>
|
<h1 style="color: #ffffff; font-family: sans-serif; margin: 0;">¡Hola {{ name }}!</h1>
|
||||||
</td>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="padding: 40px">
|
<td align="center" style="padding: 40px">
|
||||||
@@ -21,6 +21,6 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if producto.primary_image %}
|
{% if producto.primary_image %}
|
||||||
<img src="{{ producto.primary_image.image.url }}" alt="{{ producto.primary_image.alt|default:producto.name }}" class="rounded" style="width: 200px; height: 200px; object-fit: cover;">
|
<img src="{{ producto.primary_image.image.url }}" alt="{{ producto.primary_image.alt|default:producto.name }} - imagen principal" class="rounded" style="width: 200px; height: 200px; object-fit: cover;">
|
||||||
<p class="mt-2 text-muted mb-0">Esta imagen no se puede cambiar desde aquí.</p>
|
<p class="mt-2 text-muted mb-0">Esta imagen no se puede cambiar desde aquí.</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="text-muted">No hay imagen principal asignada.</p>
|
<p class="text-muted">No hay imagen principal asignada.</p>
|
||||||
|
|||||||
@@ -94,7 +94,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script type="module">
|
||||||
async function loadReviews() {
|
async function loadReviews() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("{% url 'product_reviews' product.id %}");
|
const response = await fetch("{% url 'product_reviews' product.id %}");
|
||||||
@@ -157,6 +157,6 @@ async function loadReviews() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadReviews();
|
await loadReviews();
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
+2
-26
@@ -4,30 +4,6 @@ from django.conf import settings
|
|||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger("email.system")
|
logger = logging.getLogger("email.system")
|
||||||
#
|
|
||||||
#def send_email(dest: str, title: str, body: str):
|
|
||||||
# context = ssl.create_default_context()
|
|
||||||
# try:
|
|
||||||
# with smtplib.SMTP(settings.SMTP_ENDPOINT, settings.SMTP_PORT) as server:
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# server.ehlo()
|
|
||||||
# server.starttls(context=context)
|
|
||||||
# server.ehlo()
|
|
||||||
# server.login(settings.SMTP_USERNAME, settings.SMTP_PASSWORD)
|
|
||||||
#
|
|
||||||
# message = """\
|
|
||||||
#Subject: {}
|
|
||||||
#{}
|
|
||||||
# """.format(title, body)
|
|
||||||
# server.sendmail(settings.SMTP_EMAIL, dest, message)
|
|
||||||
# logger.info("EMAIL_SENT to=%s subject=%s", dest, title)
|
|
||||||
#
|
|
||||||
# except Exception as e:
|
|
||||||
# logger.exception("EMAIL_SEND_FAILED to=%s subject=%s error=%s", dest, title, str(e))
|
|
||||||
# return (False, e)
|
|
||||||
#
|
|
||||||
# return (True,)
|
|
||||||
|
|
||||||
def send_email(dest: str, title: str, body: str):
|
def send_email(dest: str, title: str, body: str):
|
||||||
try:
|
try:
|
||||||
@@ -40,7 +16,7 @@ def send_email(dest: str, title: str, body: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
logger.info("EMAIL_SENT to=%s subject=%s", dest, title)
|
logger.info("EMAIL_SENT to=%s subject=%s", dest, title)
|
||||||
return (True,)
|
return (True, None)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("EMAIL_SEND_FAILED to=%s subject=%s error=%s", dest, title, str(e))
|
logger.exception("EMAIL_SEND_FAILED to=%s subject=%s error=%s", dest, title, str(e))
|
||||||
return (False, e)
|
return (False, e)
|
||||||
@@ -57,7 +33,7 @@ def send_hemail(dest: str, title: str, body: str, nbody: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
logger.info("EMAIL_SENT to=%s subject=%s", dest, title)
|
logger.info("EMAIL_SENT to=%s subject=%s", dest, title)
|
||||||
return (True,)
|
return (True, None)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("EMAIL_SEND_FAILED to=%s subject=%s error=%s", dest, title, str(e))
|
logger.exception("EMAIL_SEND_FAILED to=%s subject=%s error=%s", dest, title, str(e))
|
||||||
return (False, e)
|
return (False, e)
|
||||||
+177
-121
@@ -42,6 +42,16 @@ audit_logger = logging.getLogger("tienda.audit")
|
|||||||
STOCK_RESERVATION_SESSION_KEY = "stock_reservation_id"
|
STOCK_RESERVATION_SESSION_KEY = "stock_reservation_id"
|
||||||
STOCK_RESERVATION_PAYMENT_SESSION_KEY = "stock_reservation_payment_method"
|
STOCK_RESERVATION_PAYMENT_SESSION_KEY = "stock_reservation_payment_method"
|
||||||
|
|
||||||
|
# Constantes para plantillas y mensajes reutilizados
|
||||||
|
LOGIN_TEMPLATE = "tienda/login.html"
|
||||||
|
INDEX_TEMPLATE = "tienda/index.html"
|
||||||
|
EDITAR_PERFIL_TEMPLATE = "tienda/editar_perfil.html"
|
||||||
|
EDITAR_DIRECCION_TEMPLATE = "tienda/editar_direccion.html"
|
||||||
|
MSG_CAMPOS_OBLIGATORIOS = "Por favor completa todos los campos obligatorios."
|
||||||
|
MSG_DIRECCION_INVALIDA = "Debes seleccionar una dirección de envío válida."
|
||||||
|
MSG_CARRITO_VACIO = "El carrito está vacío"
|
||||||
|
MSG_CUERPO_INVALIDO = "Cuerpo de la petición inválido"
|
||||||
|
|
||||||
|
|
||||||
def _mask_email(email: str) -> str:
|
def _mask_email(email: str) -> str:
|
||||||
if not email or '@' not in email:
|
if not email or '@' not in email:
|
||||||
@@ -232,65 +242,80 @@ def index(request: HttpRequest):
|
|||||||
|
|
||||||
products = Product.objects.all()[start:end]
|
products = Product.objects.all()[start:end]
|
||||||
categorias = Category.objects.all()
|
categorias = Category.objects.all()
|
||||||
return render(request, "tienda/index.html", {"products": products, "categories": categorias})
|
return render(request, INDEX_TEMPLATE, {"products": products, "categories": categorias})
|
||||||
|
|
||||||
@require_http_methods(["GET", "POST"])
|
def _try_get_user(email: str):
|
||||||
def login(request: HttpRequest):
|
"""Intenta obtener un usuario por email, retorna (user, username) o (None, None)."""
|
||||||
if request.method == "POST":
|
|
||||||
form: UserLoginForm = UserLoginForm(request.POST)
|
|
||||||
if form.is_valid():
|
|
||||||
email: str = form.cleaned_data["email"]
|
|
||||||
password: str = form.cleaned_data["password"]
|
|
||||||
remember: bool = form.cleaned_data["remember"]
|
|
||||||
client_ip = _get_client_ip(request)
|
|
||||||
try:
|
try:
|
||||||
user: User = User.objects.get(email=email)
|
user = User.objects.get(email=email)
|
||||||
username = user.username
|
return user, user.username
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
audit_logger.warning("LOGIN FAILED email=%s reason=user_not_found ip=%s", _mask_email(email), client_ip)
|
return None, None
|
||||||
messages.error(request, "El email o la contraseña es incorrecta")
|
|
||||||
return render(request, "tienda/login.html", {"form": form})
|
|
||||||
if user.registration_status == User.RegisterStatus.BANNED:
|
|
||||||
# Usuario baneado.
|
|
||||||
messages.error(request, "Esta cuenta esta bloqueada.")
|
|
||||||
return render(request, "tienda/login.html", {"form": form})
|
|
||||||
|
|
||||||
user = authenticate(request, username = username, password=password)
|
|
||||||
|
|
||||||
if user is None:
|
def _handle_login_rate_limit(request, username, email):
|
||||||
data: str = cache.get(f"tries_login_{username}")
|
"""Verifica rate limit de intentos de login. Retorna True si está rate-limited."""
|
||||||
logins: int
|
data = cache.get(f"tries_login_{username}")
|
||||||
if data is None:
|
logins = 0 if data is None else int(data)
|
||||||
logins = 0
|
|
||||||
else:
|
|
||||||
logins = int(data)
|
|
||||||
|
|
||||||
if logins >= 5:
|
if logins >= 5:
|
||||||
audit_logger.info("LOGIN FAILED email=%s reason=rate_limited", _mask_email(email))
|
audit_logger.info("LOGIN FAILED email=%s reason=rate_limited", _mask_email(email))
|
||||||
messages.error(request, "Has sufrido de Rate Limit por fallar 5 veces la contraseña")
|
messages.error(request, "Has sufrido de Rate Limit por fallar 5 veces la contraseña")
|
||||||
return render(request, "tienda/login.html", {"form": form})
|
return True
|
||||||
logins+=1
|
|
||||||
|
logins += 1
|
||||||
cache.set(f"tries_login_{username}", str(logins), 600)
|
cache.set(f"tries_login_{username}", str(logins), 600)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@require_http_methods(["GET", "POST"])
|
||||||
|
def login(request: HttpRequest):
|
||||||
|
if request.method != "POST":
|
||||||
|
return render(request, LOGIN_TEMPLATE, {"form": UserLoginForm()})
|
||||||
|
|
||||||
|
form = UserLoginForm(request.POST)
|
||||||
|
if not form.is_valid():
|
||||||
|
return render(request, LOGIN_TEMPLATE, {"form": form})
|
||||||
|
|
||||||
|
email = form.cleaned_data["email"]
|
||||||
|
password = form.cleaned_data["password"]
|
||||||
|
remember = form.cleaned_data["remember"]
|
||||||
|
client_ip = _get_client_ip(request)
|
||||||
|
|
||||||
|
user, username = _try_get_user(email)
|
||||||
|
if user is None:
|
||||||
|
audit_logger.warning("LOGIN FAILED email=%s reason=user_not_found ip=%s", _mask_email(email), client_ip)
|
||||||
messages.error(request, "El email o la contraseña es incorrecta")
|
messages.error(request, "El email o la contraseña es incorrecta")
|
||||||
return render(request, "tienda/login.html", {"form": form})
|
return render(request, LOGIN_TEMPLATE, {"form": form})
|
||||||
if user.registration_status == User.RegisterStatus.CONFIRMATION_REQUIRED:
|
|
||||||
|
if user.registration_status == User.RegisterStatus.BANNED:
|
||||||
|
messages.error(request, "Esta cuenta esta bloqueada.")
|
||||||
|
return render(request, LOGIN_TEMPLATE, {"form": form})
|
||||||
|
|
||||||
|
authenticated_user = authenticate(request, username=username, password=password)
|
||||||
|
|
||||||
|
if authenticated_user is None:
|
||||||
|
if _handle_login_rate_limit(request, username, email):
|
||||||
|
return render(request, LOGIN_TEMPLATE, {"form": form})
|
||||||
|
messages.error(request, "El email o la contraseña es incorrecta")
|
||||||
|
return render(request, LOGIN_TEMPLATE, {"form": form})
|
||||||
|
|
||||||
|
if authenticated_user.registration_status == User.RegisterStatus.CONFIRMATION_REQUIRED:
|
||||||
audit_logger.info("LOGIN_FAILED email=%s reason=not_verified", _mask_email(email))
|
audit_logger.info("LOGIN_FAILED email=%s reason=not_verified", _mask_email(email))
|
||||||
messages.error(request, "No se puede iniciar sesión porque no has verificado tu cuenta, comprueba tu email. Si eliminaste el email pero querias verificarte, contacta con el soporte tecnico")
|
messages.error(request, "No se puede iniciar sesión porque no has verificado tu cuenta, comprueba tu email. Si eliminaste el email pero querias verificarte, contacta con el soporte tecnico")
|
||||||
return render(request, "tienda/login.html", {"form": form})
|
return render(request, LOGIN_TEMPLATE, {"form": form})
|
||||||
auth_login(request, user)
|
|
||||||
|
auth_login(request, authenticated_user)
|
||||||
|
|
||||||
if not remember:
|
if not remember:
|
||||||
request.session.set_expiry(0)
|
request.session.set_expiry(0)
|
||||||
else:
|
else:
|
||||||
request.session.set_expiry(1209600)
|
request.session.set_expiry(1209600)
|
||||||
|
|
||||||
audit_logger.info("LOGIN_SUCCESS user_id=%s email=%s ip=%s remember=%s", user.id, _mask_email(user.email), client_ip, bool(remember))
|
audit_logger.info("LOGIN_SUCCESS user_id=%s email=%s ip=%s remember=%s", authenticated_user.id, _mask_email(authenticated_user.email), client_ip, bool(remember))
|
||||||
tasks.enviar_correo_bienvenida.delay(user.email, f"{user.first_name} {user.last_name}")
|
tasks.enviar_correo_bienvenida.delay(authenticated_user.email, f"{authenticated_user.first_name} {authenticated_user.last_name}")
|
||||||
messages.success(request, f"¡Bienvenido {user.first_name or user.username}!")
|
messages.success(request, f"¡Bienvenido {authenticated_user.first_name or authenticated_user.username}!")
|
||||||
return redirect("index")
|
return redirect("index")
|
||||||
else:
|
|
||||||
form = UserLoginForm()
|
|
||||||
return render(request, "tienda/login.html", {"form": form})
|
|
||||||
|
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
def register(request: HttpRequest):
|
def register(request: HttpRequest):
|
||||||
@@ -390,7 +415,7 @@ def categoria(request: HttpRequest, id: int):
|
|||||||
category = Category.objects.get(id=id)
|
category = Category.objects.get(id=id)
|
||||||
categories = Category.objects.all()
|
categories = Category.objects.all()
|
||||||
products = Product.objects.filter(category=category)[start:end]
|
products = Product.objects.filter(category=category)[start:end]
|
||||||
return render(request, "tienda/index.html", {"products": products, "categories": categories})
|
return render(request, INDEX_TEMPLATE, {"products": products, "categories": categories})
|
||||||
|
|
||||||
|
|
||||||
# Funciones auxiliares para el carrito
|
# Funciones auxiliares para el carrito
|
||||||
@@ -610,15 +635,9 @@ def _get_selected_shipping_address(request: HttpRequest):
|
|||||||
|
|
||||||
return ShippingAddress.objects.filter(id=shipping_address_id, user=request.user).first()
|
return ShippingAddress.objects.filter(id=shipping_address_id, user=request.user).first()
|
||||||
|
|
||||||
@require_GET
|
|
||||||
def create_order_from_cart(request, payment_method, payment_reference="", shipping_address=None, stock_reservation=None):
|
|
||||||
"""Crea un pedido a partir del carrito actual, validando y descontando stock."""
|
|
||||||
cart = get_or_create_cart(request)
|
|
||||||
cart_items = list(cart.items.select_related("product", "product__creator"))
|
|
||||||
|
|
||||||
if not cart_items:
|
|
||||||
return None, "El carrito está vacío."
|
|
||||||
|
|
||||||
|
def _calculate_order_totals(cart_items):
|
||||||
|
"""Calcula los totales de los items del carrito."""
|
||||||
order_total = Decimal("0.00")
|
order_total = Decimal("0.00")
|
||||||
items_with_totals = []
|
items_with_totals = []
|
||||||
purchased_items = []
|
purchased_items = []
|
||||||
@@ -627,75 +646,73 @@ def create_order_from_cart(request, payment_method, payment_reference="", shippi
|
|||||||
product = item.product
|
product = item.product
|
||||||
unit_price_with_vat = get_price_with_vat_decimal(product.price)
|
unit_price_with_vat = get_price_with_vat_decimal(product.price)
|
||||||
line_total_with_vat = (unit_price_with_vat * item.quantity).quantize(
|
line_total_with_vat = (unit_price_with_vat * item.quantity).quantize(
|
||||||
Decimal("0.01"),
|
Decimal("0.01"), rounding=ROUND_HALF_UP,
|
||||||
rounding=ROUND_HALF_UP,
|
|
||||||
)
|
)
|
||||||
order_total += line_total_with_vat
|
order_total += line_total_with_vat
|
||||||
items_with_totals.append((item, unit_price_with_vat, line_total_with_vat))
|
items_with_totals.append((item, unit_price_with_vat, line_total_with_vat))
|
||||||
purchased_items.append(
|
purchased_items.append({
|
||||||
{
|
|
||||||
"amount": item.quantity,
|
"amount": item.quantity,
|
||||||
"product_name": product.name,
|
"product_name": product.name,
|
||||||
"price": float(unit_price_with_vat),
|
"price": float(unit_price_with_vat),
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
_release_expired_stock_reservations()
|
return order_total, items_with_totals, purchased_items
|
||||||
|
|
||||||
with transaction.atomic():
|
|
||||||
locked_reservation = None
|
|
||||||
reserved_by_product = {}
|
|
||||||
|
|
||||||
if stock_reservation is not None:
|
def _validate_locked_reservation(stock_reservation):
|
||||||
|
"""Valida y carga la reserva de stock, retorna (locked_reservation, reserved_by_product).
|
||||||
|
Si stock_reservation es None, retorna (None, {}). Si la reserva ha caducado, retorna (None, None)."""
|
||||||
|
if stock_reservation is None:
|
||||||
|
return None, {}
|
||||||
|
|
||||||
locked_reservation = StockReservation.objects.select_for_update().filter(
|
locked_reservation = StockReservation.objects.select_for_update().filter(
|
||||||
id=stock_reservation.id,
|
id=stock_reservation.id,
|
||||||
status=StockReservation.STATUS_ACTIVE,
|
status=StockReservation.STATUS_ACTIVE,
|
||||||
expires_at__gt=timezone.now(),
|
expires_at__gt=timezone.now(),
|
||||||
).first()
|
).first()
|
||||||
if locked_reservation is None:
|
if locked_reservation is None:
|
||||||
return None, (
|
return None, None
|
||||||
f"La reserva de stock ha caducado. Tienes {STOCK_RESERVATION_MINUTES} minutos "
|
|
||||||
"desde que pulsas pagar. Revisa el carrito y vuelve a intentarlo."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
reserved_by_product = {}
|
||||||
for reservation_item in locked_reservation.items.all():
|
for reservation_item in locked_reservation.items.all():
|
||||||
reserved_by_product[reservation_item.product_id] = reservation_item.quantity
|
reserved_by_product[reservation_item.product_id] = reservation_item.quantity
|
||||||
|
|
||||||
product_ids = [item.product_id for item in cart_items]
|
return locked_reservation, reserved_by_product
|
||||||
products = Product.objects.select_for_update().filter(id__in=product_ids)
|
|
||||||
product_map = {product.id: product for product in products}
|
|
||||||
|
|
||||||
reserved_from_others = _get_reserved_quantities_by_product(
|
|
||||||
product_ids,
|
|
||||||
exclude_reservation_ids=[locked_reservation.id] if locked_reservation else None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
def _validate_order_items(cart_items, product_map, locked_reservation, reserved_by_product, reserved_from_others):
|
||||||
|
"""Valida disponibilidad y stock de cada item. Retorna None si ok, o str con el error."""
|
||||||
for item in cart_items:
|
for item in cart_items:
|
||||||
product = product_map.get(item.product_id)
|
product = product_map.get(item.product_id)
|
||||||
if product is None:
|
if product is None:
|
||||||
return None, f"El producto '{item.product.name}' ya no está disponible."
|
return f"El producto '{item.product.name}' ya no está disponible."
|
||||||
|
|
||||||
if locked_reservation is not None and item.quantity > reserved_by_product.get(item.product_id, 0):
|
if locked_reservation is not None and item.quantity > reserved_by_product.get(item.product_id, 0):
|
||||||
return None, (
|
return (
|
||||||
f"La cantidad de '{item.product.name}' ha cambiado desde la reserva. "
|
f"La cantidad de '{item.product.name}' ha cambiado desde la reserva. "
|
||||||
"Vuelve a intentar el pago."
|
"Vuelve a intentar el pago."
|
||||||
)
|
)
|
||||||
|
|
||||||
available = max(product.stock - reserved_from_others.get(item.product_id, 0), 0)
|
available = max(product.stock - reserved_from_others.get(item.product_id, 0), 0)
|
||||||
if item.quantity > available:
|
if item.quantity > available:
|
||||||
return None, _build_stock_issue_message({
|
return _build_stock_issue_message({
|
||||||
"product_name": item.product.name,
|
"product_name": item.product.name,
|
||||||
"requested": item.quantity,
|
"requested": item.quantity,
|
||||||
"available": available,
|
"available": available,
|
||||||
})
|
})
|
||||||
|
|
||||||
if product.stock < item.quantity:
|
if product.stock < item.quantity:
|
||||||
return None, _build_stock_issue_message({
|
return _build_stock_issue_message({
|
||||||
"product_name": item.product.name,
|
"product_name": item.product.name,
|
||||||
"requested": item.quantity,
|
"requested": item.quantity,
|
||||||
"available": product.stock,
|
"available": product.stock,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _create_order_and_items(request, order_total, items_with_totals, product_map, payment_method, payment_reference, shipping_address, locked_reservation):
|
||||||
|
"""Crea la orden y sus items, descuenta stock y marca reserva como completada."""
|
||||||
order = Order.objects.create(
|
order = Order.objects.create(
|
||||||
buyer=request.user if request.user.is_authenticated else None,
|
buyer=request.user if request.user.is_authenticated else None,
|
||||||
shipping_address=shipping_address,
|
shipping_address=shipping_address,
|
||||||
@@ -717,19 +734,55 @@ def create_order_from_cart(request, payment_method, payment_reference="", shippi
|
|||||||
unit_price=float(unit_price_with_vat),
|
unit_price=float(unit_price_with_vat),
|
||||||
total_price=float(line_total_with_vat),
|
total_price=float(line_total_with_vat),
|
||||||
)
|
)
|
||||||
|
|
||||||
product_row = product_map.get(item.product_id)
|
product_row = product_map.get(item.product_id)
|
||||||
product_row.stock = F('stock') - item.quantity
|
product_row.stock = F('stock') - item.quantity
|
||||||
product_row.save(update_fields=["stock"])
|
product_row.save(update_fields=["stock"])
|
||||||
|
|
||||||
_invalidate_product_cache(product_ids)
|
|
||||||
|
|
||||||
cart.items.all().delete()
|
|
||||||
|
|
||||||
if locked_reservation is not None:
|
if locked_reservation is not None:
|
||||||
locked_reservation.status = StockReservation.STATUS_COMPLETED
|
locked_reservation.status = StockReservation.STATUS_COMPLETED
|
||||||
locked_reservation.save(update_fields=["status", "updated_at"])
|
locked_reservation.save(update_fields=["status", "updated_at"])
|
||||||
|
|
||||||
|
return order
|
||||||
|
|
||||||
|
|
||||||
|
@require_GET
|
||||||
|
def create_order_from_cart(request, payment_method, payment_reference="", shipping_address=None, stock_reservation=None):
|
||||||
|
"""Crea un pedido a partir del carrito actual, validando y descontando stock."""
|
||||||
|
cart = get_or_create_cart(request)
|
||||||
|
cart_items = list(cart.items.select_related("product", "product__creator"))
|
||||||
|
|
||||||
|
if not cart_items:
|
||||||
|
return None, MSG_CARRITO_VACIO + "."
|
||||||
|
|
||||||
|
order_total, items_with_totals, purchased_items = _calculate_order_totals(cart_items)
|
||||||
|
_release_expired_stock_reservations()
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
locked_reservation, reserved_by_product = _validate_locked_reservation(stock_reservation)
|
||||||
|
if locked_reservation is None and stock_reservation is not None:
|
||||||
|
return None, (
|
||||||
|
f"La reserva de stock ha caducado. Tienes {STOCK_RESERVATION_MINUTES} minutos "
|
||||||
|
"desde que pulsas pagar. Revisa el carrito y vuelve a intentarlo."
|
||||||
|
)
|
||||||
|
|
||||||
|
product_ids = [item.product_id for item in cart_items]
|
||||||
|
products = Product.objects.select_for_update().filter(id__in=product_ids)
|
||||||
|
product_map = {product.id: product for product in products}
|
||||||
|
|
||||||
|
reserved_from_others = _get_reserved_quantities_by_product(
|
||||||
|
product_ids,
|
||||||
|
exclude_reservation_ids=[locked_reservation.id] if locked_reservation else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
error_msg = _validate_order_items(cart_items, product_map, locked_reservation, reserved_by_product, reserved_from_others)
|
||||||
|
if error_msg:
|
||||||
|
return None, error_msg
|
||||||
|
|
||||||
|
order = _create_order_and_items(request, order_total, items_with_totals, product_map, payment_method, payment_reference, shipping_address, locked_reservation)
|
||||||
|
|
||||||
|
_invalidate_product_cache(product_ids)
|
||||||
|
cart.items.all().delete()
|
||||||
|
|
||||||
if request.user.is_authenticated and purchased_items:
|
if request.user.is_authenticated and purchased_items:
|
||||||
tasks.process_purchase.delay(
|
tasks.process_purchase.delay(
|
||||||
request.user.id,
|
request.user.id,
|
||||||
@@ -970,15 +1023,8 @@ def crear_producto(request: HttpRequest):
|
|||||||
form = ProductForm()
|
form = ProductForm()
|
||||||
return render(request, "tienda/crear_producto.html", {"form":form})
|
return render(request, "tienda/crear_producto.html", {"form":form})
|
||||||
|
|
||||||
@require_http_methods(["GET","POST"])
|
def _handle_edit_product_post(request, producto, form):
|
||||||
@login_required
|
"""Procesa el POST de editar producto. Retorna respuesta HTTP o None si hay error."""
|
||||||
def editar_producto(request: HttpRequest, id: int):
|
|
||||||
"""Edita un producto del usuario autenticado"""
|
|
||||||
producto = get_object_or_404(Product, id=id, creator=request.user)
|
|
||||||
|
|
||||||
if request.method == "POST":
|
|
||||||
form = ProductEditForm(request.POST, request.FILES)
|
|
||||||
if form.is_valid():
|
|
||||||
producto.name = form.cleaned_data["name"]
|
producto.name = form.cleaned_data["name"]
|
||||||
producto.briefdesc = form.cleaned_data.get("briefdesc", "") or ""
|
producto.briefdesc = form.cleaned_data.get("briefdesc", "") or ""
|
||||||
producto.description = form.cleaned_data["description"]
|
producto.description = form.cleaned_data["description"]
|
||||||
@@ -1010,8 +1056,19 @@ def editar_producto(request: HttpRequest, id: int):
|
|||||||
|
|
||||||
messages.success(request, f"¡Producto '{producto.name}' actualizado exitosamente!")
|
messages.success(request, f"¡Producto '{producto.name}' actualizado exitosamente!")
|
||||||
return redirect("mis_productos")
|
return redirect("mis_productos")
|
||||||
else:
|
|
||||||
messages.error(request, "Por favor completa todos los campos obligatorios.")
|
|
||||||
|
@require_http_methods(["GET","POST"])
|
||||||
|
@login_required
|
||||||
|
def editar_producto(request: HttpRequest, id: int):
|
||||||
|
"""Edita un producto del usuario autenticado"""
|
||||||
|
producto = get_object_or_404(Product, id=id, creator=request.user)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
form = ProductEditForm(request.POST, request.FILES)
|
||||||
|
if form.is_valid():
|
||||||
|
return _handle_edit_product_post(request, producto, form)
|
||||||
|
messages.error(request, MSG_CAMPOS_OBLIGATORIOS)
|
||||||
else:
|
else:
|
||||||
initial = {
|
initial = {
|
||||||
"name": producto.name,
|
"name": producto.name,
|
||||||
@@ -1023,10 +1080,9 @@ def editar_producto(request: HttpRequest, id: int):
|
|||||||
}
|
}
|
||||||
form = ProductEditForm(initial=initial)
|
form = ProductEditForm(initial=initial)
|
||||||
|
|
||||||
categories = Category.objects.all()
|
|
||||||
return render(request, "tienda/editar_producto.html", {
|
return render(request, "tienda/editar_producto.html", {
|
||||||
"form": form,
|
"form": form,
|
||||||
"producto": producto
|
"producto": producto,
|
||||||
})
|
})
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@@ -1130,13 +1186,13 @@ def create_checkout_session(request: HttpRequest):
|
|||||||
try:
|
try:
|
||||||
shipping_address = _get_selected_shipping_address(request)
|
shipping_address = _get_selected_shipping_address(request)
|
||||||
if shipping_address is None:
|
if shipping_address is None:
|
||||||
return JsonResponse({"error": "Debes seleccionar una dirección de envío válida."}, status=400)
|
return JsonResponse({"error": MSG_DIRECCION_INVALIDA}, status=400)
|
||||||
|
|
||||||
cart = get_or_create_cart(request)
|
cart = get_or_create_cart(request)
|
||||||
cart_items = list(cart.items.select_related("product"))
|
cart_items = list(cart.items.select_related("product"))
|
||||||
|
|
||||||
if not cart_items:
|
if not cart_items:
|
||||||
return JsonResponse({"error": "El carrito está vacío"}, status=400)
|
return JsonResponse({"error": MSG_CARRITO_VACIO}, status=400)
|
||||||
|
|
||||||
active_reservation_ids = _get_active_reservation_ids_for_request(request)
|
active_reservation_ids = _get_active_reservation_ids_for_request(request)
|
||||||
stock_issues = _get_cart_stock_issues(cart_items, exclude_reservation_ids=active_reservation_ids)
|
stock_issues = _get_cart_stock_issues(cart_items, exclude_reservation_ids=active_reservation_ids)
|
||||||
@@ -1282,7 +1338,7 @@ def create_paypal_payment(request: HttpRequest):
|
|||||||
try:
|
try:
|
||||||
shipping_address = _get_selected_shipping_address(request)
|
shipping_address = _get_selected_shipping_address(request)
|
||||||
if shipping_address is None:
|
if shipping_address is None:
|
||||||
return JsonResponse({"error": "Debes seleccionar una dirección de envío válida."}, status=400)
|
return JsonResponse({"error": MSG_DIRECCION_INVALIDA}, status=400)
|
||||||
|
|
||||||
import paypalrestsdk
|
import paypalrestsdk
|
||||||
|
|
||||||
@@ -1290,7 +1346,7 @@ def create_paypal_payment(request: HttpRequest):
|
|||||||
cart_items = list(cart.items.select_related("product"))
|
cart_items = list(cart.items.select_related("product"))
|
||||||
|
|
||||||
if not cart_items:
|
if not cart_items:
|
||||||
return JsonResponse({"error": "El carrito está vacío"}, status=400)
|
return JsonResponse({"error": MSG_CARRITO_VACIO}, status=400)
|
||||||
|
|
||||||
active_reservation_ids = _get_active_reservation_ids_for_request(request)
|
active_reservation_ids = _get_active_reservation_ids_for_request(request)
|
||||||
stock_issues = _get_cart_stock_issues(cart_items, exclude_reservation_ids=active_reservation_ids)
|
stock_issues = _get_cart_stock_issues(cart_items, exclude_reservation_ids=active_reservation_ids)
|
||||||
@@ -1379,7 +1435,7 @@ def create_paypal_payment(request: HttpRequest):
|
|||||||
else:
|
else:
|
||||||
# Loguear el error
|
# Loguear el error
|
||||||
logger.error("PAYPAL_CREATE_ERROR user_id=%s", request.user.id)
|
logger.error("PAYPAL_CREATE_ERROR user_id=%s", request.user.id)
|
||||||
return JsonResponse({"error": f"Error al crear el pago"}, status=400)
|
return JsonResponse({"error": "Error al crear el pago"}, status=400)
|
||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
logger.error("PAYPAL_SDK_NOT_INSTALLED")
|
logger.error("PAYPAL_SDK_NOT_INSTALLED")
|
||||||
@@ -1387,7 +1443,7 @@ def create_paypal_payment(request: HttpRequest):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = str(e)
|
error_msg = str(e)
|
||||||
logger.exception("PAYPAL_CREATE_EXCEPTION user_id=%s", request.user.id)
|
logger.exception("PAYPAL_CREATE_EXCEPTION user_id=%s", request.user.id)
|
||||||
return JsonResponse({"error": f"Error al crear el pago"}, status=500)
|
return JsonResponse({"error": "Error al crear el pago"}, status=500)
|
||||||
|
|
||||||
|
|
||||||
@require_GET
|
@require_GET
|
||||||
@@ -1469,17 +1525,17 @@ def crear_payment_intent(request: HttpRequest):
|
|||||||
try:
|
try:
|
||||||
payload = json.loads(request.body.decode("utf-8") or "{}")
|
payload = json.loads(request.body.decode("utf-8") or "{}")
|
||||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||||
return JsonResponse({"error": "Cuerpo de la petición inválido"}, status=400)
|
return JsonResponse({"error": MSG_CUERPO_INVALIDO}, status=400)
|
||||||
|
|
||||||
shipping_address = _get_selected_shipping_address(request)
|
shipping_address = _get_selected_shipping_address(request)
|
||||||
if shipping_address is None:
|
if shipping_address is None:
|
||||||
return JsonResponse({"error": "Debes seleccionar una dirección de envío válida."}, status=400)
|
return JsonResponse({"error": MSG_DIRECCION_INVALIDA}, status=400)
|
||||||
|
|
||||||
cart = get_or_create_cart(request)
|
cart = get_or_create_cart(request)
|
||||||
cart_items = list(cart.items.select_related("product"))
|
cart_items = list(cart.items.select_related("product"))
|
||||||
|
|
||||||
if not cart_items:
|
if not cart_items:
|
||||||
return JsonResponse({"error": "El carrito está vacío"}, status=400)
|
return JsonResponse({"error": MSG_CARRITO_VACIO}, status=400)
|
||||||
|
|
||||||
active_reservation_ids = _get_active_reservation_ids_for_request(request)
|
active_reservation_ids = _get_active_reservation_ids_for_request(request)
|
||||||
stock_issues = _get_cart_stock_issues(cart_items, exclude_reservation_ids=active_reservation_ids)
|
stock_issues = _get_cart_stock_issues(cart_items, exclude_reservation_ids=active_reservation_ids)
|
||||||
@@ -1551,7 +1607,7 @@ def confirmar_pago_tarjeta(request: HttpRequest):
|
|||||||
try:
|
try:
|
||||||
payload = json.loads(request.body.decode("utf-8") or "{}")
|
payload = json.loads(request.body.decode("utf-8") or "{}")
|
||||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||||
return JsonResponse({"error": "Cuerpo de la petición inválido"}, status=400)
|
return JsonResponse({"error": MSG_CUERPO_INVALIDO}, status=400)
|
||||||
|
|
||||||
payment_intent_id = payload.get("payment_intent_id")
|
payment_intent_id = payload.get("payment_intent_id")
|
||||||
if not payment_intent_id:
|
if not payment_intent_id:
|
||||||
@@ -1624,13 +1680,13 @@ def crear_orden_paypal(request: HttpRequest):
|
|||||||
|
|
||||||
shipping_address = _get_selected_shipping_address(request)
|
shipping_address = _get_selected_shipping_address(request)
|
||||||
if shipping_address is None:
|
if shipping_address is None:
|
||||||
return JsonResponse({"error": "Debes seleccionar una dirección de envío válida."}, status=400)
|
return JsonResponse({"error": MSG_DIRECCION_INVALIDA}, status=400)
|
||||||
|
|
||||||
cart = get_or_create_cart(request)
|
cart = get_or_create_cart(request)
|
||||||
cart_items = list(cart.items.select_related("product"))
|
cart_items = list(cart.items.select_related("product"))
|
||||||
|
|
||||||
if not cart_items:
|
if not cart_items:
|
||||||
return JsonResponse({"error": "El carrito está vacío"}, status=400)
|
return JsonResponse({"error": MSG_CARRITO_VACIO}, status=400)
|
||||||
|
|
||||||
active_reservation_ids = _get_active_reservation_ids_for_request(request)
|
active_reservation_ids = _get_active_reservation_ids_for_request(request)
|
||||||
stock_issues = _get_cart_stock_issues(cart_items, exclude_reservation_ids=active_reservation_ids)
|
stock_issues = _get_cart_stock_issues(cart_items, exclude_reservation_ids=active_reservation_ids)
|
||||||
@@ -1677,7 +1733,7 @@ def capturar_orden_paypal(request: HttpRequest):
|
|||||||
try:
|
try:
|
||||||
payload = json.loads(request.body.decode("utf-8") or "{}")
|
payload = json.loads(request.body.decode("utf-8") or "{}")
|
||||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||||
return JsonResponse({"error": "Cuerpo de la petición inválido"}, status=400)
|
return JsonResponse({"error": MSG_CUERPO_INVALIDO}, status=400)
|
||||||
|
|
||||||
paypal_order_id = payload.get("orderID")
|
paypal_order_id = payload.get("orderID")
|
||||||
if not paypal_order_id:
|
if not paypal_order_id:
|
||||||
@@ -1812,7 +1868,7 @@ def confirmar_setup_intent(request: HttpRequest):
|
|||||||
try:
|
try:
|
||||||
payload = json.loads(request.body.decode("utf-8") or "{}")
|
payload = json.loads(request.body.decode("utf-8") or "{}")
|
||||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||||
return JsonResponse({"error": "Cuerpo de la petición inválido"}, status=400)
|
return JsonResponse({"error": MSG_CUERPO_INVALIDO}, status=400)
|
||||||
|
|
||||||
payment_method_id = payload.get("payment_method_id")
|
payment_method_id = payload.get("payment_method_id")
|
||||||
if not payment_method_id:
|
if not payment_method_id:
|
||||||
@@ -1931,7 +1987,7 @@ def capturar_orden_paypal_setup(request: HttpRequest):
|
|||||||
try:
|
try:
|
||||||
payload = json.loads(request.body.decode("utf-8") or "{}")
|
payload = json.loads(request.body.decode("utf-8") or "{}")
|
||||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||||
return JsonResponse({"error": "Cuerpo de la petición inválido"}, status=400)
|
return JsonResponse({"error": MSG_CUERPO_INVALIDO}, status=400)
|
||||||
|
|
||||||
paypal_order_id = payload.get("orderID")
|
paypal_order_id = payload.get("orderID")
|
||||||
if not paypal_order_id:
|
if not paypal_order_id:
|
||||||
@@ -2037,7 +2093,7 @@ def editar_perfil(request: HttpRequest):
|
|||||||
|
|
||||||
if email != request.user.email and User.objects.filter(email=email).exists():
|
if email != request.user.email and User.objects.filter(email=email).exists():
|
||||||
messages.error(request, "Ya existe un usuario con este correo electrónico.")
|
messages.error(request, "Ya existe un usuario con este correo electrónico.")
|
||||||
return render(request, "tienda/editar_perfil.html", {"form": form})
|
return render(request, EDITAR_PERFIL_TEMPLATE, {"form": form})
|
||||||
|
|
||||||
request.user.first_name = form.cleaned_data["first_name"]
|
request.user.first_name = form.cleaned_data["first_name"]
|
||||||
request.user.last_name = form.cleaned_data["last_name"]
|
request.user.last_name = form.cleaned_data["last_name"]
|
||||||
@@ -2054,7 +2110,7 @@ def editar_perfil(request: HttpRequest):
|
|||||||
}
|
}
|
||||||
form = EditProfileForm(initial=initial)
|
form = EditProfileForm(initial=initial)
|
||||||
|
|
||||||
return render(request, "tienda/editar_perfil.html", {"form": form})
|
return render(request, EDITAR_PERFIL_TEMPLATE, {"form": form})
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@@ -2069,11 +2125,11 @@ def cambiar_contrasena(request: HttpRequest):
|
|||||||
|
|
||||||
if not request.user.check_password(current_password):
|
if not request.user.check_password(current_password):
|
||||||
messages.error(request, "La contraseña actual es incorrecta.")
|
messages.error(request, "La contraseña actual es incorrecta.")
|
||||||
return render(request, "tienda/editar_perfil.html", {"password_form": ChangePasswordForm()})
|
return render(request, EDITAR_PERFIL_TEMPLATE, {"password_form": ChangePasswordForm()})
|
||||||
|
|
||||||
if len(new_password) < 8:
|
if len(new_password) < 8:
|
||||||
messages.error(request, "La contraseña debe tener al menos 8 caracteres.")
|
messages.error(request, "La contraseña debe tener al menos 8 caracteres.")
|
||||||
return render(request, "tienda/editar_perfil.html", {"password_form": ChangePasswordForm()})
|
return render(request, EDITAR_PERFIL_TEMPLATE, {"password_form": ChangePasswordForm()})
|
||||||
|
|
||||||
request.user.set_password(new_password)
|
request.user.set_password(new_password)
|
||||||
request.user.save()
|
request.user.save()
|
||||||
@@ -2084,7 +2140,7 @@ def cambiar_contrasena(request: HttpRequest):
|
|||||||
return redirect("portal_usuario")
|
return redirect("portal_usuario")
|
||||||
else:
|
else:
|
||||||
messages.error(request, "Las contraseñas nuevas no coinciden o son inválidas.")
|
messages.error(request, "Las contraseñas nuevas no coinciden o son inválidas.")
|
||||||
return render(request, "tienda/editar_perfil.html", {"password_form": form})
|
return render(request, EDITAR_PERFIL_TEMPLATE, {"password_form": form})
|
||||||
|
|
||||||
return redirect("editar_perfil")
|
return redirect("editar_perfil")
|
||||||
|
|
||||||
@@ -2112,11 +2168,11 @@ def crear_direccion(request: HttpRequest):
|
|||||||
|
|
||||||
if not _is_almeria_city(city):
|
if not _is_almeria_city(city):
|
||||||
messages.error(request, "El pueblo/ciudad debe pertenecer a la provincia de Almería.")
|
messages.error(request, "El pueblo/ciudad debe pertenecer a la provincia de Almería.")
|
||||||
return render(request, "tienda/editar_direccion.html", _address_form_context(form=form))
|
return render(request, EDITAR_DIRECCION_TEMPLATE, _address_form_context(form=form))
|
||||||
|
|
||||||
if not _is_almeria_postal_code(postal_code):
|
if not _is_almeria_postal_code(postal_code):
|
||||||
messages.error(request, "Solo realizamos envíos en la provincia de Almería (código postal 04xxx).")
|
messages.error(request, "Solo realizamos envíos en la provincia de Almería (código postal 04xxx).")
|
||||||
return render(request, "tienda/editar_direccion.html", _address_form_context(form=form))
|
return render(request, EDITAR_DIRECCION_TEMPLATE, _address_form_context(form=form))
|
||||||
|
|
||||||
ShippingAddress.objects.create(
|
ShippingAddress.objects.create(
|
||||||
user=request.user,
|
user=request.user,
|
||||||
@@ -2133,11 +2189,11 @@ def crear_direccion(request: HttpRequest):
|
|||||||
messages.success(request, "Dirección creada correctamente.")
|
messages.success(request, "Dirección creada correctamente.")
|
||||||
return redirect("direcciones_usuario")
|
return redirect("direcciones_usuario")
|
||||||
else:
|
else:
|
||||||
messages.error(request, "Por favor completa todos los campos obligatorios.")
|
messages.error(request, MSG_CAMPOS_OBLIGATORIOS)
|
||||||
else:
|
else:
|
||||||
form = ShippingAddressForm()
|
form = ShippingAddressForm()
|
||||||
|
|
||||||
return render(request, "tienda/editar_direccion.html", _address_form_context(form=form))
|
return render(request, EDITAR_DIRECCION_TEMPLATE, _address_form_context(form=form))
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@@ -2154,11 +2210,11 @@ def editar_direccion(request: HttpRequest, id: int):
|
|||||||
|
|
||||||
if not _is_almeria_city(city):
|
if not _is_almeria_city(city):
|
||||||
messages.error(request, "El pueblo/ciudad debe pertenece a la provincia de Almería.")
|
messages.error(request, "El pueblo/ciudad debe pertenece a la provincia de Almería.")
|
||||||
return render(request, "tienda/editar_direccion.html", _address_form_context(direccion, form=form))
|
return render(request, EDITAR_DIRECCION_TEMPLATE, _address_form_context(direccion, form=form))
|
||||||
|
|
||||||
if not _is_almeria_postal_code(postal_code):
|
if not _is_almeria_postal_code(postal_code):
|
||||||
messages.error(request, "Solo realizamos envíos en la provincia de Almería (código postal 04xxx).")
|
messages.error(request, "Solo realizamos envíos en la provincia de Almería (código postal 04xxx).")
|
||||||
return render(request, "tienda/editar_direccion.html", _address_form_context(direccion, form=form))
|
return render(request, EDITAR_DIRECCION_TEMPLATE, _address_form_context(direccion, form=form))
|
||||||
|
|
||||||
direccion.full_name = form.cleaned_data["full_name"]
|
direccion.full_name = form.cleaned_data["full_name"]
|
||||||
direccion.address_line_1 = form.cleaned_data["address_line_1"]
|
direccion.address_line_1 = form.cleaned_data["address_line_1"]
|
||||||
@@ -2173,7 +2229,7 @@ def editar_direccion(request: HttpRequest, id: int):
|
|||||||
messages.success(request, "Dirección actualizada correctamente.")
|
messages.success(request, "Dirección actualizada correctamente.")
|
||||||
return redirect("direcciones_usuario")
|
return redirect("direcciones_usuario")
|
||||||
else:
|
else:
|
||||||
messages.error(request, "Por favor completa todos los campos obligatorios.")
|
messages.error(request, MSG_CAMPOS_OBLIGATORIOS)
|
||||||
else:
|
else:
|
||||||
initial = {
|
initial = {
|
||||||
"full_name": direccion.full_name,
|
"full_name": direccion.full_name,
|
||||||
@@ -2187,7 +2243,7 @@ def editar_direccion(request: HttpRequest, id: int):
|
|||||||
}
|
}
|
||||||
form = ShippingAddressForm(initial=initial)
|
form = ShippingAddressForm(initial=initial)
|
||||||
|
|
||||||
return render(request, "tienda/editar_direccion.html", _address_form_context(direccion, form=form))
|
return render(request, EDITAR_DIRECCION_TEMPLATE, _address_form_context(direccion, form=form))
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@@ -2275,7 +2331,7 @@ def reset_password(request: HttpRequest):
|
|||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
tasks.enviar_correo_recuperacion.delay(form.cleaned_data["email"])
|
tasks.enviar_correo_recuperacion.delay(form.cleaned_data["email"])
|
||||||
messages.info(request, "Si tienes una cuenta con ese correo electronico, se ha enviado un correo con un enlace")
|
messages.info(request, "Si tienes una cuenta con ese correo electronico, se ha enviado un correo con un enlace")
|
||||||
return render(request, "tienda/index.html", {})
|
return render(request, INDEX_TEMPLATE, {})
|
||||||
|
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
def reset_password_phase2(request: HttpRequest, code: str):
|
def reset_password_phase2(request: HttpRequest, code: str):
|
||||||
|
|||||||
@@ -333,6 +333,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/d5/9d/9a0ba39f33574994e5b33aea55a68e8fad72b8dd923a82300e4e91774f59/django_compressor-4.6.0-py3-none-any.whl", hash = "sha256:6e7b21020a0d86272c5e37000c33accc4ebeb77394a3dd86d775a09aae7aade4", size = 96828, upload-time = "2025-11-10T13:12:10.001Z" },
|
{ url = "https://files.pythonhosted.org/packages/d5/9d/9a0ba39f33574994e5b33aea55a68e8fad72b8dd923a82300e4e91774f59/django_compressor-4.6.0-py3-none-any.whl", hash = "sha256:6e7b21020a0d86272c5e37000c33accc4ebeb77394a3dd86d775a09aae7aade4", size = 96828, upload-time = "2025-11-10T13:12:10.001Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-environ"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/18/3c/60983e6ec9b24a8d8588eecebfd21123cba980bce0a905807a27692f0860/django_environ-0.13.0.tar.gz", hash = "sha256:6c401e4c219442c2c4588c2116d5292b5484a6f69163ed09cd41f3943bfb645f", size = 63529, upload-time = "2026-02-18T01:08:08.791Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/00/3767393ece946084e1c6830a33ffb8e39d68642e27ad5ac7d4c8bd5de866/django_environ-0.13.0-py3-none-any.whl", hash = "sha256:37799d14cd78222c6fd8298e48bfe17965ff8e586091ad66a463e52e0e7b799e", size = 20682, upload-time = "2026-02-18T01:08:07.359Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-ninja"
|
name = "django-ninja"
|
||||||
version = "1.6.2"
|
version = "1.6.2"
|
||||||
@@ -536,6 +545,7 @@ dependencies = [
|
|||||||
{ name = "celery" },
|
{ name = "celery" },
|
||||||
{ name = "django" },
|
{ name = "django" },
|
||||||
{ name = "django-compressor" },
|
{ name = "django-compressor" },
|
||||||
|
{ name = "django-environ" },
|
||||||
{ name = "django-ninja" },
|
{ name = "django-ninja" },
|
||||||
{ name = "django-redis" },
|
{ name = "django-redis" },
|
||||||
{ name = "django-storages", extra = ["s3"] },
|
{ name = "django-storages", extra = ["s3"] },
|
||||||
@@ -554,6 +564,7 @@ requires-dist = [
|
|||||||
{ name = "celery", specifier = "==5.6.3" },
|
{ name = "celery", specifier = "==5.6.3" },
|
||||||
{ name = "django", specifier = "==6.0.5" },
|
{ name = "django", specifier = "==6.0.5" },
|
||||||
{ name = "django-compressor", specifier = "==4.6.0" },
|
{ name = "django-compressor", specifier = "==4.6.0" },
|
||||||
|
{ name = "django-environ", specifier = ">=0.13.0" },
|
||||||
{ name = "django-ninja", specifier = ">=1.6.2" },
|
{ name = "django-ninja", specifier = ">=1.6.2" },
|
||||||
{ name = "django-redis", specifier = "==6.0.0" },
|
{ name = "django-redis", specifier = "==6.0.0" },
|
||||||
{ name = "django-storages", extras = ["s3"], specifier = "==1.14.6" },
|
{ name = "django-storages", extras = ["s3"], specifier = "==1.14.6" },
|
||||||
|
|||||||
Reference in New Issue
Block a user