ob-dj-store 0.0.11.7__tar.gz → 0.0.11.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/PKG-INFO +1 -1
  2. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/config/settings.py +3 -0
  3. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/apis/stores/filters.py +25 -0
  4. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/apis/stores/rest/serializers/serializers.py +53 -13
  5. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/apis/stores/urls.py +3 -3
  6. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/apis/stores/views.py +100 -37
  7. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/admin.py +8 -9
  8. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/admin_inlines.py +4 -4
  9. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/gateway/tap/models.py +1 -1
  10. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/managers.py +1 -3
  11. ob-dj-store-0.0.11.9/ob_dj_store/core/stores/migrations/0062_auto_20230226_2005.py +43 -0
  12. ob-dj-store-0.0.11.9/ob_dj_store/core/stores/migrations/0063_alter_store_payment_methods.py +23 -0
  13. ob-dj-store-0.0.11.9/ob_dj_store/core/stores/migrations/0064_auto_20230228_1814.py +24 -0
  14. ob-dj-store-0.0.11.9/ob_dj_store/core/stores/migrations/0064_auto_20230228_1932.py +34 -0
  15. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/models/_favorite.py +4 -1
  16. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/models/_order.py +2 -0
  17. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/models/_payment.py +31 -11
  18. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/models/_product.py +3 -5
  19. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/models/_store.py +15 -6
  20. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/models/_wallet.py +41 -8
  21. ob-dj-store-0.0.11.9/ob_dj_store/core/stores/receivers.py +109 -0
  22. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/utils.py +14 -6
  23. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/utils/helpers.py +7 -1
  24. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/utils/utils.py +3 -2
  25. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store.egg-info/PKG-INFO +1 -1
  26. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store.egg-info/SOURCES.txt +4 -0
  27. ob-dj-store-0.0.11.7/ob_dj_store/core/stores/receivers.py +0 -55
  28. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/.github/dependabot.yml +0 -0
  29. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/.github/workflows/docs.yml +0 -0
  30. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/.github/workflows/pre-release.yml +0 -0
  31. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/.github/workflows/release.yml +0 -0
  32. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/.github/workflows/test-build.yml +0 -0
  33. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/.gitignore +0 -0
  34. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/.isort.cfg +0 -0
  35. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/.pre-commit-config.yaml +0 -0
  36. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/Dockerfile +0 -0
  37. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/MANIFEST.in +0 -0
  38. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/Makefile +0 -0
  39. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/Pipfile +0 -0
  40. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/Pipfile.lock +0 -0
  41. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/README.md +0 -0
  42. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/config/__init__.py +0 -0
  43. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/config/urls.py +0 -0
  44. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/config/wsgi.py +0 -0
  45. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/docker-compose.env +0 -0
  46. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/docker-compose.yml +0 -0
  47. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/docs/Makefile +0 -0
  48. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/docs/make.bat +0 -0
  49. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/docs/source/admin.rst +0 -0
  50. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/docs/source/conf.py +0 -0
  51. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/docs/source/index.rst +0 -0
  52. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/docs/source/installation.rst +0 -0
  53. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/docs/source/models.rst +0 -0
  54. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/docs/source/rest_endpoints.rst +0 -0
  55. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/fixtures/initial_users.yaml +0 -0
  56. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/fixtures/stores.yaml +0 -0
  57. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/manage.py +0 -0
  58. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/__init__.py +0 -0
  59. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/apis/__init__.py +0 -0
  60. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/apis/stores/__init__.py +0 -0
  61. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/apis/tap/__init__.py +0 -0
  62. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/apis/tap/serializers.py +0 -0
  63. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/apis/tap/urls.py +0 -0
  64. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/apis/tap/views.py +0 -0
  65. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/__init__.py +0 -0
  66. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/__init__.py +0 -0
  67. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/apps.py +0 -0
  68. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/gateway/__init__.py +0 -0
  69. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/gateway/tap/__init__.py +0 -0
  70. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/gateway/tap/admin.py +0 -0
  71. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/gateway/tap/apps.py +0 -0
  72. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/gateway/tap/managers.py +0 -0
  73. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/gateway/tap/migrations/0001_initial.py +0 -0
  74. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/gateway/tap/migrations/0002_auto_20220815_1610.py +0 -0
  75. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/gateway/tap/migrations/0003_auto_20220818_1938.py +0 -0
  76. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/gateway/tap/migrations/__init__.py +0 -0
  77. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/gateway/tap/utils.py +0 -0
  78. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0001_initial.py +0 -0
  79. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0002_auto_20220422_0205.py +0 -0
  80. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0003_openinghours.py +0 -0
  81. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0004_auto_20220422_2326.py +0 -0
  82. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0005_auto_20220425_2119.py +0 -0
  83. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0005_auto_20220427_1729.py +0 -0
  84. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0006_auto_20220428_0100.py +0 -0
  85. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0007_cart_cartitem_order_orderitem.py +0 -0
  86. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0008_order_status.py +0 -0
  87. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0009_auto_20220508_2142.py +0 -0
  88. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0010_auto_20220509_1633.py +0 -0
  89. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0011_favorite.py +0 -0
  90. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0012_auto_20220514_0633.py +0 -0
  91. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0013_auto_20220518_1539.py +0 -0
  92. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0014_auto_20220519_0018.py +0 -0
  93. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0015_inventory_inventoryoperations.py +0 -0
  94. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0016_productvariant_preparation_time.py +0 -0
  95. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0017_auto_20220524_0912.py +0 -0
  96. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0018_auto_20220524_1613.py +0 -0
  97. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0019_product_is_featured.py +0 -0
  98. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0020_orderhistory.py +0 -0
  99. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0021_auto_20220531_1849.py +0 -0
  100. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0022_order_pickup_time.py +0 -0
  101. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0023_feedback_feedbackattribute_feedbackconfig.py +0 -0
  102. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0024_auto_20220609_1552.py +0 -0
  103. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0025_productvariant_is_primary.py +0 -0
  104. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0026_auto_20220630_1913.py +0 -0
  105. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0027_auto_20220713_1759.py +0 -0
  106. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0028_phonecontact.py +0 -0
  107. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0029_auto_20220726_1750.py +0 -0
  108. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0030_category_parent.py +0 -0
  109. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0031_auto_20220811_1733.py +0 -0
  110. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0032_auto_20220812_1951.py +0 -0
  111. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0033_auto_20220815_0133.py +0 -0
  112. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0034_auto_20220815_1528.py +0 -0
  113. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0035_auto_20220818_1938.py +0 -0
  114. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0036_productattribute_is_mandatory.py +0 -0
  115. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0037_auto_20220825_1736.py +0 -0
  116. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0038_cartitem_extra_infos.py +0 -0
  117. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0039_auto_20220831_1521.py +0 -0
  118. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0040_auto_20220902_1806.py +0 -0
  119. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0041_auto_20220912_1506.py +0 -0
  120. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0042_wallet_wallettransaction.py +0 -0
  121. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0043_auto_20220919_1854.py +0 -0
  122. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0044_remove_productvariant_has_inventory.py +0 -0
  123. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0045_shippingmethod_type.py +0 -0
  124. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0046_auto_20221014_1720.py +0 -0
  125. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0047_auto_20221018_1433.py +0 -0
  126. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0048_auto_20221026_1933.py +0 -0
  127. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0049_auto_20221029_1524.py +0 -0
  128. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0050_favoriteextra.py +0 -0
  129. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0051_order_car_id.py +0 -0
  130. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0052_auto_20221129_1732.py +0 -0
  131. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0053_inventory_plu.py +0 -0
  132. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0054_auto_20221230_1501.py +0 -0
  133. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0055_store_image.py +0 -0
  134. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0056_auto_20230213_2224.py +0 -0
  135. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0057_auto_20230214_1724.py +0 -0
  136. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0058_attributechoice_is_default.py +0 -0
  137. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0059_auto_20230217_2006.py +0 -0
  138. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0060_alter_orderitem_product_variant.py +0 -0
  139. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/0061_auto_20230223_1435.py +0 -0
  140. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/migrations/__init__.py +0 -0
  141. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/models/__init__.py +0 -0
  142. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/models/_address.py +0 -0
  143. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/models/_cart.py +0 -0
  144. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/models/_feedback.py +0 -0
  145. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/models/_inventory.py +0 -0
  146. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/core/stores/settings_validation.py +0 -0
  147. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/utils/__init__.py +0 -0
  148. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store/utils/model.py +0 -0
  149. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store.egg-info/dependency_links.txt +0 -0
  150. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store.egg-info/requires.txt +0 -0
  151. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/ob_dj_store.egg-info/top_level.txt +0 -0
  152. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/pytest.ini +0 -0
  153. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/setup.cfg +0 -0
  154. {ob-dj-store-0.0.11.7 → ob-dj-store-0.0.11.9}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ob-dj-store
3
- Version: 0.0.11.7
3
+ Version: 0.0.11.9
4
4
  Summary: OBytes django application for managing ecommerce stores.
5
5
  Home-page: https://www.obytes.com/
6
6
  Author: OBytes
@@ -360,3 +360,6 @@ DIFFERENT_STORE_ORDERING = False
360
360
 
361
361
  THUMBNAIL_MEDIUM_DIMENSIONS = {"width": 500, "height": 300}
362
362
  THUMBNAIL_SMALL_DIMENSIONS = {"width": 200, "height": 200}
363
+
364
+ # Wallet settings
365
+ WALLET_CURRENCIES = ["KWD", "QAR", "SAR", "AED"]
@@ -5,10 +5,12 @@ from django.utils.timezone import now
5
5
  from django_filters import rest_framework as filters
6
6
  from rest_framework.exceptions import ValidationError
7
7
 
8
+ from config import settings as store_settings
8
9
  from ob_dj_store.core.stores.models import (
9
10
  Category,
10
11
  Favorite,
11
12
  Order,
13
+ PaymentMethod,
12
14
  Product,
13
15
  ProductVariant,
14
16
  Store,
@@ -128,6 +130,7 @@ class CategoryFilter(filters.FilterSet):
128
130
  is_active=True,
129
131
  ).distinct(),
130
132
  ),
133
+ "subcategories__products__images",
131
134
  )
132
135
  .distinct()
133
136
  )
@@ -174,3 +177,25 @@ class FavoriteFilter(filters.FilterSet):
174
177
  content_type=content_type,
175
178
  object_id__in=products_ids,
176
179
  )
180
+
181
+
182
+ class PaymentMethodFilter(filters.FilterSet):
183
+ store = filters.CharFilter(method="by_store")
184
+ is_digital = filters.BooleanFilter(method="by_digital")
185
+
186
+ class Meta:
187
+ models = PaymentMethod
188
+
189
+ def by_store(self, queryset, name, value):
190
+ return queryset.filter(stores=value)
191
+
192
+ def by_digital(self, queryset, name, value):
193
+ if value:
194
+ return queryset.filter(
195
+ payment_provider__in=[
196
+ store_settings.TAP_CREDIT_CARD,
197
+ store_settings.TAP_KNET,
198
+ store_settings.TAP_ALL,
199
+ ]
200
+ )
201
+ return queryset
@@ -39,6 +39,7 @@ from ob_dj_store.core.stores.models import (
39
39
  ShippingMethod,
40
40
  Store,
41
41
  Tax,
42
+ Wallet,
42
43
  )
43
44
  from ob_dj_store.core.stores.models._inventory import Inventory
44
45
  from ob_dj_store.core.stores.utils import distance
@@ -288,7 +289,7 @@ class OrderSerializer(serializers.ModelSerializer):
288
289
  if payment_method:
289
290
  if payment_method.payment_provider == store_settings.WALLET:
290
291
  try:
291
- wallet = user.wallets.get(country=attrs["store"].address.country)
292
+ wallet = user.wallets.get(currency=attrs["store"].currency)
292
293
  except ObjectDoesNotExist:
293
294
  raise serializers.ValidationError(
294
295
  {
@@ -603,7 +604,9 @@ class ProductSerializer(FavoriteMixin, serializers.ModelSerializer):
603
604
  return data
604
605
 
605
606
 
606
- class ProductListSerializer(ProductSerializer):
607
+ class ProductListSerializer(serializers.ModelSerializer):
608
+ product_images = ProductMediaSerializer(many=True, source="images")
609
+
607
610
  class Meta:
608
611
  model = Product
609
612
  fields = (
@@ -795,9 +798,7 @@ class StoreSerializer(FavoriteMixin, serializers.ModelSerializer):
795
798
 
796
799
  def get_is_closed(self, obj):
797
800
  current_time = now()
798
- current_op_hour = obj.opening_hours.filter(
799
- weekday=current_time.weekday() + 1
800
- ).first()
801
+ current_op_hour = obj.current_opening_hours
801
802
  if current_op_hour:
802
803
  return (
803
804
  not current_op_hour.from_hour
@@ -869,9 +870,7 @@ class StoreSerializer(FavoriteMixin, serializers.ModelSerializer):
869
870
  return PhoneContactSerializer(phone_contacts, many=True).data
870
871
 
871
872
  def get_current_day_opening_hours(self, obj):
872
- current_opening_hours = obj.opening_hours.filter(
873
- weekday=now().weekday() + 1
874
- ).first()
873
+ current_opening_hours = obj.current_opening_hours
875
874
  op_hour = {}
876
875
  if current_opening_hours:
877
876
  op_hour["from_hour"] = current_opening_hours.from_hour.strftime("%I:%M%p")
@@ -978,7 +977,11 @@ class FavoriteSerializer(serializers.ModelSerializer):
978
977
  "extras",
979
978
  "object_id",
980
979
  "object_type",
980
+ "name",
981
981
  )
982
+ extra_kwargs = {
983
+ "name": {"required": True},
984
+ }
982
985
 
983
986
  def _lookup_validation(self, data):
984
987
  content_type = ContentType.objects.get_for_model(type(data["content_object"]))
@@ -1005,6 +1008,7 @@ class FavoriteSerializer(serializers.ModelSerializer):
1005
1008
 
1006
1009
  def validate(self, attrs):
1007
1010
  validated_data = super().validate(attrs)
1011
+ name = validated_data["name"]
1008
1012
  object_type = validated_data["object_type"]
1009
1013
  if object_type not in store_settings.FAVORITE_TYPES and hasattr(
1010
1014
  sys.modules[__name__], object_type
@@ -1039,14 +1043,50 @@ class FavoriteSerializer(serializers.ModelSerializer):
1039
1043
  validated_data = {
1040
1044
  "content_object": object_instance,
1041
1045
  "extras": extras,
1046
+ "name": name,
1042
1047
  }
1043
1048
  self._lookup_validation(validated_data)
1044
1049
  return validated_data
1045
1050
 
1046
1051
  def create(self, validated_data):
1047
- favorite = Favorite.add_favorite(
1048
- validated_data["content_object"],
1049
- self.context["request"].user,
1050
- validated_data["extras"],
1051
- )
1052
+ try:
1053
+ favorite = Favorite.add_favorite(
1054
+ content_object=validated_data["content_object"],
1055
+ user=self.context["request"].user,
1056
+ name=validated_data["name"],
1057
+ extras=validated_data["extras"],
1058
+ )
1059
+ except ValidationError as e:
1060
+ raise serializers.ValidationError(detail=e.message_dict)
1052
1061
  return favorite
1062
+
1063
+
1064
+ class WalletSerializer(serializers.ModelSerializer):
1065
+ class Meta:
1066
+ model = Wallet
1067
+ fields = ["id", "user", "balance", "name", "image", "image_thumbnail_medium"]
1068
+ extra_kwargs = {
1069
+ "user": {"read_only": True},
1070
+ "image_thumbnail_medium": {"read_only": True},
1071
+ }
1072
+
1073
+
1074
+ class WalletTopUpSerializer(serializers.Serializer):
1075
+ amount = serializers.DecimalField(
1076
+ max_digits=10, decimal_places=2, min_value=1, required=True
1077
+ )
1078
+ payment_method = serializers.PrimaryKeyRelatedField(
1079
+ queryset=PaymentMethod.objects.filter(
1080
+ payment_provider__in=[
1081
+ store_settings.TAP_CREDIT_CARD,
1082
+ store_settings.TAP_KNET,
1083
+ store_settings.TAP_ALL,
1084
+ ]
1085
+ ),
1086
+ required=True,
1087
+ )
1088
+
1089
+ def top_up_wallet(self, wallet):
1090
+ amount = self.validated_data["amount"]
1091
+ payment_method = self.validated_data["payment_method"]
1092
+ return wallet.top_up_wallet(amount=amount, payment_method=payment_method)
@@ -16,6 +16,7 @@ from ob_dj_store.apis.stores.views import (
16
16
  TaxViewSet,
17
17
  TransactionsViewSet,
18
18
  VariantView,
19
+ WalletViewSet,
19
20
  )
20
21
 
21
22
  app_name = "stores"
@@ -28,9 +29,6 @@ stores_router.register(r"order", OrderView, basename="order")
28
29
  stores_router.register(r"product", ProductView, basename="product")
29
30
  stores_router.register(r"variant", VariantView, basename="variant")
30
31
  stores_router.register(r"inventory", InventoryView, basename="inventory")
31
- stores_router.register(
32
- r"payment-method", PaymentMethodViewSet, basename="payment-method"
33
- )
34
32
  stores_router.register(
35
33
  r"shipping-method", ShippingMethodViewSet, basename="shipping-method"
36
34
  )
@@ -40,6 +38,8 @@ router.register(r"transaction", TransactionsViewSet, basename="transaction")
40
38
  router.register(r"cart", CartView, basename="cart")
41
39
  router.register(r"cart-item", CartItemView, basename="cart-item")
42
40
  router.register(r"favorite", FavoriteViewSet, basename="favorite")
41
+ router.register(r"wallet", WalletViewSet, basename="wallet")
42
+ router.register(r"payment-method", PaymentMethodViewSet, basename="payment-method")
43
43
  urlpatterns = [
44
44
  path(r"", include(router.urls)),
45
45
  path(r"", include(stores_router.urls)),
@@ -26,6 +26,7 @@ from ob_dj_store.apis.stores.filters import (
26
26
  FavoriteFilter,
27
27
  InventoryFilter,
28
28
  OrderFilter,
29
+ PaymentMethodFilter,
29
30
  ProductFilter,
30
31
  StoreFilter,
31
32
  VariantFilter,
@@ -47,6 +48,8 @@ from ob_dj_store.apis.stores.rest.serializers.serializers import (
47
48
  ShippingMethodSerializer,
48
49
  StoreSerializer,
49
50
  TaxSerializer,
51
+ WalletSerializer,
52
+ WalletTopUpSerializer,
50
53
  )
51
54
  from ob_dj_store.core.stores.models import (
52
55
  Cart,
@@ -62,6 +65,7 @@ from ob_dj_store.core.stores.models import (
62
65
  ShippingMethod,
63
66
  Store,
64
67
  Tax,
68
+ Wallet,
65
69
  )
66
70
  from ob_dj_store.core.stores.models._inventory import Inventory
67
71
 
@@ -93,7 +97,7 @@ class StoreView(
93
97
 
94
98
  def get_queryset(self):
95
99
  queryset = super().get_queryset()
96
- queryset.select_related("opening_hours")
100
+ queryset = queryset.prefetch_related("opening_hours")
97
101
  if self.action == "favorites":
98
102
  favorite_store_ids = Favorite.objects.favorites_for_model(
99
103
  Store, self.request.user
@@ -188,29 +192,6 @@ class StoreView(
188
192
  serializer = self.get_serializer(page, many=True)
189
193
  return self.get_paginated_response(serializer.data)
190
194
 
191
- @swagger_auto_schema(
192
- operation_summary="Add or Remove Store from Favorites",
193
- operation_description="""
194
- Add or Remove Store from Favorites
195
- """,
196
- tags=[
197
- "Store",
198
- ],
199
- )
200
- @action(
201
- detail=True,
202
- methods=["GET"],
203
- url_path="favorite",
204
- )
205
- def favorite(self, request, *args, **kwargs):
206
- instance = self.get_object()
207
- try:
208
- Favorite.objects.favorite_for_user(instance, request.user).delete()
209
- except Favorite.DoesNotExist:
210
- Favorite.add_favorite(instance, request.user)
211
- serializer = StoreSerializer(instance=instance, context={"request": request})
212
- return Response(serializer.data)
213
-
214
195
  @swagger_auto_schema(
215
196
  operation_summary="Retrieve count of store's products",
216
197
  operation_description="""
@@ -231,10 +212,13 @@ class StoreView(
231
212
  product_variants__inventories__store=store
232
213
  ).values_list("id", flat=True)
233
214
  content_type = ContentType.objects.get_for_model(Product)
234
- favorites_count = Favorite.objects.filter(
235
- content_type=content_type,
236
- object_id__in=products_ids,
237
- ).count()
215
+ favorites_count = (
216
+ Favorite.objects.filter(
217
+ content_type=content_type, object_id__in=products_ids, user=request.user
218
+ ).count()
219
+ if request.user.id
220
+ else 0
221
+ )
238
222
  menu = Product.objects.filter(
239
223
  product_variants__inventories__store=store,
240
224
  category__isnull=False,
@@ -295,7 +279,6 @@ class CartView(
295
279
  permissions.IsAuthenticated,
296
280
  ]
297
281
  queryset = Cart.objects.all()
298
- lookup_value_regex = "[0-9]+"
299
282
 
300
283
  def get_object(self):
301
284
  return self.request.user.cart
@@ -672,6 +655,9 @@ class CategoryViewSet(
672
655
  filterset_class = CategoryFilter
673
656
  lookup_value_regex = "[0-9]+"
674
657
 
658
+ def get_queryset(self):
659
+ return super().get_queryset().prefetch_related("products__images")
660
+
675
661
  def get_object(self):
676
662
  store_id = self.request.query_params.get("store", None)
677
663
  instance_pk = self.kwargs["pk"]
@@ -697,6 +683,7 @@ class CategoryViewSet(
697
683
  "products",
698
684
  queryset=product_queryset.distinct(),
699
685
  ),
686
+ "subcategories__products__images",
700
687
  ).get(pk=instance_pk)
701
688
  except Category.DoesNotExist:
702
689
  raise Http404("No Category matches the given query.")
@@ -798,14 +785,9 @@ class PaymentMethodViewSet(
798
785
  permission_classes = [
799
786
  permissions.IsAuthenticated,
800
787
  ]
801
- queryset = PaymentMethod.objects.all()
802
-
803
- def get_queryset(self):
804
- try:
805
- store = Store.objects.get(pk=self.kwargs["store_pk"])
806
- except ObjectDoesNotExist:
807
- raise ValidationError(_(f"Store does not Exist"))
808
- return store.payment_methods.filter(is_active=True)
788
+ queryset = PaymentMethod.objects.filter(is_active=True)
789
+ filterset_class = PaymentMethodFilter
790
+ lookup_value_regex = "[0-9]+"
809
791
 
810
792
  @swagger_auto_schema(
811
793
  operation_summary="List Payment Methods",
@@ -947,3 +929,84 @@ class FavoriteViewSet(
947
929
  )
948
930
  def destroy(self, request, *args, **kwargs):
949
931
  return super().destroy(request, *args, **kwargs)
932
+
933
+
934
+ class WalletViewSet(
935
+ mixins.ListModelMixin,
936
+ mixins.RetrieveModelMixin,
937
+ mixins.UpdateModelMixin,
938
+ viewsets.GenericViewSet,
939
+ ):
940
+ queryset = Wallet.objects.all()
941
+ serializer_class = WalletSerializer
942
+ permission_classes = [
943
+ permissions.IsAuthenticated,
944
+ ]
945
+
946
+ def get_queryset(self):
947
+ return Wallet.objects.filter(user=self.request.user)
948
+
949
+ @swagger_auto_schema(
950
+ operation_summary="Get User Wallet",
951
+ operation_description="""
952
+ Get User Wallet
953
+ """,
954
+ tags=[
955
+ "Wallet",
956
+ ],
957
+ )
958
+ def retrieve(
959
+ self, request: Request, *args: typing.Any, **kwargs: typing.Any
960
+ ) -> Response:
961
+ return super().retrieve(request=request, *args, **kwargs)
962
+
963
+ @swagger_auto_schema(
964
+ operation_summary="Update user wallet",
965
+ operation_description="""
966
+ Update a wallet
967
+ """,
968
+ tags=[
969
+ "Wallet",
970
+ ],
971
+ )
972
+ def partial_update(self, request: Request, *args: typing.Any, **kwargs: typing.Any):
973
+ return super().partial_update(request, *args, **kwargs)
974
+
975
+ @swagger_auto_schema(
976
+ operation_summary="List User Wallets",
977
+ operation_description="""
978
+ List User Wallets
979
+ """,
980
+ tags=[
981
+ "Wallet",
982
+ ],
983
+ )
984
+ def list(
985
+ self, request: Request, *args: typing.Any, **kwargs: typing.Any
986
+ ) -> Response:
987
+ return super().list(request=request, *args, **kwargs)
988
+
989
+ @swagger_auto_schema(
990
+ operation_summary="top up a wallet",
991
+ operation_description="""
992
+ top up a user wallet with tap payment
993
+ """,
994
+ tags=[
995
+ "Wallet",
996
+ ],
997
+ )
998
+ @action(
999
+ methods=["POST"],
1000
+ detail=True,
1001
+ url_path="top-up",
1002
+ url_name="top-up",
1003
+ serializer_class=WalletTopUpSerializer,
1004
+ )
1005
+ def top_up_wallet(
1006
+ self, request: Request, *args: typing.Any, **kwargs: typing.Any
1007
+ ) -> Response:
1008
+ serializer = self.get_serializer(data=request.data)
1009
+ serializer.is_valid(raise_exception=True)
1010
+ instance = self.get_object()
1011
+ payment_url = serializer.top_up_wallet(instance)
1012
+ return Response({"payment_url": payment_url}, status=status.HTTP_200_OK)
@@ -99,18 +99,11 @@ class CategoryAdmin(admin.ModelAdmin):
99
99
  list_display = ["name", "is_active", "parent", "image"]
100
100
  search_fields = [
101
101
  "name",
102
- "parent__name",
103
102
  ]
104
103
  list_filter = [
105
104
  "is_active",
106
105
  ]
107
106
 
108
- def save_model(self, request, obj, form, change) -> None:
109
- from ob_dj_store.core.stores.receivers import create_media_thumbnails
110
-
111
- create_media_thumbnails(None, obj, None)
112
- return super().save_model(request, obj, form, change)
113
-
114
107
 
115
108
  class ProductVariantAdmin(admin.ModelAdmin):
116
109
  inlines = [
@@ -123,6 +116,9 @@ class ProductVariantAdmin(admin.ModelAdmin):
123
116
  ]
124
117
  search_fields = ["name", "product__name", "sku"]
125
118
 
119
+ def get_queryset(self, request):
120
+ return super().get_queryset(request).prefetch_related("inventories")
121
+
126
122
 
127
123
  class ProductAdmin(admin.ModelAdmin):
128
124
  list_display = ["id", "name", "category", "type", "is_active"]
@@ -227,7 +223,7 @@ class PaymentAdmin(admin.ModelAdmin):
227
223
  "method__payment_provider",
228
224
  "status",
229
225
  ]
230
- search_fields = ["orders", "user__email"]
226
+ search_fields = ["orders__store__name", "user__email"]
231
227
 
232
228
 
233
229
  class InventoryAdmin(admin.ModelAdmin):
@@ -250,6 +246,9 @@ class InventoryAdmin(admin.ModelAdmin):
250
246
  "is_uncountable",
251
247
  ]
252
248
 
249
+ def get_queryset(self, request):
250
+ return super().get_queryset(request).prefetch_related("store")
251
+
253
252
 
254
253
  class TaxAdmin(admin.ModelAdmin):
255
254
  list_display = [
@@ -278,7 +277,7 @@ class WalletTransactionAdmin(admin.ModelAdmin):
278
277
  "type",
279
278
  ]
280
279
  search_fields = [
281
- "wallet__user_email",
280
+ "wallet__user__email",
282
281
  ]
283
282
 
284
283
 
@@ -22,10 +22,10 @@ class InventoryInlineAdmin(admin.TabularInline):
22
22
  extra = 1
23
23
 
24
24
  def get_queryset(self, request):
25
- qs = super().get_queryset(request)
26
- return qs.select_related(
27
- "store",
28
- "variant",
25
+ return (
26
+ super()
27
+ .get_queryset(request)
28
+ .select_related("variant", "store", "variant__product")
29
29
  )
30
30
 
31
31
 
@@ -93,7 +93,7 @@ class TapPayment(models.Model):
93
93
 
94
94
  @property
95
95
  def amount(self):
96
- return self.payment.amount
96
+ return self.payment.total_payment
97
97
 
98
98
  def callback_update(self, tap_payload):
99
99
  if self.status == self.Status.INITIATED:
@@ -6,7 +6,6 @@ from django.db import models
6
6
  from django.utils.translation import gettext_lazy as _
7
7
 
8
8
  from config import settings
9
- from ob_dj_store.core.stores.utils import get_country_by_currency
10
9
 
11
10
 
12
11
  class ActiveMixin:
@@ -77,8 +76,7 @@ class PaymentManager(models.Manager):
77
76
  return instance
78
77
  elif gateway == settings.WALLET:
79
78
  try:
80
- country = get_country_by_currency(currency)
81
- wallet = kwargs["user"].wallets.get(country=country)
79
+ wallet = kwargs["user"].wallets.get(currency=currency)
82
80
  except ObjectDoesNotExist:
83
81
  raise ValidationError({"wallet": _("Wallet Not Found")})
84
82
  WalletTransaction.objects.create(
@@ -0,0 +1,43 @@
1
+ # Generated by Django 3.2.8 on 2023-02-26 17:05
2
+
3
+ from django.conf import settings
4
+ from django.db import migrations, models
5
+
6
+ import ob_dj_store.core.stores.utils
7
+
8
+
9
+ class Migration(migrations.Migration):
10
+
11
+ dependencies = [
12
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
13
+ ("stores", "0061_auto_20230223_1435"),
14
+ ]
15
+
16
+ operations = [
17
+ migrations.AddField(
18
+ model_name="store",
19
+ name="currency",
20
+ field=models.CharField(
21
+ default="KWD",
22
+ max_length=3,
23
+ validators=[ob_dj_store.core.stores.utils.validate_currency],
24
+ ),
25
+ ),
26
+ migrations.AddField(
27
+ model_name="wallet",
28
+ name="currency",
29
+ field=models.CharField(
30
+ default="KWD",
31
+ max_length=3,
32
+ validators=[ob_dj_store.core.stores.utils.validate_currency],
33
+ ),
34
+ ),
35
+ migrations.AlterUniqueTogether(
36
+ name="wallet",
37
+ unique_together={("user", "currency")},
38
+ ),
39
+ migrations.RemoveField(
40
+ model_name="wallet",
41
+ name="country",
42
+ ),
43
+ ]
@@ -0,0 +1,23 @@
1
+ # Generated by Django 3.2.8 on 2023-02-27 11:15
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("stores", "0062_auto_20230226_2005"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name="store",
15
+ name="payment_methods",
16
+ field=models.ManyToManyField(
17
+ blank=True,
18
+ help_text="Payment methods within the store",
19
+ related_name="stores",
20
+ to="stores.PaymentMethod",
21
+ ),
22
+ ),
23
+ ]
@@ -0,0 +1,24 @@
1
+ # Generated by Django 3.2.8 on 2023-02-28 15:14
2
+
3
+ from django.conf import settings
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
11
+ ("stores", "0063_alter_store_payment_methods"),
12
+ ]
13
+
14
+ operations = [
15
+ migrations.AddField(
16
+ model_name="favorite",
17
+ name="name",
18
+ field=models.CharField(blank=True, max_length=200, null=True),
19
+ ),
20
+ migrations.AlterUniqueTogether(
21
+ name="favorite",
22
+ unique_together={("name", "user")},
23
+ ),
24
+ ]
@@ -0,0 +1,34 @@
1
+ # Generated by Django 3.2.8 on 2023-02-28 16:32
2
+
3
+ from django.db import migrations, models
4
+
5
+ import ob_dj_store.utils.helpers
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ dependencies = [
11
+ ("stores", "0063_alter_store_payment_methods"),
12
+ ]
13
+
14
+ operations = [
15
+ migrations.AddField(
16
+ model_name="wallet",
17
+ name="image",
18
+ field=models.ImageField(
19
+ blank=True,
20
+ null=True,
21
+ upload_to=ob_dj_store.utils.helpers.wallet_media_upload_to,
22
+ ),
23
+ ),
24
+ migrations.AddField(
25
+ model_name="wallet",
26
+ name="image_thumbnail_medium",
27
+ field=models.ImageField(blank=True, null=True, upload_to="wallets/"),
28
+ ),
29
+ migrations.AddField(
30
+ model_name="wallet",
31
+ name="name",
32
+ field=models.CharField(blank=True, max_length=200, null=True),
33
+ ),
34
+ ]
@@ -16,6 +16,7 @@ class Favorite(DjangoModelCleanMixin, models.Model):
16
16
  user = models.ForeignKey(
17
17
  get_user_model(), related_name="favorites", on_delete=models.CASCADE
18
18
  )
19
+ name = models.CharField(max_length=200, null=True, blank=True)
19
20
  content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
20
21
  content_object = GenericForeignKey("content_type", "object_id")
21
22
  object_id = models.PositiveIntegerField()
@@ -26,18 +27,20 @@ class Favorite(DjangoModelCleanMixin, models.Model):
26
27
  class Meta:
27
28
  verbose_name = _("favorite")
28
29
  verbose_name_plural = _("favorites")
30
+ unique_together = (("name", "user"),)
29
31
 
30
32
  def __str__(self):
31
33
  return f"{self.user} favorites {self.content_object}"
32
34
 
33
35
  @classmethod
34
- def add_favorite(cls, content_object, user, extras=[]):
36
+ def add_favorite(cls, content_object, user, name, extras=[]):
35
37
  content_type = ContentType.objects.get_for_model(type(content_object))
36
38
  favorite = Favorite(
37
39
  user=user,
38
40
  content_type=content_type,
39
41
  object_id=content_object.pk,
40
42
  content_object=content_object,
43
+ name=name,
41
44
  )
42
45
  favorite.save()
43
46
  for extra in extras: