Compare commits

...

6 Commits

9 changed files with 59 additions and 131 deletions
+4 -3
View File
@@ -5,9 +5,10 @@ ENV PYTHONUNBUFFERED=1
WORKDIR /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
RUN apk --no-cache update && apk --no-cache upgrade \
&& pip install --no-cache-dir uv \
&& uv sync --no-dev --no-install-project # Install only dependencies, not the local project package
COPY . /app/
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
# Usa PostgreSQL por defecto (POSTGRES_ENABLED=True); si no, SQLite.
if RUNNING_TESTS:
if RUNNING_TESTS or not env_bool('POSTGRES_ENABLED', True):
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
elif env_bool('POSTGRES_ENABLED', True):
else:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
@@ -165,14 +165,6 @@ elif env_bool('POSTGRES_ENABLED', True):
'PORT': env_int('POSTGRES_PORT', 5432),
}
}
else:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# 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):
usuarios_baneados = 0
for user in queryset:
user: User = user
# Desactiva usuario
if user.registration_status == User.RegisterStatus.BANNED:
continue
@@ -43,7 +42,6 @@ class UserAdmin(admin.ModelAdmin):
def desbanear_usuario_action(self, request, queryset):
user_desbaneados = 0
for user in queryset:
user: User = user
if user.registration_status != User.RegisterStatus.BANNED:
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.validators import FileExtensionValidator, MinLengthValidator, MaxLengthValidator
from .models import Category
from .constants import *
ALLOWED_IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp']
ALLOWED_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
def validate_image_file(value):
ext = value.name.split('.')[-1].lower()
if ext not in ALLOWED_IMAGE_EXTENSIONS:
@@ -76,7 +79,7 @@ class ProductForm(forms.Form):
widget = forms.ClearableFileInput(
attrs = {
'class': 'form-control',
'accept': 'image/*'
'accept': IMAGE_TYPE
}
)
)
@@ -133,7 +136,7 @@ class SecondaryImageForm(forms.Form):
widget = forms.ClearableFileInput(
attrs = {
'class': 'form-control',
'accept': 'image/*'
'accept': IMAGE_TYPE
}
)
)
@@ -192,7 +195,7 @@ class UserRegisterForm(forms.Form):
)
)
email = forms.EmailField(
label = "Correo Electrónico",
label = EMAIL_FORMNAME,
max_length = 255,
required = True,
widget = forms.TextInput(
@@ -236,7 +239,7 @@ class UserRegisterForm(forms.Form):
password = cleaned_data.get("password")
password_confirm = cleaned_data.get("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):
@@ -253,7 +256,7 @@ class EditProfileForm(forms.Form):
widget=forms.TextInput(attrs={'class': 'form-control'})
)
email = forms.EmailField(
label="Correo Electrónico",
label=EMAIL_FORMNAME,
max_length=254,
required=True,
widget=forms.EmailInput(attrs={'class': 'form-control'})
@@ -285,7 +288,7 @@ class ChangePasswordForm(forms.Form):
new_password = cleaned_data.get("new_password")
confirm_password = cleaned_data.get("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:
raise ValidationError("La contraseña debe tener al menos 8 caracteres.")
@@ -343,7 +346,7 @@ class ShippingAddressForm(forms.Form):
class ResetPasswordForm(forms.Form):
email = forms.EmailField(
label="Correo Electrónico",
label=EMAIL_FORMNAME,
max_length=254,
required=True,
widget=forms.EmailInput(attrs={'class': 'form-control', 'placeholder': 'tu@email.com'})
@@ -369,7 +372,7 @@ class ResetPasswordPhase2Form(forms.Form):
password = cleaned_data.get("password")
verify_password = cleaned_data.get("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):
@@ -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
)
@staticmethod
def generate(user: User, code_mode: str) -> VerificationCode:
while True:
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")
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)
payment_method = models.CharField(max_length=20, choices=PAYMENT_CHOICES)
expires_at = models.DateTimeField(db_index=True)
@@ -193,7 +194,7 @@ class StockReservationItem(models.Model):
class Cart(models.Model):
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)
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')
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)
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)
+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):
pdf = Recibo()
font_path = "/fonts/Roboto-Regular.ttf"
pdf.add_font('Roboto', '', '/fonts/Roboto-Regular.ttf')
pdf.add_font('Roboto', 'B', '/fonts/Roboto-Bold.ttf')
pdf.add_page()
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"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.append(