Skip to content

Commit 159f0bb

Browse files
berinhardewdurbinmerwok
authored
Implement new layout for the sponsorship application (#1744)
* Order benefits by amount of associated packages * Minimal HTML logic to control program and benefits exhibition * Better display package inputs * Allow the benefits table to overflow and have a width greater than the article * Remove sponsorship substring from program name * Display checkbox if it's a potential add-on * Separate add-ons benefits from packages' ones * Display add-on as checkbox in the frontend + row/col css refactoring * Add headers with form sections titles * Bass CSS defined in the specs * Section titles styling * Initial benefits table styling * Final styles to packages/benefits table * Enable filter by package as well * Style add-ons cards * Add styles to submit section * Style section titles * Run black * Implement sponsorship's footer * Remove legacy benefits input fields and styles * Move thank you section closer to footer * Simplify JS code to only select benefits after package selection * Replace png by svg img * Add selected CSS * Display selected packages when page loads * Clean-up add-ons selectons after changing the package * Potential add-on checkboxes updating django's form related ones. * Hide inputs This is not a problem because the backend code runs all validations needed as well * Display form errors * Surrounds package radio inputs in a div * Style package selection input * Add event to handle click on package input container * Add mobile style to add-on selection * Style form submit section form moble * Style benefits list for the mobile version * Refactor and load ticks on mobile if form has initial data * Make sure add-on benefits are being saved * Get value only if checked inputs * compile scss * Replace add-on input by images * Reset add-on image if package changes * Enable user to unselect benefits as well * Display image considering form initial * Add-ons considering form initial * Prevent package names from overlapping * Respect initial values for potential add-ons * Update sponsors/forms.py Co-authored-by: Éric Araujo <merwok@netwok.org> * perhaps yuglify isn't necessary? * noop compressor * trying to debug deployed js * disable wrapping Javascript * Revert "trying to debug deployed js" This reverts commit 178d8aa. * display/bolden sponsorship program title Co-authored-by: Ernest W. Durbin III <ewdurbin@gmail.com> Co-authored-by: Éric Araujo <merwok@netwok.org>
1 parent 7d00625 commit 159f0bb

18 files changed

+836
-335
lines changed

pydotorg/settings/pipeline.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,16 @@
7373
PIPELINE = {
7474
'STYLESHEETS': PIPELINE_CSS,
7575
'JAVASCRIPT': PIPELINE_JS,
76+
'DISABLE_WRAPPER': True,
7677
# TODO: ruby-sass is not installed on the server since
7778
# https://github.com/python/psf-salt/commit/044c38773ced4b8bbe8df2c4266ef3a295102785
7879
# and we pre-compile SASS files and commit them into codebase so we
7980
# don't really need this. See issue #832.
8081
# 'COMPILERS': (
8182
# 'pipeline.compilers.sass.SASSCompiler',
8283
# ),
83-
'CSS_COMPRESSOR': 'pipeline.compressors.yuglify.YuglifyCompressor',
84-
'JS_COMPRESSOR': 'pipeline.compressors.yuglify.YuglifyCompressor',
84+
'CSS_COMPRESSOR': 'pipeline.compressors.NoopCompressor',
85+
'JS_COMPRESSOR': 'pipeline.compressors.NoopCompressor',
8586
# 'SASS_BINARY': 'cd %s && exec /usr/bin/env sass' % os.path.join(BASE, 'static'),
8687
# 'SASS_ARGUMENTS': '--quiet --compass --scss -I $(dirname $(dirname $(gem which susy)))/sass'
8788
}

sponsors/admin.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class SponsorshipBenefitAdmin(OrderedModelAdmin):
4242
"internal_value",
4343
"move_up_down_links",
4444
]
45-
list_filter = ["program", "package_only"]
45+
list_filter = ["program", "package_only", "packages"]
4646
search_fields = ["name"]
4747

4848
fieldsets = [

sponsors/forms.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from django.utils.translation import ugettext_lazy as _
66
from django.utils.functional import cached_property
77
from django.conf import settings
8+
from django.db.models import Count
89

910
from sponsors.models import (
1011
SponsorshipBenefit,
@@ -48,16 +49,23 @@ class SponsorshiptBenefitsForm(forms.Form):
4849
required=False,
4950
empty_label=None,
5051
)
52+
add_ons_benefits = PickSponsorshipBenefitsField(
53+
required=False,
54+
queryset=SponsorshipBenefit.objects.add_ons().select_related("program"),
55+
)
5156

5257
def __init__(self, *args, **kwargs):
5358
super().__init__(*args, **kwargs)
54-
benefits_qs = SponsorshipBenefit.objects.select_related("program")
59+
benefits_qs = SponsorshipBenefit.objects.with_packages().select_related(
60+
"program"
61+
)
62+
5563
for program in SponsorshipProgram.objects.all():
5664
slug = slugify(program.name).replace("-", "_")
5765
self.fields[f"benefits_{slug}"] = PickSponsorshipBenefitsField(
5866
queryset=benefits_qs.filter(program=program),
5967
required=False,
60-
label=_(f"{program.name} Sponsorship Benefits"),
68+
label=_("{program_name} Benefits").format(program_name=program.name),
6169
)
6270

6371
@property

sponsors/models.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from itertools import chain
22
from django.conf import settings
33
from django.db import models
4-
from django.db.models import Sum
4+
from django.db.models import Sum, Count
55
from django.template.defaultfilters import truncatechars
66
from django.utils import timezone
77
from django.utils.functional import cached_property
@@ -83,6 +83,16 @@ def with_conflicts(self):
8383
def without_conflicts(self):
8484
return self.filter(conflicts__isnull=True)
8585

86+
def add_ons(self):
87+
return self.annotate(num_packages=Count("packages")).filter(num_packages=0)
88+
89+
def with_packages(self):
90+
return (
91+
self.annotate(num_packages=Count("packages"))
92+
.exclude(num_packages=0)
93+
.order_by("-num_packages")
94+
)
95+
8696

8797
class SponsorshipBenefit(OrderedModel):
8898
objects = SponsorshipBenefitManager()

sponsors/tests/test_forms.py

+18-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from sponsors.forms import (
77
SponsorshiptBenefitsForm,
88
SponsorshipApplicationForm,
9-
SponsorContact,
109
Sponsor,
1110
SponsorContactFormSet,
1211
SponsorBenefitAdminInlineForm,
@@ -27,21 +26,36 @@ def setUp(self):
2726
self.program_2_benefits = baker.make(
2827
SponsorshipBenefit, program=self.wk, _quantity=5
2928
)
29+
self.package = baker.make("sponsors.SponsorshipPackage")
30+
self.package.benefits.add(*self.program_1_benefits)
31+
self.package.benefits.add(*self.program_2_benefits)
32+
33+
# packages without associated packages
34+
self.add_ons = baker.make(SponsorshipBenefit, program=self.psf, _quantity=2)
3035

3136
def test_benefits_organized_by_program(self):
3237
form = SponsorshiptBenefitsForm()
3338

39+
choices = list(form.fields["add_ons_benefits"].choices)
40+
41+
self.assertEqual(len(self.add_ons), len(choices))
42+
for benefit in self.add_ons:
43+
self.assertIn(benefit.id, [c[0] for c in choices])
44+
45+
def test_specific_field_to_select_add_ons(self):
46+
form = SponsorshiptBenefitsForm()
47+
3448
field1, field2 = sorted(form.benefits_programs, key=lambda f: f.name)
3549

3650
self.assertEqual("benefits_psf", field1.name)
37-
self.assertEqual("PSF Sponsorship Benefits", field1.label)
51+
self.assertEqual("PSF Benefits", field1.label)
3852
choices = list(field1.field.choices)
3953
self.assertEqual(len(self.program_1_benefits), len(choices))
4054
for benefit in self.program_1_benefits:
4155
self.assertIn(benefit.id, [c[0] for c in choices])
4256

4357
self.assertEqual("benefits_working_group", field2.name)
44-
self.assertEqual("Working Group Sponsorship Benefits", field2.label)
58+
self.assertEqual("Working Group Benefits", field2.label)
4559
choices = list(field2.field.choices)
4660
self.assertEqual(len(self.program_2_benefits), len(choices))
4761
for benefit in self.program_2_benefits:
@@ -83,6 +97,7 @@ def test_benefits_conflicts_helper_property(self):
8397
def test_invalid_form_if_any_conflict(self):
8498
benefit_1 = baker.make("sponsors.SponsorshipBenefit", program=self.wk)
8599
benefit_1.conflicts.add(*self.program_1_benefits)
100+
self.package.benefits.add(benefit_1)
86101

87102
data = {"benefits_psf": [b.id for b in self.program_1_benefits]}
88103
form = SponsorshiptBenefitsForm(data=data)

sponsors/tests/test_views.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,13 @@ def setUp(self):
4444
SponsorshipBenefit, program=self.wk, _quantity=5
4545
)
4646
self.package = baker.make("sponsors.SponsorshipPackage")
47-
for benefit in self.program_1_benefits:
48-
benefit.packages.add(self.package)
47+
self.package.benefits.add(*self.program_1_benefits)
48+
package_2 = baker.make("sponsors.SponsorshipPackage")
49+
package_2.benefits.add(*self.program_2_benefits)
50+
self.add_on_benefits = baker.make(
51+
SponsorshipBenefit, program=self.psf, _quantity=2
52+
)
53+
4954
self.user = baker.make(settings.AUTH_USER_MODEL, is_staff=True, is_active=True)
5055
self.client.force_login(self.user)
5156

@@ -54,6 +59,7 @@ def setUp(self):
5459
self.data = {
5560
"benefits_psf": [b.id for b in self.program_1_benefits],
5661
"benefits_working_group": [b.id for b in self.program_2_benefits],
62+
"add_ons_benefits": [b.id for b in self.add_on_benefits],
5763
"package": self.package.id,
5864
}
5965

@@ -73,7 +79,7 @@ def test_display_template_with_form_and_context(self):
7379
self.assertTemplateUsed(r, "sponsors/sponsorship_benefits_form.html")
7480
self.assertIsInstance(r.context["form"], SponsorshiptBenefitsForm)
7581
self.assertEqual(r.context["benefit_model"], SponsorshipBenefit)
76-
self.assertEqual(3, packages.count())
82+
self.assertEqual(4, packages.count())
7783
self.assertIn(psf_package, packages)
7884
self.assertIn(extra_package, packages)
7985
self.assertEqual(

sponsors/views.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ def _set_form_data_cookie(self, form, response):
7777
"package": "" if not form.get_package() else form.get_package().id,
7878
}
7979
for fname, benefits in [
80-
(f, v) for f, v in form.cleaned_data.items() if f.startswith("benefits_")
80+
(f, v)
81+
for f, v in form.cleaned_data.items()
82+
if f.startswith("benefits_") or f == 'add_ons_benefits'
8183
]:
8284
data[fname] = sorted([b.id for b in benefits])
8385

740 Bytes
Loading

static/img/sponsors/tick.svg

+3
Loading

static/img/sponsors/title-1.png

953 Bytes
Loading

static/img/sponsors/title-2.png

1.03 KB
Loading

static/img/sponsors/title-3.png

1.11 KB
Loading

static/img/sponsors/title-4.png

1 KB
Loading

static/js/sponsors/applicationForm.js

+79-93
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,89 @@
11
$(document).ready(function(){
2-
const SELECTORS = {
3-
checkboxesContainer: function() { return $("#benefits_container"); },
4-
costLabel: function() { return $("#cost_label"); },
5-
clearFormBtn: function() { return $("#clear_form_btn"); },
6-
packageInput: function() { return $("input[name=package]"); },
7-
applicationForm: function() { return $("#application_form"); },
8-
getPackageInfo: function(packageId) { return $("#package_benefits_" + packageId); },
9-
getPackageBenefits: function(packageId) { return SELECTORS.getPackageInfo(packageId).children(); },
10-
benefitsInputs: function() { return $("input[id^=id_benefits_]"); },
11-
getBenefitLabel: function(benefitId) { return $('label[benefit_id=' + benefitId + ']'); },
12-
getBenefitInput: function(benefitId) { return SELECTORS.benefitsInputs().filter('[value=' + benefitId + ']'); },
13-
getBenefitConflicts: function(benefitId) { return $('#conflicts_with_' + benefitId).children(); },
14-
getSelectedBenefits: function() { return SELECTORS.benefitsInputs().filter(":checked"); },
15-
}
16-
17-
displayPackageCost = function(packageId) {
18-
let packageInfo = SELECTORS.getPackageInfo(packageId);
19-
let cost = packageInfo.attr("data-cost");
20-
SELECTORS.costLabel().html('Sponsorship cost is $' + cost.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + ' USD')
21-
}
22-
23-
24-
25-
SELECTORS.clearFormBtn().click(function(){
26-
SELECTORS.applicationForm().trigger("reset");
27-
SELECTORS.applicationForm().find(".active").removeClass("active");
28-
SELECTORS.packageInput().prop("checked", false);
29-
SELECTORS.checkboxesContainer().find(':checkbox').each(function(){
30-
$(this).prop('checked', false);
31-
if ($(this).attr("package_only")) $(this).attr("disabled", true);
32-
});
33-
SELECTORS.costLabel().html("");
34-
});
2+
const SELECTORS = {
3+
packageInput: function() { return $("input[name=package]"); },
4+
getPackageInfo: function(packageId) { return $("#package_benefits_" + packageId); },
5+
getPackageBenefits: function(packageId) { return SELECTORS.getPackageInfo(packageId).children(); },
6+
benefitsInputs: function() { return $("input[id^=id_benefits_]"); },
7+
getBenefitInput: function(benefitId) { return SELECTORS.benefitsInputs().filter('[value=' + benefitId + ']'); },
8+
getSelectedBenefits: function() { return SELECTORS.benefitsInputs().filter(":checked"); },
9+
tickImages: function() { return $(`.benefit-within-package img`) },
10+
}
3511

36-
SELECTORS.packageInput().change(function(){
37-
let package = this.value;
38-
if (package.length == 0) return;
39-
40-
SELECTORS.costLabel().html("Updating cost...")
41-
42-
SELECTORS.checkboxesContainer().find(':checkbox').each(function(){
43-
$(this).prop('checked', false);
44-
let packageOnlyBenefit = $(this).attr("package_only");
45-
if (packageOnlyBenefit) $(this).attr("disabled", true);
46-
});
47-
48-
SELECTORS.getPackageBenefits(package).each(function(){
49-
let benefit = $(this).html()
50-
let benefitInput = SELECTORS.getBenefitInput(benefit);
51-
let packageOnlyBenefit = benefitInput.attr("package_only");
52-
benefitInput.removeAttr("disabled");
53-
benefitInput.trigger("click");
54-
});
55-
displayPackageCost(package);
56-
});
12+
const initialPackage = $("input[name=package]:checked").val();
13+
if (initialPackage && initialPackage.length > 0) mobileUpdate(initialPackage);
5714

58-
SELECTORS.benefitsInputs().change(function(){
59-
let benefit = this.value;
60-
if (benefit.length == 0) return;
61-
62-
// display package cost if custom benefit change result matches with package's benefits list
63-
let isChangeFromPackageChange = SELECTORS.costLabel().html() == "Updating cost..."
64-
if (!isChangeFromPackageChange) {
65-
let selectedBenefits = SELECTORS.getSelectedBenefits();
66-
selectedBenefits = $.map(selectedBenefits, function(b) { return $(b).val() }).sort();
67-
let selectedPackageId = SELECTORS.packageInput().filter(":checked").val()
68-
let packageBenefits = SELECTORS.getPackageBenefits(selectedPackageId);
69-
packageBenefits = $.map(packageBenefits, function(b) { return $(b).text() }).sort();
70-
71-
// check same num of benefits and join with string. if same string, both lists have the same benefits
72-
if (packageBenefits.length == selectedBenefits.length && packageBenefits.join(',') === selectedBenefits.join(',')){
73-
displayPackageCost(selectedPackageId);
74-
} else {
75-
let msg = "Please submit your customized sponsorship package application and we'll contact you within 2 business days.";
76-
SELECTORS.costLabel().html(msg);
77-
}
78-
}
15+
SELECTORS.packageInput().click(function(){
16+
let package = this.value;
17+
if (package.length == 0) return;
7918

80-
// updates the input to be active if needed
81-
let active = SELECTORS.getBenefitInput(benefit).prop("checked");
82-
if (!active) {
83-
return;
84-
} else {
85-
SELECTORS.getBenefitLabel(benefit).addClass("active");
19+
// clear previous customizations
20+
SELECTORS.tickImages().each((i, img) => {
21+
const initImg = img.getAttribute('data-initial-state');
22+
const src = img.getAttribute('src');
23+
24+
if (src !== initImg) {
25+
img.setAttribute('data-next-state', src);
8626
}
8727

88-
// check and ensure conflicts constraints between checked benefits
89-
SELECTORS.getBenefitConflicts(benefit).each(function(){
90-
let conflictId = $(this).html();
91-
let checked = SELECTORS.getBenefitInput(conflictId).prop("checked");
92-
if (checked){
93-
conflictCheckbox.trigger("click");
94-
conflictCheckbox.parent().removeClass("active");
95-
}
96-
});
28+
img.setAttribute('src', initImg);
29+
});
30+
$(".selected").removeClass("selected");
31+
32+
// clear hidden form inputs
33+
SELECTORS.getSelectedBenefits().each(function(){
34+
$(this).prop('checked', false);
35+
});
36+
37+
// update package benefits display
38+
$(`.package-${package}-benefit`).addClass("selected");
39+
$(`.package-${package}-benefit input`).prop("disabled", false);
40+
41+
// populate hidden inputs according to package's benefits
42+
SELECTORS.getPackageBenefits(package).each(function(){
43+
let benefit = $(this).html();
44+
let benefitInput = SELECTORS.getBenefitInput(benefit);
45+
benefitInput.prop("checked", true);
9746
});
9847

99-
$(document).tooltip({
100-
show: { effect: "blind", duration: 0 },
101-
hide: false
48+
mobileUpdate(package);
10249
});
10350
});
51+
52+
53+
function mobileUpdate(packageId) {
54+
// Mobile version lists a single column to controle the selected
55+
// benefits and potential add-ons. So, this part of the code
56+
// controls this logic.
57+
const mobileVersion = $(".benefit-within-package:hidden").length > 0;
58+
if (!mobileVersion) return;
59+
$(".benefit-within-package").hide(); // hide all ticks and potential add-ons inputs
60+
$(`div[data-package-reference=${packageId}]`).show() // display only package's ones
61+
}
62+
63+
64+
// For an unknown reason I couldn't make this logic work with jQuery.
65+
// To don't block the development process, I pulled it off using the classic
66+
// onclick attribute. Refactorings are welcome =]
67+
function benefitUpdate(benefitId, packageId) {
68+
// Change tick image for the benefit. Can't directly change the url for the image
69+
// due to our current static files storage.
70+
const clickedImg = document.getElementById(`benefit-${ benefitId }-package-${ packageId }`);
71+
72+
// Img container must have "selected" to class to be editable
73+
if (!clickedImg.parentElement.classList.contains('selected')) return;
74+
75+
const newSrc = clickedImg.getAttribute("data-next-state");
76+
clickedImg.setAttribute("data-next-state", clickedImg.src);
77+
78+
// Update benefit's hidden input (can't rely on click event though)
79+
const benefitsInputs = Array(...document.querySelectorAll('[data-benefit-id]'));
80+
const hiddenInput = benefitsInputs.filter((b) => b.getAttribute('data-benefit-id') == benefitId)[0];
81+
hiddenInput.checked = !hiddenInput.checked;
82+
clickedImg.src = newSrc;
83+
};
84+
85+
86+
function updatePackageInput(packageId){
87+
const packageInput = document.getElementById(`id_package_${ packageId }`);
88+
packageInput.click();
89+
}

0 commit comments

Comments
 (0)