Skip to content

Commit

Permalink
Update benefits validation rules (#2120)
Browse files Browse the repository at this point in the history
* Add flag to control if package allows a la carte benefits

* Applications for standalone benefits cannot have packages

* Don't validate a la carte application with a disabled a la carte package

* Don't display Potential a la carte option if package disabled it

* Disable standalone inputs if package was selected

* Fix breaking tests which have standalone benefits with packages and a la carte ones

* Disable a la carte benefits if package disables it

* Force a la carte and standalone cards positions to the start of the div

* Disable a la carte benefits when form is empty

* Add title to inputs to provide more information about the disabled inputs

* ensure state for sold-out a la carte benefits is rendered in UI

* un-select any a la carte when selecting a package that doesn't allow them

* add notes to a la carte and standalone instructions to clarify why options are enabled/disabled

* Initial load of benefits state on the page load.

* Display clear form button at the bottom of the form

* Add JS code to clear form and reset application form to its initial state

Co-authored-by: Ee Durbin <ewdurbin@gmail.com>
  • Loading branch information
berinhard and ewdurbin committed Aug 16, 2022
1 parent 9f967f2 commit ae08791
Show file tree
Hide file tree
Showing 8 changed files with 188 additions and 23 deletions.
4 changes: 2 additions & 2 deletions sponsors/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,8 @@ def update_related_sponsorships(self, *args, **kwargs):
@admin.register(SponsorshipPackage)
class SponsorshipPackageAdmin(OrderedModelAdmin):
ordering = ("-year", "order",)
list_display = ["name", "year", "advertise", "move_up_down_links"]
list_filter = ["advertise", "year"]
list_display = ["name", "year", "advertise", "allow_a_la_carte", "move_up_down_links"]
list_filter = ["advertise", "year", "allow_a_la_carte"]
search_fields = ["name"]

def get_readonly_fields(self, request, obj=None):
Expand Down
9 changes: 9 additions & 0 deletions sponsors/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ def _clean_benefits(self, cleaned_data):
"""
package = cleaned_data.get("package")
benefits = self.get_benefits(cleaned_data, include_a_la_carte=True)
a_la_carte = cleaned_data.get("a_la_carte_benefits")
standalone = cleaned_data.get("standalone_benefits")

if not benefits and not standalone:
Expand All @@ -151,6 +152,14 @@ def _clean_benefits(self, cleaned_data):
raise forms.ValidationError(
_("You must pick a package to include the selected benefits.")
)
elif standalone and package:
raise forms.ValidationError(
_("Application with package cannot have standalone benefits.")
)
elif package and a_la_carte and not package.allow_a_la_carte:
raise forms.ValidationError(
_("Package does not accept a la carte benefits.")
)

benefits_ids = [b.id for b in benefits]
for benefit in benefits:
Expand Down
18 changes: 18 additions & 0 deletions sponsors/migrations/0091_sponsorshippackage_allow_a_la_carte.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 2.2.24 on 2022-08-13 10:51

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('sponsors', '0090_auto_20220812_1314'),
]

operations = [
migrations.AddField(
model_name='sponsorshippackage',
name='allow_a_la_carte',
field=models.BooleanField(default=True, help_text='If disabled, a la carte benefits will be disabled in application form'),
),
]
4 changes: 4 additions & 0 deletions sponsors/models/sponsorship.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ class SponsorshipPackage(OrderedModel):
"to reference this package.")
year = models.PositiveIntegerField(null=True, validators=YEAR_VALIDATORS, db_index=True)

allow_a_la_carte = models.BooleanField(
default=True, help_text="If disabled, a la carte benefits will be disabled in application form"
)

def __str__(self):
return f'{self.name} ({self.year})'

Expand Down
47 changes: 44 additions & 3 deletions sponsors/tests/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,15 +145,56 @@ def test_validate_form_without_package_but_with_standalone_benefits(self):
self.assertEqual([], form.get_benefits())
self.assertEqual([benefit], form.get_benefits(include_standalone=True))

def test_should_not_validate_form_without_package_with_a_la_carte_and_standalone_benefits(self):
def test_do_not_validate_form_with_package_and_standalone_benefits(self):
benefit = self.standalone[0]
data = {
"standalone_benefits": [benefit.id],
"package": self.package.id,
"benefits_psf": [self.program_1_benefits[0].id],
}
form = SponsorshipsBenefitsForm(data=data)
self.assertFalse(form.is_valid())
self.assertIn(
"Application with package cannot have standalone benefits.",
form.errors["__all__"]
)

def test_should_not_validate_form_without_package_with_a_la_carte_benefits(self):
data = {
"standalone_benefits": [self.standalone[0]],
"a_la_carte_benefits": [self.a_la_carte[0]],
"a_la_carte_benefits": [self.a_la_carte[0].id],
}

form = SponsorshipsBenefitsForm(data=data)

self.assertFalse(form.is_valid())
self.assertIn(
"You must pick a package to include the selected benefits.",
form.errors["__all__"]
)

data.update({
"package": self.package.id,
})
form = SponsorshipsBenefitsForm(data=data)
self.assertTrue(form.is_valid())

def test_do_not_validate_package_package_with_disabled_a_la_carte_benefits(self):
self.package.allow_a_la_carte = False
self.package.save()
data = {
"package": self.package.id,
"benefits_psf": [self.program_1_benefits[0].id],
"a_la_carte_benefits": [self.a_la_carte[0].id],
}
form = SponsorshipsBenefitsForm(data=data)
self.assertFalse(form.is_valid())
self.assertIn(
"Package does not accept a la carte benefits.",
form.errors["__all__"]
)
data.pop("a_la_carte_benefits")
form = SponsorshipsBenefitsForm(data=data)
self.assertTrue(form.is_valid(), form.errors)

def test_benefits_conflicts_helper_property(self):
benefit_1, benefit_2 = baker.make("sponsors.SponsorshipBenefit", _quantity=2)
Expand Down
8 changes: 4 additions & 4 deletions sponsors/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def setUp(self):
"benefits_psf": [b.id for b in self.program_1_benefits],
"benefits_working_group": [b.id for b in self.program_2_benefits],
"a_la_carte_benefits": [b.id for b in self.a_la_carte_benefits],
"standalone_benefits": [b.id for b in self.standalone_benefits],
"standalone_benefits": [],
"package": self.package.id,
}

Expand Down Expand Up @@ -150,6 +150,7 @@ def test_valid_only_with_standalone(self):
self.data["benefits_working_group"] = []
self.data["a_la_carte_benefits"] = []
self.data["package"] = ""
self.data["standalone_benefits"] = [b.id for b in self.standalone_benefits]

response = self.client.post(self.url, data=self.data)

Expand Down Expand Up @@ -218,7 +219,6 @@ def setUp(self):
"package": self.package.id,
"benefits_psf": [b.id for b in self.program_1_benefits],
"a_la_carte_benefits": [self.a_la_carte.id],
"standalone_benefits": [self.standalone.id],
}
)
self.data = {
Expand Down Expand Up @@ -349,8 +349,8 @@ def test_create_new_sponsorship(self):
)
sponsorship = Sponsorship.objects.get(sponsor__name="CompanyX")
self.assertTrue(sponsorship.benefits.exists())
# 3 benefits + 1 a-la-carte + 1 standalone
self.assertEqual(5, sponsorship.benefits.count())
# 3 benefits + 1 a-la-carte + 0 standalone
self.assertEqual(4, sponsorship.benefits.count())
self.assertTrue(sponsorship.level_name)
self.assertTrue(sponsorship.submited_by, self.user)
self.assertEqual(
Expand Down
82 changes: 78 additions & 4 deletions static/js/sponsors/applicationForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,76 @@ $(document).ready(function(){
getBenefitInput: function(benefitId) { return SELECTORS.benefitsInputs().filter('[value=' + benefitId + ']'); },
getSelectedBenefits: function() { return SELECTORS.benefitsInputs().filter(":checked"); },
tickImages: function() { return $(`.benefit-within-package img`) },
sectionToggleBtns: function() { return $(".toggle_btn")}
sectionToggleBtns: function() { return $(".toggle_btn")},
aLaCarteInputs: function() { return $("input[name=a_la_carte_benefits]"); },
standaloneInputs: function() { return $("input[name=standalone_benefits]"); },
aLaCarteMessage: function() { return $("#a-la-cart-benefits-disallowed"); },
standaloneMessage: function() { return $("#standalone-benefits-disallowed"); },
clearFormButton: function() { return $("#clear_form_btn"); },
applicationForm: function() { return $("#application_form"); },
}

const initialPackage = $("input[name=package]:checked").val();
if (initialPackage && initialPackage.length > 0) mobileUpdate(initialPackage);
const pkgInputs = $("input[name=package]:checked");
if (pkgInputs.length > 0 && pkgInputs.val()) {

// Disable A La Carte inputs based on initial package value
if (pkgInputs.attr("allow_a_la_carte") !== "true"){
let msg = "Cannot add a la carte benefit with the selected package.";
SELECTORS.aLaCarteInputs().attr("title", msg);
SELECTORS.aLaCarteMessage().removeClass("hidden");
SELECTORS.aLaCarteInputs().prop("checked", false);
SELECTORS.aLaCarteInputs().prop("disabled", true);

}

// Disable Standalone benefits inputs
let msg ="Cannot apply for standalone benefit with the selected package.";
SELECTORS.standaloneInputs().prop("checked", false);
SELECTORS.standaloneInputs().prop("disabled", true);
SELECTORS.standaloneMessage().removeClass("hidden");
SELECTORS.standaloneInputs().attr("title", msg);

// Update mobile selection
mobileUpdate(pkgInputs.val());
} else {
// disable a la carte if no package selected at the first step
SELECTORS.aLaCarteInputs().prop("disabled", true);
}

SELECTORS.sectionToggleBtns().click(function(){
const section = $(this).data('section');
const className = ".section-" + section + "-content";
$(className).toggle();
});

SELECTORS.clearFormButton().click(function(){
SELECTORS.aLaCarteInputs().prop('checked', false).prop('selected', false);
SELECTORS.benefitsInputs().prop('checked', false).prop('selected', false);
SELECTORS.packageInput().prop('checked', false).prop('selected', false);
SELECTORS.standaloneInputs()
.prop('checked', false).prop('selected', false).prop("disabled", false);

SELECTORS.tickImages().each((i, img) => {
const initImg = img.getAttribute('data-initial-state');
const src = img.getAttribute('src');

if (src !== initImg) {
img.setAttribute('data-next-state', src);
}

img.setAttribute('src', initImg);
});
$(".selected").removeClass("selected");
$('.custom-fee').hide();
});

SELECTORS.packageInput().click(function(){
let package = this.value;
if (package.length == 0) return;
if (package.length == 0) {
SELECTORS.standaloneInputs().prop("disabled", false);
SELECTORS.standaloneMessage().addClass("hidden");
return;
}

// clear previous customizations
SELECTORS.tickImages().each((i, img) => {
Expand All @@ -48,6 +103,25 @@ $(document).ready(function(){
$(`.package-${package}-benefit`).addClass("selected");
$(`.package-${package}-benefit input`).prop("disabled", false);

let msg ="Cannot apply for standalone benefit with the selected package.";
SELECTORS.standaloneInputs().prop("checked", false);
SELECTORS.standaloneInputs().prop("disabled", true);
SELECTORS.standaloneMessage().removeClass("hidden");
SELECTORS.standaloneInputs().attr("title", msg);

// Disable a la carte benefits if package disables it
if ($(this).attr("allow_a_la_carte") !== "true") {
msg ="Cannot add a la carte benefit with the selected package.";
SELECTORS.aLaCarteInputs().attr("title", msg);
SELECTORS.aLaCarteMessage().removeClass("hidden");
SELECTORS.aLaCarteInputs().prop("checked", false);
SELECTORS.aLaCarteInputs().prop("disabled", true);
} else {
SELECTORS.aLaCarteInputs().attr("title", "");
SELECTORS.aLaCarteMessage().addClass("hidden");
SELECTORS.aLaCarteInputs().not('.soldout').prop("disabled", false);
}

// populate hidden inputs according to package's benefits
SELECTORS.getPackageBenefits(package).each(function(){
let benefit = $(this).html();
Expand Down
39 changes: 29 additions & 10 deletions templates/sponsors/sponsorship_benefits_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,25 @@ <h4 class="col">Select a Sponsorship Package</h4>
{% endif %}
</span>
{% for package in form.fields.package.queryset %}
<div id="pkg_container_{{ package.id }}" class="col col-items package-input {% if package.id == form.initial.package %}selected{% endif %}" data-package-id="{{ package.id}}" onclick="updatePackageInput({{forloop.counter0}})">
<div
id="pkg_container_{{ package.id }}"
class="col col-items package-input
{% if package.id == form.initial.package %}selected{% endif %}"
data-package-id="{{ package.id}}"
onclick="updatePackageInput({{forloop.counter0}})"
{% if not package.allow_a_la_carte %}title="This package does not accept customization with a la carte benefits."{% endif %}
>
<h4>{{ package.name|upper }}</h4>
<span class="package-price">${{ package.sponsorship_amount|intcomma }}<span class="custom-fee-{{ package.id }} custom-fee">*</span></span>
<input type="radio" name="package" value="{{ package.id }}" id="id_package_{{ forloop.counter0 }}" {% if package.id == form.initial.package %}checked="checked"{% endif %} data-pos={{ forloop.counter0 }} />
<input
type="radio"
name="package"
value="{{ package.id }}"
id="id_package_{{ forloop.counter0 }}"
{% if package.id == form.initial.package %}checked="checked"{% endif %}
data-pos={{ forloop.counter0 }}
{% if package.allow_a_la_carte %}allow_a_la_carte="true"{% endif %}
/>
<span class="custom-fee-{{ package.id }} custom-fee">* Subject to change</span>
</div>
{% endfor %}
Expand Down Expand Up @@ -101,7 +116,7 @@ <h4 class="benefit-title">{{ benefit.name }}</h4>
{% endif %}
data-initial-state="{% static 'img/sponsors/tick.svg' %}"
/>
{% elif not benefit.package_only %}
{% elif not benefit.package_only and package.allow_a_la_carte%}
<img
id="benefit-{{ benefit.id }}-package-{{ package.id }}"
onclick="benefitUpdate({{ benefit.id }}, {{ package.id }})"
Expand Down Expand Up @@ -144,15 +159,16 @@ <h4 class="benefit-title">{{ benefit.name }}</h4>
<img class="col" src='{% static "img/sponsors/title-2.svg" %}'/>
<div class="col with-description">
<h4>Select a la carte benefits</h4>
<span>Available at any level of sponsorship</span>
<div>Available to add to sponsorship packages.</div>
<div id="a-la-cart-benefits-disallowed" class="hidden"><b>Selected sponsorship package does not allow a la carte benefit additions.</b></div>
</div>
</div>

<div id="a-la-carte-benefits" class="custom-benefits">
<div class="row">
<div class="row" style="justify-content: start;">
{% for benefit in form.fields.a_la_carte_benefits.queryset %}
<div class="col col-items">
<input type="checkbox" name="a_la_carte_benefits" value="{{ benefit.id }}" {% if benefit.id in form.initial.a_la_carte_benefits|default:"" %}checked{% endif %}/>
<input type="checkbox" name="a_la_carte_benefits" {% if benefit.unavailability_message and benefit.id not in field.initial or not benefit.has_capacity %}disabled{% endif %} class="{% if benefit.unavailability_message or not benefit.has_capacity %}soldout{% endif %}" value="{{ benefit.id }}" {% if benefit.id in form.initial.a_la_carte_benefits|default:"" %}checked{% endif %}/>
<div class="a-la-carte-description">
<span class="benefit-program">{{ benefit.program }}</span> -
<span class="benefit-title">{{ benefit.name }}</span>
Expand All @@ -161,7 +177,7 @@ <h4>Select a la carte benefits</h4>
</div>
{% if forloop.counter|divisibleby:4 %}
</div>
<div class="row">
<div class="row" style="justify-content: start;">
{% endif %}
{% endfor %}
</div>
Expand All @@ -178,11 +194,12 @@ <h4>Select a la carte benefits</h4>
<div class="col with-description">
<h4>Select standalone benefits</h4>
<span>Available to be selected without package</span>
<div id="standalone-benefits-disallowed" class="hidden"><b>Standalone benefits are not available when selecting a package.</b></div>
</div>
</div>

<div id="standalone-benefits" class="custom-benefits">
<div class="row">
<div class="row" style="justify-content: start;">
{% for benefit in form.fields.standalone_benefits.queryset %}
<div class="col col-items">
<input type="checkbox" name="standalone_benefits" value="{{ benefit.id }}" {% if benefit.id in form.initial.standalone_benefits|default:"" %}checked{% endif %}/>
Expand Down Expand Up @@ -218,9 +235,11 @@ <h4 class="col">Submit your contact information</h4>
<li><p>Submit your contact information</p></li>
</ol>
<div>
<input class="btn btn-sponsorship-submit" type="submit" value="Submit" {% if custom_year %}disabled{% endif %}>
<label>
<input class="btn btn-sponsorship-submit" type="submit" value="Submit" {% if custom_year %}disabled{% endif %}/>
| <a id="clear_form_btn" href="#package-selection">Clear form</a>
</label>
</div>
<span></span>
</div>

<div class="row section-title">
Expand Down

0 comments on commit ae08791

Please sign in to comment.