You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632
  1. import datetime
  2. from decimal import Decimal as D
  3. from http import client as http_client
  4. from http.cookies import _unquote
  5. import django
  6. from django.contrib.messages.storage import cookie
  7. from django.core import signing
  8. from django.test import TestCase, override_settings
  9. from django.urls import reverse
  10. from django.utils.translation import gettext
  11. from oscar.apps.basket import reports
  12. from oscar.apps.basket.models import Basket
  13. from oscar.apps.partner import strategy
  14. from oscar.core.compat import get_user_model
  15. from oscar.test import factories
  16. from oscar.test.basket import add_product
  17. from oscar.test.factories import OptionFactory, create_product
  18. from oscar.test.testcases import WebTestCase
  19. User = get_user_model()
  20. class TestBasketMerging(TestCase):
  21. def setUp(self):
  22. self.product = create_product(num_in_stock=10)
  23. self.user_basket = Basket()
  24. self.user_basket.strategy = strategy.Default()
  25. add_product(self.user_basket, product=self.product)
  26. self.cookie_basket = Basket()
  27. self.cookie_basket.strategy = strategy.Default()
  28. add_product(self.cookie_basket, quantity=2, product=self.product)
  29. self.user_basket.merge(self.cookie_basket, add_quantities=False)
  30. def test_cookie_basket_has_status_set(self):
  31. self.assertEqual(Basket.MERGED, self.cookie_basket.status)
  32. def test_lines_are_moved_across(self):
  33. self.assertEqual(1, self.user_basket.lines.all().count())
  34. def test_merge_line_takes_max_quantity(self):
  35. line = self.user_basket.lines.get(product=self.product)
  36. self.assertEqual(2, line.quantity)
  37. class AnonAddToBasketViewTests(WebTestCase):
  38. csrf_checks = False
  39. def setUp(self):
  40. self.product = create_product(price=D("10.00"), num_in_stock=10)
  41. self.url = reverse("basket:add", kwargs={"pk": self.product.pk})
  42. self.post_params = {
  43. "product_id": self.product.id,
  44. "action": "add",
  45. "quantity": 1,
  46. }
  47. self.response = self.app.post(self.url, params=self.post_params)
  48. def test_cookie_is_created(self):
  49. self.assertTrue("oscar_open_basket" in self.response.test_app.cookies)
  50. def test_price_is_recorded(self):
  51. oscar_open_basket_cookie = _unquote(
  52. self.response.test_app.cookies["oscar_open_basket"]
  53. )
  54. basket_id = oscar_open_basket_cookie.split(":")[0]
  55. basket = Basket.objects.get(id=basket_id)
  56. line = basket.lines.get(product=self.product)
  57. stockrecord = self.product.stockrecords.all()[0]
  58. self.assertEqual(stockrecord.price, line.price_excl_tax)
  59. class TestMergedBasketsMessage(TestCase):
  60. def setUp(self):
  61. product = create_product(price=D("10.00"), num_in_stock=10)
  62. self.url = reverse("basket:add", args=(product.pk,))
  63. self.post_params = {
  64. "product_id": product.id,
  65. "action": "add",
  66. "quantity": 1,
  67. }
  68. self.user = User.objects.create(
  69. username="lucy", email="lucy@example.com", password="password"
  70. )
  71. def test_merged_baskets_message(self):
  72. # add product to anonymous user's basket
  73. response = self.client.post(self.url, self.post_params)
  74. self.assertEqual(response.status_code, 302)
  75. self.assertIn("oscar_open_basket", response.cookies)
  76. oscar_open_basket_cookie = response.cookies["oscar_open_basket"]
  77. basket = response.context["basket"]
  78. self.assertEqual(basket.all_lines().count(), 1)
  79. self.assertIsNone(basket.owner)
  80. # set registered user
  81. self.client.force_login(self.user)
  82. sess = self.client.session
  83. sess["oscar_open_basket"] = oscar_open_basket_cookie
  84. sess.save()
  85. self.assertTrue("oscar_open_basket" in self.client.session)
  86. response = self.client.get("/", follow=True)
  87. self.assertEqual(response.status_code, 200)
  88. basket = response.context["basket"]
  89. self.assertEqual(basket.all_lines().count(), 1)
  90. self.assertEqual(basket.owner, self.user)
  91. self.assertIn("messages", response.cookies)
  92. expected = "We have merged 1 item from a previous session to your basket."
  93. messages = list(response.context["messages"])
  94. self.assertEqual(len(messages), 3)
  95. self.assertEqual(messages[2].message, expected)
  96. class BasketSummaryViewTests(WebTestCase):
  97. def setUp(self):
  98. url = reverse("basket:summary")
  99. self.response = self.app.get(url)
  100. def test_shipping_method_in_context(self):
  101. self.assertTrue("shipping_method" in self.response.context)
  102. def test_order_total_in_context(self):
  103. self.assertTrue("order_total" in self.response.context)
  104. def test_view_does_not_error(self):
  105. self.assertEqual(http_client.OK, self.response.status_code)
  106. def test_basket_in_context(self):
  107. self.assertTrue("basket" in self.response.context)
  108. def test_basket_is_empty(self):
  109. basket = self.response.context["basket"]
  110. self.assertEqual(0, basket.num_lines)
  111. class BasketThresholdTest(WebTestCase):
  112. csrf_checks = False
  113. @override_settings(OSCAR_MAX_BASKET_QUANTITY_THRESHOLD=3)
  114. def test_adding_more_than_threshold_raises(self):
  115. dummy_product = create_product(price=D("10.00"), num_in_stock=10)
  116. url = reverse("basket:add", kwargs={"pk": dummy_product.pk})
  117. post_params = {"product_id": dummy_product.id, "action": "add", "quantity": 2}
  118. response = self.app.post(url, params=post_params)
  119. # pylint: disable=no-member
  120. self.assertIn("oscar_open_basket", response.test_app.cookies)
  121. post_params = {"product_id": dummy_product.id, "action": "add", "quantity": 2}
  122. response = self.app.post(url, params=post_params)
  123. expected = gettext(
  124. "Due to technical limitations we are not able to ship more "
  125. "than %(threshold)d items in one order. Your basket currently "
  126. "has %(basket)d items."
  127. ) % ({"threshold": 3, "basket": 2})
  128. if django.VERSION < (3, 2):
  129. self.assertIn(expected, response.test_app.cookies["messages"])
  130. else:
  131. signer = signing.get_cookie_signer(salt="django.contrib.messages")
  132. message_strings = [
  133. m.message
  134. # pylint: disable=no-member
  135. for m in signer.unsign_object(
  136. response.test_app.cookies["messages"],
  137. serializer=cookie.MessageSerializer,
  138. )
  139. ]
  140. self.assertIn(expected, message_strings)
  141. class BasketReportTests(TestCase):
  142. def test_open_report_doesnt_error(self):
  143. data = {
  144. "start_date": datetime.date(2012, 5, 1),
  145. "end_date": datetime.date(2012, 5, 17),
  146. "formatter": "CSV",
  147. }
  148. generator = reports.OpenBasketReportGenerator(**data)
  149. generator.generate()
  150. def test_submitted_report_doesnt_error(self):
  151. data = {
  152. "start_date": datetime.date(2012, 5, 1),
  153. "end_date": datetime.date(2012, 5, 17),
  154. "formatter": "CSV",
  155. }
  156. generator = reports.SubmittedBasketReportGenerator(**data)
  157. generator.generate()
  158. class SavedBasketTests(WebTestCase):
  159. csrf_checks = False
  160. def test_moving_to_saved_basket_creates_new(self):
  161. self.user = factories.UserFactory()
  162. product = factories.ProductFactory()
  163. basket = factories.BasketFactory(owner=self.user)
  164. basket.add_product(product)
  165. response = self.get(reverse("basket:summary"))
  166. formset = response.context["formset"]
  167. form = formset.forms[0]
  168. data = {
  169. formset.add_prefix("INITIAL_FORMS"): 1,
  170. formset.add_prefix("TOTAL_FORMS"): 1,
  171. formset.add_prefix("MIN_FORMS"): 0,
  172. formset.add_prefix("MAX_NUM_FORMS"): 1,
  173. form.add_prefix("id"): form.instance.pk,
  174. form.add_prefix("quantity"): form.initial["quantity"],
  175. form.add_prefix("save_for_later"): True,
  176. }
  177. response = self.post(reverse("basket:summary"), params=data)
  178. self.assertRedirects(response, reverse("basket:summary"))
  179. self.assertFalse(Basket.open.get(pk=basket.pk).lines.exists())
  180. self.assertEqual(
  181. Basket.saved.get(owner=self.user).lines.get(product=product).quantity, 1
  182. )
  183. def test_moving_to_saved_basket_updates_existing(self):
  184. self.user = factories.UserFactory()
  185. product = factories.ProductFactory()
  186. basket = factories.BasketFactory(owner=self.user)
  187. basket.add_product(product)
  188. saved_basket = factories.BasketFactory(owner=self.user, status=Basket.SAVED)
  189. saved_basket.add_product(product)
  190. response = self.get(reverse("basket:summary"))
  191. formset = response.context["formset"]
  192. form = formset.forms[0]
  193. data = {
  194. formset.add_prefix("INITIAL_FORMS"): 1,
  195. formset.add_prefix("TOTAL_FORMS"): 1,
  196. formset.add_prefix("MIN_FORMS"): 0,
  197. formset.add_prefix("MAX_NUM_FORMS"): 1,
  198. form.add_prefix("id"): form.instance.pk,
  199. form.add_prefix("quantity"): form.initial["quantity"],
  200. form.add_prefix("save_for_later"): True,
  201. }
  202. response = self.post(reverse("basket:summary"), params=data)
  203. self.assertRedirects(response, reverse("basket:summary"))
  204. self.assertFalse(Basket.open.get(pk=basket.pk).lines.exists())
  205. self.assertEqual(
  206. Basket.saved.get(pk=saved_basket.pk).lines.get(product=product).quantity, 2
  207. )
  208. def test_moving_from_saved_basket(self):
  209. self.user = User.objects.create_user(
  210. username="test", password="pass", email="test@example.com"
  211. )
  212. product = create_product(price=D("10.00"), num_in_stock=2)
  213. basket = factories.create_basket(empty=True)
  214. basket.owner = self.user
  215. basket.save()
  216. add_product(basket, product=product)
  217. saved_basket, _ = Basket.saved.get_or_create(owner=self.user)
  218. saved_basket.strategy = basket.strategy
  219. add_product(saved_basket, product=product)
  220. response = self.get(reverse("basket:summary"))
  221. saved_formset = response.context["saved_formset"]
  222. saved_form = saved_formset.forms[0]
  223. data = {
  224. saved_formset.add_prefix("INITIAL_FORMS"): 1,
  225. saved_formset.add_prefix("MAX_NUM_FORMS"): 1,
  226. saved_formset.add_prefix("TOTAL_FORMS"): 1,
  227. saved_form.add_prefix("id"): saved_form.initial["id"],
  228. saved_form.add_prefix("move_to_basket"): True,
  229. }
  230. response = self.post(reverse("basket:saved"), params=data)
  231. self.assertEqual(
  232. Basket.open.get(id=basket.id).lines.get(product=product).quantity, 2
  233. )
  234. self.assertRedirects(response, reverse("basket:summary"))
  235. def test_moving_from_saved_basket_more_than_stocklevel_raises(self):
  236. self.user = User.objects.create_user(
  237. username="test", password="pass", email="test@example.com"
  238. )
  239. product = create_product(price=D("10.00"), num_in_stock=1)
  240. basket, _ = Basket.open.get_or_create(owner=self.user)
  241. add_product(basket, product=product)
  242. saved_basket, _ = Basket.saved.get_or_create(owner=self.user)
  243. add_product(saved_basket, product=product)
  244. response = self.get(reverse("basket:summary"))
  245. saved_formset = response.context["saved_formset"]
  246. saved_form = saved_formset.forms[0]
  247. data = {
  248. saved_formset.add_prefix("INITIAL_FORMS"): 1,
  249. saved_formset.add_prefix("MAX_NUM_FORMS"): 1,
  250. saved_formset.add_prefix("TOTAL_FORMS"): 1,
  251. saved_form.add_prefix("id"): saved_form.initial["id"],
  252. saved_form.add_prefix("move_to_basket"): True,
  253. }
  254. response = self.post(reverse("basket:saved"), params=data)
  255. # we can't add more than stock level into basket
  256. self.assertEqual(
  257. Basket.open.get(id=basket.id).lines.get(product=product).quantity, 1
  258. )
  259. self.assertRedirects(response, reverse("basket:summary"))
  260. class BasketFormSetTests(WebTestCase):
  261. csrf_checks = False
  262. def test_formset_with_removed_line(self):
  263. products = [create_product() for i in range(3)]
  264. basket = factories.create_basket(empty=True)
  265. basket.owner = self.user
  266. basket.save()
  267. add_product(basket, product=products[0])
  268. add_product(basket, product=products[1])
  269. add_product(basket, product=products[2])
  270. response = self.get(reverse("basket:summary"))
  271. formset = response.context["formset"]
  272. self.assertEqual(len(formset.forms), 3)
  273. basket.lines.filter(product=products[0]).delete()
  274. management_form = formset.management_form
  275. data = {
  276. formset.add_prefix("INITIAL_FORMS"): management_form.initial[
  277. "INITIAL_FORMS"
  278. ],
  279. formset.add_prefix("MAX_NUM_FORMS"): management_form.initial[
  280. "MAX_NUM_FORMS"
  281. ],
  282. formset.add_prefix("TOTAL_FORMS"): management_form.initial["TOTAL_FORMS"],
  283. "form-0-quantity": 1,
  284. "form-0-id": formset.forms[0].instance.id,
  285. "form-1-quantity": 2,
  286. "form-1-id": formset.forms[1].instance.id,
  287. "form-2-quantity": 2,
  288. "form-2-id": formset.forms[2].instance.id,
  289. }
  290. response = self.post(reverse("basket:summary"), params=data)
  291. self.assertEqual(response.status_code, 302)
  292. formset = response.follow().context["formset"]
  293. self.assertEqual(len(formset.forms), 2)
  294. self.assertEqual(len(formset.forms_with_instances), 2)
  295. self.assertEqual(basket.lines.all()[0].quantity, 2)
  296. self.assertEqual(basket.lines.all()[1].quantity, 2)
  297. def test_invalid_formset_with_removed_line(self):
  298. products = [create_product() for i in range(3)]
  299. basket = factories.create_basket(empty=True)
  300. basket.owner = self.user
  301. basket.save()
  302. add_product(basket, product=products[0])
  303. add_product(basket, product=products[1])
  304. add_product(basket, product=products[2])
  305. response = self.get(reverse("basket:summary"))
  306. formset = response.context["formset"]
  307. self.assertEqual(len(formset.forms), 3)
  308. basket.lines.filter(product=products[0]).delete()
  309. stockrecord = products[1].stockrecords.first()
  310. stockrecord.num_in_stock = 0
  311. stockrecord.save()
  312. management_form = formset.management_form
  313. data = {
  314. formset.add_prefix("INITIAL_FORMS"): management_form.initial[
  315. "INITIAL_FORMS"
  316. ],
  317. formset.add_prefix("MIN_NUM_FORMS"): management_form.initial[
  318. "MIN_NUM_FORMS"
  319. ],
  320. formset.add_prefix("MAX_NUM_FORMS"): management_form.initial[
  321. "MAX_NUM_FORMS"
  322. ],
  323. formset.add_prefix("TOTAL_FORMS"): management_form.initial["TOTAL_FORMS"],
  324. "form-0-quantity": 1,
  325. "form-0-id": formset.forms[0].instance.id,
  326. "form-1-quantity": 2,
  327. "form-1-id": formset.forms[1].instance.id,
  328. "form-2-quantity": 2,
  329. "form-2-id": formset.forms[2].instance.id,
  330. }
  331. response = self.post(reverse("basket:summary"), params=data)
  332. self.assertEqual(response.status_code, 200)
  333. formset = response.context["formset"]
  334. self.assertEqual(len(formset.forms), 2)
  335. self.assertEqual(len(formset.forms_with_instances), 2)
  336. self.assertEqual(basket.lines.all()[0].quantity, 1)
  337. self.assertEqual(basket.lines.all()[1].quantity, 1)
  338. def test_deleting_valid_line_with_other_valid_line(self):
  339. product_1 = create_product()
  340. product_2 = create_product()
  341. basket = factories.create_basket(empty=True)
  342. basket.owner = self.user
  343. basket.save()
  344. add_product(basket, product=product_1)
  345. add_product(basket, product=product_2)
  346. response = self.get(reverse("basket:summary"))
  347. formset = response.context["formset"]
  348. self.assertEqual(len(formset.forms), 2)
  349. data = {
  350. formset.add_prefix("TOTAL_FORMS"): formset.management_form.initial[
  351. "TOTAL_FORMS"
  352. ],
  353. formset.add_prefix("INITIAL_FORMS"): formset.management_form.initial[
  354. "INITIAL_FORMS"
  355. ],
  356. formset.add_prefix("MIN_NUM_FORMS"): formset.management_form.initial[
  357. "MIN_NUM_FORMS"
  358. ],
  359. formset.add_prefix("MAX_NUM_FORMS"): formset.management_form.initial[
  360. "MAX_NUM_FORMS"
  361. ],
  362. formset.forms[0].add_prefix("id"): formset.forms[0].instance.pk,
  363. formset.forms[0].add_prefix("quantity"): formset.forms[0].instance.quantity,
  364. formset.forms[0].add_prefix("DELETE"): "on",
  365. formset.forms[1].add_prefix("id"): formset.forms[1].instance.pk,
  366. formset.forms[1].add_prefix("quantity"): formset.forms[1].instance.quantity,
  367. }
  368. response = self.post(reverse("basket:summary"), params=data, xhr=True)
  369. self.assertEqual(response.status_code, 200)
  370. self.assertEqual(len(response.context["formset"].forms), 1)
  371. self.assertFalse(
  372. response.context["formset"].is_bound
  373. ) # new formset is rendered
  374. self.assertEqual(basket.lines.count(), 1)
  375. self.assertEqual(basket.lines.all()[0].quantity, 1)
  376. def test_deleting_valid_line_with_other_invalid_line(self):
  377. product_1 = create_product()
  378. product_2 = create_product()
  379. basket = factories.create_basket(empty=True)
  380. basket.owner = self.user
  381. basket.save()
  382. add_product(basket, product=product_1)
  383. add_product(basket, product=product_2)
  384. response = self.get(reverse("basket:summary"))
  385. formset = response.context["formset"]
  386. self.assertEqual(len(formset.forms), 2)
  387. # Render product for other line out of stock
  388. product_2.stockrecords.update(num_in_stock=0)
  389. data = {
  390. formset.add_prefix("TOTAL_FORMS"): formset.management_form.initial[
  391. "TOTAL_FORMS"
  392. ],
  393. formset.add_prefix("INITIAL_FORMS"): formset.management_form.initial[
  394. "INITIAL_FORMS"
  395. ],
  396. formset.add_prefix("MIN_NUM_FORMS"): formset.management_form.initial[
  397. "MIN_NUM_FORMS"
  398. ],
  399. formset.add_prefix("MAX_NUM_FORMS"): formset.management_form.initial[
  400. "MAX_NUM_FORMS"
  401. ],
  402. formset.forms[0].add_prefix("id"): formset.forms[0].instance.pk,
  403. formset.forms[0].add_prefix("quantity"): formset.forms[0].instance.quantity,
  404. formset.forms[0].add_prefix("DELETE"): "on",
  405. formset.forms[1].add_prefix("id"): formset.forms[1].instance.pk,
  406. formset.forms[1].add_prefix("quantity"): formset.forms[1].instance.quantity,
  407. }
  408. response = self.post(reverse("basket:summary"), params=data, xhr=True)
  409. self.assertEqual(response.status_code, 200)
  410. self.assertEqual(len(response.context["formset"].forms), 1)
  411. self.assertTrue(
  412. response.context["formset"].is_bound
  413. ) # formset with errors is rendered
  414. self.assertFalse(response.context["formset"].forms[0].is_valid())
  415. self.assertEqual(basket.lines.count(), 1)
  416. self.assertEqual(basket.lines.all()[0].quantity, 1)
  417. def test_deleting_invalid_line_with_other_valid_line(self):
  418. product_1 = create_product()
  419. product_2 = create_product()
  420. basket = factories.create_basket(empty=True)
  421. basket.owner = self.user
  422. basket.save()
  423. add_product(basket, product=product_1)
  424. add_product(basket, product=product_2)
  425. response = self.get(reverse("basket:summary"))
  426. formset = response.context["formset"]
  427. self.assertEqual(len(formset.forms), 2)
  428. # Render product for to-be-deleted line out of stock
  429. product_1.stockrecords.update(num_in_stock=0)
  430. data = {
  431. formset.add_prefix("TOTAL_FORMS"): formset.management_form.initial[
  432. "TOTAL_FORMS"
  433. ],
  434. formset.add_prefix("INITIAL_FORMS"): formset.management_form.initial[
  435. "INITIAL_FORMS"
  436. ],
  437. formset.add_prefix("MIN_NUM_FORMS"): formset.management_form.initial[
  438. "MIN_NUM_FORMS"
  439. ],
  440. formset.add_prefix("MAX_NUM_FORMS"): formset.management_form.initial[
  441. "MAX_NUM_FORMS"
  442. ],
  443. formset.forms[0].add_prefix("id"): formset.forms[0].instance.pk,
  444. formset.forms[0].add_prefix("quantity"): formset.forms[0].instance.quantity,
  445. formset.forms[0].add_prefix("DELETE"): "on",
  446. formset.forms[1].add_prefix("id"): formset.forms[1].instance.pk,
  447. formset.forms[1].add_prefix("quantity"): formset.forms[1].instance.quantity,
  448. }
  449. response = self.post(reverse("basket:summary"), params=data, xhr=True)
  450. self.assertEqual(response.status_code, 200)
  451. self.assertEqual(len(response.context["formset"].forms), 1)
  452. self.assertFalse(
  453. response.context["formset"].is_bound
  454. ) # new formset is rendered
  455. self.assertEqual(basket.lines.count(), 1)
  456. self.assertEqual(basket.lines.all()[0].quantity, 1)
  457. def test_deleting_invalid_line_with_other_invalid_line(self):
  458. product_1 = create_product()
  459. product_2 = create_product()
  460. basket = factories.create_basket(empty=True)
  461. basket.owner = self.user
  462. basket.save()
  463. add_product(basket, product=product_1)
  464. add_product(basket, product=product_2)
  465. response = self.get(reverse("basket:summary"))
  466. formset = response.context["formset"]
  467. self.assertEqual(len(formset.forms), 2)
  468. # Render products for both lines out of stock
  469. product_1.stockrecords.update(num_in_stock=0)
  470. product_2.stockrecords.update(num_in_stock=0)
  471. data = {
  472. formset.add_prefix("TOTAL_FORMS"): formset.management_form.initial[
  473. "TOTAL_FORMS"
  474. ],
  475. formset.add_prefix("INITIAL_FORMS"): formset.management_form.initial[
  476. "INITIAL_FORMS"
  477. ],
  478. formset.add_prefix("MIN_NUM_FORMS"): formset.management_form.initial[
  479. "MIN_NUM_FORMS"
  480. ],
  481. formset.add_prefix("MAX_NUM_FORMS"): formset.management_form.initial[
  482. "MAX_NUM_FORMS"
  483. ],
  484. formset.forms[0].add_prefix("id"): formset.forms[0].instance.pk,
  485. formset.forms[0].add_prefix("quantity"): formset.forms[0].instance.quantity,
  486. formset.forms[0].add_prefix("DELETE"): "on",
  487. formset.forms[1].add_prefix("id"): formset.forms[1].instance.pk,
  488. formset.forms[1].add_prefix("quantity"): formset.forms[1].instance.quantity,
  489. }
  490. response = self.post(reverse("basket:summary"), params=data, xhr=True)
  491. self.assertEqual(response.status_code, 200)
  492. self.assertEqual(len(response.context["formset"].forms), 1)
  493. self.assertTrue(
  494. response.context["formset"].is_bound
  495. ) # formset with errors is rendered
  496. self.assertFalse(response.context["formset"].forms[0].is_valid())
  497. self.assertEqual(basket.lines.count(), 1)
  498. self.assertEqual(basket.lines.all()[0].quantity, 1)
  499. def test_formset_quantity_update_with_options(self):
  500. product = create_product(num_in_stock=2)
  501. option = OptionFactory()
  502. # Add the option to the product class
  503. product.get_product_class().options.add(option)
  504. basket = factories.create_basket(empty=True)
  505. basket.owner = self.user
  506. basket.save()
  507. basket.add_product(product, options=[{"option": option, "value": "Test 1"}])
  508. basket.add_product(product, options=[{"option": option, "value": "Test 2"}])
  509. response = self.get(reverse("basket:summary"))
  510. formset = response.context["formset"]
  511. self.assertEqual(len(formset.forms), 2)
  512. # Now update one of the quantities to 2
  513. data = {
  514. formset.add_prefix("TOTAL_FORMS"): formset.management_form.initial[
  515. "TOTAL_FORMS"
  516. ],
  517. formset.add_prefix("INITIAL_FORMS"): formset.management_form.initial[
  518. "INITIAL_FORMS"
  519. ],
  520. formset.add_prefix("MIN_NUM_FORMS"): formset.management_form.initial[
  521. "MIN_NUM_FORMS"
  522. ],
  523. formset.add_prefix("MAX_NUM_FORMS"): formset.management_form.initial[
  524. "MAX_NUM_FORMS"
  525. ],
  526. formset.forms[0].add_prefix("id"): formset.forms[0].instance.pk,
  527. formset.forms[0].add_prefix("quantity"): 2,
  528. formset.forms[1].add_prefix("id"): formset.forms[1].instance.pk,
  529. formset.forms[1].add_prefix("quantity"): formset.forms[1].instance.quantity,
  530. }
  531. response = self.post(reverse("basket:summary"), params=data, xhr=True)
  532. self.assertEqual(response.status_code, 200)
  533. self.assertFalse(response.context["formset"].forms[0].is_valid())
  534. self.assertIn(
  535. "Available stock is only %s, which has been exceeded because multiple lines contain the same product."
  536. % 2,
  537. str(response.context["formset"].forms[0].errors),
  538. )