diff --git a/registrasion/admin.py b/registrasion/admin.py index 17969cd1..7155e177 100644 --- a/registrasion/admin.py +++ b/registrasion/admin.py @@ -1,4 +1,5 @@ from django.contrib import admin +from django.core.exceptions import ObjectDoesNotExist from django.utils.translation import ugettext_lazy as _ import nested_admin @@ -6,21 +7,36 @@ import nested_admin from registrasion import models as rego +class EffectsDisplayMixin(object): + def effects(self, obj): + return list(obj.effects()) + # Inventory admin + class ProductInline(admin.TabularInline): model = rego.Product + ordering = ("order", ) @admin.register(rego.Category) class CategoryAdmin(admin.ModelAdmin): model = rego.Category - verbose_name_plural = _("Categories") + fields = ("name", "description", "required", "render_type", + "limit_per_user", "order",) + list_display = ("name", "description") + ordering = ("order", ) inlines = [ ProductInline, ] -admin.site.register(rego.Product) + +@admin.register(rego.Product) +class ProductAdmin(admin.ModelAdmin): + model = rego.Product + list_display = ("name", "category", "description") + list_filter = ("category", ) + ordering = ("category__order", "order", ) # Discounts @@ -37,11 +53,34 @@ class DiscountForCategoryInline(admin.TabularInline): verbose_name_plural = _("Categories included in discount") -@admin.register( - rego.TimeOrStockLimitDiscount, - rego.IncludedProductDiscount, -) -class DiscountAdmin(admin.ModelAdmin): +@admin.register(rego.TimeOrStockLimitDiscount) +class TimeOrStockLimitDiscountAdmin(admin.ModelAdmin, EffectsDisplayMixin): + list_display = ( + "description", + "start_time", + "end_time", + "limit", + "effects", + ) + ordering = ("start_time", "end_time", "limit") + + inlines = [ + DiscountForProductInline, + DiscountForCategoryInline, + ] + + +@admin.register(rego.IncludedProductDiscount) +class IncludedProductDiscountAdmin(admin.ModelAdmin): + + def enablers(self, obj): + return list(obj.enabling_products.all()) + + def effects(self, obj): + return list(obj.effects()) + + list_display = ("description", "enablers", "effects") + inlines = [ DiscountForProductInline, DiscountForCategoryInline, @@ -75,7 +114,30 @@ class VoucherEnablingConditionInline(nested_admin.NestedStackedInline): @admin.register(rego.Voucher) class VoucherAdmin(nested_admin.NestedAdmin): + + def effects(self, obj): + ''' List the effects of the voucher in the admin. ''' + out = [] + + try: + discount_effects = obj.voucherdiscount.effects() + except ObjectDoesNotExist: + discount_effects = None + + try: + enabling_effects = obj.voucherenablingcondition.effects() + except ObjectDoesNotExist: + enabling_effects = None + + if discount_effects: + out.append("Discounts: " + str(list(discount_effects))) + if enabling_effects: + out.append("Enables: " + str(list(enabling_effects))) + + return "\n".join(out) + model = rego.Voucher + list_display = ("recipient", "code", "effects") inlines = [ VoucherDiscountInline, VoucherEnablingConditionInline, @@ -84,11 +146,46 @@ class VoucherAdmin(nested_admin.NestedAdmin): # Enabling conditions @admin.register(rego.ProductEnablingCondition) -class ProductEnablingConditionAdmin(nested_admin.NestedAdmin): +class ProductEnablingConditionAdmin( + nested_admin.NestedAdmin, + EffectsDisplayMixin): + + def enablers(self, obj): + return list(obj.enabling_products.all()) + model = rego.ProductEnablingCondition + fields = ("description", "enabling_products", "mandatory", "products", + "categories"), + + list_display = ("description", "enablers", "effects") # Enabling conditions @admin.register(rego.CategoryEnablingCondition) -class CategoryEnablingConditionAdmin(nested_admin.NestedAdmin): +class CategoryEnablingConditionAdmin( + nested_admin.NestedAdmin, + EffectsDisplayMixin): + model = rego.CategoryEnablingCondition + fields = ("description", "enabling_category", "mandatory", "products", + "categories"), + + list_display = ("description", "enabling_category", "effects") + ordering = ("enabling_category",) + + +# Enabling conditions +@admin.register(rego.TimeOrStockLimitEnablingCondition) +class TimeOrStockLimitEnablingConditionAdmin( + nested_admin.NestedAdmin, + EffectsDisplayMixin): + model = rego.TimeOrStockLimitEnablingCondition + + list_display = ( + "description", + "start_time", + "end_time", + "limit", + "effects", + ) + ordering = ("start_time", "end_time", "limit") diff --git a/registrasion/migrations/0009_auto_20160330_2336.py b/registrasion/migrations/0009_auto_20160330_2336.py new file mode 100644 index 00000000..9d81a3b6 --- /dev/null +++ b/registrasion/migrations/0009_auto_20160330_2336.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import datetime + + +class Migration(migrations.Migration): + + dependencies = [ + ('registrasion', '0008_cart_released'), + ] + + operations = [ + migrations.AlterModelOptions( + name='category', + options={'verbose_name_plural': 'categories'}, + ), + migrations.AlterModelOptions( + name='timeorstocklimitenablingcondition', + options={'verbose_name': 'ceiling'}, + ), + migrations.AlterField( + model_name='category', + name='limit_per_user', + field=models.PositiveIntegerField(help_text='The total number of items from this category one attendee may purchase.', null=True, verbose_name='Limit per user', blank=True), + ), + migrations.AlterField( + model_name='category', + name='render_type', + field=models.IntegerField(help_text='The registration form will render this category in this style.', verbose_name='Render type', choices=[(1, 'Radio button'), (2, 'Quantity boxes')]), + ), + migrations.AlterField( + model_name='category', + name='required', + field=models.BooleanField(help_text='If enabled, a user must select an item from this category.'), + ), + migrations.AlterField( + model_name='categoryenablingcondition', + name='enabling_category', + field=models.ForeignKey(help_text='If a product from this category is purchased, this condition is met.', to='registrasion.Category'), + ), + migrations.AlterField( + model_name='discountbase', + name='description', + field=models.CharField(help_text='A description of this discount. This will be included on invoices where this discount is applied.', max_length=255, verbose_name='Description'), + ), + migrations.AlterField( + model_name='enablingconditionbase', + name='categories', + field=models.ManyToManyField(help_text='Categories whose products are enabled if this condition is met.', to='registrasion.Category', blank=True), + ), + migrations.AlterField( + model_name='enablingconditionbase', + name='mandatory', + field=models.BooleanField(default=False, help_text='If there is at least one mandatory condition defined on a product or category, all such conditions must be met. Otherwise, at least one non-mandatory condition must be met.'), + ), + migrations.AlterField( + model_name='enablingconditionbase', + name='products', + field=models.ManyToManyField(help_text='Products that are enabled if this condition is met.', to='registrasion.Product', blank=True), + ), + migrations.AlterField( + model_name='includedproductdiscount', + name='enabling_products', + field=models.ManyToManyField(help_text='If one of these products are purchased, the discounts below will be enabled.', to='registrasion.Product', verbose_name='Including product'), + ), + migrations.AlterField( + model_name='product', + name='description', + field=models.CharField(max_length=255, null=True, verbose_name='Description', blank=True), + ), + migrations.AlterField( + model_name='product', + name='reservation_duration', + field=models.DurationField(default=datetime.timedelta(0, 3600), help_text='The length of time this product will be reserved before it is released for someone else to purchase.', verbose_name='Reservation duration'), + ), + migrations.AlterField( + model_name='productenablingcondition', + name='enabling_products', + field=models.ManyToManyField(help_text='If one of these products are purchased, this condition is met.', to='registrasion.Product'), + ), + migrations.AlterField( + model_name='timeorstocklimitdiscount', + name='end_time', + field=models.DateTimeField(help_text='This discount will only be available before this time.', null=True, verbose_name='End time', blank=True), + ), + migrations.AlterField( + model_name='timeorstocklimitdiscount', + name='limit', + field=models.PositiveIntegerField(help_text='This discount may only be applied this many times.', null=True, verbose_name='Limit', blank=True), + ), + migrations.AlterField( + model_name='timeorstocklimitdiscount', + name='start_time', + field=models.DateTimeField(help_text='This discount will only be available after this time.', null=True, verbose_name='Start time', blank=True), + ), + migrations.AlterField( + model_name='timeorstocklimitenablingcondition', + name='end_time', + field=models.DateTimeField(help_text='Products included in this condition will only be available before this time.', null=True), + ), + migrations.AlterField( + model_name='timeorstocklimitenablingcondition', + name='limit', + field=models.PositiveIntegerField(help_text='The number of items under this grouping that can be purchased.', null=True), + ), + migrations.AlterField( + model_name='timeorstocklimitenablingcondition', + name='start_time', + field=models.DateTimeField(help_text='Products included in this condition will only be available after this time.', null=True), + ), + ] diff --git a/registrasion/models.py b/registrasion/models.py index 2e1f95d1..5887394c 100644 --- a/registrasion/models.py +++ b/registrasion/models.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import datetime +import itertools from django.core.exceptions import ValidationError from django.core.exceptions import ObjectDoesNotExist @@ -118,6 +119,9 @@ class BadgeAndProfile(models.Model): class Category(models.Model): ''' Registration product categories ''' + class Meta: + verbose_name_plural = _("categories") + def __str__(self): return self.name @@ -129,17 +133,35 @@ class Category(models.Model): (RENDER_TYPE_QUANTITY, _("Quantity boxes")), ] - name = models.CharField(max_length=65, verbose_name=_("Name")) - description = models.CharField(max_length=255, - verbose_name=_("Description")) + name = models.CharField( + max_length=65, + verbose_name=_("Name"), + ) + description = models.CharField( + max_length=255, + verbose_name=_("Description"), + ) limit_per_user = models.PositiveIntegerField( null=True, blank=True, - verbose_name=_("Limit per user")) - required = models.BooleanField(blank=True) - order = models.PositiveIntegerField(verbose_name=("Display order")) - render_type = models.IntegerField(choices=CATEGORY_RENDER_TYPES, - verbose_name=_("Render type")) + verbose_name=_("Limit per user"), + help_text=_("The total number of items from this category one " + "attendee may purchase."), + ) + required = models.BooleanField( + blank=True, + help_text=_("If enabled, a user must select an " + "item from this category."), + ) + order = models.PositiveIntegerField( + verbose_name=("Display order"), + ) + render_type = models.IntegerField( + choices=CATEGORY_RENDER_TYPES, + verbose_name=_("Render type"), + help_text=_("The registration form will render this category in this " + "style."), + ) @python_2_unicode_compatible @@ -147,23 +169,41 @@ class Product(models.Model): ''' Registration products ''' def __str__(self): - return self.name + return "%s - %s" % (self.category.name, self.name) - name = models.CharField(max_length=65, verbose_name=_("Name")) - description = models.CharField(max_length=255, - verbose_name=_("Description")) - category = models.ForeignKey(Category, verbose_name=_("Product category")) - price = models.DecimalField(max_digits=8, - decimal_places=2, - verbose_name=_("Price")) + name = models.CharField( + max_length=65, + verbose_name=_("Name"), + ) + description = models.CharField( + max_length=255, + verbose_name=_("Description"), + null=True, + blank=True, + ) + category = models.ForeignKey( + Category, + verbose_name=_("Product category") + ) + price = models.DecimalField( + max_digits=8, + decimal_places=2, + verbose_name=_("Price"), + ) limit_per_user = models.PositiveIntegerField( null=True, blank=True, - verbose_name=_("Limit per user")) + verbose_name=_("Limit per user"), + ) reservation_duration = models.DurationField( default=datetime.timedelta(hours=1), - verbose_name=_("Reservation duration")) - order = models.PositiveIntegerField(verbose_name=("Display order")) + verbose_name=_("Reservation duration"), + help_text=_("The length of time this product will be reserved before " + "it is released for someone else to purchase."), + ) + order = models.PositiveIntegerField( + verbose_name=("Display order"), + ) @python_2_unicode_compatible @@ -206,8 +246,18 @@ class DiscountBase(models.Model): def __str__(self): return "Discount: " + self.description - description = models.CharField(max_length=255, - verbose_name=_("Description")) + def effects(self): + ''' Returns all of the effects of this discount. ''' + products = self.discountforproduct_set.all() + categories = self.discountforcategory_set.all() + return itertools.chain(products, categories) + + description = models.CharField( + max_length=255, + verbose_name=_("Description"), + help_text=_("A description of this discount. This will be included on " + "invoices where this discount is applied."), + ) @python_2_unicode_compatible @@ -292,11 +342,23 @@ class TimeOrStockLimitDiscount(DiscountBase): verbose_name = _("Promotional discount") start_time = models.DateTimeField( - null=True, blank=True, verbose_name=_("Start time")) + null=True, + blank=True, + verbose_name=_("Start time"), + help_text=_("This discount will only be available after this time."), + ) end_time = models.DateTimeField( - null=True, blank=True, verbose_name=_("End time")) + null=True, + blank=True, + verbose_name=_("End time"), + help_text=_("This discount will only be available before this time."), + ) limit = models.PositiveIntegerField( - null=True, blank=True, verbose_name=_("Limit")) + null=True, + blank=True, + verbose_name=_("Limit"), + help_text=_("This discount may only be applied this many times."), + ) class VoucherDiscount(DiscountBase): @@ -306,7 +368,8 @@ class VoucherDiscount(DiscountBase): voucher = models.OneToOneField( Voucher, on_delete=models.CASCADE, - verbose_name=_("Voucher")) + verbose_name=_("Voucher"), + ) class IncludedProductDiscount(DiscountBase): @@ -318,7 +381,10 @@ class IncludedProductDiscount(DiscountBase): enabling_products = models.ManyToManyField( Product, - verbose_name=_("Including product")) + verbose_name=_("Including product"), + help_text=_("If one of these products are purchased, the discounts " + "below will be enabled."), + ) class RoleDiscount(object): @@ -340,20 +406,54 @@ class EnablingConditionBase(models.Model): objects = InheritanceManager() def __str__(self): - return self.name + return self.description + + def effects(self): + ''' Returns all of the items enabled by this condition. ''' + return itertools.chain(self.products.all(), self.categories.all()) description = models.CharField(max_length=255) - mandatory = models.BooleanField(default=False) - products = models.ManyToManyField(Product, blank=True) - categories = models.ManyToManyField(Category, blank=True) + mandatory = models.BooleanField( + default=False, + help_text=_("If there is at least one mandatory condition defined on " + "a product or category, all such conditions must be met. " + "Otherwise, at least one non-mandatory condition must be " + "met."), + ) + products = models.ManyToManyField( + Product, + blank=True, + help_text=_("Products that are enabled if this condition is met."), + ) + categories = models.ManyToManyField( + Category, + blank=True, + help_text=_("Categories whose products are enabled if this condition " + "is met."), + ) class TimeOrStockLimitEnablingCondition(EnablingConditionBase): ''' Registration product ceilings ''' - start_time = models.DateTimeField(null=True, verbose_name=_("Start time")) - end_time = models.DateTimeField(null=True, verbose_name=_("End time")) - limit = models.PositiveIntegerField(null=True, verbose_name=_("Limit")) + class Meta: + verbose_name = _("ceiling") + + start_time = models.DateTimeField( + null=True, + help_text=_("Products included in this condition will only be " + "available after this time."), + ) + end_time = models.DateTimeField( + null=True, + help_text=_("Products included in this condition will only be " + "available before this time."), + ) + limit = models.PositiveIntegerField( + null=True, + help_text=_("The number of items under this grouping that can be " + "purchased."), + ) @python_2_unicode_compatible @@ -361,9 +461,13 @@ class ProductEnablingCondition(EnablingConditionBase): ''' The condition is met because a specific product is purchased. ''' def __str__(self): - return "Enabled by product: " + return "Enabled by products: " + str(self.enabling_products.all()) - enabling_products = models.ManyToManyField(Product) + enabling_products = models.ManyToManyField( + Product, + help_text=_("If one of these products are purchased, this condition " + "is met."), + ) @python_2_unicode_compatible @@ -372,9 +476,13 @@ class CategoryEnablingCondition(EnablingConditionBase): purchased. ''' def __str__(self): - return "Enabled by product in category: " + return "Enabled by product in category: " + str(self.enabling_category) - enabling_category = models.ForeignKey(Category) + enabling_category = models.ForeignKey( + Category, + help_text=_("If a product from this category is purchased, this " + "condition is met."), + ) @python_2_unicode_compatible