7 Commits

Author SHA1 Message Date
d9137404a1 Merge branch 'main' of ssh://gitea.onecluster.org:6666/OneTeam/don_confiao_backend 2026-06-22 11:20:21 -05:00
3b635cf17a feat: catálogo público sin autenticación, solo confirmar compra requiere auth
- Cambia default global de IsAuthenticated a AllowAny en settings/base.py
- ProductView: AllowAny para GET, IsAuthenticated para POST/PUT/DELETE
- CatalogueImageViewSet: AllowAny para reads, IsAuthenticated+Admin para writes
- PaymentMethodView: AllowAny (público)
- SaleView, CatalogSaleView, SaleSummary, CatalogSaleSummary: IsAuthenticated
- CustomerView: IsAuthenticated
2026-06-22 11:18:08 -05:00
d406c734ac feat: Nginx configurated to server images for catalog in production. 2026-06-22 10:20:21 -05:00
193198918b Merge pull request 'feat: add standalone script to upload catalogue images via API' (#41) from feature/upload-catalogue-script into main
Reviewed-on: #41
2026-06-14 00:46:45 -05:00
mono
1007584d3e Merge branch 'main' into feature/upload-catalogue-script 2026-06-14 00:46:21 -05:00
mono
ac22adb558 feat: add standalone script to upload catalogue images via API 2026-06-14 00:39:33 -05:00
2415ed3564 Merge pull request 'feat: add catalogue image management (don_confiao_catalog_generator/issues/1)' (#40) from feature/1-catalogue-images into main
Reviewed-on: #40
2026-06-13 20:31:40 -05:00
9 changed files with 196 additions and 22 deletions

View File

@@ -44,7 +44,7 @@ services:
- media_volume:/app/media
- logs_volume:/app/logs
ports:
- "8000:8000"
- "127.0.0.1:9000:8000"
depends_on:
postgres:
condition: service_healthy
@@ -57,22 +57,20 @@ services:
gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers 4 --timeout 120"
# Optional: Nginx reverse proxy para servir archivos estáticos
# nginx:
# image: nginx:alpine
# container_name: tienda_ilusion_nginx
# ports:
# - "80:80"
# - "443:443"
# volumes:
# - ./nginx.conf:/etc/nginx/nginx.conf:ro
# - static_volume:/var/www/static:ro
# - media_volume:/var/www/media:ro
# - ./ssl:/etc/nginx/ssl:ro
# depends_on:
# - django
# networks:
# - tienda_network
# restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "10297:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- static_volume:/var/www/static
- media_volume:/var/www/media
- ./dist:/var/www/donconfiao
depends_on:
- django
networks:
- tienda_network
restart: unless-stopped
volumes:
postgres_data:

36
nginx.conf Normal file
View File

@@ -0,0 +1,36 @@
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
server {
listen 80;
server_name _;
client_max_body_size 10M;
location /media/ {
alias /var/www/media/;
expires 30d;
add_header Cache-Control "public, immutable";
}
location /static/ {
alias /var/www/static/;
expires 365d;
add_header Cache-Control "public, immutable";
}
location / {
root /var/www/donconfiao/;
try_files $uri /index.html;
expires -1;
add_header Cache-Control "no-cache";
}
}
}

View File

@@ -0,0 +1,126 @@
#!/usr/bin/env python3
import argparse
import getpass
import os
import re
import sys
from pathlib import Path
import requests
TOKEN_URL = "/api/token/"
PRODUCTS_URL = "/don_confiao/api/products/"
CATALOGUE_IMAGES_URL = "/don_confiao/api/catalogue_images/"
def get_credentials():
username = input("Usuario: ")
password = getpass.getpass("Contraseña: ")
return username, password
def get_token(domain, username, password):
url = domain.rstrip("/") + TOKEN_URL
response = requests.post(url, json={"username": username, "password": password})
if response.status_code != 200:
print(f"Error al obtener token: {response.status_code} {response.text}", file=sys.stderr)
sys.exit(1)
data = response.json()
return data["access"]
def get_products(domain, token):
url = domain.rstrip("/") + PRODUCTS_URL
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(url, headers=headers)
if response.status_code != 200:
print(f"Error al obtener productos: {response.status_code} {response.text}", file=sys.stderr)
sys.exit(1)
return response.json()
MIME_TYPES = {
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
}
def find_images(image_dir):
images = {}
pattern = re.compile(r"^(\d+)\.(jpg|jpeg|png)$", re.IGNORECASE)
for f in os.listdir(image_dir):
m = pattern.match(f)
if m:
external_id = m.group(1)
images[external_id] = os.path.join(image_dir, f)
return images
def main():
parser = argparse.ArgumentParser(
description="Sube imágenes de catálogo para productos usando el external_id como nombre de archivo."
)
parser.add_argument("image_dir", help="Directorio con imágenes nombradas como ##.jpg")
parser.add_argument("domain", help="Dominio del backend (ej: http://localhost:8000)")
args = parser.parse_args()
if not os.path.isdir(args.image_dir):
print(f"Error: el directorio '{args.image_dir}' no existe.", file=sys.stderr)
sys.exit(1)
username, password = get_credentials()
token = get_token(args.domain, username, password)
print("Token obtenido correctamente.")
products = get_products(args.domain, token)
ext_id_to_product_id = {}
for p in products:
if p.get("external_id"):
ext_id_to_product_id[p["external_id"]] = p["id"]
if not ext_id_to_product_id:
print("No se encontraron productos con external_id.", file=sys.stderr)
sys.exit(1)
images = find_images(args.image_dir)
if not images:
print(f"No se encontraron imágenes con el patrón ##.jpg en '{args.image_dir}'.", file=sys.stderr)
sys.exit(1)
headers = {"Authorization": f"Bearer {token}"}
upload_url = args.domain.rstrip("/") + CATALOGUE_IMAGES_URL
uploaded = 0
skipped = 0
errors = 0
for external_id, img_path in sorted(images.items()):
if external_id not in ext_id_to_product_id:
print(f" [SKIP] {Path(img_path).name}: no hay producto con external_id={external_id}")
skipped += 1
continue
product_id = ext_id_to_product_id[external_id]
filename = Path(img_path).name
ext = Path(img_path).suffix.lower()
mime = MIME_TYPES.get(ext, "application/octet-stream")
with open(img_path, "rb") as f:
files = {"image": (filename, f, mime)}
data = {"product": product_id}
response = requests.post(upload_url, headers=headers, files=files, data=data)
if response.status_code == 201:
print(f" [OK] {filename} -> producto {product_id} (id imagen: {response.json()['id']})")
uploaded += 1
else:
print(f" [ERR] {filename} -> producto {product_id}: {response.status_code} {response.text}")
errors += 1
print(f"\nResumen: {uploaded} subidas, {skipped} saltadas, {errors} errores")
if __name__ == "__main__":
main()

View File

@@ -166,7 +166,7 @@ REST_FRAMEWORK = {
"rest_framework_simplejwt.authentication.JWTAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
"rest_framework.permissions.AllowAny",
],
}

View File

@@ -1,5 +1,5 @@
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from rest_framework.permissions import AllowAny, IsAuthenticated
from ..image_service import resize_catalogue_image
from ..models.catalogue_images import CatalogueImage
@@ -14,7 +14,7 @@ class CatalogueImageViewSet(viewsets.ModelViewSet):
def get_permissions(self):
if self.action in ("create", "update", "partial_update", "destroy"):
return [IsAuthenticated(), IsAdministrator()]
return [IsAuthenticated()]
return [AllowAny()]
def perform_create(self, serializer):
instance = serializer.save()

View File

@@ -13,6 +13,7 @@ from ..services.tryton.client import get_tryton_client
class CustomerView(viewsets.ModelViewSet):
queryset = Customer.objects.all()
serializer_class = CustomerSerializer
permission_classes = [IsAuthenticated]
class CustomersFromTrytonView(APIView):

View File

@@ -3,7 +3,7 @@ from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.status import HTTP_400_BAD_REQUEST
from rest_framework.pagination import PageNumberPagination
from rest_framework.permissions import IsAuthenticated
from rest_framework.permissions import AllowAny, IsAuthenticated
from decimal import Decimal
from ..models.sales import Sale
@@ -87,6 +87,8 @@ class ReconciliateJarModelView(viewsets.ModelViewSet):
class PaymentMethodView(APIView):
permission_classes = [AllowAny]
def get(self, request):
serializer = PaymentMethodSerializer(PaymentMethods.choices, many=True)
return Response(serializer.data)

View File

@@ -1,7 +1,7 @@
from rest_framework import viewsets
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework.permissions import AllowAny, IsAuthenticated
from ..models.products import Product
from ..serializers import ProductSerializer
@@ -14,6 +14,11 @@ class ProductView(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
def get_permissions(self):
if self.action in ("list", "retrieve"):
return [AllowAny()]
return [IsAuthenticated()]
def get_queryset(self):
"""
Filters products by active status for list operations.

View File

@@ -23,6 +23,7 @@ from ..views import sales_to_tryton_csv
class SaleView(viewsets.ModelViewSet):
queryset = Sale.objects.all()
serializer_class = SaleSerializer
permission_classes = [IsAuthenticated]
def create(self, request):
data = request.data
@@ -58,9 +59,12 @@ class SaleView(viewsets.ModelViewSet):
class CatalogSaleView(viewsets.ModelViewSet):
queryset = CatalogSale.objects.all()
serializer_class = CatalogSaleSerializer
permission_classes = [IsAuthenticated]
class SaleSummary(APIView):
permission_classes = [IsAuthenticated]
def get(self, request, id):
sale = Sale.objects.get(pk=id)
serializer = SaleSummarySerializer(sale)
@@ -68,6 +72,8 @@ class SaleSummary(APIView):
class CatalogSaleSummary(APIView):
permission_classes = [IsAuthenticated]
def get(self, request, id):
catalog_sale = CatalogSale.objects.get(pk=id)
serializer = CatalogSaleSummarySerializer(catalog_sale)