From 1333fcdea1790b3ab70a7d9e4d913b302b7e78b6 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Sun, 4 Sep 2016 11:30:29 +1000 Subject: [PATCH 1/9] Refactors flags and discount classes to be DRYer. --- registrasion/models/conditions.py | 142 ++++++++++++++++-------------- 1 file changed, 75 insertions(+), 67 deletions(-) diff --git a/registrasion/models/conditions.py b/registrasion/models/conditions.py index 41e1a320..837753e1 100644 --- a/registrasion/models/conditions.py +++ b/registrasion/models/conditions.py @@ -9,7 +9,75 @@ from django.utils.translation import ugettext_lazy as _ from model_utils.managers import InheritanceManager -# Product Modifiers +# Condition Types + +class TimeOrStockLimitCondition(models.Model): + ''' Attributes for a condition that is limited by timespan or a count of + purchased or reserved items. + + Attributes: + start_time (Optional[datetime]): When the condition should start being + true. + + end_time (Optional[datetime]): When the condition should stop being + true. + + limit (Optional[int]): How many items may fall under the condition + the condition until it stops being false -- for all users. + ''' + + class Meta: + abstract = True + + start_time = models.DateTimeField( + null=True, + blank=True, + verbose_name=_("Start time"), + help_text=_("When the condition should start being true"), + ) + end_time = models.DateTimeField( + null=True, + blank=True, + verbose_name=_("End time"), + help_text=_("When the condition should stop being true."), + ) + limit = models.PositiveIntegerField( + null=True, + blank=True, + verbose_name=_("Limit"), + help_text=_( + "How many times this condition may be applied for all users." + ), + ) + + +class VoucherCondition(models.Model): + ''' A condition is met when a voucher code is in the current cart. ''' + + class Meta: + abstract = True + + voucher = models.OneToOneField( + inventory.Voucher, + on_delete=models.CASCADE, + verbose_name=_("Voucher"), + db_index=True, + ) + + +class IncludedProductCondition(models.Model): + class Meta: + abstract = True + + enabling_products = models.ManyToManyField( + inventory.Product, + verbose_name=_("Including product"), + help_text=_("If one of these products are purchased, this condition " + "is met."), + ) + + +# Discounts @python_2_unicode_compatible class DiscountBase(models.Model): @@ -154,7 +222,7 @@ class DiscountForCategory(models.Model): quantity = models.PositiveIntegerField() -class TimeOrStockLimitDiscount(DiscountBase): +class TimeOrStockLimitDiscount(TimeOrStockLimitCondition, DiscountBase): ''' Discounts that are generally available, but are limited by timespan or usage count. This is for e.g. Early Bird discounts. @@ -175,27 +243,8 @@ class TimeOrStockLimitDiscount(DiscountBase): verbose_name = _("discount (time/stock limit)") verbose_name_plural = _("discounts (time/stock limit)") - start_time = models.DateTimeField( - 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"), - help_text=_("This discount will only be available before this time."), - ) - limit = models.PositiveIntegerField( - null=True, - blank=True, - verbose_name=_("Limit"), - help_text=_("This discount may only be applied this many times."), - ) - -class VoucherDiscount(DiscountBase): +class VoucherDiscount(VoucherCondition, DiscountBase): ''' Discounts that are enabled when a voucher code is in the current cart. These are normally configured in the Admin page at the same time as creating a Voucher object. @@ -210,15 +259,8 @@ class VoucherDiscount(DiscountBase): verbose_name = _("discount (enabled by voucher)") verbose_name_plural = _("discounts (enabled by voucher)") - voucher = models.OneToOneField( - inventory.Voucher, - on_delete=models.CASCADE, - verbose_name=_("Voucher"), - db_index=True, - ) - -class IncludedProductDiscount(DiscountBase): +class IncludedProductDiscount(IncludedProductCondition, DiscountBase): ''' Discounts that are enabled because another product has been purchased. e.g. A conference ticket includes a free t-shirt. @@ -233,13 +275,6 @@ class IncludedProductDiscount(DiscountBase): verbose_name = _("discount (product inclusions)") verbose_name_plural = _("discounts (product inclusions)") - enabling_products = models.ManyToManyField( - inventory.Product, - verbose_name=_("Including product"), - help_text=_("If one of these products are purchased, the discounts " - "below will be enabled."), - ) - class RoleDiscount(object): ''' Discounts that are enabled because the active user has a specific @@ -330,7 +365,7 @@ class FlagBase(models.Model): ) -class TimeOrStockLimitFlag(FlagBase): +class TimeOrStockLimitFlag(TimeOrStockLimitCondition, FlagBase): ''' Product groupings that can be used to enable a product during a specific date range, or when fewer than a limit of products have been sold. @@ -352,28 +387,9 @@ class TimeOrStockLimitFlag(FlagBase): verbose_name = _("flag (time/stock limit)") verbose_name_plural = _("flags (time/stock limit)") - start_time = models.DateTimeField( - null=True, - blank=True, - help_text=_("Products included in this condition will only be " - "available after this time."), - ) - end_time = models.DateTimeField( - null=True, - blank=True, - help_text=_("Products included in this condition will only be " - "available before this time."), - ) - limit = models.PositiveIntegerField( - null=True, - blank=True, - help_text=_("The number of items under this grouping that can be " - "purchased."), - ) - @python_2_unicode_compatible -class ProductFlag(FlagBase): +class ProductFlag(IncludedProductCondition, FlagBase): ''' The condition is met because a specific product is purchased. Attributes: @@ -389,12 +405,6 @@ class ProductFlag(FlagBase): def __str__(self): return "Enabled by products: " + str(self.enabling_products.all()) - enabling_products = models.ManyToManyField( - inventory.Product, - help_text=_("If one of these products are purchased, this condition " - "is met."), - ) - @python_2_unicode_compatible class CategoryFlag(FlagBase): @@ -422,7 +432,7 @@ class CategoryFlag(FlagBase): @python_2_unicode_compatible -class VoucherFlag(FlagBase): +class VoucherFlag(VoucherCondition, FlagBase): ''' The condition is met because a Voucher is present. This is for e.g. enabling sponsor tickets. ''' @@ -434,8 +444,6 @@ class VoucherFlag(FlagBase): def __str__(self): return "Enabled by voucher: %s" % self.voucher - voucher = models.OneToOneField(inventory.Voucher) - # @python_2_unicode_compatible class RoleFlag(object): From 63fe8196e2ba4bae277ccaf05729a5e16f3f43d7 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Sun, 4 Sep 2016 12:36:20 +1000 Subject: [PATCH 2/9] Adds SpeakerCondition, SpeakerDiscount, and SpeakerFlag --- registrasion/models/conditions.py | 72 +++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/registrasion/models/conditions.py b/registrasion/models/conditions.py index 837753e1..c04b453f 100644 --- a/registrasion/models/conditions.py +++ b/registrasion/models/conditions.py @@ -8,6 +8,8 @@ from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ from model_utils.managers import InheritanceManager +from symposion import proposals + # Condition Types @@ -77,6 +79,30 @@ class IncludedProductCondition(models.Model): ) +class SpeakerCondition(models.Model): + ''' Conditions that are met if a user is a presenter, or copresenter, + of a specific of presentation. ''' + + class Meta: + abstract = True + + is_presenter = models.BooleanField( + blank=True, + help_text=_("This condition is met if the user is the primary " + "presenter of a presentation."), + ) + is_copresenter = models.BooleanField( + blank=True, + help_text=_("This condition is met if the user is a copresenter of a " + "presentation."), + ) + proposal_kind = models.ManyToManyField( + proposals.models.ProposalKind, + help_text=_("The types of proposals that these users may be " + "presenters of."), + ) + + # Discounts @python_2_unicode_compatible @@ -276,6 +302,29 @@ class IncludedProductDiscount(IncludedProductCondition, DiscountBase): verbose_name_plural = _("discounts (product inclusions)") +class SpeakerDiscount(SpeakerCondition, DiscountBase): + ''' Discounts that are enabled because the user is a presenter or + co-presenter of a kind of presentation. + + Attributes: + is_presenter (bool): The condition should be met if the user is a + presenter of a presentation. + + is_copresenter (bool): The condition should be met if the user is a + copresenter of a presentation. + + proposal_kind ([symposion.proposals.models.ProposalKind, ...]): The + kinds of proposals that the user may be a presenter or + copresenter of for this condition to be met. + + ''' + + class Meta: + app_label = "registrasion" + verbose_name = _("discount (speaker)") + verbose_name_plural = _("discounts (speaker)") + + class RoleDiscount(object): ''' Discounts that are enabled because the active user has a specific role. This is for e.g. volunteers who can get a discount ticket. ''' @@ -445,6 +494,29 @@ class VoucherFlag(VoucherCondition, FlagBase): return "Enabled by voucher: %s" % self.voucher +class SpeakerFlag(SpeakerCondition, FlagBase): + ''' Conditions that are enabled because the user is a presenter or + co-presenter of a kind of presentation. + + Attributes: + is_presenter (bool): The condition should be met if the user is a + presenter of a presentation. + + is_copresenter (bool): The condition should be met if the user is a + copresenter of a presentation. + + proposal_kind ([symposion.proposals.models.ProposalKind, ...]): The + kinds of proposals that the user may be a presenter or + copresenter of for this condition to be met. + + ''' + + class Meta: + app_label = "registrasion" + verbose_name = _("flag (speaker)") + verbose_name_plural = _("flags (speaker)") + + # @python_2_unicode_compatible class RoleFlag(object): ''' The condition is met because the active user has a particular Role. From b3d86e214876b52638b66d3e34532dcd25ac7055 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Sun, 4 Sep 2016 12:39:52 +1000 Subject: [PATCH 3/9] Adds stub for SpeakerConditionController --- registrasion/controllers/conditions.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/registrasion/controllers/conditions.py b/registrasion/controllers/conditions.py index 72e592a2..831faa78 100644 --- a/registrasion/controllers/conditions.py +++ b/registrasion/controllers/conditions.py @@ -25,6 +25,8 @@ class ConditionController(object): conditions.CategoryFlag: CategoryConditionController, conditions.IncludedProductDiscount: ProductConditionController, conditions.ProductFlag: ProductConditionController, + conditions.SpeakerFlag: SpeakerConditionController, + conditions.SpeakerDiscount: SpeakerConditionController, conditions.TimeOrStockLimitDiscount: TimeOrStockLimitDiscountController, conditions.TimeOrStockLimitFlag: @@ -299,3 +301,13 @@ class VoucherConditionController(IsMetByFilter, ConditionController): a voucher that invokes that item's condition in one of their carts. ''' return queryset.filter(voucher__cart__user=user) + + +class SpeakerConditionController(IsMetByFilter, ConditionController): + + @classmethod + def pre_filter(self, queryset, user): + ''' Returns all of the items from queryset which are enabled by a user + being a presenter or copresenter of a proposal. ''' + + return queryset From 9134fa5ed29621f78c8ced7b11a88ab1b6b6bd53 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Sun, 4 Sep 2016 13:11:45 +1000 Subject: [PATCH 4/9] Initial version of test_speaker, which creates all of the boilerplate for proposals --- registrasion/tests/test_speaker.py | 106 +++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 registrasion/tests/test_speaker.py diff --git a/registrasion/tests/test_speaker.py b/registrasion/tests/test_speaker.py new file mode 100644 index 00000000..cdad2181 --- /dev/null +++ b/registrasion/tests/test_speaker.py @@ -0,0 +1,106 @@ +import pytz + +from django.core.exceptions import ValidationError + +from registrasion.models import commerce +from registrasion.models import conditions +from registrasion.controllers.category import CategoryController +from controller_helpers import TestingCartController +from controller_helpers import TestingInvoiceController +from registrasion.controllers.product import ProductController + +from symposion.conference import models as conference_models +from symposion.proposals import models as proposal_models +from symposion.speakers import models as speaker_models + +from test_cart import RegistrationCartTestCase + +UTC = pytz.timezone('UTC') + + +class SpeakerTestCase(RegistrationCartTestCase): + + @classmethod + def _create_proposals(cls): + ''' Creates two proposals: + + - User 1 will be presenter + - User 2 will be an additional presenter + + Each proposal is of a different ProposalKind. + ''' + + conference = conference_models.Conference.objects.create( + title="TEST CONFERENCE.", + ) + section = conference_models.Section.objects.create( + conference=conference, + name="TEST_SECTION", + slug="testsection", + ) + proposal_section = proposal_models.ProposalSection.objects.create( + section=section, + closed=False, + published=False, + ) + + kind_1 = proposal_models.ProposalKind.objects.create( + section=section, + name="Kind 1", + slug="kind1", + ) + kind_2 = proposal_models.ProposalKind.objects.create( + section=section, + name="Kind 2", + slug="kind2", + ) + + speaker_1 = speaker_models.Speaker.objects.create( + user=cls.USER_1, + name="Speaker 1", + annotation="", + ) + speaker_2 = speaker_models.Speaker.objects.create( + user=cls.USER_2, + name="Speaker 2", + annotation="", + ) + + proposal_1 = proposal_models.ProposalBase.objects.create( + kind=kind_1, + title="Proposal 1", + abstract="Abstract", + description="Description", + speaker=speaker_1, + ) + proposal_models.AdditionalSpeaker.objects.create( + speaker=speaker_2, + proposalbase=proposal_1, + status=proposal_models.AdditionalSpeaker.SPEAKING_STATUS_ACCEPTED, + ) + + proposal_2 = proposal_models.ProposalBase.objects.create( + kind=kind_2, + title="Proposal 2", + abstract="Abstract", + description="Description", + speaker=speaker_1, + ) + proposal_models.AdditionalSpeaker.objects.create( + speaker=speaker_2, + proposalbase=proposal_2, + status=proposal_models.AdditionalSpeaker.SPEAKING_STATUS_ACCEPTED, + ) + + cls.KIND_1 = kind_1 + cls.KIND_2 = kind_2 + cls.PROPOSAL_1 = proposal_1 + cls.PROPOSAL_2 = proposal_2 + + def test_create_proposals(self): + self._create_proposals() + + self.assertIsNotNone(self.KIND_1) + self.assertIsNotNone(self.KIND_2) + self.assertIsNotNone(self.PROPOSAL_1) + self.assertIsNotNone(self.PROPOSAL_2) From 786bc0324a59b6e76b5adba4c6c7cdba1a14b28d Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Sun, 4 Sep 2016 13:17:56 +1000 Subject: [PATCH 5/9] Stubs out tests for test_speaker --- registrasion/tests/test_speaker.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/registrasion/tests/test_speaker.py b/registrasion/tests/test_speaker.py index cdad2181..15a614c8 100644 --- a/registrasion/tests/test_speaker.py +++ b/registrasion/tests/test_speaker.py @@ -104,3 +104,12 @@ class SpeakerTestCase(RegistrationCartTestCase): self.assertIsNotNone(self.KIND_2) self.assertIsNotNone(self.PROPOSAL_1) self.assertIsNotNone(self.PROPOSAL_2) + + def test_primary_speaker_enables_item(self): + raise NotImplementedError() + + def test_additional_speaker_enables_item(self): + raise NotImplementedError() + + def test_speaker_on_different_proposal_kind_does_not_enable_item(self): + raise NotImplementedError() From 0b306fd59edc1d20617903fdc4ab6fe417d4a578 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Sun, 4 Sep 2016 13:41:49 +1000 Subject: [PATCH 6/9] Adds test for user being a primary presenter of a proposal --- registrasion/controllers/conditions.py | 4 +- registrasion/tests/test_speaker.py | 89 +++++++++++++++++++++++++- 2 files changed, 89 insertions(+), 4 deletions(-) diff --git a/registrasion/controllers/conditions.py b/registrasion/controllers/conditions.py index 831faa78..f9007096 100644 --- a/registrasion/controllers/conditions.py +++ b/registrasion/controllers/conditions.py @@ -310,4 +310,6 @@ class SpeakerConditionController(IsMetByFilter, ConditionController): ''' Returns all of the items from queryset which are enabled by a user being a presenter or copresenter of a proposal. ''' - return queryset + # User is a presenter + + return queryset.filter(proposal_kind__section__presentations__speaker__user=user) diff --git a/registrasion/tests/test_speaker.py b/registrasion/tests/test_speaker.py index 15a614c8..0262561d 100644 --- a/registrasion/tests/test_speaker.py +++ b/registrasion/tests/test_speaker.py @@ -12,6 +12,7 @@ from registrasion.controllers.product import ProductController from symposion.conference import models as conference_models from symposion.proposals import models as proposal_models from symposion.speakers import models as speaker_models +from symposion.reviews.models import promote_proposal from test_cart import RegistrationCartTestCase @@ -97,6 +98,32 @@ class SpeakerTestCase(RegistrationCartTestCase): cls.PROPOSAL_1 = proposal_1 cls.PROPOSAL_2 = proposal_2 + @classmethod + def _create_flag_for_primary_speaker(cls): + ''' Adds flag -- PROD_1 is not available unless user is a primary + presenter of a KIND_1 ''' + flag = conditions.SpeakerFlag.objects.create( + description="User must be presenter", + condition=conditions.FlagBase.ENABLE_IF_TRUE, + is_presenter=True, + is_copresenter=False, + ) + flag.proposal_kind.add(cls.KIND_1) + flag.products.add(cls.PROD_1) + + @classmethod + def _create_flag_for_additional_speaker(cls): + ''' Adds flag -- PROD_1 is not available unless user is a primary + presenter of a KIND_2 ''' + flag = conditions.SpeakerFlag.objects.create( + description="User must be copresenter", + condition=conditions.FlagBase.ENABLE_IF_TRUE, + is_presenter=False, + is_copresenter=True, + ) + flag.proposal_kind.add(cls.KIND_1) + flag.products.add(cls.PROD_1) + def test_create_proposals(self): self._create_proposals() @@ -106,10 +133,66 @@ class SpeakerTestCase(RegistrationCartTestCase): self.assertIsNotNone(self.PROPOSAL_2) def test_primary_speaker_enables_item(self): - raise NotImplementedError() + self._create_proposals() + self._create_flag_for_primary_speaker() + + # USER_1 cannot see PROD_1 until proposal is promoted. + available = ProductController.available_products( + self.USER_1, + products=[self.PROD_1], + ) + self.assertNotIn(self.PROD_1, available) + + # promote proposal_1 so that USER_1 becomes a speaker + promote_proposal(self.PROPOSAL_1) + + # USER_1 can see PROD_1 + available_1 = ProductController.available_products( + self.USER_1, + products=[self.PROD_1], + ) + self.assertIn(self.PROD_1, available_1) + # USER_2 can *NOT* see PROD_1 because they're a copresenter + available_2 = ProductController.available_products( + self.USER_2, + products=[self.PROD_1], + ) + self.assertNotIn(self.PROD_1, available_2) def test_additional_speaker_enables_item(self): - raise NotImplementedError() + self._create_proposals() + self._create_flag_for_additional_speaker() + + # USER_2 cannot see PROD_1 until proposal is promoted. + available = ProductController.available_products( + self.USER_2, + products=[self.PROD_1], + ) + self.assertNotIn(self.PROD_1, available) + + # promote proposal_1 so that USER_2 becomes an additional speaker + promote_proposal(self.PROPOSAL_1) + + # USER_2 can see PROD_1 + available_2 = ProductController.available_products( + self.USER_2, + products=[self.PROD_1], + ) + self.assertIn(self.PROD_1, available_2) + # USER_1 can *NOT* see PROD_1 because they're a presenter + available_1 = ProductController.available_products( + self.USER_1, + products=[self.PROD_1], + ) + self.assertNotIn(self.PROD_1, available_1) def test_speaker_on_different_proposal_kind_does_not_enable_item(self): - raise NotImplementedError() + self._create_proposals() + self._create_flag_for_primary_speaker() + + # USER_1 cannot see PROD_1 until proposal is promoted. + + # promote proposal_2 so that USER_1 becomes a speaker, but of + # KIND_2, which is not covered by this condition + + # USER_2 cannot see PROD_1 From 04eefa4e0e7642330a72fe527932261d4aa69131 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Sun, 4 Sep 2016 13:54:05 +1000 Subject: [PATCH 7/9] Passes first two tests --- registrasion/controllers/conditions.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/registrasion/controllers/conditions.py b/registrasion/controllers/conditions.py index f9007096..7ca8e431 100644 --- a/registrasion/controllers/conditions.py +++ b/registrasion/controllers/conditions.py @@ -310,6 +310,17 @@ class SpeakerConditionController(IsMetByFilter, ConditionController): ''' Returns all of the items from queryset which are enabled by a user being a presenter or copresenter of a proposal. ''' - # User is a presenter + u = user - return queryset.filter(proposal_kind__section__presentations__speaker__user=user) + # User is a presenter + user_is_presenter = Q( + is_presenter=True, + proposal_kind__section__presentations__speaker__user=u, + ) + # User is a copresenter + user_is_copresenter = Q( + is_copresenter=True, + proposal_kind__section__presentations__additional_speakers__user=u, + ) + + return queryset.filter(user_is_presenter | user_is_copresenter) From af30063a92c07c50ba8646d6eea8802944630c10 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Sun, 4 Sep 2016 14:00:56 +1000 Subject: [PATCH 8/9] Adds final test, all three now pass. --- registrasion/controllers/conditions.py | 5 ++--- registrasion/tests/test_speaker.py | 13 ++++++++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/registrasion/controllers/conditions.py b/registrasion/controllers/conditions.py index 7ca8e431..ac198347 100644 --- a/registrasion/controllers/conditions.py +++ b/registrasion/controllers/conditions.py @@ -311,16 +311,15 @@ class SpeakerConditionController(IsMetByFilter, ConditionController): being a presenter or copresenter of a proposal. ''' u = user - # User is a presenter user_is_presenter = Q( is_presenter=True, - proposal_kind__section__presentations__speaker__user=u, + proposal_kind__proposalbase__presentation__speaker__user=u, ) # User is a copresenter user_is_copresenter = Q( is_copresenter=True, - proposal_kind__section__presentations__additional_speakers__user=u, + proposal_kind__proposalbase__presentation__additional_speakers__user=u, ) return queryset.filter(user_is_presenter | user_is_copresenter) diff --git a/registrasion/tests/test_speaker.py b/registrasion/tests/test_speaker.py index 0262561d..e3b14ed0 100644 --- a/registrasion/tests/test_speaker.py +++ b/registrasion/tests/test_speaker.py @@ -191,8 +191,19 @@ class SpeakerTestCase(RegistrationCartTestCase): self._create_flag_for_primary_speaker() # USER_1 cannot see PROD_1 until proposal is promoted. + available = ProductController.available_products( + self.USER_1, + products=[self.PROD_1], + ) + self.assertNotIn(self.PROD_1, available) # promote proposal_2 so that USER_1 becomes a speaker, but of # KIND_2, which is not covered by this condition + promote_proposal(self.PROPOSAL_2) - # USER_2 cannot see PROD_1 + # USER_1 cannot see PROD_1 + available_1 = ProductController.available_products( + self.USER_1, + products=[self.PROD_1], + ) + self.assertNotIn(self.PROD_1, available_1) From c2a702d699f70b68860e5a4bd433d4c91fd1338d Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Sun, 4 Sep 2016 14:21:30 +1000 Subject: [PATCH 9/9] Adds admin and migration for speaker tickets. --- registrasion/admin.py | 43 +++++---- .../migrations/0003_auto_20160904_0235.py | 90 +++++++++++++++++++ 2 files changed, 115 insertions(+), 18 deletions(-) create mode 100644 registrasion/migrations/0003_auto_20160904_0235.py diff --git a/registrasion/admin.py b/registrasion/admin.py index 35f73e9f..4d704c9b 100644 --- a/registrasion/admin.py +++ b/registrasion/admin.py @@ -12,6 +12,7 @@ class EffectsDisplayMixin(object): def effects(self, obj): return list(obj.effects()) + # Inventory admin @@ -69,14 +70,11 @@ class TimeOrStockLimitDiscountAdmin(admin.ModelAdmin, EffectsDisplayMixin): @admin.register(conditions.IncludedProductDiscount) -class IncludedProductDiscountAdmin(admin.ModelAdmin): +class IncludedProductDiscountAdmin(admin.ModelAdmin, EffectsDisplayMixin): def enablers(self, obj): return list(obj.enabling_products.all()) - def effects(self, obj): - return list(obj.effects()) - list_display = ("description", "enablers", "effects") inlines = [ @@ -84,6 +82,20 @@ class IncludedProductDiscountAdmin(admin.ModelAdmin): DiscountForCategoryInline, ] +@admin.register(conditions.SpeakerDiscount) +class SpeakerDiscountAdmin(admin.ModelAdmin, EffectsDisplayMixin): + + fields = ("description", "is_presenter", "is_copresenter", "proposal_kind") + + list_display = ("description", "is_presenter", "is_copresenter", "effects") + + ordering = ("-is_presenter", "-is_copresenter") + + inlines = [ + DiscountForProductInline, + DiscountForCategoryInline, + ] + # Vouchers @@ -172,18 +184,13 @@ class CategoryFlagAdmin( ordering = ("enabling_category",) -# Enabling conditions -@admin.register(conditions.TimeOrStockLimitFlag) -class TimeOrStockLimitFlagAdmin( - nested_admin.NestedAdmin, - EffectsDisplayMixin): - model = conditions.TimeOrStockLimitFlag +@admin.register(conditions.SpeakerFlag) +class SpeakerFlagAdmin(nested_admin.NestedAdmin, EffectsDisplayMixin): - list_display = ( - "description", - "start_time", - "end_time", - "limit", - "effects", - ) - ordering = ("start_time", "end_time", "limit") + model = conditions.SpeakerFlag + fields = ("description", "is_presenter", "is_copresenter", "proposal_kind", + "products", "categories") + + list_display = ("description", "is_presenter", "is_copresenter", "effects") + + ordering = ("-is_presenter", "-is_copresenter") diff --git a/registrasion/migrations/0003_auto_20160904_0235.py b/registrasion/migrations/0003_auto_20160904_0235.py new file mode 100644 index 00000000..77b74ab0 --- /dev/null +++ b/registrasion/migrations/0003_auto_20160904_0235.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-09-04 02:35 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('symposion_proposals', '0001_initial'), + ('registrasion', '0002_auto_20160822_0034'), + ] + + operations = [ + migrations.CreateModel( + name='SpeakerDiscount', + fields=[ + ('discountbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='registrasion.DiscountBase')), + ('is_presenter', models.BooleanField(help_text='This condition is met if the user is the primary presenter of a presentation.')), + ('is_copresenter', models.BooleanField(help_text='This condition is met if the user is a copresenter of a presentation.')), + ('proposal_kind', models.ManyToManyField(help_text='The types of proposals that these users may be presenters of.', to='symposion_proposals.ProposalKind')), + ], + options={ + 'verbose_name': 'discount (speaker)', + 'verbose_name_plural': 'discounts (speaker)', + }, + bases=('registrasion.discountbase', models.Model), + ), + migrations.CreateModel( + name='SpeakerFlag', + fields=[ + ('flagbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='registrasion.FlagBase')), + ('is_presenter', models.BooleanField(help_text='This condition is met if the user is the primary presenter of a presentation.')), + ('is_copresenter', models.BooleanField(help_text='This condition is met if the user is a copresenter of a presentation.')), + ('proposal_kind', models.ManyToManyField(help_text='The types of proposals that these users may be presenters of.', to='symposion_proposals.ProposalKind')), + ], + options={ + 'verbose_name': 'flag (speaker)', + 'verbose_name_plural': 'flags (speaker)', + }, + bases=('registrasion.flagbase', models.Model), + ), + migrations.AlterField( + model_name='includedproductdiscount', + name='enabling_products', + field=models.ManyToManyField(help_text='If one of these products are purchased, this condition is met.', to='registrasion.Product', verbose_name='Including product'), + ), + migrations.AlterField( + model_name='productflag', + name='enabling_products', + field=models.ManyToManyField(help_text='If one of these products are purchased, this condition is met.', to='registrasion.Product', verbose_name='Including product'), + ), + migrations.AlterField( + model_name='timeorstocklimitdiscount', + name='end_time', + field=models.DateTimeField(blank=True, help_text='When the condition should stop being true.', null=True, verbose_name='End time'), + ), + migrations.AlterField( + model_name='timeorstocklimitdiscount', + name='limit', + field=models.PositiveIntegerField(blank=True, help_text='How many times this condition may be applied for all users.', null=True, verbose_name='Limit'), + ), + migrations.AlterField( + model_name='timeorstocklimitdiscount', + name='start_time', + field=models.DateTimeField(blank=True, help_text='When the condition should start being true', null=True, verbose_name='Start time'), + ), + migrations.AlterField( + model_name='timeorstocklimitflag', + name='end_time', + field=models.DateTimeField(blank=True, help_text='When the condition should stop being true.', null=True, verbose_name='End time'), + ), + migrations.AlterField( + model_name='timeorstocklimitflag', + name='limit', + field=models.PositiveIntegerField(blank=True, help_text='How many times this condition may be applied for all users.', null=True, verbose_name='Limit'), + ), + migrations.AlterField( + model_name='timeorstocklimitflag', + name='start_time', + field=models.DateTimeField(blank=True, help_text='When the condition should start being true', null=True, verbose_name='Start time'), + ), + migrations.AlterField( + model_name='voucherflag', + name='voucher', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='registrasion.Voucher', verbose_name='Voucher'), + ), + ]