feat: create/edit product views

This commit is contained in:
2026-07-23 16:19:19 +02:00
parent 2a9407e676
commit 1d11ea9b6e
16 changed files with 574 additions and 175 deletions
+9 -38
View File
@@ -17,53 +17,24 @@ class TestBackofficeHtmxContract(TestCase, CreateProductsMixin):
self.client.force_login(self.superuser)
self.product = self.create_product()
def test_htmx_get_create_form_returns_fragment_not_full_page(self):
def test_product_create_is_a_full_page_not_a_modal_fragment(self):
# ProductCreateView ya no es modal: navegar a la URL de creación siempre
# devuelve la página completa, incluso si la petición lleva HX-Request.
response = self.client.get(reverse('backoffice:product_create'), HTTP_HX_REQUEST='true')
assert response.status_code == 200
body = response.content.decode()
assert '<html' not in body
assert 'Añadir producto' in body
assert '<html' in body
assert 'Nuevo producto' in body
def test_htmx_create_success_returns_204_and_triggers_modal_close_and_refresh(self):
def test_product_create_redirects_to_detail_even_with_hx_request(self):
response = self.client.post(
reverse('backoffice:product_create'),
{
'sku': 'HTMX-1',
'name': 'Producto htmx',
'description': '',
'stock': '0',
'categories': [],
'tags': [],
},
{'sku': 'HTMX-1', 'name': 'Producto htmx', 'description': '', 'stock': '0'},
HTTP_HX_REQUEST='true',
)
assert response.status_code == 204
trigger = json.loads(response['HX-Trigger'])
assert trigger == {'backoffice:modal-close': True, 'backoffice:list-changed': True}
assert Product.objects.filter(sku='HTMX-1').exists()
def test_htmx_create_invalid_returns_200_with_errors_not_204(self):
response = self.client.post(
reverse('backoffice:product_create'),
{'sku': '', 'name': '', 'description': '', 'stock': '0', 'categories': [], 'tags': []},
HTTP_HX_REQUEST='true',
)
assert response.status_code == 200
assert 'HX-Trigger' not in response
def test_non_htmx_create_still_redirects(self):
response = self.client.post(
reverse('backoffice:product_create'),
{
'sku': 'PLAIN-1',
'name': 'Producto normal',
'description': '',
'stock': '0',
'categories': [],
'tags': [],
},
)
product = Product.objects.get(sku='HTMX-1')
assert response.status_code == 302
assert response.url == reverse('backoffice:product_detail', args=[product.pk])
assert 'HX-Trigger' not in response
def test_htmx_variant_create_triggers_refresh(self):
+118 -6
View File
@@ -7,7 +7,7 @@ from django.test import TestCase
from django.urls import reverse
from PIL import Image
from shop.models import Product, ProductImage, ProductPrice, ProductVariant, Tax
from shop.models import Brand, Product, ProductCategory, ProductImage, ProductPrice, ProductVariant, Tag, Tax
from shop.tests.mixins import CreateProductsMixin
@@ -23,19 +23,40 @@ class TestBackofficeProducts(TestCase, CreateProductsMixin):
assert self.product.name in response.content.decode()
def test_create_product(self):
response = self.client.get(reverse('backoffice:product_create'))
assert response.status_code == 200
response = self.client.post(
reverse('backoffice:product_create'),
{'sku': 'NEW-1', 'name': 'Producto nuevo', 'description': '', 'stock': '0'},
)
product = Product.objects.get(sku='NEW-1')
assert response.status_code == 302
assert response.url == reverse('backoffice:product_detail', args=[product.pk])
def test_create_product_with_brand_categories_and_tags_in_one_submit(self):
brand = Brand.objects.create(name='Marca X')
cat1 = ProductCategory.objects.create(name='Cat 1')
cat2 = ProductCategory.objects.create(name='Cat 2')
tag1 = Tag.objects.create(name='Tag 1')
response = self.client.post(
reverse('backoffice:product_create'),
{
'sku': 'NEW-1',
'name': 'Producto nuevo',
'sku': 'NEW-2',
'name': 'Producto completo',
'description': '',
'stock': '0',
'categories': [],
'tags': [],
'brand': brand.pk,
'categories': [cat1.pk, cat2.pk],
'tags': [tag1.pk],
},
)
assert response.status_code == 302
assert Product.objects.filter(sku='NEW-1').exists()
product = Product.objects.get(sku='NEW-2')
assert product.brand == brand
assert set(product.categories.all()) == {cat1, cat2}
assert set(product.tags.all()) == {tag1}
def test_create_variant_for_product(self):
size_m = self.create_attribute_value('Talla', 'M')
@@ -104,6 +125,97 @@ class TestBackofficeProducts(TestCase, CreateProductsMixin):
assert response.status_code == 302
assert not ProductImage.objects.filter(pk=image.pk).exists()
def test_inline_view_name(self):
response = self.client.get(reverse('backoffice:product_inline_view', args=[self.product.pk, 'name']))
assert response.status_code == 200
assert self.product.name in response.content.decode()
def test_inline_edit_name_form(self):
response = self.client.get(reverse('backoffice:product_inline_edit', args=[self.product.pk, 'name']))
assert response.status_code == 200
assert 'name="name"' in response.content.decode()
assert 'description' not in response.content.decode()
def test_inline_update_name(self):
response = self.client.post(
reverse('backoffice:product_inline_edit', args=[self.product.pk, 'name']), {'name': 'Nuevo nombre'}
)
assert response.status_code == 200
self.product.refresh_from_db()
assert self.product.name == 'Nuevo nombre'
assert 'Nuevo nombre' in response.content.decode()
def test_inline_update_description(self):
response = self.client.post(
reverse('backoffice:product_inline_edit', args=[self.product.pk, 'description']),
{'description': 'Descripción actualizada'},
)
assert response.status_code == 200
self.product.refresh_from_db()
assert self.product.description == 'Descripción actualizada'
def test_inline_update_brand(self):
brand = Brand.objects.create(name='Marca X')
response = self.client.post(
reverse('backoffice:product_inline_edit', args=[self.product.pk, 'brand']), {'brand': brand.pk}
)
assert response.status_code == 200
self.product.refresh_from_db()
assert self.product.brand == brand
assert 'Marca X' in response.content.decode()
def test_inline_update_categories_accepts_multiple(self):
cat1 = ProductCategory.objects.create(name='Cat 1')
cat2 = ProductCategory.objects.create(name='Cat 2')
response = self.client.post(
reverse('backoffice:product_inline_edit', args=[self.product.pk, 'categories']),
{'categories': [cat1.pk, cat2.pk]},
)
assert response.status_code == 200
self.product.refresh_from_db()
assert set(self.product.categories.all()) == {cat1, cat2}
def test_inline_update_tags_accepts_multiple(self):
tag1 = Tag.objects.create(name='Tag 1')
tag2 = Tag.objects.create(name='Tag 2')
response = self.client.post(
reverse('backoffice:product_inline_edit', args=[self.product.pk, 'tags']), {'tags': [tag1.pk, tag2.pk]}
)
assert response.status_code == 200
self.product.refresh_from_db()
assert set(self.product.tags.all()) == {tag1, tag2}
def test_inline_edit_rejects_arbitrary_field(self):
response = self.client.get(reverse('backoffice:product_inline_edit', args=[self.product.pk, 'sku']))
assert response.status_code == 404
def test_field_search_filters_by_query(self):
Brand.objects.create(name='Acme')
Brand.objects.create(name='Globex')
response = self.client.get(reverse('backoffice:product_field_search', args=['brand']), {'q': 'Acm'})
assert response.status_code == 200
body = response.content.decode()
assert 'Acme' in body
assert 'Globex' not in body
def test_field_search_without_query_returns_all(self):
Brand.objects.create(name='Acme')
Brand.objects.create(name='Globex')
response = self.client.get(reverse('backoffice:product_field_search', args=['brand']))
assert response.status_code == 200
body = response.content.decode()
assert 'Acme' in body
assert 'Globex' in body
def test_field_search_rejects_arbitrary_field(self):
response = self.client.get(reverse('backoffice:product_field_search', args=['sku']))
assert response.status_code == 404
def test_delete_batch_reduces_stock(self):
self.product.stock = Decimal('10')
self.product.save()