163 lines
6.8 KiB
Python
163 lines
6.8 KiB
Python
import json
|
|
|
|
import weasyprint
|
|
from django.core.exceptions import SuspiciousFileOperation
|
|
from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse
|
|
from django.shortcuts import get_object_or_404
|
|
from django.template.loader import render_to_string
|
|
from django.utils.text import get_valid_filename
|
|
from django.utils.translation import get_language, gettext_lazy as _
|
|
from django.views import View
|
|
from django.views.generic import DetailView
|
|
|
|
from crochet.models import Pattern, PatternImage, StitchType
|
|
from crochet.pattern_render import render_pattern_html, sanitize_page_settings
|
|
|
|
# gettext_lazy (no gettext): se evalúa al construir el HTML de salida (dentro
|
|
# de render_pattern_html, vía format_html), no aquí al importar el módulo, así
|
|
# que respeta el idioma activo de cada petición aunque esto se cree una vez.
|
|
PATTERN_EMPTY_MESSAGE = _('Este patrón todavía no tiene contenido.')
|
|
|
|
|
|
def _stitch_types_data():
|
|
# Catálogo de puntos arrastrables (ver StitchType): es el mismo para
|
|
# cualquier patrón, no depende de ninguno en concreto.
|
|
return [
|
|
{'id': stitch_type.id, 'translations': stitch_type.translations}
|
|
for stitch_type in StitchType.objects.all()
|
|
]
|
|
|
|
|
|
def _pattern_detail_context(pattern):
|
|
# Compartido por PatternDetailView (la pinta) y PatternPdfView (la
|
|
# convierte a PDF con WeasyPrint a partir del mismo HTML), para no
|
|
# reconstruir esto dos veces ni arriesgarse a que diverjan.
|
|
lang = get_language()
|
|
page_settings = sanitize_page_settings(pattern.page_settings)
|
|
return {
|
|
'pattern': pattern,
|
|
'lang': lang,
|
|
'page_settings': page_settings,
|
|
'rendered_pattern': render_pattern_html(
|
|
pattern.sections, page_settings, lang, _stitch_types_data(), PATTERN_EMPTY_MESSAGE,
|
|
),
|
|
}
|
|
|
|
|
|
class PatternEditView(DetailView):
|
|
model = Pattern
|
|
template_name = 'crochet/pattern.html'
|
|
context_object_name = 'pattern'
|
|
slug_field = 'uuid'
|
|
slug_url_kwarg = 'uuid'
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
# Resuelto por LocaleMiddleware a partir del prefijo de idioma de la
|
|
# URL (/es/..., /en/...; ver i18n_patterns en config/urls.py).
|
|
context['lang'] = get_language()
|
|
# Se pasa tal cual a la plantilla con {{ pattern_data|json_script:"..." }}
|
|
# para que main.js reconstruya el editor con el contenido guardado
|
|
# en vez del esqueleto por defecto.
|
|
context['pattern_data'] = {
|
|
'sections': self.object.sections,
|
|
'pageSettings': self.object.page_settings,
|
|
}
|
|
context['stitch_types'] = _stitch_types_data()
|
|
return context
|
|
|
|
|
|
class PatternDetailView(DetailView):
|
|
# Plantilla propia, sin nada en común con la del editor: esta vista es
|
|
# de solo lectura y no debe cargar ningún <input>/<button> de edición ni
|
|
# el JS que los activa, ni siquiera oculto con CSS, porque cualquiera
|
|
# podría "revelarlo" quitando esa clase desde las herramientas de
|
|
# desarrollo del navegador y guardar cambios reales sobre el patrón (el
|
|
# enlace de solo lectura comparte el mismo uuid que el de edición). El
|
|
# HTML se genera entero en el servidor a partir de los datos guardados
|
|
# (ver pattern_render.py), así que aquí no hay nada que "revelar".
|
|
model = Pattern
|
|
template_name = 'crochet/pattern_detail.html'
|
|
context_object_name = 'pattern'
|
|
slug_field = 'uuid'
|
|
slug_url_kwarg = 'uuid'
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context.update(_pattern_detail_context(self.object))
|
|
return context
|
|
|
|
|
|
class PatternPdfView(DetailView):
|
|
# Genera el PDF con WeasyPrint a partir del mismo HTML que
|
|
# PatternDetailView (misma plantilla, mismo contexto), no del editor:
|
|
# así el PDF sale siempre de la última versión guardada. A diferencia de
|
|
# pattern.html (editor), pattern_detail.html no depende del CDN de
|
|
# Tailwind con compilador JIT en JS (WeasyPrint no ejecuta JavaScript,
|
|
# así que ese CDN no generaría ningún estilo); usa en su lugar el CSS ya
|
|
# compilado en static/css/pattern-detail.css (ver .tools/build/).
|
|
model = Pattern
|
|
slug_field = 'uuid'
|
|
slug_url_kwarg = 'uuid'
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
self.object = self.get_object()
|
|
context = _pattern_detail_context(self.object)
|
|
html_string = render_to_string('crochet/pattern_detail.html', context, request=request)
|
|
|
|
# base_url para que WeasyPrint resuelva las rutas relativas de
|
|
# static (css/main.css, css/pattern-detail.css) haciendo una
|
|
# petición HTTP normal a este mismo servidor, igual que haría un
|
|
# navegador.
|
|
pdf_bytes = weasyprint.HTML(
|
|
string=html_string, base_url=request.build_absolute_uri('/'),
|
|
).write_pdf()
|
|
|
|
response = HttpResponse(pdf_bytes, content_type='application/pdf')
|
|
response['Content-Disposition'] = f'attachment; filename="{self._pdf_filename()}"'
|
|
return response
|
|
|
|
def _pdf_filename(self):
|
|
# page_settings lo guarda pattern_save() sin validar su contenido
|
|
# (ver PatternSaveView), así que el título podría traer comillas o
|
|
# caracteres fuera de ASCII que no encajan bien en Content-Disposition.
|
|
# get_valid_filename() lanza SuspiciousFileOperation (no devuelve '')
|
|
# si no queda nada válido tras limpiarlo -pasa con un título vacío,
|
|
# solo espacios, o hecho solo de caracteres como "." o "?"-, así que
|
|
# hay que cubrir ese caso a mano en vez de fiarse de un resultado
|
|
# "falsy".
|
|
title = (self.object.page_settings or {}).get('title') or ''
|
|
try:
|
|
safe_title = get_valid_filename(title) if title.strip() else ''
|
|
except SuspiciousFileOperation:
|
|
safe_title = ''
|
|
return f'{safe_title or "patron"}.pdf'
|
|
|
|
|
|
class PatternSaveView(View):
|
|
def post(self, request, uuid):
|
|
pattern = get_object_or_404(Pattern, uuid=uuid)
|
|
|
|
try:
|
|
data = json.loads(request.body)
|
|
except json.JSONDecodeError:
|
|
return HttpResponseBadRequest('JSON inválido')
|
|
|
|
pattern.sections = data.get('sections', [])
|
|
pattern.page_settings = data.get('pageSettings', {})
|
|
pattern.save(update_fields=['sections', 'page_settings', 'updated_at'])
|
|
|
|
return JsonResponse({'ok': True})
|
|
|
|
|
|
class PatternImageUploadView(View):
|
|
def post(self, request, uuid):
|
|
pattern = get_object_or_404(Pattern, uuid=uuid)
|
|
|
|
image_file = request.FILES.get('image')
|
|
if not image_file:
|
|
return HttpResponseBadRequest('Falta el archivo "image"')
|
|
|
|
pattern_image = PatternImage.objects.create(pattern=pattern, image=image_file)
|
|
return JsonResponse({'id': pattern_image.id, 'url': pattern_image.image.url})
|