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) --- class ProductVariantCreateView(BackofficeCRUDMixin, BackofficeModalFormMixin, CreateView): model = ProductVariant form_class = ProductVariantForm permission_required = 'shop.add_productvariant' 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_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.update( {'title': f'Añadir variante 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 ProductVariantUpdateView(BackofficeCRUDMixin, BackofficeModalFormMixin, UpdateView): model = ProductVariant form_class = ProductVariantForm permission_required = 'shop.change_productvariant' section = SECTION template_name = 'backoffice/generic/form.html' fragment_template_name = 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