diff --git a/Makefile b/Makefile index a831224..f8bf912 100644 --- a/Makefile +++ b/Makefile @@ -22,14 +22,17 @@ migrate: shell: ./tests/manage.py shell +dbshell: + ./tests/manage.py dbshell + package: python3 setup.py sdist bdist_wheel upload-testpypi: - python3 -m twine upload --repository testpypi dist/django_admin_confirm-$(VERSION)* + python3 -m twine upload --repository testpypi dist/django_admin_confirm-$(VERSION)-* i-have-tested-with-testpypi-and-am-ready-to-release: - python3 -m twine upload --repository pypi dist/django_admin_confirm-$(VERSION)* + python3 -m twine upload --repository pypi dist/django_admin_confirm-$(VERSION)-* install-testpypi: pip uninstall django_admin_confirm diff --git a/admin_confirm/tests/README.md b/admin_confirm/tests/README.md index 5d99fc5..f1a0b6b 100644 --- a/admin_confirm/tests/README.md +++ b/admin_confirm/tests/README.md @@ -32,7 +32,8 @@ These are some areas which might/probably have issues that are not currently tes - [x] ManyToManyField - [x] OneToOneField - [x] ForeignKey - +- [x] DateField +- [x] DateTimeField - [x] Custom Readonly fields ### Options @@ -104,3 +105,22 @@ Note: Currently the code always calls super().\_changeform_view(), which would e - [x] ModelAdmin.has_add_permission - [x] ModelAdmin.has_change_permission + +### Tests for confirming models/forms with validations + +- [x] ModelForm.clean_field +- [x] ModelForm.clean +- [x] Model.clean +- [x] validator on the model field + +There are other possible combos of theses + +### Tests where save functions are overridden + +- [ ] ModelForm.save +- [ ] ModelAdmin.save_model + +### Tests for storage backends for ImageField and FileField + +- [x] Local storage +- [ ] S3 diff --git a/admin_confirm/tests/integration/test_with_form_input_types.py b/admin_confirm/tests/integration/test_with_form_input_types.py index 39b9cf0..1157e77 100644 --- a/admin_confirm/tests/integration/test_with_form_input_types.py +++ b/admin_confirm/tests/integration/test_with_form_input_types.py @@ -1,8 +1,10 @@ """ Tests with different form input types """ +from datetime import timedelta +from django.utils import timezone from importlib import reload -from tests.factories import ShopFactory +from tests.factories import ShopFactory, TransactionFactory from tests.market.models import GeneralManager, Item, ShoppingMall, Town from admin_confirm.tests.helpers import AdminConfirmIntegrationTestCase @@ -98,3 +100,37 @@ class ConfirmWithFormInputTypes(AdminConfirmIntegrationTestCase): mall.refresh_from_db() self.assertIn("New Name", mall.name) self.assertEqual(gm2, mall.general_manager) + + def test_datetime_and_field_should_work(self): + original_timestamp = timezone.now() - timedelta(hours=1) + transaction = TransactionFactory(timestamp=original_timestamp) + + self.selenium.get( + self.live_server_url + f"/admin/market/transaction/{transaction.id}/change/" + ) + self.assertIn(CONFIRM_CHANGE, self.selenium.page_source) + + # Set date via text input + date_input = self.selenium.find_element(By.ID, "id_date") + date_input.clear() + date_input.send_keys("2021-01-01") + self.assertEqual(date_input.get_attribute("value"), "2021-01-01") + + # Set timestamp via text input + timestamp_date = self.selenium.find_element(By.ID, "id_timestamp_0") + timestamp_date.clear() + timestamp_date.send_keys(str(timezone.now().date())) + timestamp_time = self.selenium.find_element(By.ID, "id_timestamp_1") + timestamp_time.clear() + timestamp_time.send_keys(str(timezone.now().time())) + + # Click save and continue + self.selenium.find_element(By.NAME, "_continue").click() + + # Click Yes I'm Sure on confirmation page + self.assertIn("Confirm", self.selenium.page_source) + self.selenium.find_element(By.NAME, "_continue").click() + + transaction.refresh_from_db() + self.assertEqual(str(transaction.date), "2021-01-01") + self.assertTrue(transaction.timestamp > original_timestamp) diff --git a/admin_confirm/tests/unit/test_model_field_types.py b/admin_confirm/tests/unit/test_model_field_types.py new file mode 100644 index 0000000..54ec728 --- /dev/null +++ b/admin_confirm/tests/unit/test_model_field_types.py @@ -0,0 +1,109 @@ +from django.urls import reverse +from django.utils import timezone + +from admin_confirm.tests.helpers import AdminConfirmTestCase +from tests.market.models import Transaction +from tests.factories import ShopFactory, TransactionFactory + + +class TestModelFieldTypes(AdminConfirmTestCase): + def test_confirm_add_of_datetime_and_field(self): + shop = ShopFactory() + expected_date = timezone.now().date() + expected_timestamp = timezone.now() + data = { + "date": str(expected_date), + "timestamp_0": str(expected_timestamp.date()), + "timestamp_1": str(expected_timestamp.time()), + "currency": "USD", + "shop": shop.id, + "total": 0, + "_confirm_add": True, + "_save": True, + } + response = self.client.post(reverse("admin:market_transaction_add"), data) + + # Should not have been added yet + self.assertEqual(Transaction.objects.count(), 0) + + # Ensure not redirected (confirmation page does not redirect) + self.assertEqual(response.status_code, 200) + expected_templates = [ + "admin/market/transaction/change_confirmation.html", + "admin/market/change_confirmation.html", + "admin/change_confirmation.html", + ] + self.assertEqual(response.template_name, expected_templates) + + self._assertSubmitHtml(rendered_content=response.rendered_content) + + # Confirmation page would not have the _confirm_add sent on submit + del data["_confirm_add"] + # Selecting to "Yes, I'm sure" on the confirmation page + # Would post to the same endpoint + response = self.client.post(reverse("admin:market_transaction_add"), data) + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, "/admin/market/transaction/") + self.assertEqual(Transaction.objects.count(), 1) + + # Ensure that the date and timestamp saved correctly + transaction = Transaction.objects.first() + self.assertEqual(transaction.date, expected_date) + self.assertEqual(transaction.timestamp, expected_timestamp) + + def test_confirm_change_of_datetime_and_date_field(self): + transaction = TransactionFactory() + original_date = transaction.date + original_timestamp = transaction.timestamp + data = { + "id": transaction.id, + "date": "2021-01-01", + "timestamp_0": "2021-01-01", + "timestamp_1": "12:30:00", + "currency": "USD", + "shop": transaction.shop.id, + "total": 0, + "_confirm_change": True, + "csrfmiddlewaretoken": "fake token", + "_continue": True, + } + response = self.client.post( + f"/admin/market/transaction/{transaction.id}/change/", data + ) + # Ensure not redirected (confirmation page does not redirect) + self.assertEqual(response.status_code, 200) + expected_templates = [ + "admin/market/transaction/change_confirmation.html", + "admin/market/change_confirmation.html", + "admin/change_confirmation.html", + ] + self.assertEqual(response.template_name, expected_templates) + + self._assertSubmitHtml( + rendered_content=response.rendered_content, save_action="_continue" + ) + + # Hasn't changed item yet + transaction.refresh_from_db() + self.assertEqual(transaction.date, original_date) + self.assertEqual(transaction.timestamp, original_timestamp) + + # Selecting to "Yes, I'm sure" on the confirmation page + # Would post to the same endpoint + del data["_confirm_change"] + response = self.client.post( + f"/admin/market/transaction/{transaction.id}/change/", data + ) + # will show the change page for this transaction + self.assertEqual(response.status_code, 302) + self.assertEqual( + response.url, f"/admin/market/transaction/{transaction.id}/change/" + ) + # Should not be the confirmation page, we already confirmed change + self.assertNotEqual(response.templates, expected_templates) + self.assertEqual(Transaction.objects.count(), 1) + + transaction.refresh_from_db() + self.assertEqual(str(transaction.date), "2021-01-01") + self.assertEqual(str(transaction.timestamp.date()), "2021-01-01") + self.assertEqual(str(transaction.timestamp.time()), "12:30:00") diff --git a/admin_confirm/tests/unit/test_with_validators.py b/admin_confirm/tests/unit/test_with_validators.py new file mode 100644 index 0000000..8874bb7 --- /dev/null +++ b/admin_confirm/tests/unit/test_with_validators.py @@ -0,0 +1,334 @@ +""" +Ensures that confirmations work with validators on the Model and on the Modelform. +""" + +from unittest import mock +from django.urls import reverse +from django.utils import timezone + +from admin_confirm.tests.helpers import AdminConfirmTestCase +from tests.market.models import Checkout, ItemSale +from tests.factories import ( + InventoryFactory, + ItemFactory, + ShopFactory, + TransactionFactory, +) + + +class TestWithValidators(AdminConfirmTestCase): + @mock.patch("tests.market.models.ItemSale.clean") + def test_can_confirm_for_models_with_validator_on_model_field(self, _mock_clean): + # ItemSale.currency has a validator on it + item = ItemFactory() + transaction = TransactionFactory() + data = { + "transaction": transaction.id, + "item": item.id, + "quantity": 1, + "currency": "USD", + "total": 10.00, + "_confirm_add": True, + "_save": True, + } + response = self.client.post(reverse("admin:market_itemsale_add"), data) + + # Should not have been added yet + self.assertEqual(ItemSale.objects.count(), 0) + + # Ensure not redirected (confirmation page does not redirect) + self.assertEqual(response.status_code, 200) + expected_templates = [ + "admin/market/itemsale/change_confirmation.html", + "admin/market/change_confirmation.html", + "admin/change_confirmation.html", + ] + self.assertEqual(response.template_name, expected_templates) + + self._assertSubmitHtml(rendered_content=response.rendered_content) + + # Confirmation page would not have the _confirm_add sent on submit + del data["_confirm_add"] + # Selecting to "Yes, I'm sure" on the confirmation page + # Would post to the same endpoint + response = self.client.post(reverse("admin:market_itemsale_add"), data) + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, "/admin/market/itemsale/") + self.assertEqual(ItemSale.objects.count(), 1) + + # Ensure that the date and timestamp saved correctly + item_sale = ItemSale.objects.first() + self.assertEqual(item_sale.transaction, transaction) + self.assertEqual(item_sale.item, item) + self.assertEqual(item_sale.currency, "USD") + + def test_cannot_confirm_for_models_with_validator_on_model_field_if_validator_fails( + self, + ): + # ItemSale.currency has a validator on it + shop = ShopFactory() + item = ItemFactory() + InventoryFactory(shop=shop, item=item, quantity=10) + transaction = TransactionFactory(shop=shop) + data = { + "transaction": transaction.id, + "item": item.id, + "quantity": 1, + "currency": "FAKE", + "total": 10.00, + "_confirm_add": True, + "_save": True, + } + response = self.client.post(reverse("admin:market_itemsale_add"), data) + # Should not have been added yet + self.assertEqual(ItemSale.objects.count(), 0) + + # Ensure not redirected (confirmation page does not redirect) + self.assertEqual(response.status_code, 200) + expected_templates = [ + "admin/market/itemsale/change_confirmation.html", + "admin/market/change_confirmation.html", + "admin/change_confirmation.html", + ] + self.assertEqual(response.template_name, expected_templates) + + self._assertSubmitHtml(rendered_content=response.rendered_content) + + # Confirmation page would not have the _confirm_add sent on submit + del data["_confirm_add"] + # Selecting to "Yes, I'm sure" on the confirmation page + # Would post to the same endpoint + response = self.client.post(reverse("admin:market_itemsale_add"), data) + + # Should not have redirected, since there was an error + self.assertEqual(response.status_code, 200) + expected_templates = [ + "admin/market/itemsale/change_form.html", + "admin/market/change_form.html", + "admin/change_form.html", + ] + self.assertEqual(response.template_name, expected_templates) + self.assertEqual(ItemSale.objects.count(), 0) + self.assertTrue("error" in str(response.content)) + self.assertTrue("Invalid Currency" in str(response.content)) + + def test_can_confirm_for_models_with_clean_overridden(self): + shop = ShopFactory() + item = ItemFactory() + InventoryFactory(shop=shop, item=item, quantity=10) + transaction = TransactionFactory(shop=shop) + data = { + "transaction": transaction.id, + "item": item.id, + "quantity": 9, + "currency": "USD", + "total": 10.00, + "_confirm_add": True, + "_save": True, + } + response = self.client.post(reverse("admin:market_itemsale_add"), data) + + # Should not have been added yet + self.assertEqual(ItemSale.objects.count(), 0) + + # Ensure not redirected (confirmation page does not redirect) + self.assertEqual(response.status_code, 200) + expected_templates = [ + "admin/market/itemsale/change_confirmation.html", + "admin/market/change_confirmation.html", + "admin/change_confirmation.html", + ] + self.assertEqual(response.template_name, expected_templates) + + self._assertSubmitHtml(rendered_content=response.rendered_content) + + # Confirmation page would not have the _confirm_add sent on submit + del data["_confirm_add"] + # Selecting to "Yes, I'm sure" on the confirmation page + # Would post to the same endpoint + response = self.client.post(reverse("admin:market_itemsale_add"), data) + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, "/admin/market/itemsale/") + self.assertEqual(ItemSale.objects.count(), 1) + + # Ensure that the date and timestamp saved correctly + item_sale = ItemSale.objects.first() + self.assertEqual(item_sale.transaction, transaction) + self.assertEqual(item_sale.item, item) + self.assertEqual(item_sale.currency, "USD") + + def test_cannot_confirm_for_models_with_clean_overridden_if_clean_fails(self): + shop = ShopFactory() + item = ItemFactory() + InventoryFactory(shop=shop, item=item, quantity=1) + transaction = TransactionFactory(shop=shop) + data = { + "transaction": transaction.id, + "item": item.id, + "quantity": 9, + "currency": "USD", + "total": 10.00, + "_confirm_add": True, + "_save": True, + } + response = self.client.post(reverse("admin:market_itemsale_add"), data) + + # Should not have been added yet + self.assertEqual(ItemSale.objects.count(), 0) + + # Ensure not redirected (confirmation page does not redirect) + self.assertEqual(response.status_code, 200) + expected_templates = [ + "admin/market/itemsale/change_confirmation.html", + "admin/market/change_confirmation.html", + "admin/change_confirmation.html", + ] + self.assertEqual(response.template_name, expected_templates) + + self._assertSubmitHtml(rendered_content=response.rendered_content) + + # Confirmation page would not have the _confirm_add sent on submit + del data["_confirm_add"] + # Selecting to "Yes, I'm sure" on the confirmation page + # Would post to the same endpoint + response = self.client.post(reverse("admin:market_itemsale_add"), data) + + # Should not have redirected, since there was an error + self.assertEqual(response.status_code, 200) + expected_templates = [ + "admin/market/itemsale/change_form.html", + "admin/market/change_form.html", + "admin/change_form.html", + ] + self.assertEqual(response.template_name, expected_templates) + self.assertEqual(ItemSale.objects.count(), 0) + self.assertTrue("error" in str(response.content)) + self.assertTrue( + "Shop does not have enough of the item stocked" in str(response.content) + ) + + def test_can_confirm_for_modelform_with_clean_field_and_clean_overridden(self): + shop = ShopFactory() + data = { + "shop": shop.id, + "currency": "USD", + "total": 10.00, + "date": str(timezone.now().date()), + "timestamp_0": str(timezone.now().date()), + "timestamp_1": str(timezone.now().time()), + "_confirm_add": True, + "_save": True, + } + response = self.client.post(reverse("admin:market_checkout_add"), data) + + # Should not have been added yet + self.assertEqual(Checkout.objects.count(), 0) + + # Ensure not redirected (confirmation page does not redirect) + self.assertEqual(response.status_code, 200) + expected_templates = [ + "admin/market/checkout/change_confirmation.html", + "admin/market/change_confirmation.html", + "admin/change_confirmation.html", + ] + self.assertEqual(response.template_name, expected_templates) + + self._assertSubmitHtml(rendered_content=response.rendered_content) + + # Confirmation page would not have the _confirm_add sent on submit + del data["_confirm_add"] + # Selecting to "Yes, I'm sure" on the confirmation page + # Would post to the same endpoint + response = self.client.post(reverse("admin:market_checkout_add"), data) + print(response.content) + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, "/admin/market/checkout/") + self.assertEqual(Checkout.objects.count(), 1) + + # Ensure that the date and timestamp saved correctly + checkout = Checkout.objects.first() + self.assertEqual(checkout.shop, shop) + self.assertEqual(checkout.total, 10.00) + self.assertEqual(checkout.currency, "USD") + + def test_cannot_confirm_for_modelform_with_clean_field_overridden_if_validation_fails( + self, + ): + shop = ShopFactory() + data = { + "shop": shop.id, + "currency": "USD", + "total": "111", + "date": str(timezone.now().date()), + "timestamp_0": str(timezone.now().date()), + "timestamp_1": str(timezone.now().time()), + "_confirm_add": True, + "_save": True, + } + response = self.client.post(reverse("admin:market_checkout_add"), data) + + # Should not have been added yet + self.assertEqual(Checkout.objects.count(), 0) + + # Ensure not redirected (confirmation page does not redirect) + self.assertEqual(response.status_code, 200) + expected_templates = [ + "admin/market/checkout/change_confirmation.html", + "admin/market/change_confirmation.html", + "admin/change_confirmation.html", + ] + self.assertEqual(response.template_name, expected_templates) + + self._assertSubmitHtml(rendered_content=response.rendered_content) + + # Confirmation page would not have the _confirm_add sent on submit + del data["_confirm_add"] + # Selecting to "Yes, I'm sure" on the confirmation page + # Would post to the same endpoint + response = self.client.post(reverse("admin:market_checkout_add"), data) + print(response.content) + self.assertEqual(response.status_code, 200) + self.assertEqual(Checkout.objects.count(), 0) + self.assertIn("error", str(response.content)) + self.assertIn("Invalid Total 111", str(response.content)) + + def test_cannot_confirm_for_modelform_with_clean_overridden_if_validation_fails( + self, + ): + shop = ShopFactory() + data = { + "shop": shop.id, + "currency": "USD", + "total": "222", + "date": str(timezone.now().date()), + "timestamp_0": str(timezone.now().date()), + "timestamp_1": str(timezone.now().time()), + "_confirm_add": True, + "_save": True, + } + response = self.client.post(reverse("admin:market_checkout_add"), data) + + # Should not have been added yet + self.assertEqual(Checkout.objects.count(), 0) + + # Ensure not redirected (confirmation page does not redirect) + self.assertEqual(response.status_code, 200) + expected_templates = [ + "admin/market/checkout/change_confirmation.html", + "admin/market/change_confirmation.html", + "admin/change_confirmation.html", + ] + self.assertEqual(response.template_name, expected_templates) + + self._assertSubmitHtml(rendered_content=response.rendered_content) + + # Confirmation page would not have the _confirm_add sent on submit + del data["_confirm_add"] + # Selecting to "Yes, I'm sure" on the confirmation page + # Would post to the same endpoint + response = self.client.post(reverse("admin:market_checkout_add"), data) + print(response.content) + self.assertEqual(response.status_code, 200) + self.assertEqual(Checkout.objects.count(), 0) + self.assertIn("error", str(response.content)) + self.assertIn("Invalid Total 222", str(response.content)) diff --git a/tests/factories.py b/tests/factories.py index 9180118..6dbbea0 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -1,8 +1,10 @@ import factory from random import choice, randint +from django.utils import timezone -from tests.market.models import Item, Shop, Inventory +from .market.models import Item, Shop, Inventory, Transaction +from .market.constants import VALID_CURRENCIES class ItemFactory(factory.django.DjangoModelFactory): @@ -11,7 +13,7 @@ class ItemFactory(factory.django.DjangoModelFactory): name = factory.Faker("name") price = factory.LazyAttribute(lambda _: randint(5, 500)) - currency = factory.LazyAttribute(lambda _: choice(Item.VALID_CURRENCIES)) + currency = "CAD" class ShopFactory(factory.django.DjangoModelFactory): @@ -28,3 +30,14 @@ class InventoryFactory(factory.django.DjangoModelFactory): shop = factory.SubFactory(ShopFactory) item = factory.SubFactory(ItemFactory) quantity = factory.Sequence(lambda n: n) + + +class TransactionFactory(factory.django.DjangoModelFactory): + class Meta: + model = Transaction + + currency = "CAD" + total = 0 + date = factory.LazyAttribute(lambda _: timezone.now().date()) + timestamp = factory.LazyAttribute(lambda _: timezone.now()) + shop = factory.SubFactory(ShopFactory) diff --git a/tests/market/admin/__init__.py b/tests/market/admin/__init__.py index 03cb423..4b49cdc 100644 --- a/tests/market/admin/__init__.py +++ b/tests/market/admin/__init__.py @@ -1,15 +1,30 @@ from django.contrib import admin -from ..models import GeneralManager, Item, Inventory, Shop, ShoppingMall +from ..models import ( + GeneralManager, + Item, + Inventory, + ItemSale, + Shop, + ShoppingMall, + Transaction, + Checkout, +) from .item_admin import ItemAdmin from .inventory_admin import InventoryAdmin from .shop_admin import ShopAdmin from .shoppingmall_admin import ShoppingMallAdmin from .generalmanager_admin import GeneralManagerAdmin +from .item_sale_admin import ItemSaleAdmin +from .transaction_admin import TransactionAdmin +from .checkout_admin import CheckoutAdmin admin.site.register(Item, ItemAdmin) admin.site.register(Inventory, InventoryAdmin) admin.site.register(Shop, ShopAdmin) admin.site.register(ShoppingMall, ShoppingMallAdmin) admin.site.register(GeneralManager, GeneralManagerAdmin) +admin.site.register(Transaction, TransactionAdmin) +admin.site.register(ItemSale, ItemSaleAdmin) +admin.site.register(Checkout, CheckoutAdmin) diff --git a/tests/market/admin/checkout_admin.py b/tests/market/admin/checkout_admin.py new file mode 100644 index 0000000..50ec65f --- /dev/null +++ b/tests/market/admin/checkout_admin.py @@ -0,0 +1,46 @@ +from django.core.exceptions import ValidationError +from admin_confirm.admin import AdminConfirmMixin + +from django.contrib.admin import ModelAdmin +from django.forms import ModelForm + +from ..models import Checkout + + +class CheckoutForm(ModelForm): + class Meta: + model = Checkout + fields = [ + "currency", + "shop", + "total", + "timestamp", + "date", + ] + + def clean_total(self): + try: + total = float(self.cleaned_data["total"]) + except: + raise ValidationError("Invalid Total From clean_total") + if total == 111: # Use to cause error in test + raise ValidationError("Invalid Total 111") + + return total + + def clean(self): + try: + total = float(self.data["total"]) + except: + raise ValidationError("Invalid Total From clean") + if total == 222: # Use to cause error in test + raise ValidationError("Invalid Total 222") + + self.cleaned_data["total"] = total + + +class CheckoutAdmin(AdminConfirmMixin, ModelAdmin): + confirm_add = True + confirm_change = True + autocomplete_fields = ["shop"] + form = CheckoutForm diff --git a/tests/market/admin/item_sale_admin.py b/tests/market/admin/item_sale_admin.py new file mode 100644 index 0000000..4a819fa --- /dev/null +++ b/tests/market/admin/item_sale_admin.py @@ -0,0 +1,9 @@ +from admin_confirm.admin import AdminConfirmMixin + + +from django.contrib.admin import ModelAdmin + + +class ItemSaleAdmin(AdminConfirmMixin, ModelAdmin): + confirm_add = True + confirm_change = True diff --git a/tests/market/admin/transaction_admin.py b/tests/market/admin/transaction_admin.py new file mode 100644 index 0000000..ddd4da0 --- /dev/null +++ b/tests/market/admin/transaction_admin.py @@ -0,0 +1,9 @@ +from admin_confirm.admin import AdminConfirmMixin + + +from django.contrib.admin import ModelAdmin + + +class TransactionAdmin(AdminConfirmMixin, ModelAdmin): + confirm_add = True + confirm_change = True diff --git a/tests/market/constants.py b/tests/market/constants.py new file mode 100644 index 0000000..cf32153 --- /dev/null +++ b/tests/market/constants.py @@ -0,0 +1,4 @@ +VALID_CURRENCIES = ( + ("CAD", "CAD"), + ("USD", "USD"), +) diff --git a/tests/market/migrations/0010_checkout_itemsale_transaction.py b/tests/market/migrations/0010_checkout_itemsale_transaction.py new file mode 100644 index 0000000..f30bcb4 --- /dev/null +++ b/tests/market/migrations/0010_checkout_itemsale_transaction.py @@ -0,0 +1,45 @@ +# Generated by Django 3.1.7 on 2021-03-10 23:41 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('market', '0009_auto_20210304_0355'), + ] + + operations = [ + migrations.CreateModel( + name='Transaction', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('timestamp', models.DateField(auto_created=True)), + ('total', models.DecimalField(decimal_places=2, editable=False, max_digits=5)), + ('currency', models.CharField(choices=[('CAD', 'CAD'), ('USD', 'USD')], max_length=3)), + ('shop', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='market.shop')), + ], + ), + migrations.CreateModel( + name='ItemSale', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('total', models.DecimalField(decimal_places=2, editable=False, max_digits=5)), + ('currency', models.CharField(choices=[('CAD', 'CAD'), ('USD', 'USD')], max_length=3)), + ('item', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='market.item')), + ('transaction', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='item_sales', to='market.transaction')), + ], + ), + migrations.CreateModel( + name='Checkout', + fields=[ + ], + options={ + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('market.transaction',), + ), + ] diff --git a/tests/market/migrations/0011_auto_20210326_0130.py b/tests/market/migrations/0011_auto_20210326_0130.py new file mode 100644 index 0000000..92bb5ac --- /dev/null +++ b/tests/market/migrations/0011_auto_20210326_0130.py @@ -0,0 +1,30 @@ +# Generated by Django 3.1.7 on 2021-03-26 01:30 + +from django.db import migrations, models +from ..validators import validate_currency + + +class Migration(migrations.Migration): + + dependencies = [ + ("market", "0010_checkout_itemsale_transaction"), + ] + + operations = [ + migrations.AddField( + model_name="itemsale", + name="quantity", + field=models.PositiveIntegerField(default=1), + ), + migrations.AddField( + model_name="transaction", + name="date", + field=models.DateTimeField(default=None), + preserve_default=False, + ), + migrations.AlterField( + model_name="itemsale", + name="currency", + field=models.CharField(max_length=5, validators=[validate_currency]), + ), + ] diff --git a/tests/market/migrations/0012_auto_20210326_0240.py b/tests/market/migrations/0012_auto_20210326_0240.py new file mode 100644 index 0000000..b43b2c0 --- /dev/null +++ b/tests/market/migrations/0012_auto_20210326_0240.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.7 on 2021-03-26 02:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('market', '0011_auto_20210326_0130'), + ] + + operations = [ + migrations.AlterField( + model_name='transaction', + name='date', + field=models.DateField(), + ), + migrations.AlterField( + model_name='transaction', + name='timestamp', + field=models.DateTimeField(auto_created=True), + ), + ] diff --git a/tests/market/models.py b/tests/market/models.py index b4f1d04..29c66a7 100644 --- a/tests/market/models.py +++ b/tests/market/models.py @@ -1,11 +1,14 @@ +from django.core.exceptions import NON_FIELD_ERRORS, ValidationError +from django.db.models.aggregates import Sum from django.db import models +from .constants import VALID_CURRENCIES +from .validators import validate_currency class Item(models.Model): - VALID_CURRENCIES = ( - ("CAD", "CAD"), - ("USD", "USD"), - ) + # Because I'm lazy and don't want to update all test references + VALID_CURRENCIES = VALID_CURRENCIES + name = models.CharField(max_length=120) price = models.DecimalField(max_digits=5, decimal_places=2) currency = models.CharField(max_length=3, choices=VALID_CURRENCIES) @@ -57,3 +60,48 @@ class ShoppingMall(models.Model): def __str__(self): return self.name + + +class Transaction(models.Model): + total = models.DecimalField(max_digits=5, decimal_places=2, default=0) + currency = models.CharField(max_length=3, choices=VALID_CURRENCIES) + shop = models.ForeignKey(Shop, on_delete=models.CASCADE) + timestamp = models.DateTimeField(auto_created=True) + date = models.DateField() + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + + +class ItemSale(models.Model): + transaction = models.ForeignKey( + Transaction, on_delete=models.CASCADE, related_name="item_sales" + ) + item = models.ForeignKey(Item, on_delete=models.SET_NULL, null=True) + quantity = models.PositiveIntegerField(default=1) + total = models.DecimalField(max_digits=5, decimal_places=2) + currency = models.CharField(max_length=5, validators=[validate_currency]) + + def clean(self): + errors = {} + # check that shop has the stock + shop = self.transaction.shop + inventory = Inventory.objects.filter(shop=shop, item=self.item) + if not inventory: + errors["item"] = "Shop does not have the item stocked" + else: + in_stock = inventory.aggregate(Sum("quantity")).get("quantity__sum", 0) + if in_stock < self.quantity: + errors["item"] = "Shop does not have enough of the item stocked" + if errors: + raise ValidationError(errors) + + +class Checkout(Transaction): + """ + Proxy Model to use in Django Admin to create a Transaction + As if a customer was checking out at a physical checkout + """ + + class Meta: + proxy = True diff --git a/tests/market/validators.py b/tests/market/validators.py new file mode 100644 index 0000000..7da58db --- /dev/null +++ b/tests/market/validators.py @@ -0,0 +1,8 @@ +from django.core.exceptions import ValidationError +from .constants import VALID_CURRENCIES + + +def validate_currency(value: str): + currency_values = [c[0] for c in VALID_CURRENCIES] + if value not in currency_values: + raise ValidationError("Invalid Currency")