diff --git a/poetry.lock b/poetry.lock index 6a34183..bfea066 100644 --- a/poetry.lock +++ b/poetry.lock @@ -333,6 +333,18 @@ python-versions = ">=3.6" [package.dependencies] pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" +[[package]] +name = "pillow" +version = "9.1.0" +description = "Python Imaging Library (Fork)" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinx-rtd-theme (>=1.0)", "sphinxext-opengraph"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] + [[package]] name = "platformdirs" version = "2.5.2" @@ -630,7 +642,7 @@ brotli = ["brotli"] [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "e141bb1b1bfaaea951e57b266d4cc4619e598f853d27f40964405303000e7ea8" +content-hash = "86cec3b5cbae86ec8a2e77f3e77139e12c48bcc8aa79918daed41f56f48597f4" [metadata.files] asgiref = [ @@ -932,6 +944,46 @@ packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] +pillow = [ + {file = "Pillow-9.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:af79d3fde1fc2e33561166d62e3b63f0cc3e47b5a3a2e5fea40d4917754734ea"}, + {file = "Pillow-9.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:55dd1cf09a1fd7c7b78425967aacae9b0d70125f7d3ab973fadc7b5abc3de652"}, + {file = "Pillow-9.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66822d01e82506a19407d1afc104c3fcea3b81d5eb11485e593ad6b8492f995a"}, + {file = "Pillow-9.1.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5eaf3b42df2bcda61c53a742ee2c6e63f777d0e085bbc6b2ab7ed57deb13db7"}, + {file = "Pillow-9.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01ce45deec9df310cbbee11104bae1a2a43308dd9c317f99235b6d3080ddd66e"}, + {file = "Pillow-9.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:aea7ce61328e15943d7b9eaca87e81f7c62ff90f669116f857262e9da4057ba3"}, + {file = "Pillow-9.1.0-cp310-cp310-win32.whl", hash = "sha256:7a053bd4d65a3294b153bdd7724dce864a1d548416a5ef61f6d03bf149205160"}, + {file = "Pillow-9.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:97bda660702a856c2c9e12ec26fc6d187631ddfd896ff685814ab21ef0597033"}, + {file = "Pillow-9.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:21dee8466b42912335151d24c1665fcf44dc2ee47e021d233a40c3ca5adae59c"}, + {file = "Pillow-9.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b6d4050b208c8ff886fd3db6690bf04f9a48749d78b41b7a5bf24c236ab0165"}, + {file = "Pillow-9.1.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5cfca31ab4c13552a0f354c87fbd7f162a4fafd25e6b521bba93a57fe6a3700a"}, + {file = "Pillow-9.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed742214068efa95e9844c2d9129e209ed63f61baa4d54dbf4cf8b5e2d30ccf2"}, + {file = "Pillow-9.1.0-cp37-cp37m-win32.whl", hash = "sha256:c9efef876c21788366ea1f50ecb39d5d6f65febe25ad1d4c0b8dff98843ac244"}, + {file = "Pillow-9.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:de344bcf6e2463bb25179d74d6e7989e375f906bcec8cb86edb8b12acbc7dfef"}, + {file = "Pillow-9.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:17869489de2fce6c36690a0c721bd3db176194af5f39249c1ac56d0bb0fcc512"}, + {file = "Pillow-9.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:25023a6209a4d7c42154073144608c9a71d3512b648a2f5d4465182cb93d3477"}, + {file = "Pillow-9.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8782189c796eff29dbb37dd87afa4ad4d40fc90b2742704f94812851b725964b"}, + {file = "Pillow-9.1.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:463acf531f5d0925ca55904fa668bb3461c3ef6bc779e1d6d8a488092bdee378"}, + {file = "Pillow-9.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f42364485bfdab19c1373b5cd62f7c5ab7cc052e19644862ec8f15bb8af289e"}, + {file = "Pillow-9.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3fddcdb619ba04491e8f771636583a7cc5a5051cd193ff1aa1ee8616d2a692c5"}, + {file = "Pillow-9.1.0-cp38-cp38-win32.whl", hash = "sha256:4fe29a070de394e449fd88ebe1624d1e2d7ddeed4c12e0b31624561b58948d9a"}, + {file = "Pillow-9.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:c24f718f9dd73bb2b31a6201e6db5ea4a61fdd1d1c200f43ee585fc6dcd21b34"}, + {file = "Pillow-9.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fb89397013cf302f282f0fc998bb7abf11d49dcff72c8ecb320f76ea6e2c5717"}, + {file = "Pillow-9.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c870193cce4b76713a2b29be5d8327c8ccbe0d4a49bc22968aa1e680930f5581"}, + {file = "Pillow-9.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69e5ddc609230d4408277af135c5b5c8fe7a54b2bdb8ad7c5100b86b3aab04c6"}, + {file = "Pillow-9.1.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35be4a9f65441d9982240e6966c1eaa1c654c4e5e931eaf580130409e31804d4"}, + {file = "Pillow-9.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82283af99c1c3a5ba1da44c67296d5aad19f11c535b551a5ae55328a317ce331"}, + {file = "Pillow-9.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a325ac71914c5c043fa50441b36606e64a10cd262de12f7a179620f579752ff8"}, + {file = "Pillow-9.1.0-cp39-cp39-win32.whl", hash = "sha256:a598d8830f6ef5501002ae85c7dbfcd9c27cc4efc02a1989369303ba85573e58"}, + {file = "Pillow-9.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0c51cb9edac8a5abd069fd0758ac0a8bfe52c261ee0e330f363548aca6893595"}, + {file = "Pillow-9.1.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a336a4f74baf67e26f3acc4d61c913e378e931817cd1e2ef4dfb79d3e051b481"}, + {file = "Pillow-9.1.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb1b89b11256b5b6cad5e7593f9061ac4624f7651f7a8eb4dfa37caa1dfaa4d0"}, + {file = "Pillow-9.1.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:255c9d69754a4c90b0ee484967fc8818c7ff8311c6dddcc43a4340e10cd1636a"}, + {file = "Pillow-9.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5a3ecc026ea0e14d0ad7cd990ea7f48bfcb3eb4271034657dc9d06933c6629a7"}, + {file = "Pillow-9.1.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5b0ff59785d93b3437c3703e3c64c178aabada51dea2a7f2c5eccf1bcf565a3"}, + {file = "Pillow-9.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7110ec1701b0bf8df569a7592a196c9d07c764a0a74f65471ea56816f10e2c8"}, + {file = "Pillow-9.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8d79c6f468215d1a8415aa53d9868a6b40c4682165b8cb62a221b1baa47db458"}, + {file = "Pillow-9.1.0.tar.gz", hash = "sha256:f401ed2bbb155e1ade150ccc63db1a4f6c1909d3d378f7d1235a44e90d75fb97"}, +] platformdirs = [ {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, diff --git a/pyproject.toml b/pyproject.toml index 05b7e34..1cd2fa6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ requests = "^2.27.1" django-extensions = "^3.1.5" bpython = "^0.22.1" gunicorn = "^20.1.0" +Pillow = "^9.1.0" [tool.poetry.dev-dependencies] pre-commit = "^2.7" diff --git a/src/checkout/settings.py b/src/checkout/settings.py index 6783d5b..6ac93c2 100644 --- a/src/checkout/settings.py +++ b/src/checkout/settings.py @@ -81,6 +81,7 @@ INSTALLED_APPS = [ "anymail", "django_cleanup.apps.CleanupConfig", "common", + "purchase", ] MIDDLEWARE = [ diff --git a/src/purchase/__init__.py b/src/purchase/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/purchase/admin.py b/src/purchase/admin.py new file mode 100644 index 0000000..e306886 --- /dev/null +++ b/src/purchase/admin.py @@ -0,0 +1,42 @@ +from django.contrib import admin +from django.contrib.admin import register + +from purchase.models import Basket, BasketItem, PaymentMethod, Product + + +@register(Product) +class ProductAdmin(admin.ModelAdmin): + list_display = ["name", "display_order", "unit_price"] + search_fields = ["name"] + + def unit_price(self, instance: Product): + return instance.unit_price_cents / 100 + + +@register(PaymentMethod) +class PaymentMethodAdmin(admin.ModelAdmin): + list_display = ["name"] + search_fields = ["name"] + + +class BasketItemInline(admin.TabularInline): + model = BasketItem + fields = ["product", "quantity", "price"] + extra = 0 + readonly_fields = ["price"] + + def price(self, instance) -> float: + return instance.price / 100 + + +@register(Basket) +class BasketAdmin(admin.ModelAdmin): + list_display = ["id", "status", "payment_method", "created_at", "price"] + fields = ["created_at", "status", "payment_method"] + list_filter = ["status", "payment_method"] + date_hierarchy = "created_at" + readonly_fields = ["created_at"] + inlines = [BasketItemInline] + + def price(self, instance) -> float: + return instance.price / 100 diff --git a/src/purchase/apps.py b/src/purchase/apps.py new file mode 100644 index 0000000..4f36992 --- /dev/null +++ b/src/purchase/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PurchaseConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "purchase" diff --git a/src/purchase/migrations/0001_initial.py b/src/purchase/migrations/0001_initial.py new file mode 100644 index 0000000..0c12697 --- /dev/null +++ b/src/purchase/migrations/0001_initial.py @@ -0,0 +1,131 @@ +# Generated by Django 4.0.4 on 2022-04-24 14:14 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Basket", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "status", + models.CharField( + choices=[("DRAFT", "Draft"), ("COMPLETE", "Complete")], + default="DRAFT", + max_length=20, + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="PaymentMethod", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("name", models.CharField(max_length=50, unique=True)), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="Product", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("name", models.CharField(max_length=250, unique=True)), + ("image", models.ImageField(null=True, upload_to="")), + ("unit_price_cents", models.PositiveIntegerField()), + ("display_order", models.PositiveIntegerField()), + ], + options={ + "ordering": ["display_order", "name"], + }, + ), + migrations.CreateModel( + name="BasketItem", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("quantity", models.PositiveIntegerField()), + ( + "basket", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="items", + to="purchase.basket", + ), + ), + ( + "product", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="basket_items", + to="purchase.product", + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.AddField( + model_name="basket", + name="payment_method", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="baskets", + to="purchase.paymentmethod", + ), + ), + ] diff --git a/src/purchase/migrations/0002_alter_product_image.py b/src/purchase/migrations/0002_alter_product_image.py new file mode 100644 index 0000000..c358ec4 --- /dev/null +++ b/src/purchase/migrations/0002_alter_product_image.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.4 on 2022-04-24 14:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("purchase", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="product", + name="image", + field=models.ImageField(blank=True, null=True, upload_to=""), + ), + ] diff --git a/src/purchase/migrations/0003_alter_product_display_order.py b/src/purchase/migrations/0003_alter_product_display_order.py new file mode 100644 index 0000000..1d9b8e8 --- /dev/null +++ b/src/purchase/migrations/0003_alter_product_display_order.py @@ -0,0 +1,22 @@ +# Generated by Django 4.0.4 on 2022-04-24 14:17 + +from django.db import migrations, models + +import purchase.models + + +class Migration(migrations.Migration): + + dependencies = [ + ("purchase", "0002_alter_product_image"), + ] + + operations = [ + migrations.AlterField( + model_name="product", + name="display_order", + field=models.PositiveIntegerField( + default=purchase.models.default_product_display_order + ), + ), + ] diff --git a/src/purchase/migrations/__init__.py b/src/purchase/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/purchase/models.py b/src/purchase/models.py new file mode 100644 index 0000000..410f695 --- /dev/null +++ b/src/purchase/models.py @@ -0,0 +1,73 @@ +from django.db import models + + +class Model(models.Model): + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + abstract = True + + +class PaymentMethod(Model): + name = models.CharField(max_length=50, unique=True) + + def __str__(self): + return self.name + + +def default_product_display_order(): + return Product.objects.last().display_order + 1 + + +class Product(Model): + name = models.CharField(max_length=250, unique=True) + image = models.ImageField(null=True, blank=True) + unit_price_cents = models.PositiveIntegerField() + display_order = models.PositiveIntegerField(default=default_product_display_order) + + class Meta: + ordering = ["display_order", "name"] + + def __str__(self): + return self.name + + +class Basket(Model): + STATUS_DRAFT = "DRAFT" + STATUS_COMPLETE = "COMPLETE" + _STATUS_CHOICES = [ + (STATUS_DRAFT, "Draft"), + (STATUS_COMPLETE, "Complete"), + ] + payment_method = models.ForeignKey( + to=PaymentMethod, + on_delete=models.PROTECT, + related_name="baskets", + null=True, + blank=True, + ) + status = models.CharField( + max_length=20, choices=_STATUS_CHOICES, default=STATUS_DRAFT + ) + + def __str__(self): + return f"Panier #{self.id}" + + @property + def price(self) -> int: + return sum(item.price for item in self.items.all()) + + +class BasketItem(Model): + product = models.ForeignKey( + to=Product, on_delete=models.PROTECT, related_name="basket_items" + ) + basket = models.ForeignKey( + to=Basket, on_delete=models.CASCADE, related_name="items" + ) + quantity = models.PositiveIntegerField() + + @property + def price(self) -> int: + return self.product.unit_price_cents * self.quantity diff --git a/src/purchase/tests.py b/src/purchase/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/src/purchase/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/src/purchase/views.py b/src/purchase/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/src/purchase/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here.