Formatted with black and reached 100% coverage

main
Thu Trang Pham 2020-11-08 09:51:49 -08:00
parent c586100098
commit a95383cfa2
17 changed files with 465 additions and 179 deletions

View File

@ -1,2 +1,11 @@
run: run:
./tests/manage.py runserver ./tests/manage.py runserver
test:
coverage run --branch -m pytest
coverage html
coverage-badge -f -o coverage.svg
migrate:
./tests/manage.py makemigrations
./tests/manage.py migrate

View File

@ -1,5 +1,7 @@
# Django Admin Confirm # Django Admin Confirm
![coverage](/coverage.svg)
AdminConfirmMixin is a mixin for ModelAdmin to add confirmations to changes and additions. AdminConfirmMixin is a mixin for ModelAdmin to add confirmations to changes and additions.
![Screenshot of Confirmation Page](/screenshot.png) ![Screenshot of Confirmation Page](/screenshot.png)

View File

@ -52,21 +52,23 @@ class AdminConfirmMixin:
def changeform_view(self, request, object_id=None, form_url="", extra_context=None): def changeform_view(self, request, object_id=None, form_url="", extra_context=None):
if request.method == "POST": if request.method == "POST":
if (not object_id and "_confirm_add" in request.POST) or (object_id and "_confirm_change" in request.POST): if (not object_id and "_confirm_add" in request.POST) or (
return self._change_confirmation_view(request, object_id, form_url, extra_context) object_id and "_confirm_change" in request.POST
):
return self._change_confirmation_view(
request, object_id, form_url, extra_context
)
extra_context = { extra_context = {
**(extra_context or {}), **(extra_context or {}),
'confirm_add': self.confirm_add, "confirm_add": self.confirm_add,
'confirm_change': self.confirm_change "confirm_change": self.confirm_change,
} }
return super().changeform_view(request, object_id, form_url, extra_context) return super().changeform_view(request, object_id, form_url, extra_context)
def _change_confirmation_view(self, request, object_id, form_url, extra_context): def _change_confirmation_view(self, request, object_id, form_url, extra_context):
# This code is taken from super()._changeform_view # This code is taken from super()._changeform_view
to_field = request.POST.get( to_field = request.POST.get(TO_FIELD_VAR, request.GET.get(TO_FIELD_VAR))
TO_FIELD_VAR, request.GET.get(TO_FIELD_VAR)
)
if to_field and not self.to_field_allowed(request, to_field): if to_field and not self.to_field_allowed(request, to_field):
raise DisallowedModelAdminToField( raise DisallowedModelAdminToField(
"The field %s cannot be referenced." % to_field "The field %s cannot be referenced." % to_field
@ -83,7 +85,6 @@ class AdminConfirmMixin:
obj = None obj = None
else: else:
obj = self.get_object(request, unquote(object_id), to_field) obj = self.get_object(request, unquote(object_id), to_field)
if obj is None: if obj is None:
return self._get_obj_does_not_exist_redirect(request, opts, object_id) return self._get_obj_does_not_exist_redirect(request, opts, object_id)
@ -105,21 +106,30 @@ class AdminConfirmMixin:
# End code from super()._changeform_view # End code from super()._changeform_view
changed_data = {} changed_data = {}
if add: if form_validated:
for name in form.changed_data: if add:
new_value = new_object.__getattribute__(name) for name in form.changed_data:
if new_value is not None: new_value = getattr(new_object, name)
changed_data[name] = [None, new_value] # Don't consider default values as changed for adding
else: if (
# Parse the changed data - Note that using form.changed_data would not work because initial is not set new_value is not None
for name, field in form.fields.items(): and new_value != model._meta.get_field(name).default
initial_value = obj.__getattribute__(name) ):
new_value = new_object.__getattribute__(name) changed_data[name] = [None, new_value]
if field.has_changed(initial_value, new_value) and initial_value != new_value: else:
changed_data[name] = [initial_value, new_value] # Parse the changed data - Note that using form.changed_data would not work because initial is not set
for name, field in form.fields.items():
initial_value = getattr(obj, name)
new_value = getattr(new_object, name)
if (
field.has_changed(initial_value, new_value)
and initial_value != new_value
):
changed_data[name] = [initial_value, new_value]
changed_confirmation_fields = set(self.get_confirmation_fields( changed_confirmation_fields = set(
request, obj)) & set(changed_data.keys()) self.get_confirmation_fields(request, obj)
) & set(changed_data.keys())
if not bool(changed_confirmation_fields): if not bool(changed_confirmation_fields):
# No confirmation required for changed fields, continue to save # No confirmation required for changed fields, continue to save
return super()._changeform_view(request, object_id, form_url, extra_context) return super()._changeform_view(request, object_id, form_url, extra_context)
@ -132,14 +142,14 @@ class AdminConfirmMixin:
if key in ["_save", "_saveasnew", "_addanother", "_continue"]: if key in ["_save", "_saveasnew", "_addanother", "_continue"]:
save_action = key save_action = key
if key.startswith("_") or key == 'csrfmiddlewaretoken': if key.startswith("_") or key == "csrfmiddlewaretoken":
continue continue
form_data[key] = request.POST.get(key) form_data[key] = request.POST.get(key)
if add: if add:
title_action = _('adding') title_action = _("adding")
else: else:
title_action = _('changing') title_action = _("changing")
context = { context = {
**self.admin_site.each_context(request), **self.admin_site.each_context(request),

View File

@ -1,135 +1,303 @@
from django.test import TestCase, RequestFactory from django.test import TestCase, RequestFactory
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.admin.sites import AdminSite from django.contrib.admin.sites import AdminSite
from django.contrib.admin.options import TO_FIELD_VAR
from tests.market.admin import ItemAdmin from django.http import HttpResponseForbidden, HttpResponseBadRequest
from tests.market.models import Item, Inventory
from django.urls import reverse from django.urls import reverse
from tests.factories import ItemFactory, ShopFactory
from tests.market.admin import ItemAdmin, InventoryAdmin
from tests.market.models import Item, Inventory
from tests.factories import ItemFactory, ShopFactory, InventoryFactory
class TestAdminConfirmMixin(TestCase): class TestAdminConfirmMixin(TestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
cls.superuser = User.objects.create_superuser( cls.superuser = User.objects.create_superuser(
username='super', email='super@email.org', password='pass') username="super", email="super@email.org", password="pass"
)
def setUp(self): def setUp(self):
self.client.force_login(self.superuser) self.client.force_login(self.superuser)
self.factory = RequestFactory() self.factory = RequestFactory()
def test_get_add_without_confirm_add(self): def test_get_add_without_confirm_add(self):
response = self.client.get(reverse('admin:market_item_add')) response = self.client.get(reverse("admin:market_item_add"))
self.assertFalse(response.context_data.get('confirm_add')) self.assertFalse(response.context_data.get("confirm_add"))
self.assertNotIn('_confirm_add', response.rendered_content) self.assertNotIn("_confirm_add", response.rendered_content)
def test_get_add_with_confirm_add(self): def test_get_add_with_confirm_add(self):
response = self.client.get(reverse('admin:market_inventory_add')) response = self.client.get(reverse("admin:market_inventory_add"))
self.assertTrue(response.context_data.get('confirm_add')) self.assertTrue(response.context_data.get("confirm_add"))
self.assertIn('_confirm_add', response.rendered_content) self.assertIn("_confirm_add", response.rendered_content)
def test_get_change_without_confirm_change(self): def test_get_change_without_confirm_change(self):
response = self.client.get(reverse('admin:market_shop_add')) response = self.client.get(reverse("admin:market_shop_add"))
self.assertFalse(response.context_data.get('confirm_change')) self.assertFalse(response.context_data.get("confirm_change"))
self.assertNotIn('_confirm_change', response.rendered_content) self.assertNotIn("_confirm_change", response.rendered_content)
def test_get_change_with_confirm_change(self): def test_get_change_with_confirm_change(self):
response = self.client.get(reverse('admin:market_inventory_add')) response = self.client.get(reverse("admin:market_inventory_add"))
self.assertTrue(response.context_data.get('confirm_change')) self.assertTrue(response.context_data.get("confirm_change"))
self.assertIn('_confirm_change', response.rendered_content) self.assertIn("_confirm_change", response.rendered_content)
def test_post_add_without_confirm_add(self): def test_post_add_without_confirm_add(self):
data = {'name': 'name', 'price': 2.0, data = {"name": "name", "price": 2.0, "currency": Item.VALID_CURRENCIES[0]}
'currency': Item.VALID_CURRENCIES[0]} response = self.client.post(reverse("admin:market_item_add"), data)
response = self.client.post(reverse('admin:market_item_add'), data)
# Redirects to item changelist and item is added # Redirects to item changelist and item is added
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, '/admin/market/item/') self.assertEqual(response.url, "/admin/market/item/")
self.assertEqual(Item.objects.count(), 1) self.assertEqual(Item.objects.count(), 1)
def test_post_add_with_confirm_add(self): def test_post_add_with_confirm_add(self):
item = ItemFactory() item = ItemFactory()
shop = ShopFactory() shop = ShopFactory()
data = {'shop': shop.id, 'item': item.id, data = {"shop": shop.id, "item": item.id, "quantity": 5, "_confirm_add": True}
'quantity': 5, '_confirm_add': True} response = self.client.post(reverse("admin:market_inventory_add"), data)
response = self.client.post(
reverse('admin:market_inventory_add'), data)
# Ensure not redirected (confirmation page does not redirect) # Ensure not redirected (confirmation page does not redirect)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
expected_templates = [ expected_templates = [
'admin/market/inventory/change_confirmation.html', "admin/market/inventory/change_confirmation.html",
'admin/market/change_confirmation.html', "admin/market/change_confirmation.html",
'admin/change_confirmation.html' "admin/change_confirmation.html",
] ]
self.assertEqual(response.template_name, expected_templates) self.assertEqual(response.template_name, expected_templates)
form_data = {'shop': str(shop.id), 'item': str( form_data = {"shop": str(shop.id), "item": str(item.id), "quantity": str(5)}
item.id), 'quantity': str(5)} self.assertEqual(response.context_data["form_data"], form_data)
self.assertEqual(
response.context_data['form_data'], form_data)
for k, v in form_data.items(): for k, v in form_data.items():
self.assertIn( self.assertIn(
f'<input type="hidden" name="{ k }" value="{ v }">', response.rendered_content) f'<input type="hidden" name="{ k }" value="{ v }">',
response.rendered_content,
)
# Should not have been added yet # Should not have been added yet
self.assertEqual(Inventory.objects.count(), 0) self.assertEqual(Inventory.objects.count(), 0)
def test_post_change_with_confirm_change(self): def test_post_change_with_confirm_change(self):
item = ItemFactory(name='item') item = ItemFactory(name="item")
data = {'name': 'name', 'price': 2.0, data = {
'currency': Item.VALID_CURRENCIES[0], '_confirm_change': True} "name": "name",
response = self.client.post( "price": 2.0,
f'/admin/market/item/{item.id}/change/', data) "currency": Item.VALID_CURRENCIES[0],
"id": item.id,
"_confirm_change": True,
"csrfmiddlewaretoken": "fake token",
"_save": True,
}
response = self.client.post(f"/admin/market/item/{item.id}/change/", data)
# Ensure not redirected (confirmation page does not redirect) # Ensure not redirected (confirmation page does not redirect)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
expected_templates = [ expected_templates = [
'admin/market/item/change_confirmation.html', "admin/market/item/change_confirmation.html",
'admin/market/change_confirmation.html', "admin/market/change_confirmation.html",
'admin/change_confirmation.html' "admin/change_confirmation.html",
] ]
self.assertEqual(response.template_name, expected_templates) self.assertEqual(response.template_name, expected_templates)
form_data = {'name': 'name', 'price': str(2.0), form_data = {
'currency': Item.VALID_CURRENCIES[0][0]} "name": "name",
self.assertEqual( "price": str(2.0),
response.context_data['form_data'], form_data) "id": str(item.id),
"currency": Item.VALID_CURRENCIES[0][0],
}
self.assertEqual(response.context_data["form_data"], form_data)
for k, v in form_data.items(): for k, v in form_data.items():
self.assertIn( self.assertIn(
f'<input type="hidden" name="{ k }" value="{ v }">', response.rendered_content) f'<input type="hidden" name="{ k }" value="{ v }">',
response.rendered_content,
)
# Hasn't changed item yet # Hasn't changed item yet
item.refresh_from_db() item.refresh_from_db()
self.assertEqual(item.name, 'item') self.assertEqual(item.name, "item")
def test_post_change_without_confirm_change(self): def test_post_change_without_confirm_change(self):
shop = ShopFactory(name='bob') shop = ShopFactory(name="bob")
data = {'name': 'sally'} data = {"name": "sally"}
response = self.client.post( response = self.client.post(f"/admin/market/shop/{shop.id}/change/", data)
f'/admin/market/shop/{shop.id}/change/', data)
# Redirects to changelist # Redirects to changelist
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, '/admin/market/shop/') self.assertEqual(response.url, "/admin/market/shop/")
# Shop has changed # Shop has changed
shop.refresh_from_db() shop.refresh_from_db()
self.assertEqual(shop.name, 'sally') self.assertEqual(shop.name, "sally")
def test_get_confirmation_fields_should_default_if_not_set(self): def test_get_confirmation_fields_should_default_if_not_set(self):
expected_fields = [f.name for f in Item._meta.fields if f.name != 'id'] expected_fields = [f.name for f in Item._meta.fields if f.name != "id"]
ItemAdmin.confirmation_fields = None ItemAdmin.confirmation_fields = None
admin = ItemAdmin(Item, AdminSite()) admin = ItemAdmin(Item, AdminSite())
actual_fields = admin.get_confirmation_fields(self.factory.request()) actual_fields = admin.get_confirmation_fields(self.factory.request())
self.assertEqual(expected_fields, actual_fields) self.assertEqual(expected_fields, actual_fields)
def test_get_confirmation_fields_if_set(self): def test_get_confirmation_fields_if_set(self):
expected_fields = ['name', 'currency'] expected_fields = ["name", "currency"]
ItemAdmin.confirmation_fields = expected_fields ItemAdmin.confirmation_fields = expected_fields
admin = ItemAdmin(Item, AdminSite()) admin = ItemAdmin(Item, AdminSite())
actual_fields = admin.get_confirmation_fields(self.factory.request()) actual_fields = admin.get_confirmation_fields(self.factory.request())
self.assertEqual(expected_fields, actual_fields) self.assertEqual(expected_fields, actual_fields)
def test_custom_template(self): def test_custom_template(self):
expected_template = 'market/admin/my_custom_template.html' expected_template = "market/admin/my_custom_template.html"
ItemAdmin.confirmation_template = expected_template ItemAdmin.confirmation_template = expected_template
admin = ItemAdmin(Item, AdminSite()) admin = ItemAdmin(Item, AdminSite())
actual_template = admin.render_change_confirmation( actual_template = admin.render_change_confirmation(
self.factory.request(), context={}).template_name self.factory.request(), context={}
).template_name
self.assertEqual(expected_template, actual_template) self.assertEqual(expected_template, actual_template)
ItemAdmin.confirmation_template = None ItemAdmin.confirmation_template = None
def test_form_invalid(self):
self.assertEqual(InventoryAdmin.confirmation_fields, ["quantity"])
inventory = InventoryFactory(quantity=1)
data = {
"quantity": 1,
"shop": "Invalid value",
"item": "Invalid value",
"id": inventory.id,
"_confirm_change": True,
"csrfmiddlewaretoken": "fake token",
}
response = self.client.post(
f"/admin/market/inventory/{inventory.id}/change/", data
)
# Form invalid should show erros on form
self.assertEqual(response.status_code, 200)
print(response.rendered_content)
self.assertIsNotNone(response.context_data.get("errors"))
self.assertEqual(
response.context_data["errors"][0],
["Select a valid choice. That choice is not one of the available choices."],
)
# Should not have updated inventory
inventory.refresh_from_db()
self.assertEqual(inventory.quantity, 1)
def test_confirmation_fields_set_with_confirm_change(self):
self.assertEqual(InventoryAdmin.confirmation_fields, ["quantity"])
inventory = InventoryFactory()
another_shop = ShopFactory()
data = {
"quantity": inventory.quantity,
"id": inventory.id,
"item": inventory.item.id,
"shop": another_shop.id,
"_confirm_change": True,
"csrfmiddlewaretoken": "fake token",
}
response = self.client.post(
f"/admin/market/inventory/{inventory.id}/change/", data
)
# Should not have shown confirmation page since shop did not change
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse("admin:market_inventory_changelist"))
# Should have updated inventory
inventory.refresh_from_db()
self.assertEqual(inventory.shop, another_shop)
def test_confirmation_fields_set_with_confirm_add(self):
self.assertEqual(InventoryAdmin.confirmation_fields, ["quantity"])
item = ItemFactory()
shop = ShopFactory()
# Don't set quantity - let it default
data = {"shop": shop.id, "item": item.id, "_confirm_add": True}
response = self.client.post(reverse("admin:market_inventory_add"), data)
# No confirmation needed
self.assertEqual(response.status_code, 302)
# Should have been added
self.assertEqual(Inventory.objects.count(), 1)
new_inventory = Inventory.objects.all().first()
self.assertEqual(new_inventory.shop, shop)
self.assertEqual(new_inventory.item, item)
self.assertEqual(
new_inventory.quantity, Inventory._meta.get_field("quantity").default
)
def test_no_change_permissions(self):
user = User.objects.create_user(username="user", is_staff=True)
self.client.force_login(user)
inventory = InventoryFactory()
data = {
"quantity": 1000,
"id": inventory.id,
"item": inventory.item.id,
"shop": inventory.shop.id,
"_confirm_change": True,
"csrfmiddlewaretoken": "fake token",
}
response = self.client.post(
f"/admin/market/inventory/{inventory.id}/change/", data
)
self.assertEqual(response.status_code, 403)
self.assertTrue(isinstance(response, HttpResponseForbidden))
old_quantity = inventory.quantity
inventory.refresh_from_db()
self.assertEqual(inventory.quantity, old_quantity)
def test_no_add_permissions(self):
user = User.objects.create_user(username="user", is_staff=True)
self.client.force_login(user)
item = ItemFactory()
shop = ShopFactory()
data = {"shop": shop.id, "item": item.id, "quantity": 5, "_confirm_add": True}
response = self.client.post(reverse("admin:market_inventory_add"), data)
# Ensure not redirected (confirmation page does not redirect)
self.assertEqual(response.status_code, 403)
self.assertTrue(isinstance(response, HttpResponseForbidden))
# Should not have been added
self.assertEqual(Inventory.objects.count(), 0)
def test_obj_not_found(self):
inventory = InventoryFactory()
data = {
"quantity": 1000,
"id": 100,
"item": inventory.item.id,
"shop": inventory.shop.id,
"_confirm_change": True,
"csrfmiddlewaretoken": "fake token",
}
response = self.client.post("/admin/market/inventory/100/change/", data)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/admin/")
self.assertEqual(response.reason_phrase, "Found")
old_quantity = inventory.quantity
inventory.refresh_from_db()
self.assertEqual(inventory.quantity, old_quantity)
self.assertEqual(Inventory.objects.count(), 1)
def test_handles_to_field_not_allowed(self):
item = ItemFactory()
shop = ShopFactory()
data = {
"shop": shop.id,
"item": item.id,
"quantity": 5,
"_confirm_add": True,
TO_FIELD_VAR: "shop",
}
response = self.client.post(reverse("admin:market_inventory_add"), data)
# Ensure not redirected (confirmation page does not redirect)
self.assertEqual(response.status_code, 400)
self.assertTrue(isinstance(response, HttpResponseBadRequest))
self.assertEqual(response.reason_phrase, "Bad Request")
self.assertEqual(
response.context.get("exception_value"),
"The field shop cannot be referenced.",
)
# Should not have been added
self.assertEqual(Inventory.objects.count(), 0)

21
coverage.svg 100644
View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="99" height="20">
<linearGradient id="b" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/>
</linearGradient>
<mask id="a">
<rect width="99" height="20" rx="3" fill="#fff"/>
</mask>
<g mask="url(#a)">
<path fill="#555" d="M0 0h63v20H0z"/>
<path fill="#4c1" d="M63 0h36v20H63z"/>
<path fill="url(#b)" d="M0 0h99v20H0z"/>
</g>
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
<text x="31.5" y="15" fill="#010101" fill-opacity=".3">coverage</text>
<text x="31.5" y="14">coverage</text>
<text x="80" y="15" fill="#010101" fill-opacity=".3">100%</text>
<text x="80" y="14">100%</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 903 B

View File

@ -2,19 +2,19 @@ import os
from setuptools import setup from setuptools import setup
here = os.path.abspath(os.path.dirname(__file__)) here = os.path.abspath(os.path.dirname(__file__))
README = open(os.path.join(here, 'README.md')).read() README = open(os.path.join(here, "README.md")).read()
setup( setup(
name='django-admin-confirm', name="django-admin-confirm",
version='0.1', version="0.1",
packages=['admin_confirm'], packages=["admin_confirm"],
description='Adds confirmation to Django Admin changes and additions', description="Adds confirmation to Django Admin changes and additions",
long_description=README, long_description=README,
author='Thu Trang Pham', author="Thu Trang Pham",
author_email='thuutrangpham@gmail.com', author_email="thuutrangpham@gmail.com",
url='https://github.com/trangpham/django-admin-confirm/', url="https://github.com/trangpham/django-admin-confirm/",
license='Apache 2.0', license="Apache 2.0",
install_requires=[ install_requires=[
'Django>=1.7', "Django>=1.7",
] ],
) )

View File

@ -9,7 +9,7 @@ class ItemFactory(factory.django.DjangoModelFactory):
class Meta: class Meta:
model = Item model = Item
name = factory.Faker('name') name = factory.Faker("name")
price = factory.LazyAttribute(lambda _: randint(5, 500)) price = factory.LazyAttribute(lambda _: randint(5, 500))
currency = factory.LazyAttribute(lambda _: choice(Item.VALID_CURRENCIES)) currency = factory.LazyAttribute(lambda _: choice(Item.VALID_CURRENCIES))
@ -18,7 +18,7 @@ class ShopFactory(factory.django.DjangoModelFactory):
class Meta: class Meta:
model = Shop model = Shop
name = factory.Faker('name') name = factory.Faker("name")
class InventoryFactory(factory.django.DjangoModelFactory): class InventoryFactory(factory.django.DjangoModelFactory):
@ -28,4 +28,3 @@ class InventoryFactory(factory.django.DjangoModelFactory):
shop = factory.SubFactory(ShopFactory) shop = factory.SubFactory(ShopFactory)
item = factory.SubFactory(ItemFactory) item = factory.SubFactory(ItemFactory)
quantity = factory.Sequence(lambda n: n) quantity = factory.Sequence(lambda n: n)

View File

@ -5,7 +5,7 @@ import sys
def main(): def main():
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'test_project.settings') os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings")
try: try:
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
except ImportError as exc: except ImportError as exc:
@ -17,5 +17,5 @@ def main():
execute_from_command_line(sys.argv) execute_from_command_line(sys.argv)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@ -6,19 +6,19 @@ from .models import Item, Inventory, Shop
class ItemAdmin(AdminConfirmMixin, admin.ModelAdmin): class ItemAdmin(AdminConfirmMixin, admin.ModelAdmin):
list_display = ('name', 'price', 'currency') list_display = ("name", "price", "currency")
confirm_change = True confirm_change = True
class InventoryAdmin(AdminConfirmMixin, admin.ModelAdmin): class InventoryAdmin(AdminConfirmMixin, admin.ModelAdmin):
list_display = ('shop', 'item', 'quantity') list_display = ("shop", "item", "quantity")
confirm_change = True confirm_change = True
confirm_add = True confirm_add = True
confirmation_fields = ['shop'] confirmation_fields = ["quantity"]
class ShopAdmin(AdminConfirmMixin, admin.ModelAdmin): class ShopAdmin(AdminConfirmMixin, admin.ModelAdmin):
confirmation_fields = ['name'] confirmation_fields = ["name"]
admin.site.register(Item, ItemAdmin) admin.site.register(Item, ItemAdmin)

View File

@ -2,4 +2,4 @@ from django.apps import AppConfig
class MarketConfig(AppConfig): class MarketConfig(AppConfig):
name = 'market' name = "market"

View File

@ -8,25 +8,52 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = []
]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Item', name="Item",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('name', models.CharField(max_length=120)), "id",
('price', models.DecimalField(decimal_places=2, max_digits=5)), models.AutoField(
('currency', models.CharField(choices=[('CAD', 'CAD'), ('USD', 'USD')], max_length=3)), auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=120)),
("price", models.DecimalField(decimal_places=2, max_digits=5)),
(
"currency",
models.CharField(
choices=[("CAD", "CAD"), ("USD", "USD")], max_length=3
),
),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name='Stock', name="Stock",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('quantity', models.PositiveIntegerField()), "id",
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='all_stock', to='market.Item')), models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("quantity", models.PositiveIntegerField()),
(
"item",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="all_stock",
to="market.Item",
),
),
], ],
), ),
] ]

View File

@ -7,38 +7,63 @@ import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('market', '0001_initial'), ("market", "0001_initial"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Inventory', name="Inventory",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('quantity', models.PositiveIntegerField()), "id",
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='market.Item')), models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("quantity", models.PositiveIntegerField()),
(
"item",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="market.Item"
),
),
], ],
options={ options={
'ordering': ['shop', 'item__name'], "ordering": ["shop", "item__name"],
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='Shop', name="Shop",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('name', models.CharField(max_length=120)), "id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=120)),
], ],
), ),
migrations.DeleteModel( migrations.DeleteModel(
name='Stock', name="Stock",
), ),
migrations.AddField( migrations.AddField(
model_name='inventory', model_name="inventory",
name='shop', name="shop",
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='inventory', to='market.Shop'), field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="inventory",
to="market.Shop",
),
), ),
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(
name='inventory', name="inventory",
unique_together={('shop', 'item')}, unique_together={("shop", "item")},
), ),
] ]

View File

@ -0,0 +1,25 @@
# Generated by Django 3.0.10 on 2020-11-08 17:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("market", "0002_auto_20201031_2057"),
]
operations = [
migrations.AlterModelOptions(
name="inventory",
options={
"ordering": ["shop", "item__name"],
"verbose_name_plural": "Inventory",
},
),
migrations.AlterField(
model_name="inventory",
name="quantity",
field=models.PositiveIntegerField(blank=True, default=0, null=True),
),
]

View File

@ -3,8 +3,8 @@ from django.db import models
class Item(models.Model): class Item(models.Model):
VALID_CURRENCIES = ( VALID_CURRENCIES = (
('CAD', 'CAD'), ("CAD", "CAD"),
('USD', 'USD'), ("USD", "USD"),
) )
name = models.CharField(max_length=120) name = models.CharField(max_length=120)
price = models.DecimalField(max_digits=5, decimal_places=2) price = models.DecimalField(max_digits=5, decimal_places=2)
@ -23,10 +23,12 @@ class Shop(models.Model):
class Inventory(models.Model): class Inventory(models.Model):
class Meta: class Meta:
unique_together = ['shop', 'item'] unique_together = ["shop", "item"]
ordering = ['shop', 'item__name'] ordering = ["shop", "item__name"]
verbose_name_plural = 'Inventory' verbose_name_plural = "Inventory"
shop = models.ForeignKey(to=Shop, on_delete=models.CASCADE, related_name='inventory') shop = models.ForeignKey(
to=Shop, on_delete=models.CASCADE, related_name="inventory"
)
item = models.ForeignKey(to=Item, on_delete=models.CASCADE) item = models.ForeignKey(to=Item, on_delete=models.CASCADE)
quantity = models.PositiveIntegerField() quantity = models.PositiveIntegerField(default=0, null=True, blank=True)

View File

@ -19,67 +19,65 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '=yddl-40388w3e2hl$e8)revce=n67_idi8pfejtn3!+2%!_qt' SECRET_KEY = "=yddl-40388w3e2hl$e8)revce=n67_idi8pfejtn3!+2%!_qt"
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True
ALLOWED_HOSTS = ['127.0.0.1'] ALLOWED_HOSTS = ["127.0.0.1"]
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
'admin_confirm', "admin_confirm",
"django.contrib.admin",
'django.contrib.admin', "django.contrib.auth",
'django.contrib.auth', "django.contrib.contenttypes",
'django.contrib.contenttypes', "django.contrib.sessions",
'django.contrib.sessions', "django.contrib.messages",
'django.contrib.messages', "django.contrib.staticfiles",
'django.contrib.staticfiles', "tests.market",
'tests.market',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', "django.middleware.security.SecurityMiddleware",
'django.contrib.sessions.middleware.SessionMiddleware', "django.contrib.sessions.middleware.SessionMiddleware",
'django.middleware.common.CommonMiddleware', "django.middleware.common.CommonMiddleware",
'django.middleware.csrf.CsrfViewMiddleware', "django.middleware.csrf.CsrfViewMiddleware",
'django.contrib.auth.middleware.AuthenticationMiddleware', "django.contrib.auth.middleware.AuthenticationMiddleware",
'django.contrib.messages.middleware.MessageMiddleware', "django.contrib.messages.middleware.MessageMiddleware",
'django.middleware.clickjacking.XFrameOptionsMiddleware', "django.middleware.clickjacking.XFrameOptionsMiddleware",
] ]
ROOT_URLCONF = 'tests.test_project.urls' ROOT_URLCONF = "tests.test_project.urls"
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', "BACKEND": "django.template.backends.django.DjangoTemplates",
'DIRS': [], "DIRS": [],
'APP_DIRS': True, "APP_DIRS": True,
'OPTIONS': { "OPTIONS": {
'context_processors': [ "context_processors": [
'django.template.context_processors.debug', "django.template.context_processors.debug",
'django.template.context_processors.request', "django.template.context_processors.request",
'django.contrib.auth.context_processors.auth', "django.contrib.auth.context_processors.auth",
'django.contrib.messages.context_processors.messages', "django.contrib.messages.context_processors.messages",
], ],
}, },
}, },
] ]
WSGI_APPLICATION = 'tests.test_project.wsgi.application' WSGI_APPLICATION = "tests.test_project.wsgi.application"
# Database # Database
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases # https://docs.djangoproject.com/en/3.0/ref/settings/#databases
DATABASES = { DATABASES = {
'default': { "default": {
'ENGINE': 'django.db.backends.sqlite3', "ENGINE": "django.db.backends.sqlite3",
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
} }
} }
@ -89,16 +87,16 @@ DATABASES = {
AUTH_PASSWORD_VALIDATORS = [ AUTH_PASSWORD_VALIDATORS = [
{ {
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
}, },
{ {
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
}, },
{ {
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
}, },
{ {
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
}, },
] ]
@ -106,9 +104,9 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/3.0/topics/i18n/ # https://docs.djangoproject.com/en/3.0/topics/i18n/
LANGUAGE_CODE = 'en-us' LANGUAGE_CODE = "en-us"
TIME_ZONE = 'UTC' TIME_ZONE = "UTC"
USE_I18N = True USE_I18N = True
@ -120,4 +118,4 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.0/howto/static-files/ # https://docs.djangoproject.com/en/3.0/howto/static-files/
STATIC_URL = '/static/' STATIC_URL = "/static/"

View File

@ -2,5 +2,5 @@ from django.contrib import admin
from django.urls import path from django.urls import path
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path("admin/", admin.site.urls),
] ]

View File

@ -2,6 +2,6 @@ import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'test_project.settings') os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings")
application = get_wsgi_application() application = get_wsgi_application()