feat: create/edit product views
This commit is contained in:
+106
-29
@@ -1,7 +1,8 @@
|
||||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse_lazy
|
||||
from django.views.generic import CreateView, DeleteView, DetailView, ListView, UpdateView
|
||||
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 ProductVariantForm
|
||||
from backoffice.mixins import (
|
||||
@@ -9,6 +10,7 @@ from backoffice.mixins import (
|
||||
BackofficeHtmxMixin,
|
||||
BackofficeModalDeleteMixin,
|
||||
BackofficeModalFormMixin,
|
||||
BackofficeStyledFormMixin,
|
||||
)
|
||||
from shop.models import (
|
||||
Brand,
|
||||
@@ -49,6 +51,7 @@ class ProductListView(BackofficeCRUDMixin, BackofficeHtmxMixin, ListView):
|
||||
'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',
|
||||
}
|
||||
@@ -56,35 +59,18 @@ class ProductListView(BackofficeCRUDMixin, BackofficeHtmxMixin, ListView):
|
||||
return context
|
||||
|
||||
|
||||
class ProductCreateView(BackofficeCRUDMixin, BackofficeModalFormMixin, CreateView):
|
||||
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" (variantes, precios, imágenes y remesas sí requieren que el
|
||||
producto exista, así que 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/generic/form.html'
|
||||
fragment_template_name = FORM_FRAGMENT
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context.update({'title': 'Añadir producto', 'cancel_url': reverse_lazy('backoffice:product_list')})
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('backoffice:product_detail', args=[self.object.pk])
|
||||
|
||||
|
||||
class ProductUpdateView(BackofficeCRUDMixin, BackofficeModalFormMixin, UpdateView):
|
||||
model = Product
|
||||
permission_required = 'shop.change_product'
|
||||
section = SECTION
|
||||
fields = ('sku', 'name', 'description', 'stock', 'brand', 'categories', 'tags', 'hidden', 'is_shipping_method')
|
||||
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 {self.object.name}', 'cancel_url': reverse_lazy('backoffice:product_list')})
|
||||
return context
|
||||
template_name = 'backoffice/products/product_create.html'
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('backoffice:product_detail', args=[self.object.pk])
|
||||
@@ -138,6 +124,97 @@ class ProductDetailView(BackofficeCRUDMixin, DetailView):
|
||||
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) ---
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user