Files
shoppy/backoffice/views/products.py
T
pablo 393b39cacd
CI / test (push) Successful in 1m56s
CI / build (push) Successful in 48s
feat: websockets
2026-07-31 14:27:50 +02:00

873 lines
32 KiB
Python

from django import forms
from django.core.exceptions import ValidationError
from django.forms import modelform_factory
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse, reverse_lazy
from django.views.generic import CreateView, DeleteView, DetailView, ListView, UpdateView, View
from backoffice.forms import ProductInitialPriceForm, ProductPriceForm, ProductVariantForm
from backoffice.mixins import (
BackofficeCRUDMixin,
BackofficeHtmxMixin,
BackofficeModalDeleteMixin,
BackofficeModalFormMixin,
BackofficeStyledFormMixin,
)
from shop.models import (
Brand,
Product,
ProductAttribute,
ProductAttributeValue,
ProductBatch,
ProductCategory,
ProductImage,
ProductPrice,
ProductVariant,
Tag,
)
from shop.utils import delete_product_batch
SECTION = 'products'
LIST_FRAGMENT = 'backoffice/generic/_list_fragment.html'
FORM_FRAGMENT = 'backoffice/generic/_form_fragment.html'
DELETE_FRAGMENT = 'backoffice/generic/_confirm_delete_fragment.html'
# --- Product ---
class ProductListView(BackofficeCRUDMixin, BackofficeHtmxMixin, ListView):
model = Product
permission_required = 'shop.view_product'
section = SECTION
paginate_by = 20
template_name = 'backoffice/generic/list.html'
fragment_template_name = LIST_FRAGMENT
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update(
{
'title': 'Productos',
'columns': [('SKU', 'sku'), ('Nombre', 'name'), ('Stock', 'stock'), ('Oculto', 'hidden')],
'create_url': reverse_lazy('backoffice:product_create'),
'create_is_page': True,
'detail_url_name': 'backoffice:product_detail',
'delete_url_name': 'backoffice:product_delete',
}
)
return context
class ProductCreateView(BackofficeCRUDMixin, BackofficeStyledFormMixin, CreateView):
"""Página completa (no modal), con el mismo aspecto/widgets que la página de
detalle (incluido el combobox de marca/categorías/etiquetas), pero todo en
un único formulario: nada se envía al servidor hasta pulsar "Crear
producto". Las imágenes y el precio inicial se suben/crean en el mismo
envío (variantes y remesas sí requieren que el producto ya exista, así que
esas se añaden después desde el detalle)."""
model = Product
permission_required = 'shop.add_product'
section = SECTION
fields = ('sku', 'name', 'description', 'stock', 'brand', 'categories', 'tags', 'hidden', 'is_shipping_method')
template_name = 'backoffice/products/product_create.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.setdefault('price_form', ProductInitialPriceForm())
context.setdefault('image_errors', [])
return context
def get_success_url(self):
return reverse_lazy('backoffice:product_detail', args=[self.object.pk])
def form_valid(self, form):
price_form = ProductInitialPriceForm(self.request.POST)
image_files = self.request.FILES.getlist('images')
image_field = forms.ImageField()
image_errors = []
for file in image_files:
try:
image_field.clean(file)
except ValidationError as exc:
image_errors.append(f'{file.name}: {", ".join(exc.messages)}')
if not price_form.is_valid() or image_errors:
return self.render_to_response(
self.get_context_data(form=form, price_form=price_form, image_errors=image_errors)
)
self.object = form.save()
for file in image_files:
ProductImage.objects.create(product=self.object, original=file)
if price_form.cleaned_data.get('price') is not None:
ProductPrice.objects.create(
product=self.object,
price=price_form.get_price_without_tax(),
tax=price_form.cleaned_data['tax'],
current=True,
)
return HttpResponseRedirect(self.get_success_url())
class ProductDeleteView(BackofficeCRUDMixin, BackofficeModalDeleteMixin, DeleteView):
model = Product
permission_required = 'shop.delete_product'
section = SECTION
template_name = 'backoffice/generic/confirm_delete.html'
fragment_template_name = DELETE_FRAGMENT
success_url = reverse_lazy('backoffice:product_list')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['cancel_url'] = reverse_lazy('backoffice:product_list')
return context
def form_valid(self, form):
# Borrar el producto invalida la página en la que estuvieras (lista o su
# propio detalle), así que aquí sí navegamos de verdad en vez de solo
# cerrar el modal y refrescar en sitio.
success_url = self.get_success_url()
self.perform_delete()
if self.is_htmx():
response = HttpResponse(status=200)
response['HX-Redirect'] = success_url
return response
return HttpResponseRedirect(success_url)
class ProductDetailView(BackofficeCRUDMixin, DetailView):
model = Product
permission_required = 'shop.view_product'
section = SECTION
template_name = 'backoffice/products/product_detail.html'
fragment_template_name = 'backoffice/products/_product_detail_fragment.html'
def get_template_names(self):
if self.request.headers.get('HX-Request') == 'true':
return [self.fragment_template_name]
return super().get_template_names()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['variants'] = self.object.variants.prefetch_related('attribute_values__attribute')
context['batches'] = ProductBatch.objects.filter(product=self.object)
context['images'] = self.object.images.all()
context['prices'] = self.object.prices.select_related('tax')
return context
# --- Edición inline de Product.name / Product.description / Product.brand /
# Product.categories / Product.tags ---
INLINE_EDITABLE_PRODUCT_FIELDS = ('name', 'description', 'brand', 'categories', 'tags')
MULTI_VALUE_INLINE_FIELDS = ('categories', 'tags')
COMBOBOX_INLINE_FIELDS = {
'brand': Brand,
'categories': ProductCategory,
'tags': Tag,
}
class ProductInlineFieldMixin:
def get_field_name(self):
field_name = self.kwargs['field_name']
if field_name not in INLINE_EDITABLE_PRODUCT_FIELDS:
raise Http404
return field_name
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update(
{
'field_name': self.get_field_name(),
'view_url': reverse('backoffice:product_inline_view', args=[self.object.pk, self.get_field_name()]),
'edit_url': reverse('backoffice:product_inline_edit', args=[self.object.pk, self.get_field_name()]),
}
)
return context
class ProductInlineViewFragment(BackofficeCRUDMixin, ProductInlineFieldMixin, DetailView):
model = Product
permission_required = 'shop.view_product'
section = SECTION
template_name = 'backoffice/products/_inline_view.html'
class ProductInlineEditView(BackofficeCRUDMixin, BackofficeStyledFormMixin, ProductInlineFieldMixin, UpdateView):
model = Product
permission_required = 'shop.change_product'
section = SECTION
template_name = 'backoffice/products/_inline_edit.html'
def get_form_class(self):
return modelform_factory(Product, fields=[self.get_field_name()])
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
field_name = self.get_field_name()
if field_name in COMBOBOX_INLINE_FIELDS:
multiple = field_name in MULTI_VALUE_INLINE_FIELDS
value = getattr(self.object, field_name)
context.update(
{'multiple': multiple, 'selected': list(value.all()) if multiple else list(filter(None, [value]))}
)
return context
def form_valid(self, form):
self.object = form.save()
return render(self.request, 'backoffice/products/_inline_view.html', self.get_context_data())
class ProductFieldSearchView(BackofficeCRUDMixin, View):
permission_required = 'shop.change_product'
section = SECTION
def get(self, request, *args, **kwargs):
field_name = kwargs['field_name']
model = COMBOBOX_INLINE_FIELDS.get(field_name)
if model is None:
raise Http404
query = request.GET.get('q', '').strip()
queryset = model.objects.all()
if query:
queryset = queryset.filter(name__icontains=query)
return render(
request,
'backoffice/products/_combobox_results.html',
{'results': queryset[:10], 'field_name': field_name},
)
# --- ProductImage (nested under a product) ---
class ProductImageCreateView(BackofficeCRUDMixin, BackofficeModalFormMixin, CreateView):
model = ProductImage
permission_required = 'shop.add_productimage'
section = SECTION
fields = ('original',)
template_name = 'backoffice/generic/form.html'
fragment_template_name = FORM_FRAGMENT
def get_product(self):
return get_object_or_404(Product, pk=self.kwargs['product_pk'])
def get_form(self, form_class=None):
form = super().get_form(form_class)
form.instance.product = self.get_product()
return form
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
product = self.get_product()
context.update(
{'title': f'Añadir imagen a {product.name}', 'cancel_url': reverse_lazy('backoffice:product_detail', args=[product.pk])}
)
return context
def get_success_url(self):
return reverse_lazy('backoffice:product_detail', args=[self.object.product_id])
class ProductImageDeleteView(BackofficeCRUDMixin, BackofficeModalDeleteMixin, DeleteView):
model = ProductImage
permission_required = 'shop.delete_productimage'
section = SECTION
template_name = 'backoffice/generic/confirm_delete.html'
fragment_template_name = DELETE_FRAGMENT
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['cancel_url'] = reverse_lazy('backoffice:product_detail', args=[self.object.product_id])
return context
def get_success_url(self):
return reverse_lazy('backoffice:product_detail', args=[self.object.product_id])
# --- ProductVariant (nested under a product) ---
VARIANT_FORM_FRAGMENT = 'backoffice/products/_variant_form_fragment.html'
class ProductVariantAttributesContextMixin:
"""Agrupa los valores de atributo por atributo para pintarlos como tabla
(ver _variant_form_fragment.html) en vez del <ul> de checkboxes que genera
por defecto CheckboxSelectMultiple. Si el form viene con datos (reenvío
tras un error de validación, p.ej. combinación duplicada) se respeta lo
que el usuario había marcado en vez de lo que tuviera guardado la instancia."""
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
form = context['form']
if form.is_bound:
selected_value_ids = {int(v) for v in form.data.getlist('attribute_values') if v.isdigit()}
else:
instance = getattr(self, 'object', None)
selected_value_ids = set(instance.attribute_values.values_list('pk', flat=True)) if instance and instance.pk else set()
context['attributes'] = ProductAttribute.objects.prefetch_related('values')
context['selected_value_ids'] = selected_value_ids
return context
class ProductVariantCreateView(ProductVariantAttributesContextMixin, BackofficeCRUDMixin, BackofficeModalFormMixin, CreateView):
model = ProductVariant
form_class = ProductVariantForm
permission_required = 'shop.add_productvariant'
section = SECTION
template_name = 'backoffice/products/variant_form.html'
fragment_template_name = VARIANT_FORM_FRAGMENT
def get_product(self):
return get_object_or_404(Product, pk=self.kwargs['product_pk'])
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['product'] = self.get_product()
return kwargs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
product = self.get_product()
context.setdefault('price_form', ProductInitialPriceForm())
context.update(
{'title': f'Añadir variante a {product.name}', 'cancel_url': reverse_lazy('backoffice:product_detail', args=[product.pk])}
)
return context
def form_valid(self, form):
# El precio es opcional y se crea en el mismo envío que la variante,
# igual que en ProductCreateView (ver ProductInitialPriceForm).
price_form = ProductInitialPriceForm(self.request.POST)
if not price_form.is_valid():
return self.render_to_response(self.get_context_data(form=form, price_form=price_form))
response = super().form_valid(form)
if price_form.cleaned_data.get('price') is not None:
ProductPrice.objects.create(
variant=self.object,
price=price_form.get_price_without_tax(),
tax=price_form.cleaned_data['tax'],
current=True,
)
return response
def get_success_url(self):
return reverse_lazy('backoffice:product_detail', args=[self.object.product_id])
class ProductVariantUpdateView(ProductVariantAttributesContextMixin, BackofficeCRUDMixin, BackofficeModalFormMixin, UpdateView):
model = ProductVariant
form_class = ProductVariantForm
permission_required = 'shop.change_productvariant'
section = SECTION
template_name = 'backoffice/products/variant_form.html'
fragment_template_name = VARIANT_FORM_FRAGMENT
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update(
{
'title': f'Editar variante {self.object.sku}',
'cancel_url': reverse_lazy('backoffice:product_detail', args=[self.object.product_id]),
}
)
return context
def get_success_url(self):
return reverse_lazy('backoffice:product_detail', args=[self.object.product_id])
class ProductVariantDeleteView(BackofficeCRUDMixin, BackofficeModalDeleteMixin, DeleteView):
model = ProductVariant
permission_required = 'shop.delete_productvariant'
section = SECTION
template_name = 'backoffice/generic/confirm_delete.html'
fragment_template_name = DELETE_FRAGMENT
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['cancel_url'] = reverse_lazy('backoffice:product_detail', args=[self.object.product_id])
return context
def get_success_url(self):
return reverse_lazy('backoffice:product_detail', args=[self.object.product_id])
# --- ProductPrice (create only — la historia es de solo alta, ver shop/models.py:ProductPrice) ---
class ProductPriceCreateView(BackofficeCRUDMixin, BackofficeModalFormMixin, CreateView):
model = ProductPrice
form_class = ProductPriceForm
permission_required = 'shop.add_productprice'
section = SECTION
template_name = 'backoffice/generic/form.html'
fragment_template_name = FORM_FRAGMENT
def get_product(self):
return get_object_or_404(Product, pk=self.kwargs['product_pk'])
def get_form(self, form_class=None):
# El FK debe fijarse antes de is_valid(), porque ProductPrice.clean()
# exige exactamente uno de product/variant en el momento de validar.
form = super().get_form(form_class)
form.instance.product = self.get_product()
return form
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
product = self.get_product()
context.update(
{'title': f'Añadir precio a {product.name}', 'cancel_url': reverse_lazy('backoffice:product_detail', args=[product.pk])}
)
return context
def get_success_url(self):
return reverse_lazy('backoffice:product_detail', args=[self.object.product_id])
class ProductVariantPriceCreateView(BackofficeCRUDMixin, BackofficeModalFormMixin, CreateView):
model = ProductPrice
form_class = ProductPriceForm
permission_required = 'shop.add_productprice'
section = SECTION
template_name = 'backoffice/generic/form.html'
fragment_template_name = FORM_FRAGMENT
def get_variant(self):
return get_object_or_404(ProductVariant, pk=self.kwargs['variant_pk'])
def get_form(self, form_class=None):
form = super().get_form(form_class)
form.instance.variant = self.get_variant()
return form
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
variant = self.get_variant()
context.update(
{
'title': f'Añadir precio a la variante {variant.sku}',
'cancel_url': reverse_lazy('backoffice:product_detail', args=[variant.product_id]),
}
)
return context
def get_success_url(self):
return reverse_lazy('backoffice:product_detail', args=[self.object.variant.product_id])
# --- ProductBatch (nested under a product) ---
class ProductBatchCreateView(BackofficeCRUDMixin, BackofficeModalFormMixin, CreateView):
model = ProductBatch
permission_required = 'shop.add_productbatch'
section = SECTION
fields = ('code', 'quantity', 'expiration_date', 'provider')
template_name = 'backoffice/generic/form.html'
fragment_template_name = FORM_FRAGMENT
def get_product(self):
return get_object_or_404(Product, pk=self.kwargs['product_pk'])
def get_form(self, form_class=None):
form = super().get_form(form_class)
form.instance.product = self.get_product()
return form
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
product = self.get_product()
context.update(
{'title': f'Añadir remesa a {product.name}', 'cancel_url': reverse_lazy('backoffice:product_detail', args=[product.pk])}
)
return context
def get_success_url(self):
return reverse_lazy('backoffice:product_detail', args=[self.object.product_id])
class ProductBatchDeleteView(BackofficeCRUDMixin, BackofficeModalDeleteMixin, DeleteView):
model = ProductBatch
permission_required = 'shop.delete_productbatch'
section = SECTION
template_name = 'backoffice/generic/confirm_delete.html'
fragment_template_name = DELETE_FRAGMENT
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['cancel_url'] = reverse_lazy('backoffice:product_detail', args=[self.object.product_id])
return context
def get_success_url(self):
return reverse_lazy('backoffice:product_detail', args=[self.object.product_id])
def perform_delete(self):
# Reutiliza la lógica ya existente de descuento de stock al borrar una remesa.
delete_product_batch(self.object)
# --- ProductCategory ---
class ProductCategoryListView(BackofficeCRUDMixin, BackofficeHtmxMixin, ListView):
model = ProductCategory
permission_required = 'shop.view_productcategory'
section = SECTION
template_name = 'backoffice/generic/list.html'
fragment_template_name = LIST_FRAGMENT
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update(
{
'title': 'Categorías de producto',
'columns': [('Nombre', 'name'), ('Padre', 'parent'), ('Oculta', 'hidden')],
'create_url': reverse_lazy('backoffice:product_category_create'),
'update_url_name': 'backoffice:product_category_update',
'delete_url_name': 'backoffice:product_category_delete',
}
)
return context
class ProductCategoryCreateView(BackofficeCRUDMixin, BackofficeModalFormMixin, CreateView):
model = ProductCategory
permission_required = 'shop.add_productcategory'
section = SECTION
fields = ('name', 'parent', 'promoted', 'hidden', 'show_in_navbar')
template_name = 'backoffice/generic/form.html'
fragment_template_name = FORM_FRAGMENT
success_url = reverse_lazy('backoffice:product_category_list')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({'title': 'Añadir categoría', 'cancel_url': reverse_lazy('backoffice:product_category_list')})
return context
class ProductCategoryUpdateView(BackofficeCRUDMixin, BackofficeModalFormMixin, UpdateView):
model = ProductCategory
permission_required = 'shop.change_productcategory'
section = SECTION
fields = ('name', 'parent', 'promoted', 'hidden', 'show_in_navbar')
template_name = 'backoffice/generic/form.html'
fragment_template_name = FORM_FRAGMENT
success_url = reverse_lazy('backoffice:product_category_list')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({'title': f'Editar {self.object.name}', 'cancel_url': reverse_lazy('backoffice:product_category_list')})
return context
class ProductCategoryDeleteView(BackofficeCRUDMixin, BackofficeModalDeleteMixin, DeleteView):
model = ProductCategory
permission_required = 'shop.delete_productcategory'
section = SECTION
template_name = 'backoffice/generic/confirm_delete.html'
fragment_template_name = DELETE_FRAGMENT
success_url = reverse_lazy('backoffice:product_category_list')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['cancel_url'] = reverse_lazy('backoffice:product_category_list')
return context
# --- ProductAttribute / ProductAttributeValue ---
class ProductAttributeListView(BackofficeCRUDMixin, BackofficeHtmxMixin, ListView):
model = ProductAttribute
permission_required = 'shop.view_productattribute'
section = SECTION
template_name = 'backoffice/generic/list.html'
fragment_template_name = LIST_FRAGMENT
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update(
{
'title': 'Atributos de producto',
'columns': [('Nombre', 'name')],
'create_url': reverse_lazy('backoffice:product_attribute_create'),
'detail_url_name': 'backoffice:product_attribute_detail',
'update_url_name': 'backoffice:product_attribute_update',
'delete_url_name': 'backoffice:product_attribute_delete',
}
)
return context
class ProductAttributeCreateView(BackofficeCRUDMixin, BackofficeModalFormMixin, CreateView):
model = ProductAttribute
permission_required = 'shop.add_productattribute'
section = SECTION
fields = ('name',)
template_name = 'backoffice/generic/form.html'
fragment_template_name = FORM_FRAGMENT
success_url = reverse_lazy('backoffice:product_attribute_list')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({'title': 'Añadir atributo', 'cancel_url': reverse_lazy('backoffice:product_attribute_list')})
return context
class ProductAttributeUpdateView(BackofficeCRUDMixin, BackofficeModalFormMixin, UpdateView):
"""Solo edita el nombre del atributo. Los valores se gestionan en ProductAttributeDetailView."""
model = ProductAttribute
permission_required = 'shop.change_productattribute'
section = SECTION
fields = ('name',)
template_name = 'backoffice/generic/form.html'
fragment_template_name = FORM_FRAGMENT
success_url = reverse_lazy('backoffice:product_attribute_list')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({'title': f'Editar {self.object.name}', 'cancel_url': reverse_lazy('backoffice:product_attribute_list')})
return context
class ProductAttributeDeleteView(BackofficeCRUDMixin, BackofficeModalDeleteMixin, DeleteView):
model = ProductAttribute
permission_required = 'shop.delete_productattribute'
section = SECTION
template_name = 'backoffice/generic/confirm_delete.html'
fragment_template_name = DELETE_FRAGMENT
success_url = reverse_lazy('backoffice:product_attribute_list')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['cancel_url'] = reverse_lazy('backoffice:product_attribute_list')
return context
class ProductAttributeDetailView(BackofficeCRUDMixin, DetailView):
"""Gestión de los valores (ProductAttributeValue) de un atributo."""
model = ProductAttribute
permission_required = 'shop.view_productattribute'
section = SECTION
template_name = 'backoffice/products/attribute_detail.html'
fragment_template_name = 'backoffice/products/_attribute_detail_fragment.html'
def get_template_names(self):
if self.request.headers.get('HX-Request') == 'true':
return [self.fragment_template_name]
return super().get_template_names()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['values'] = self.object.values.all()
return context
class ProductAttributeValueCreateView(BackofficeCRUDMixin, BackofficeModalFormMixin, CreateView):
model = ProductAttributeValue
permission_required = 'shop.add_productattributevalue'
section = SECTION
fields = ('value',)
template_name = 'backoffice/generic/form.html'
fragment_template_name = FORM_FRAGMENT
def get_attribute(self):
return get_object_or_404(ProductAttribute, pk=self.kwargs['attribute_pk'])
def get_form(self, form_class=None):
form = super().get_form(form_class)
form.instance.attribute = self.get_attribute()
return form
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
attribute = self.get_attribute()
context.update(
{
'title': f'Añadir valor a {attribute.name}',
'cancel_url': reverse_lazy('backoffice:product_attribute_detail', args=[attribute.pk]),
}
)
return context
def get_success_url(self):
return reverse_lazy('backoffice:product_attribute_detail', args=[self.object.attribute_id])
class ProductAttributeValueDeleteView(BackofficeCRUDMixin, BackofficeModalDeleteMixin, DeleteView):
model = ProductAttributeValue
permission_required = 'shop.delete_productattributevalue'
section = SECTION
template_name = 'backoffice/generic/confirm_delete.html'
fragment_template_name = DELETE_FRAGMENT
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['cancel_url'] = reverse_lazy('backoffice:product_attribute_detail', args=[self.object.attribute_id])
return context
def get_success_url(self):
return reverse_lazy('backoffice:product_attribute_detail', args=[self.object.attribute_id])
# --- Brand / Tag ---
class BrandListView(BackofficeCRUDMixin, BackofficeHtmxMixin, ListView):
model = Brand
permission_required = 'shop.view_brand'
section = SECTION
template_name = 'backoffice/generic/list.html'
fragment_template_name = LIST_FRAGMENT
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update(
{
'title': 'Marcas',
'columns': [('Nombre', 'name')],
'create_url': reverse_lazy('backoffice:brand_create'),
'update_url_name': 'backoffice:brand_update',
'delete_url_name': 'backoffice:brand_delete',
}
)
return context
class BrandCreateView(BackofficeCRUDMixin, BackofficeModalFormMixin, CreateView):
model = Brand
permission_required = 'shop.add_brand'
section = SECTION
fields = ('name',)
template_name = 'backoffice/generic/form.html'
fragment_template_name = FORM_FRAGMENT
success_url = reverse_lazy('backoffice:brand_list')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({'title': 'Añadir marca', 'cancel_url': reverse_lazy('backoffice:brand_list')})
return context
class BrandUpdateView(BackofficeCRUDMixin, BackofficeModalFormMixin, UpdateView):
model = Brand
permission_required = 'shop.change_brand'
section = SECTION
fields = ('name',)
template_name = 'backoffice/generic/form.html'
fragment_template_name = FORM_FRAGMENT
success_url = reverse_lazy('backoffice:brand_list')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({'title': f'Editar {self.object.name}', 'cancel_url': reverse_lazy('backoffice:brand_list')})
return context
class BrandDeleteView(BackofficeCRUDMixin, BackofficeModalDeleteMixin, DeleteView):
model = Brand
permission_required = 'shop.delete_brand'
section = SECTION
template_name = 'backoffice/generic/confirm_delete.html'
fragment_template_name = DELETE_FRAGMENT
success_url = reverse_lazy('backoffice:brand_list')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['cancel_url'] = reverse_lazy('backoffice:brand_list')
return context
class TagListView(BackofficeCRUDMixin, BackofficeHtmxMixin, ListView):
model = Tag
permission_required = 'shop.view_tag'
section = SECTION
template_name = 'backoffice/generic/list.html'
fragment_template_name = LIST_FRAGMENT
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update(
{
'title': 'Etiquetas',
'columns': [('Nombre', 'name')],
'create_url': reverse_lazy('backoffice:tag_create'),
'update_url_name': 'backoffice:tag_update',
'delete_url_name': 'backoffice:tag_delete',
}
)
return context
class TagCreateView(BackofficeCRUDMixin, BackofficeModalFormMixin, CreateView):
model = Tag
permission_required = 'shop.add_tag'
section = SECTION
fields = ('name',)
template_name = 'backoffice/generic/form.html'
fragment_template_name = FORM_FRAGMENT
success_url = reverse_lazy('backoffice:tag_list')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({'title': 'Añadir etiqueta', 'cancel_url': reverse_lazy('backoffice:tag_list')})
return context
class TagUpdateView(BackofficeCRUDMixin, BackofficeModalFormMixin, UpdateView):
model = Tag
permission_required = 'shop.change_tag'
section = SECTION
fields = ('name',)
template_name = 'backoffice/generic/form.html'
fragment_template_name = FORM_FRAGMENT
success_url = reverse_lazy('backoffice:tag_list')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({'title': f'Editar {self.object.name}', 'cancel_url': reverse_lazy('backoffice:tag_list')})
return context
class TagDeleteView(BackofficeCRUDMixin, BackofficeModalDeleteMixin, DeleteView):
model = Tag
permission_required = 'shop.delete_tag'
section = SECTION
template_name = 'backoffice/generic/confirm_delete.html'
fragment_template_name = DELETE_FRAGMENT
success_url = reverse_lazy('backoffice:tag_list')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['cancel_url'] = reverse_lazy('backoffice:tag_list')
return context