Compare commits

..

6 Commits

9 changed files with 59 additions and 131 deletions
+5 -4
View File
@@ -4,10 +4,11 @@ ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1 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 apk --no-cache update && apk --no-cache upgrade \
RUN uv sync --no-dev --no-install-project # Install only dependencies, not the local project package && pip install --no-cache-dir uv \
&& uv sync --no-dev --no-install-project # Install only dependencies, not the local project package
COPY . /app/ COPY . /app/
RUN chmod +x /app/entrypoint.sh RUN chmod +x /app/entrypoint.sh
+2 -10
View File
@@ -147,14 +147,14 @@ 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', True):
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): else:
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.postgresql', 'ENGINE': 'django.db.backends.postgresql',
@@ -165,14 +165,6 @@ elif env_bool('POSTGRES_ENABLED', True):
'PORT': env_int('POSTGRES_PORT', 5432), 'PORT': env_int('POSTGRES_PORT', 5432),
} }
} }
else:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# 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
-101
View File
@@ -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
-2
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
IMAGE_TYPE = "image/*"
EMAIL_FORMNAME = "Correo Electrónico"
INCORRECT_PASSWORDS = "Las contraseñas no coinciden"
+12 -9
View File
@@ -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 *
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),
),
]
+4 -3
View File
@@ -48,6 +48,7 @@ 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(random.choices(string.ascii_letters+string.digits, k=64))
@@ -163,7 +164,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,7 +194,7 @@ 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 = models.CharField(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)
@@ -258,7 +259,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
View File
@@ -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(