Compare commits
32 Commits
9d7a7f7432
...
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 | |||
| a61664a46e | |||
| 1a73a9e373 | |||
| 4877e859bd | |||
| 848a49c92d | |||
| ac9efaaf91 | |||
| 2024e2f90c | |||
| 6ec0f4e732 | |||
| 35e7e93600 | |||
| a7f43483f0 | |||
| d773addc53 | |||
| b143d92cb2 |
@@ -7,3 +7,13 @@ venv
|
|||||||
db.sqlite3
|
db.sqlite3
|
||||||
static
|
static
|
||||||
media
|
media
|
||||||
|
docs
|
||||||
|
logs
|
||||||
|
staticfiles
|
||||||
|
.gitignore
|
||||||
|
AGENTS.md
|
||||||
|
Dockerfile
|
||||||
|
Makefile
|
||||||
|
nginx.conf
|
||||||
|
Procfile
|
||||||
|
uv.lock
|
||||||
@@ -14,13 +14,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
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
|
||||||
@@ -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
|
||||||
@@ -38,13 +38,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: Build (sin push)
|
- name: Build (sin push)
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: false
|
push: false
|
||||||
|
|||||||
@@ -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
|
|
||||||
+21
-4
@@ -5,16 +5,33 @@ ENV PYTHONUNBUFFERED=1
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY pyproject.toml uv.lock /app/
|
COPY pyproject.toml uv.lock /app/
|
||||||
RUN apk --no-cache update && apk --no-cache upgrade
|
|
||||||
RUN pip install --no-cache-dir uv
|
|
||||||
RUN uv sync --no-dev --no-install-project # Install only dependencies, not the local project package
|
|
||||||
|
|
||||||
COPY . /app/
|
RUN apk --no-cache update \
|
||||||
|
&& apk --no-cache upgrade \
|
||||||
|
&& apk --no-cache add \
|
||||||
|
build-base \
|
||||||
|
freetype-dev \
|
||||||
|
jpeg-dev \
|
||||||
|
zlib-dev \
|
||||||
|
&& pip install --no-cache-dir uv \
|
||||||
|
&& uv sync --no-dev --no-install-project # Install only dependencies, not the local project package
|
||||||
|
|
||||||
|
COPY ./entrypoint.sh /app/entrypoint.sh
|
||||||
RUN chmod +x /app/entrypoint.sh
|
RUN chmod +x /app/entrypoint.sh
|
||||||
|
|
||||||
|
COPY ./proyecto /app/proyecto
|
||||||
|
COPY ./tienda /app/tienda
|
||||||
|
COPY ./manage.py /app/manage.py
|
||||||
|
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
RUN mkdir -pv /fonts
|
RUN mkdir -pv /fonts
|
||||||
COPY tienda/static/fonts/ /fonts/
|
COPY tienda/static/fonts/ /fonts/
|
||||||
|
|
||||||
|
RUN addgroup -S app \
|
||||||
|
&& adduser -S app -G app \
|
||||||
|
&& chown -R app:app /app /fonts
|
||||||
|
|
||||||
|
USER app
|
||||||
|
|
||||||
ENTRYPOINT ["/bin/sh", "/app/entrypoint.sh"]
|
ENTRYPOINT ["/bin/sh", "/app/entrypoint.sh"]
|
||||||
|
|||||||
+71
-109
@@ -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,33 +111,25 @@ 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:
|
if RUNNING_TESTS or not env.bool('POSTGRES_ENABLED'):
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
'NAME': BASE_DIR / 'db.sqlite3',
|
'NAME': BASE_DIR / 'db.sqlite3',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
elif env_bool('POSTGRES_ENABLED', True):
|
|
||||||
DATABASES = {
|
|
||||||
'default': {
|
|
||||||
'ENGINE': 'django.db.backends.postgresql',
|
|
||||||
'NAME': os.getenv('POSTGRES_DB', 'tienda'),
|
|
||||||
'USER': os.getenv('POSTGRES_USER', 'postgres'),
|
|
||||||
'PASSWORD': os.getenv('POSTGRES_PASSWORD', ''),
|
|
||||||
'HOST': os.getenv('POSTGRES_HOST', '127.0.0.1'),
|
|
||||||
'PORT': env_int('POSTGRES_PORT', 5432),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
'ENGINE': 'django.db.backends.postgresql',
|
||||||
'NAME': BASE_DIR / 'db.sqlite3',
|
'NAME': env('POSTGRES_DB', default='tienda'),
|
||||||
|
'USER': env('POSTGRES_USER', default='postgres'),
|
||||||
|
'PASSWORD': env('POSTGRES_PASSWORD', default=''),
|
||||||
|
'HOST': env('POSTGRES_HOST', default='127.0.0.1'),
|
||||||
|
'PORT': env.int('POSTGRES_PORT'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
|
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
@@ -208,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',
|
||||||
]
|
]
|
||||||
@@ -230,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 = {
|
||||||
@@ -250,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',
|
||||||
@@ -258,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',
|
||||||
}
|
}
|
||||||
@@ -291,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:
|
||||||
@@ -324,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 = {
|
||||||
@@ -407,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",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
-101
@@ -1,101 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -u
|
|
||||||
|
|
||||||
readonly HOSTS=(
|
|
||||||
"aws-docker-mysql"
|
|
||||||
"aws-docker-redis"
|
|
||||||
"aws-docker-celery"
|
|
||||||
"aws-docker"
|
|
||||||
)
|
|
||||||
|
|
||||||
readonly WAIT_SECONDS=5
|
|
||||||
readonly REMOTE_DEPLOY_DIR="/root/deploys"
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
echo "Uso: $0 {start|stop|restart|update}"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_status() {
|
|
||||||
local action="$1"
|
|
||||||
local host="$2"
|
|
||||||
local status="$3"
|
|
||||||
|
|
||||||
# Estilo similar al output de OpenRC.
|
|
||||||
printf "* %-8s %-16s [%s]\n" "$action" "$host" "$status"
|
|
||||||
}
|
|
||||||
|
|
||||||
run_remote_compose() {
|
|
||||||
local host="$1"
|
|
||||||
local command="$2"
|
|
||||||
|
|
||||||
ssh -o BatchMode=yes -o LogLevel=ERROR -T "$host" "sudo -n sh -c \"cd '$REMOTE_DEPLOY_DIR' || exit 1; if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then docker compose $command; elif command -v docker-compose >/dev/null 2>&1; then docker-compose $command; else exit 1; fi\"" >/dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
run_for_all_hosts() {
|
|
||||||
local mode="$1"
|
|
||||||
local host=""
|
|
||||||
local i=0
|
|
||||||
local total=${#HOSTS[@]}
|
|
||||||
|
|
||||||
for host in "${HOSTS[@]}"; do
|
|
||||||
case "$mode" in
|
|
||||||
start)
|
|
||||||
if run_remote_compose "$host" "up -d"; then
|
|
||||||
print_status "Started" "$host" "ok"
|
|
||||||
else
|
|
||||||
print_status "Started" "$host" "fail"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
stop)
|
|
||||||
if run_remote_compose "$host" "down"; then
|
|
||||||
print_status "Stopped" "$host" "ok"
|
|
||||||
else
|
|
||||||
print_status "Stopped" "$host" "fail"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
restart)
|
|
||||||
if run_remote_compose "$host" "down" && run_remote_compose "$host" "up -d"; then
|
|
||||||
print_status "Restarted" "$host" "ok"
|
|
||||||
else
|
|
||||||
print_status "Restarted" "$host" "fail"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
update)
|
|
||||||
if run_remote_compose "$host" "pull" && run_remote_compose "$host" "down" && run_remote_compose "$host" "up -d"; then
|
|
||||||
print_status "Updated" "$host" "ok"
|
|
||||||
else
|
|
||||||
print_status "Updated" "$host" "fail"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
i=$((i + 1))
|
|
||||||
if [ "$i" -lt "$total" ]; then
|
|
||||||
sleep "$WAIT_SECONDS"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
if [ "$#" -ne 1 ]; then
|
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "$1" in
|
|
||||||
start|stop|restart|update)
|
|
||||||
run_for_all_hosts "$1"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
@@ -20,7 +20,6 @@ class UserAdmin(admin.ModelAdmin):
|
|||||||
def banear_usuario_action(self, request, queryset):
|
def banear_usuario_action(self, request, queryset):
|
||||||
usuarios_baneados = 0
|
usuarios_baneados = 0
|
||||||
for user in queryset:
|
for user in queryset:
|
||||||
user: User = user
|
|
||||||
# Desactiva usuario
|
# Desactiva usuario
|
||||||
if user.registration_status == User.RegisterStatus.BANNED:
|
if user.registration_status == User.RegisterStatus.BANNED:
|
||||||
continue
|
continue
|
||||||
@@ -43,7 +42,6 @@ class UserAdmin(admin.ModelAdmin):
|
|||||||
def desbanear_usuario_action(self, request, queryset):
|
def desbanear_usuario_action(self, request, queryset):
|
||||||
user_desbaneados = 0
|
user_desbaneados = 0
|
||||||
for user in queryset:
|
for user in queryset:
|
||||||
user: User = user
|
|
||||||
if user.registration_status != User.RegisterStatus.BANNED:
|
if user.registration_status != User.RegisterStatus.BANNED:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
IMAGE_TYPE = "image/*"
|
||||||
|
EMAIL_FORMNAME = "Correo Electrónico"
|
||||||
|
INCORRECT_PASSWORDS = "Las contraseñas no coinciden"
|
||||||
+12
-9
@@ -2,10 +2,13 @@ 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 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']
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def validate_image_file(value):
|
def validate_image_file(value):
|
||||||
ext = value.name.split('.')[-1].lower()
|
ext = value.name.split('.')[-1].lower()
|
||||||
if ext not in ALLOWED_IMAGE_EXTENSIONS:
|
if ext not in ALLOWED_IMAGE_EXTENSIONS:
|
||||||
@@ -76,7 +79,7 @@ class ProductForm(forms.Form):
|
|||||||
widget = forms.ClearableFileInput(
|
widget = forms.ClearableFileInput(
|
||||||
attrs = {
|
attrs = {
|
||||||
'class': 'form-control',
|
'class': 'form-control',
|
||||||
'accept': 'image/*'
|
'accept': IMAGE_TYPE
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -133,7 +136,7 @@ class SecondaryImageForm(forms.Form):
|
|||||||
widget = forms.ClearableFileInput(
|
widget = forms.ClearableFileInput(
|
||||||
attrs = {
|
attrs = {
|
||||||
'class': 'form-control',
|
'class': 'form-control',
|
||||||
'accept': 'image/*'
|
'accept': IMAGE_TYPE
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -192,7 +195,7 @@ class UserRegisterForm(forms.Form):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
email = forms.EmailField(
|
email = forms.EmailField(
|
||||||
label = "Correo Electrónico",
|
label = EMAIL_FORMNAME,
|
||||||
max_length = 255,
|
max_length = 255,
|
||||||
required = True,
|
required = True,
|
||||||
widget = forms.TextInput(
|
widget = forms.TextInput(
|
||||||
@@ -236,7 +239,7 @@ class UserRegisterForm(forms.Form):
|
|||||||
password = cleaned_data.get("password")
|
password = cleaned_data.get("password")
|
||||||
password_confirm = cleaned_data.get("password_confirm")
|
password_confirm = cleaned_data.get("password_confirm")
|
||||||
if password and password_confirm and password != password_confirm:
|
if password and password_confirm and password != password_confirm:
|
||||||
raise ValidationError("Las contraseñas no coinciden.")
|
raise ValidationError(INCORRECT_PASSWORDS)
|
||||||
|
|
||||||
|
|
||||||
class EditProfileForm(forms.Form):
|
class EditProfileForm(forms.Form):
|
||||||
@@ -253,7 +256,7 @@ class EditProfileForm(forms.Form):
|
|||||||
widget=forms.TextInput(attrs={'class': 'form-control'})
|
widget=forms.TextInput(attrs={'class': 'form-control'})
|
||||||
)
|
)
|
||||||
email = forms.EmailField(
|
email = forms.EmailField(
|
||||||
label="Correo Electrónico",
|
label=EMAIL_FORMNAME,
|
||||||
max_length=254,
|
max_length=254,
|
||||||
required=True,
|
required=True,
|
||||||
widget=forms.EmailInput(attrs={'class': 'form-control'})
|
widget=forms.EmailInput(attrs={'class': 'form-control'})
|
||||||
@@ -285,7 +288,7 @@ class ChangePasswordForm(forms.Form):
|
|||||||
new_password = cleaned_data.get("new_password")
|
new_password = cleaned_data.get("new_password")
|
||||||
confirm_password = cleaned_data.get("confirm_password")
|
confirm_password = cleaned_data.get("confirm_password")
|
||||||
if new_password and confirm_password and new_password != confirm_password:
|
if new_password and confirm_password and new_password != confirm_password:
|
||||||
raise ValidationError("Las contraseñas no coinciden.")
|
raise ValidationError(INCORRECT_PASSWORDS)
|
||||||
if new_password and len(new_password) < 8:
|
if new_password and len(new_password) < 8:
|
||||||
raise ValidationError("La contraseña debe tener al menos 8 caracteres.")
|
raise ValidationError("La contraseña debe tener al menos 8 caracteres.")
|
||||||
|
|
||||||
@@ -343,7 +346,7 @@ class ShippingAddressForm(forms.Form):
|
|||||||
|
|
||||||
class ResetPasswordForm(forms.Form):
|
class ResetPasswordForm(forms.Form):
|
||||||
email = forms.EmailField(
|
email = forms.EmailField(
|
||||||
label="Correo Electrónico",
|
label=EMAIL_FORMNAME,
|
||||||
max_length=254,
|
max_length=254,
|
||||||
required=True,
|
required=True,
|
||||||
widget=forms.EmailInput(attrs={'class': 'form-control', 'placeholder': 'tu@email.com'})
|
widget=forms.EmailInput(attrs={'class': 'form-control', 'placeholder': 'tu@email.com'})
|
||||||
@@ -369,7 +372,7 @@ class ResetPasswordPhase2Form(forms.Form):
|
|||||||
password = cleaned_data.get("password")
|
password = cleaned_data.get("password")
|
||||||
verify_password = cleaned_data.get("verify_password")
|
verify_password = cleaned_data.get("verify_password")
|
||||||
if password and verify_password and password != verify_password:
|
if password and verify_password and password != verify_password:
|
||||||
raise ValidationError("Las contraseñas no coinciden.")
|
raise ValidationError(INCORRECT_PASSWORDS)
|
||||||
|
|
||||||
|
|
||||||
class ReviewForm(forms.Form):
|
class ReviewForm(forms.Form):
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 6.0.5 on 2026-05-26 08:25
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('tienda', '0009_alter_cartitem_quantity_alter_orderitem_quantity_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cart',
|
||||||
|
name='session_key',
|
||||||
|
field=models.CharField(blank=True, default='', max_length=40),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='order',
|
||||||
|
name='session_key',
|
||||||
|
field=models.CharField(blank=True, default='', max_length=40),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='stockreservation',
|
||||||
|
name='session_key',
|
||||||
|
field=models.CharField(blank=True, default='', max_length=40),
|
||||||
|
),
|
||||||
|
]
|
||||||
+39
-5
@@ -6,11 +6,37 @@ from django.contrib.auth.models import User, AbstractUser
|
|||||||
from django.core.validators import MaxValueValidator
|
from django.core.validators import MaxValueValidator
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from .vars import VAT_RATE, TRANSACTION_CODE_PREFIX, TRANSACTION_CODE_LENGTH, TRANSACTION_CODE_ALPHABET
|
from .vars import VAT_RATE, TRANSACTION_CODE_PREFIX, TRANSACTION_CODE_LENGTH, TRANSACTION_CODE_ALPHABET
|
||||||
import random, string
|
import secrets
|
||||||
|
import string
|
||||||
|
|
||||||
MAX_QUANTITY = 9999
|
MAX_QUANTITY = 9999
|
||||||
|
|
||||||
|
|
||||||
|
class BlankToNoneCharField(models.CharField):
|
||||||
|
"""Treat empty strings as None in Python, but store as empty strings in DB."""
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
value = super().to_python(value)
|
||||||
|
if value == "":
|
||||||
|
return None
|
||||||
|
return value
|
||||||
|
|
||||||
|
def from_db_value(self, value, expression, connection):
|
||||||
|
if value == "":
|
||||||
|
return None
|
||||||
|
return value
|
||||||
|
|
||||||
|
def get_prep_value(self, value):
|
||||||
|
if value is None or value == "":
|
||||||
|
return ""
|
||||||
|
return super().get_prep_value(value)
|
||||||
|
|
||||||
|
def deconstruct(self):
|
||||||
|
name, path, args, kwargs = super().deconstruct()
|
||||||
|
path = "django.db.models.CharField"
|
||||||
|
return name, path, args, kwargs
|
||||||
|
|
||||||
|
|
||||||
def generate_transaction_code() -> str:
|
def generate_transaction_code() -> str:
|
||||||
while True:
|
while True:
|
||||||
code = f"{TRANSACTION_CODE_PREFIX}{get_random_string(TRANSACTION_CODE_LENGTH, TRANSACTION_CODE_ALPHABET)}"
|
code = f"{TRANSACTION_CODE_PREFIX}{get_random_string(TRANSACTION_CODE_LENGTH, TRANSACTION_CODE_ALPHABET)}"
|
||||||
@@ -48,9 +74,10 @@ class VerificationCode(models.Model):
|
|||||||
default = VerificationModes.VERIFY_ACCOUNT
|
default = VerificationModes.VERIFY_ACCOUNT
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def generate(user: User, code_mode: str) -> VerificationCode:
|
def generate(user: User, code_mode: str) -> VerificationCode:
|
||||||
while True:
|
while True:
|
||||||
code = "".join(random.choices(string.ascii_letters+string.digits, k=64))
|
code = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
if not VerificationCode.objects.filter(code=code).exists():
|
if not VerificationCode.objects.filter(code=code).exists():
|
||||||
return VerificationCode.objects.create(
|
return VerificationCode.objects.create(
|
||||||
code = code,
|
code = code,
|
||||||
@@ -163,7 +190,7 @@ class StockReservation(models.Model):
|
|||||||
]
|
]
|
||||||
|
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True, related_name="stock_reservations")
|
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True, related_name="stock_reservations")
|
||||||
session_key = models.CharField(max_length=40, null=True, blank=True)
|
session_key = models.CharField(max_length=40, default="", blank=True)
|
||||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=STATUS_ACTIVE)
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=STATUS_ACTIVE)
|
||||||
payment_method = models.CharField(max_length=20, choices=PAYMENT_CHOICES)
|
payment_method = models.CharField(max_length=20, choices=PAYMENT_CHOICES)
|
||||||
expires_at = models.DateTimeField(db_index=True)
|
expires_at = models.DateTimeField(db_index=True)
|
||||||
@@ -193,10 +220,17 @@ class StockReservationItem(models.Model):
|
|||||||
|
|
||||||
class Cart(models.Model):
|
class Cart(models.Model):
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True)
|
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True)
|
||||||
session_key = models.CharField(max_length=40, null=True, blank=True)
|
session_key = BlankToNoneCharField(max_length=40, default="", blank=True)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.session_key is None:
|
||||||
|
self.session_key = ""
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
if self.session_key == "":
|
||||||
|
self.session_key = None
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Cart {self.id} - {self.user or self.session_key}"
|
return f"Cart {self.id} - {self.user or self.session_key}"
|
||||||
|
|
||||||
@@ -258,7 +292,7 @@ class Order(models.Model):
|
|||||||
|
|
||||||
buyer = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='orders')
|
buyer = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='orders')
|
||||||
shipping_address = models.ForeignKey('ShippingAddress', on_delete=models.SET_NULL, null=True, blank=True, related_name='orders')
|
shipping_address = models.ForeignKey('ShippingAddress', on_delete=models.SET_NULL, null=True, blank=True, related_name='orders')
|
||||||
session_key = models.CharField(max_length=40, null=True, blank=True)
|
session_key = models.CharField(max_length=40, default="", blank=True)
|
||||||
total = models.FloatField(default=0)
|
total = models.FloatField(default=0)
|
||||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=STATUS_PAID)
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=STATUS_PAID)
|
||||||
payment_method = models.CharField(max_length=20, choices=PAYMENT_CHOICES, default=PAYMENT_MANUAL)
|
payment_method = models.CharField(max_length=20, choices=PAYMENT_CHOICES, default=PAYMENT_MANUAL)
|
||||||
|
|||||||
+5
-2
@@ -17,15 +17,18 @@ class Recibo(FPDF):
|
|||||||
|
|
||||||
def generar_recibo(cliente: str, total: float, objetos: list, metodo_pago: str, transaction_code: str):
|
def generar_recibo(cliente: str, total: float, objetos: list, metodo_pago: str, transaction_code: str):
|
||||||
pdf = Recibo()
|
pdf = Recibo()
|
||||||
font_path = "/fonts/Roboto-Regular.ttf"
|
|
||||||
pdf.add_font('Roboto', '', '/fonts/Roboto-Regular.ttf')
|
pdf.add_font('Roboto', '', '/fonts/Roboto-Regular.ttf')
|
||||||
pdf.add_font('Roboto', 'B', '/fonts/Roboto-Bold.ttf')
|
pdf.add_font('Roboto', 'B', '/fonts/Roboto-Bold.ttf')
|
||||||
pdf.add_page()
|
pdf.add_page()
|
||||||
pdf.set_font('Roboto', size=12)
|
pdf.set_font('Roboto', size=12)
|
||||||
|
|
||||||
|
METODOS_MAP = {"stripe": "Stripe", "paypal": "PayPal", "manual": "Manual"}
|
||||||
|
metodo_mostrar = METODOS_MAP.get(metodo_pago, metodo_pago)
|
||||||
|
|
||||||
pdf.cell(0, 10, f"Cliente: {cliente}", ln=True)
|
pdf.cell(0, 10, f"Cliente: {cliente}", ln=True)
|
||||||
pdf.cell(0, 10, f"ID de transaccion: {transaction_code}", ln=True)
|
pdf.cell(0, 10, f"ID de transaccion: {transaction_code}", ln=True)
|
||||||
pdf.cell(0, 10, f"")
|
pdf.cell(0, 10, f"Metodo de pago: {metodo_mostrar}", ln=True)
|
||||||
|
pdf.cell(0, 10, "")
|
||||||
|
|
||||||
DATA = []
|
DATA = []
|
||||||
DATA.append(
|
DATA.append(
|
||||||
|
|||||||
+4
-3
@@ -4,7 +4,8 @@ from django.template.loader import render_to_string
|
|||||||
from django.core.mail import EmailMessage
|
from django.core.mail import EmailMessage
|
||||||
from .utilities import send_email, send_hemail
|
from .utilities import send_email, send_hemail
|
||||||
from .vars import login_message, verify_message
|
from .vars import login_message, verify_message
|
||||||
import random, string
|
import secrets
|
||||||
|
import string
|
||||||
from . import pdf
|
from . import pdf
|
||||||
|
|
||||||
from .models import User, VerificationCode
|
from .models import User, VerificationCode
|
||||||
@@ -43,7 +44,7 @@ def enviar_correo_confirmacion(id: int):
|
|||||||
code = VerificationCode.objects.create(
|
code = VerificationCode.objects.create(
|
||||||
user = usuario,
|
user = usuario,
|
||||||
code_mode = VerificationCode.VerificationModes.VERIFY_ACCOUNT,
|
code_mode = VerificationCode.VerificationModes.VERIFY_ACCOUNT,
|
||||||
code = ''.join(random.choices(string.digits, k=12))
|
code = ''.join(secrets.choice(string.digits) for _ in range(12))
|
||||||
)
|
)
|
||||||
|
|
||||||
message = verify_message.format(name = usuario.get_full_name(), protocol = settings.PROTOCOL, domain = settings.DOMAIN, code = code.code)
|
message = verify_message.format(name = usuario.get_full_name(), protocol = settings.PROTOCOL, domain = settings.DOMAIN, code = code.code)
|
||||||
@@ -60,7 +61,7 @@ def enviar_correo_recuperacion(email: str):
|
|||||||
ver_code = VerificationCode.objects.create(
|
ver_code = VerificationCode.objects.create(
|
||||||
code_mode = VerificationCode.VerificationModes.RESET_PASSWORD,
|
code_mode = VerificationCode.VerificationModes.RESET_PASSWORD,
|
||||||
user = usuario,
|
user = usuario,
|
||||||
code = ''.join(random.choices(string.digits, k=12))
|
code = ''.join(secrets.choice(string.digits) for _ in range(12))
|
||||||
)
|
)
|
||||||
ver_code.save()
|
ver_code.save()
|
||||||
html_content = render_to_string(
|
html_content = render_to_string(
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
{% elif user_has_review %}
|
{% elif user_has_review %}
|
||||||
<div class="ms-auto">
|
<div class="ms-auto">
|
||||||
<a href="{% url 'add_review' product.id %}" class="btn btn-sm btn-outline-primary">Editar mi valoración</a>
|
<a href="{% url 'add_review' product.id %}" class="btn btn-sm btn-outline-primary">Editar mi valoración</a>
|
||||||
<form method="post" action="{% url 'delete_review' product.id %}" style="display:inline;">
|
<form method="post" action="{% url 'delete_review' user_review_id %}" style="display:inline;">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('¿Eliminar esta valoración?');">Eliminar</button>
|
<button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('¿Eliminar esta valoración?');">Eliminar</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -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 %}
|
||||||
|
|||||||
+5
-4
@@ -16,6 +16,7 @@ from .models import (
|
|||||||
)
|
)
|
||||||
from .forms import UserRegisterForm, UserLoginForm, EditProfileForm, ChangePasswordForm, ShippingAddressForm, ResetPasswordForm, ResetPasswordPhase2Form
|
from .forms import UserRegisterForm, UserLoginForm, EditProfileForm, ChangePasswordForm, ShippingAddressForm, ResetPasswordForm, ResetPasswordPhase2Form
|
||||||
from .vars import VAT_RATE, TRANSACTION_CODE_PREFIX
|
from .vars import VAT_RATE, TRANSACTION_CODE_PREFIX
|
||||||
|
import secrets
|
||||||
import string
|
import string
|
||||||
import random
|
import random
|
||||||
|
|
||||||
@@ -335,7 +336,7 @@ class VerificationCodeModelTests(TestCase):
|
|||||||
"""50 códigos pueden crearse sin conflictos."""
|
"""50 códigos pueden crearse sin conflictos."""
|
||||||
codes = []
|
codes = []
|
||||||
for i in range(50):
|
for i in range(50):
|
||||||
mode = random.choice([
|
mode = secrets.choice([
|
||||||
VerificationCode.VerificationModes.VERIFY_ACCOUNT,
|
VerificationCode.VerificationModes.VERIFY_ACCOUNT,
|
||||||
VerificationCode.VerificationModes.RESET_PASSWORD
|
VerificationCode.VerificationModes.RESET_PASSWORD
|
||||||
])
|
])
|
||||||
@@ -377,7 +378,7 @@ class CategoryModelTests(TestCase):
|
|||||||
"""100 categorías pueden crearse sin problemas."""
|
"""100 categorías pueden crearse sin problemas."""
|
||||||
categories = []
|
categories = []
|
||||||
for i in range(100):
|
for i in range(100):
|
||||||
cat = Category.objects.create(name=f"Category_{i}_{random.randint(1000, 9999)}")
|
cat = Category.objects.create(name=f"Category_{i}_{1000 + secrets.randbelow(9000)}")
|
||||||
categories.append(cat)
|
categories.append(cat)
|
||||||
|
|
||||||
self.assertEqual(len(categories), 100)
|
self.assertEqual(len(categories), 100)
|
||||||
@@ -1786,7 +1787,7 @@ class EndpointViewTests(TestCase):
|
|||||||
self.assertTrue(OrderMessage.objects.filter(order_item=item, sender=self.seller).exists())
|
self.assertTrue(OrderMessage.objects.filter(order_item=item, sender=self.seller).exists())
|
||||||
|
|
||||||
delete_get = self.client.get(reverse("borrar_producto", args=[created.id]))
|
delete_get = self.client.get(reverse("borrar_producto", args=[created.id]))
|
||||||
self.assertEqual(delete_get.status_code, 302)
|
self.assertEqual(delete_get.status_code, 405)
|
||||||
delete_post = self.client.post(reverse("borrar_producto", args=[created.id]))
|
delete_post = self.client.post(reverse("borrar_producto", args=[created.id]))
|
||||||
self.assertEqual(delete_post.status_code, 302)
|
self.assertEqual(delete_post.status_code, 302)
|
||||||
self.assertFalse(Product.objects.filter(id=created.id).exists())
|
self.assertFalse(Product.objects.filter(id=created.id).exists())
|
||||||
@@ -2068,7 +2069,7 @@ class EndpointViewTests(TestCase):
|
|||||||
self.assertEqual(new_address.full_name, "Comprador Dos Editado")
|
self.assertEqual(new_address.full_name, "Comprador Dos Editado")
|
||||||
|
|
||||||
delete_get = self.client.get(reverse("eliminar_direccion", args=[new_address.id]))
|
delete_get = self.client.get(reverse("eliminar_direccion", args=[new_address.id]))
|
||||||
self.assertEqual(delete_get.status_code, 302)
|
self.assertEqual(delete_get.status_code, 405)
|
||||||
delete_post = self.client.post(reverse("eliminar_direccion", args=[new_address.id]))
|
delete_post = self.client.post(reverse("eliminar_direccion", args=[new_address.id]))
|
||||||
self.assertEqual(delete_post.status_code, 302)
|
self.assertEqual(delete_post.status_code, 302)
|
||||||
self.assertFalse(ShippingAddress.objects.filter(id=new_address.id).exists())
|
self.assertFalse(ShippingAddress.objects.filter(id=new_address.id).exists())
|
||||||
|
|||||||
+1
-1
@@ -70,6 +70,6 @@ urlpatterns = [
|
|||||||
path("reset-password", views.reset_password, name="reset_password"),
|
path("reset-password", views.reset_password, name="reset_password"),
|
||||||
path("reset-password-phase2/<str:code>", views.reset_password_phase2, name="reset_password_phase2"),
|
path("reset-password-phase2/<str:code>", views.reset_password_phase2, name="reset_password_phase2"),
|
||||||
path("producto/<int:product_id>/valorar/", views.add_review, name="add_review"),
|
path("producto/<int:product_id>/valorar/", views.add_review, name="add_review"),
|
||||||
path("producto/<int:product_id>/valorar/eliminar/", views.delete_review, name="delete_review"),
|
path("valoracion/<int:review_id>/eliminar/", views.delete_review, name="delete_review"),
|
||||||
path("api/producto/<int:product_id>/valoraciones/", views.product_reviews, name="product_reviews"),
|
path("api/producto/<int:product_id>/valoraciones/", views.product_reviews, name="product_reviews"),
|
||||||
]
|
]
|
||||||
|
|||||||
+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)
|
||||||
+243
-220
File diff suppressed because it is too large
Load Diff
@@ -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