generated from westfarn/web_django_template
Stand up Print Forge as a 3D-printed toy shop with color variants and printer photography.
Replaces the client_site template branding, adds shop/shipping, and points beta CI at master for easy deploy. Closes #1
This commit is contained in:
@@ -0,0 +1,593 @@
|
||||
import struct
|
||||
from decimal import Decimal
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core import mail
|
||||
from django.core.files.storage import default_storage
|
||||
from django.core.files.storage.memory import InMemoryStorage
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile, TemporaryUploadedFile
|
||||
from django.db import models
|
||||
from django.test import Client, TestCase, override_settings
|
||||
from django.urls import reverse
|
||||
from PIL import Image
|
||||
|
||||
from core.models import StoredFile
|
||||
from shop.models import Order, Product, ProductColor, ProductImage
|
||||
from shop.services import (
|
||||
ShopError,
|
||||
add_to_cart,
|
||||
adjust_stock,
|
||||
available_qty,
|
||||
create_order_from_cart,
|
||||
looks_like_stl,
|
||||
mark_paid,
|
||||
next_order_number,
|
||||
store_product_image,
|
||||
store_product_stl,
|
||||
)
|
||||
|
||||
def _tiny_png() -> bytes:
|
||||
image = Image.new("RGBA", (8, 8), (200, 40, 40, 255))
|
||||
buf = BytesIO()
|
||||
image.save(buf, format="PNG")
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def _tiny_stl() -> bytes:
|
||||
header = b"tiny" + b"\x00" * 76
|
||||
count = struct.pack("<I", 1)
|
||||
triangle = struct.pack(
|
||||
"<12fH",
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
0,
|
||||
)
|
||||
return header + count + triangle
|
||||
|
||||
|
||||
def _product(**kwargs):
|
||||
defaults = dict(
|
||||
name="Dragon figurine",
|
||||
sku="TOY-001",
|
||||
price=Decimal("18.00"),
|
||||
stock_qty=5,
|
||||
is_published=True,
|
||||
fulfillment=Product.Fulfillment.STOCKED,
|
||||
)
|
||||
defaults.update(kwargs)
|
||||
return Product.objects.create(**defaults)
|
||||
|
||||
|
||||
class ShopPublicTests(TestCase):
|
||||
def test_list_hides_unpublished(self):
|
||||
_product(name="Live", sku="LIVE-1")
|
||||
_product(name="Draft", sku="DRAFT-1", is_published=False)
|
||||
response = Client().get(reverse("shop:list"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Live")
|
||||
self.assertNotContains(response, "Draft")
|
||||
|
||||
def test_add_to_cart_then_checkout_form(self):
|
||||
product = _product()
|
||||
client = Client()
|
||||
response = client.post(
|
||||
reverse("shop:cart_add", kwargs={"slug": product.slug}),
|
||||
{"quantity": "2"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
cart = client.get(reverse("shop:cart"))
|
||||
self.assertContains(cart, "Dragon figurine")
|
||||
self.assertContains(cart, "36.00")
|
||||
checkout = client.get(reverse("shop:checkout"))
|
||||
self.assertEqual(checkout.status_code, 200)
|
||||
self.assertContains(checkout, "Pay with Stripe")
|
||||
|
||||
def test_detail_and_list_show_product_image_and_colors(self):
|
||||
stored = StoredFile.objects.create(
|
||||
kind=StoredFile.Kind.PRODUCT_IMAGE,
|
||||
filename="dragon.png",
|
||||
content_type="image/png",
|
||||
size=len(_tiny_png()),
|
||||
data=_tiny_png(),
|
||||
)
|
||||
product = _product(image=stored)
|
||||
ProductColor.objects.create(
|
||||
product=product, name="Crimson", hex="#c41e3a", sort_order=0
|
||||
)
|
||||
ProductColor.objects.create(
|
||||
product=product, name="Navy", hex="#1e3a8a", sort_order=1
|
||||
)
|
||||
client = Client()
|
||||
listing = client.get(reverse("shop:list"))
|
||||
self.assertContains(listing, product.image_url)
|
||||
self.assertContains(listing, 'title="Crimson"')
|
||||
detail = client.get(product.get_absolute_url())
|
||||
self.assertEqual(detail.status_code, 200)
|
||||
self.assertContains(detail, product.image_url)
|
||||
self.assertContains(detail, "Crimson")
|
||||
self.assertContains(detail, 'name="color"')
|
||||
html = detail.content.decode()
|
||||
self.assertIn('data-kind="photo"', html)
|
||||
|
||||
def test_detail_shows_photo_before_3d_and_multiple_photos(self):
|
||||
photo_a = StoredFile.objects.create(
|
||||
kind=StoredFile.Kind.PRODUCT_IMAGE,
|
||||
filename="a.png",
|
||||
content_type="image/png",
|
||||
size=len(_tiny_png()),
|
||||
data=_tiny_png(),
|
||||
)
|
||||
photo_b = StoredFile.objects.create(
|
||||
kind=StoredFile.Kind.PRODUCT_IMAGE,
|
||||
filename="b.png",
|
||||
content_type="image/png",
|
||||
size=len(_tiny_png()),
|
||||
data=_tiny_png(),
|
||||
)
|
||||
stl = StoredFile.objects.create(
|
||||
kind=StoredFile.Kind.PRODUCT_STL,
|
||||
filename="dragon.stl",
|
||||
content_type="model/stl",
|
||||
size=len(_tiny_stl()),
|
||||
data=_tiny_stl(),
|
||||
)
|
||||
product = _product(image=photo_a, stl=stl)
|
||||
ProductImage.objects.create(product=product, file=photo_a, sort_order=0)
|
||||
ProductImage.objects.create(product=product, file=photo_b, sort_order=1)
|
||||
detail = Client().get(product.get_absolute_url())
|
||||
html = detail.content.decode()
|
||||
self.assertLess(html.index('data-kind="photo"'), html.index('data-kind="model"'))
|
||||
self.assertIn("data-gallery-model", html)
|
||||
self.assertContains(detail, reverse("core:stored_file", kwargs={"pk": photo_a.pk}))
|
||||
self.assertContains(detail, reverse("core:stored_file", kwargs={"pk": photo_b.pk}))
|
||||
self.assertRegex(html, r"data-gallery-model\s+hidden")
|
||||
|
||||
def test_detail_color_swaps_photos_and_keeps_shared_stl(self):
|
||||
red_file = StoredFile.objects.create(
|
||||
kind=StoredFile.Kind.PRODUCT_IMAGE,
|
||||
filename="red.png",
|
||||
content_type="image/png",
|
||||
size=len(_tiny_png()),
|
||||
data=_tiny_png(),
|
||||
)
|
||||
blue_file = StoredFile.objects.create(
|
||||
kind=StoredFile.Kind.PRODUCT_IMAGE,
|
||||
filename="blue.png",
|
||||
content_type="image/png",
|
||||
size=len(_tiny_png()),
|
||||
data=_tiny_png(),
|
||||
)
|
||||
stl = StoredFile.objects.create(
|
||||
kind=StoredFile.Kind.PRODUCT_STL,
|
||||
filename="toy.stl",
|
||||
content_type="model/stl",
|
||||
size=len(_tiny_stl()),
|
||||
data=_tiny_stl(),
|
||||
)
|
||||
product = _product(image=red_file, stl=stl)
|
||||
red = ProductColor.objects.create(
|
||||
product=product, name="Red", hex="#ff0000", stock_qty=2, sort_order=0
|
||||
)
|
||||
blue = ProductColor.objects.create(
|
||||
product=product, name="Blue", hex="#0000ff", stock_qty=9, sort_order=1
|
||||
)
|
||||
ProductImage.objects.create(
|
||||
product=product, color=red, file=red_file, sort_order=0
|
||||
)
|
||||
ProductImage.objects.create(
|
||||
product=product, color=blue, file=blue_file, sort_order=0
|
||||
)
|
||||
detail = Client().get(product.get_absolute_url())
|
||||
self.assertContains(detail, reverse("core:stored_file", kwargs={"pk": red_file.pk}))
|
||||
self.assertContains(detail, reverse("core:stored_file", kwargs={"pk": blue_file.pk}))
|
||||
self.assertContains(detail, product.stl_url)
|
||||
self.assertContains(detail, "2 in stock")
|
||||
payload = detail.context["gallery_data"]
|
||||
self.assertEqual(payload["stl"], product.stl_url)
|
||||
self.assertEqual(
|
||||
payload["colors"][str(red.pk)]["images"][0],
|
||||
reverse("core:stored_file", kwargs={"pk": red_file.pk}),
|
||||
)
|
||||
self.assertEqual(payload["colors"][str(blue.pk)]["available"], 9)
|
||||
|
||||
def test_detail_embeds_stl_viewer(self):
|
||||
stored = StoredFile.objects.create(
|
||||
kind=StoredFile.Kind.PRODUCT_STL,
|
||||
filename="dragon.stl",
|
||||
content_type="model/stl",
|
||||
size=len(_tiny_stl()),
|
||||
data=_tiny_stl(),
|
||||
)
|
||||
product = _product(stl=stored)
|
||||
detail = Client().get(product.get_absolute_url())
|
||||
self.assertEqual(detail.status_code, 200)
|
||||
self.assertContains(detail, 'data-stl-viewer')
|
||||
self.assertContains(detail, product.stl_url)
|
||||
self.assertContains(detail, "stl-viewer.js")
|
||||
self.assertContains(detail, "Drag to spin")
|
||||
|
||||
def test_add_to_cart_requires_color_when_product_has_colors(self):
|
||||
product = _product()
|
||||
color = ProductColor.objects.create(
|
||||
product=product, name="Gold", hex="#d4af37"
|
||||
)
|
||||
client = Client()
|
||||
missing = client.post(
|
||||
reverse("shop:cart_add", kwargs={"slug": product.slug}),
|
||||
{"quantity": "1"},
|
||||
)
|
||||
self.assertEqual(missing.status_code, 302)
|
||||
self.assertEqual(missing["Location"], product.get_absolute_url())
|
||||
added = client.post(
|
||||
reverse("shop:cart_add", kwargs={"slug": product.slug}),
|
||||
{"quantity": "1", "color": str(color.pk)},
|
||||
)
|
||||
self.assertEqual(added.status_code, 302)
|
||||
cart = client.get(reverse("shop:cart"))
|
||||
self.assertContains(cart, "Color: Gold")
|
||||
checkout = client.get(reverse("shop:checkout"))
|
||||
self.assertContains(checkout, "Gold")
|
||||
|
||||
def test_order_snapshots_selected_color(self):
|
||||
product = _product()
|
||||
color = ProductColor.objects.create(
|
||||
product=product, name="Forest", hex="#228b22", stock_qty=2
|
||||
)
|
||||
session = self.client.session
|
||||
add_to_cart(session, product, 1, color=color)
|
||||
session.save()
|
||||
order = create_order_from_cart(
|
||||
self.client.session, email="buyer@example.com"
|
||||
)
|
||||
item = order.items.get()
|
||||
self.assertEqual(item.color, color)
|
||||
self.assertEqual(item.color_name, "Forest")
|
||||
self.assertIn("Forest", item.name)
|
||||
|
||||
def test_paid_order_decrements_color_stock(self):
|
||||
product = _product(stock_qty=10)
|
||||
red = ProductColor.objects.create(
|
||||
product=product, name="Red", hex="#ff0000", stock_qty=4
|
||||
)
|
||||
blue = ProductColor.objects.create(
|
||||
product=product, name="Blue", hex="#0000ff", stock_qty=6
|
||||
)
|
||||
session = self.client.session
|
||||
add_to_cart(session, product, 3, color=red)
|
||||
session.save()
|
||||
order = create_order_from_cart(
|
||||
self.client.session, email="buyer@example.com"
|
||||
)
|
||||
mark_paid(order)
|
||||
red.refresh_from_db()
|
||||
blue.refresh_from_db()
|
||||
product.refresh_from_db()
|
||||
self.assertEqual(red.stock_qty, 1)
|
||||
self.assertEqual(blue.stock_qty, 6)
|
||||
self.assertEqual(product.stock_qty, 10)
|
||||
|
||||
|
||||
class ShopInventoryTests(TestCase):
|
||||
def test_stocked_availability_and_adjust(self):
|
||||
product = _product(stock_qty=4)
|
||||
self.assertEqual(available_qty(product), 4)
|
||||
adjust_stock(product, -2)
|
||||
product.refresh_from_db()
|
||||
self.assertEqual(product.stock_qty, 2)
|
||||
with self.assertRaises(ShopError):
|
||||
adjust_stock(product, -5)
|
||||
|
||||
def test_mark_paid_decrements_stock_and_emails(self):
|
||||
product = _product(stock_qty=3)
|
||||
session = self.client.session
|
||||
add_to_cart(session, product, 2)
|
||||
session.save()
|
||||
order = create_order_from_cart(
|
||||
self.client.session, email="buyer@example.com", customer_name="Pat"
|
||||
)
|
||||
mark_paid(order)
|
||||
product.refresh_from_db()
|
||||
self.assertEqual(product.stock_qty, 1)
|
||||
order.refresh_from_db()
|
||||
self.assertEqual(order.status, Order.Status.PAID)
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertIn(order.number, mail.outbox[0].subject)
|
||||
|
||||
def test_insufficient_stock_blocks_order(self):
|
||||
product = _product(stock_qty=1)
|
||||
session = self.client.session
|
||||
add_to_cart(session, product, 3)
|
||||
session.save()
|
||||
with self.assertRaises(ShopError):
|
||||
create_order_from_cart(self.client.session, email="buyer@example.com")
|
||||
|
||||
def test_insufficient_color_stock_blocks_order(self):
|
||||
product = _product(stock_qty=10)
|
||||
color = ProductColor.objects.create(
|
||||
product=product, name="Red", hex="#ff0000", stock_qty=1
|
||||
)
|
||||
session = self.client.session
|
||||
add_to_cart(session, product, 3, color=color)
|
||||
session.save()
|
||||
with self.assertRaises(ShopError):
|
||||
create_order_from_cart(self.client.session, email="buyer@example.com")
|
||||
|
||||
@override_settings(SHOP_PRINT_QUEUE_LIMIT_MINUTES=60)
|
||||
def test_made_to_order_queue_limit(self):
|
||||
product = _product(
|
||||
sku="MTO-1",
|
||||
fulfillment=Product.Fulfillment.MADE_TO_ORDER,
|
||||
print_minutes=30,
|
||||
stock_qty=0,
|
||||
)
|
||||
self.assertEqual(available_qty(product), 2)
|
||||
|
||||
def test_next_number_increments(self):
|
||||
n1 = next_order_number()
|
||||
Order.objects.create(
|
||||
number=n1,
|
||||
email="a@example.com",
|
||||
amount=Decimal("1.00"),
|
||||
)
|
||||
n2 = next_order_number()
|
||||
self.assertNotEqual(n1, n2)
|
||||
|
||||
def test_looks_like_stl(self):
|
||||
self.assertTrue(looks_like_stl(_tiny_stl(), "toy.stl"))
|
||||
self.assertFalse(looks_like_stl(_tiny_stl(), "toy.bin"))
|
||||
self.assertFalse(looks_like_stl(b"not an stl file at all" + b"x" * 80, "toy.stl"))
|
||||
ascii_stl = (
|
||||
b"solid test\n"
|
||||
b" facet normal 0 0 1\n"
|
||||
b" outer loop\n"
|
||||
b" vertex 0 0 0\n"
|
||||
b" vertex 1 0 0\n"
|
||||
b" vertex 0 1 0\n"
|
||||
b" endloop\n"
|
||||
b" endfacet\n"
|
||||
b"endsolid test\n"
|
||||
)
|
||||
self.assertTrue(looks_like_stl(ascii_stl, "ascii.stl"))
|
||||
|
||||
|
||||
class ShopPortalTests(TestCase):
|
||||
def setUp(self):
|
||||
User = get_user_model()
|
||||
self.user = User.objects.create_user("merchant", password="test-pass-123")
|
||||
self.client = Client()
|
||||
self.client.login(username="merchant", password="test-pass-123")
|
||||
|
||||
def test_list_requires_login(self):
|
||||
anon = Client()
|
||||
self.assertEqual(anon.get(reverse("shop_portal:product_list")).status_code, 302)
|
||||
|
||||
def test_create_and_adjust_stock(self):
|
||||
response = self.client.post(
|
||||
reverse("shop_portal:product_new"),
|
||||
{
|
||||
"name": "Booster box",
|
||||
"sku": "TCG-BOX",
|
||||
"price": "89.99",
|
||||
"stock_qty": "10",
|
||||
"fulfillment": "stocked",
|
||||
"is_published": "on",
|
||||
"track_inventory": "on",
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
product = Product.objects.get(sku="TCG-BOX")
|
||||
self.assertTrue(product.is_published)
|
||||
self.assertEqual(product.slug, "booster-box")
|
||||
adjust = self.client.post(
|
||||
reverse("shop_portal:product_stock", kwargs={"pk": product.pk}),
|
||||
{"delta": "-3"},
|
||||
)
|
||||
self.assertEqual(adjust.status_code, 302)
|
||||
product.refresh_from_db()
|
||||
self.assertEqual(product.stock_qty, 7)
|
||||
|
||||
def test_portal_uses_public_favicon(self):
|
||||
response = self.client.get(reverse("shop_portal:product_list"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "brand/favicon.ico")
|
||||
self.assertContains(response, "apple-touch-icon")
|
||||
|
||||
def test_new_product_form_has_card_preview(self):
|
||||
response = self.client.get(reverse("shop_portal:product_new"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Shop card preview")
|
||||
self.assertContains(response, 'name="images"')
|
||||
self.assertContains(response, 'name="stl"')
|
||||
self.assertContains(response, "Add color")
|
||||
|
||||
def test_create_product_with_stl(self):
|
||||
upload = SimpleUploadedFile(
|
||||
"dragon.stl", _tiny_stl(), content_type="application/octet-stream"
|
||||
)
|
||||
response = self.client.post(
|
||||
reverse("shop_portal:product_new"),
|
||||
{
|
||||
"name": "Spinny toy",
|
||||
"sku": "STL-1",
|
||||
"price": "12.00",
|
||||
"stock_qty": "2",
|
||||
"fulfillment": "stocked",
|
||||
"is_published": "on",
|
||||
"track_inventory": "on",
|
||||
"stl": upload,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
product = Product.objects.get(sku="STL-1")
|
||||
self.assertTrue(product.stl_id)
|
||||
self.assertEqual(product.stl.kind, StoredFile.Kind.PRODUCT_STL)
|
||||
self.assertEqual(product.stl.content_type, "model/stl")
|
||||
detail = Client().get(product.get_absolute_url())
|
||||
self.assertContains(detail, product.stl_url)
|
||||
|
||||
def test_rejects_invalid_stl(self):
|
||||
upload = SimpleUploadedFile(
|
||||
"dragon.stl", b"nope" + b"\x00" * 100, content_type="application/octet-stream"
|
||||
)
|
||||
response = self.client.post(
|
||||
reverse("shop_portal:product_new"),
|
||||
{
|
||||
"name": "Bad mesh",
|
||||
"sku": "STL-BAD",
|
||||
"price": "12.00",
|
||||
"stock_qty": "1",
|
||||
"fulfillment": "stocked",
|
||||
"stl": upload,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertFalse(Product.objects.filter(sku="STL-BAD").exists())
|
||||
|
||||
def test_create_product_with_image_and_colors(self):
|
||||
upload = SimpleUploadedFile("dot.png", _tiny_png(), content_type="image/png")
|
||||
with patch(
|
||||
"shop.imaging.cutout_subject",
|
||||
side_effect=lambda image: image.convert("RGBA"),
|
||||
):
|
||||
response = self.client.post(
|
||||
reverse("shop_portal:product_new"),
|
||||
{
|
||||
"name": "Mech dragon",
|
||||
"sku": "MECH-1",
|
||||
"price": "24.00",
|
||||
"stock_qty": "3",
|
||||
"fulfillment": "made_to_order",
|
||||
"print_minutes": "45",
|
||||
"filament_grams": "80",
|
||||
"description": "Articulated toy.",
|
||||
"is_published": "on",
|
||||
"track_inventory": "on",
|
||||
"images": upload,
|
||||
"color_id": ["", ""],
|
||||
"color_key": ["new-1", "new-2"],
|
||||
"color_name": ["Red", "Blue"],
|
||||
"color_hex": ["#ff0000", "#0000ff"],
|
||||
"color_stock": ["4", "7"],
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
product = Product.objects.get(sku="MECH-1")
|
||||
self.assertTrue(product.image_id)
|
||||
self.assertEqual(product.image.kind, StoredFile.Kind.PRODUCT_IMAGE)
|
||||
self.assertEqual(product.image.content_type, "image/png")
|
||||
self.assertEqual(product.image.filename, "dot.png")
|
||||
framed = Image.open(BytesIO(bytes(product.image.data)))
|
||||
self.assertEqual(framed.size, (1200, 1200))
|
||||
colors = list(product.colors.all())
|
||||
self.assertEqual([c.name for c in colors], ["Red", "Blue"])
|
||||
self.assertEqual(colors[0].hex, "#ff0000")
|
||||
self.assertEqual([c.stock_qty for c in colors], [4, 7])
|
||||
self.assertEqual(product.images.count(), 1)
|
||||
self.assertIsNone(product.images.get().color_id)
|
||||
listing = Client().get(reverse("shop:list"))
|
||||
self.assertContains(listing, product.image_url)
|
||||
self.assertContains(listing, 'title="Red"')
|
||||
self.assertTrue(product.image_url.startswith("/files/"))
|
||||
self.assertNotIn("/media/", product.image_url)
|
||||
fetch = Client().get(product.image_url)
|
||||
self.assertEqual(fetch.status_code, 200)
|
||||
self.assertEqual(
|
||||
b"".join(fetch.streaming_content), bytes(product.image.data)
|
||||
)
|
||||
|
||||
def test_create_product_with_color_photos_and_shared_stl(self):
|
||||
catalog = SimpleUploadedFile("card.png", _tiny_png(), content_type="image/png")
|
||||
red_photo = SimpleUploadedFile("red.png", _tiny_png(), content_type="image/png")
|
||||
extra = SimpleUploadedFile("red-2.png", _tiny_png(), content_type="image/png")
|
||||
stl = SimpleUploadedFile(
|
||||
"toy.stl", _tiny_stl(), content_type="application/octet-stream"
|
||||
)
|
||||
with patch(
|
||||
"shop.imaging.cutout_subject",
|
||||
side_effect=lambda image: image.convert("RGBA"),
|
||||
):
|
||||
response = self.client.post(
|
||||
reverse("shop_portal:product_new"),
|
||||
{
|
||||
"name": "Color dragon",
|
||||
"sku": "COLOR-1",
|
||||
"price": "30.00",
|
||||
"stock_qty": "0",
|
||||
"fulfillment": "stocked",
|
||||
"is_published": "on",
|
||||
"track_inventory": "on",
|
||||
"images": catalog,
|
||||
"stl": stl,
|
||||
"color_id": ["", ""],
|
||||
"color_key": ["new-1", "new-2"],
|
||||
"color_name": ["Crimson", "Navy"],
|
||||
"color_hex": ["#c41e3a", "#1e3a8a"],
|
||||
"color_stock": ["3", "5"],
|
||||
"color_images_new-1": [red_photo, extra],
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
product = Product.objects.get(sku="COLOR-1")
|
||||
crimson = product.colors.get(name="Crimson")
|
||||
navy = product.colors.get(name="Navy")
|
||||
self.assertTrue(product.stl_id)
|
||||
self.assertEqual(product.images.filter(color=None).count(), 1)
|
||||
self.assertEqual(product.images.filter(color=crimson).count(), 2)
|
||||
self.assertEqual(product.images.filter(color=navy).count(), 0)
|
||||
self.assertEqual(crimson.stock_qty, 3)
|
||||
detail = Client().get(product.get_absolute_url())
|
||||
html = detail.content.decode()
|
||||
self.assertLess(html.index('data-kind="photo"'), html.index('data-kind="model"'))
|
||||
self.assertContains(detail, product.stl_url)
|
||||
self.assertEqual(detail.context["available"], 3)
|
||||
|
||||
def test_product_image_is_database_blob_not_file_field(self):
|
||||
image_field = Product._meta.get_field("image")
|
||||
self.assertIsInstance(image_field, models.ForeignKey)
|
||||
self.assertFalse(isinstance(image_field, models.FileField))
|
||||
self.assertEqual(image_field.related_model, StoredFile)
|
||||
self.assertIsInstance(StoredFile._meta.get_field("data"), models.BinaryField)
|
||||
self.assertIsInstance(default_storage, InMemoryStorage)
|
||||
self.assertGreaterEqual(settings.FILE_UPLOAD_MAX_MEMORY_SIZE, 15 * 1024 * 1024)
|
||||
|
||||
def test_temporary_image_upload_copied_into_database_then_unlinked(self):
|
||||
with TemporaryUploadedFile("dot.png", "image/png", 0, "utf-8") as tmp:
|
||||
tmp.write(_tiny_png())
|
||||
tmp.seek(0)
|
||||
tmp_path = Path(tmp.temporary_file_path())
|
||||
self.assertTrue(tmp_path.exists())
|
||||
with patch(
|
||||
"shop.imaging.cutout_subject",
|
||||
side_effect=lambda image: image.convert("RGBA"),
|
||||
):
|
||||
stored = store_product_image(upload=tmp, user=self.user)
|
||||
self.assertFalse(tmp_path.exists())
|
||||
self.assertTrue(bytes(stored.data))
|
||||
self.assertEqual(stored.kind, StoredFile.Kind.PRODUCT_IMAGE)
|
||||
|
||||
def test_temporary_stl_upload_copied_into_database_then_unlinked(self):
|
||||
with TemporaryUploadedFile(
|
||||
"toy.stl", "application/octet-stream", 0, "utf-8"
|
||||
) as tmp:
|
||||
tmp.write(_tiny_stl())
|
||||
tmp.seek(0)
|
||||
tmp_path = Path(tmp.temporary_file_path())
|
||||
self.assertTrue(tmp_path.exists())
|
||||
stored = store_product_stl(upload=tmp, user=self.user)
|
||||
self.assertFalse(tmp_path.exists())
|
||||
self.assertEqual(bytes(stored.data), _tiny_stl())
|
||||
self.assertEqual(stored.kind, StoredFile.Kind.PRODUCT_STL)
|
||||
Reference in New Issue
Block a user