diff --git a/hackathon_site/event/admin.py b/hackathon_site/event/admin.py index 20d9592..4f78ff1 100644 --- a/hackathon_site/event/admin.py +++ b/hackathon_site/event/admin.py @@ -4,7 +4,13 @@ from import_export import resources from import_export.admin import ExportMixin -from event.models import Profile, Team as EventTeam, User, UserActivity +from event.models import ( + InterestSubmission, + Profile, + Team as EventTeam, + User, + UserActivity, +) from hardware.admin import OrderInline admin.site.unregister(User) @@ -120,5 +126,45 @@ def get_export_queryset(self, request): return super().get_queryset(request).select_related("user") +class InterestSubmissionResource(resources.ModelResource): + class Meta: + model = InterestSubmission + fields = ( + "first_name", + "last_name", + "email", + "age", + "phone_number", + "school", + "study_level", + "country", + "conduct_agree", + "logistics_agree", + "email_agree", + "created_at", + ) + export_order = tuple(fields) + + +@admin.register(InterestSubmission) +class InterestSubmissionAdmin(ExportMixin, admin.ModelAdmin): + resource_class = InterestSubmissionResource + list_display = ( + "get_full_name", + "email", + "school", + "study_level", + "country", + "created_at", + ) + search_fields = ("first_name", "last_name", "email", "school") + list_filter = ("country", "study_level", "email_agree") + + def get_full_name(self, obj): + return f"{obj.first_name} {obj.last_name}" + + get_full_name.short_description = "Name" + + # Register your models here. admin.site.register(Profile) diff --git a/hackathon_site/event/forms.py b/hackathon_site/event/forms.py index e02dbb4..d6240d0 100644 --- a/hackathon_site/event/forms.py +++ b/hackathon_site/event/forms.py @@ -1,3 +1,6 @@ +import re + +from django import forms from django.contrib.auth.forms import ( PasswordChangeForm as _PasswordChangeForm, PasswordResetForm as _PasswordResetForm, @@ -5,6 +8,8 @@ AuthenticationForm as _AuthenticationForm, ) +from event.models import InterestSubmission + class PasswordChangeForm(_PasswordChangeForm): def __init__(self, user, *args, **kwargs): @@ -30,3 +35,53 @@ def __init__(self, *args, **kwargs): class AuthenticationForm(_AuthenticationForm): def clean_username(self): return self.cleaned_data["username"].lower() + + +class InterestForm(forms.ModelForm): + """Public, ungated MLH interest form shown on the landing page. + + Modelled on registration.forms.ApplicationForm so the MLH-required fields, + formats, and the two mandatory checkboxes behave identically. + """ + + error_css_class = "invalid" + + class Meta: + model = InterestSubmission + fields = [ + "first_name", + "last_name", + "email", + "age", + "phone_number", + "school", + "study_level", + "country", + "conduct_agree", + "logistics_agree", + "email_agree", + ] + widgets = { + "school": forms.Select( + # Choices are populated client-side by Select2 from MLH's + # verified schools list (see landing.html). + attrs={"class": "select2-school-select"}, + choices=((None, ""),), + ), + "phone_number": forms.TextInput(attrs={"placeholder": "+1 (123) 456-7890"}), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.label_suffix = "" + self.fields["conduct_agree"].required = True + self.fields["logistics_agree"].required = True + + def save(self, commit=True): + self.instance = super().save(commit=False) + self.instance.phone_number = re.sub( + "[^0-9]", "", self.instance.phone_number + ) + if commit: + self.instance.save() + return self.instance diff --git a/hackathon_site/event/jinja2/event/form_macros.html b/hackathon_site/event/jinja2/event/form_macros.html new file mode 100644 index 0000000..9b95546 --- /dev/null +++ b/hackathon_site/event/jinja2/event/form_macros.html @@ -0,0 +1,52 @@ +{# Standalone form-rendering macros. + + These mirror the macros defined inline in event/form_base.html, extracted + here so templates that do NOT extend form_base.html (e.g. the landing page) + can `{% from "event/form_macros.html" import ... %}` them. This file must not + `{% extends %}` anything — importing an extending template forces its base to + render, which fails outside a full request context. #} + +{% macro render_field(field, show_help_text=False, show_label=True, show_help_text_as_label=False, size="s12", id="") %} +
+ {{ field }} + {% if show_label %} + {% if show_help_text_as_label %} + + {% else %} + {{ field.label_tag() }} + {% endif %} + {% endif %} + {% if show_help_text %} + {{ field.help_text|safe }} + {% endif %} + {% if field.errors %} + + {% for error in field.errors %} + {{ error }} +
+ {% endfor %} +
+ {% endif %} +
+{% endmacro %} + +{% macro render_checkbox_field(field, size="s12") %} +
+ + {% if field.errors %} + + {% for error in field.errors %} + {{ error }} + {% endfor %} + + {% endif %} +
+{% endmacro %} diff --git a/hackathon_site/event/jinja2/event/landing.html b/hackathon_site/event/jinja2/event/landing.html index 901486f..e0ded37 100644 --- a/hackathon_site/event/jinja2/event/landing.html +++ b/hackathon_site/event/jinja2/event/landing.html @@ -1,4 +1,5 @@ {% extends "event/base.html" %} +{% from "event/form_macros.html" import render_field, render_checkbox_field %} {% block nav_background_color %}{% endblock %} @@ -8,6 +9,37 @@ const registrationCloseDate = new Date("{{ localtime(registration_close_date)|strftime('%b %-d, %Y, %H:%M:%S') }}"); const eventStartDate = new Date("{{ localtime(event_start_date)|strftime('%b %-d, %Y, %H:%M:%S') }}"); + + + + {% endblock %} {% block nav_links %} @@ -46,7 +78,7 @@
@@ -107,6 +139,74 @@




+
+
+ {% set msgs = get_messages(request) %} + {# Open the panel automatically when there's feedback to show. #} + {% set open_form = form.errors or msgs %} +
+
+

Register Your Interest

+

+ MakeUofT 2027 is on its way! Leave your info below and we'll email you as soon as applications open. +

+
+
+ +
+
+
+
+
@@ -399,4 +499,85 @@

Contact Us

+ + + + {% endblock %} diff --git a/hackathon_site/event/migrations/0011_interestsubmission.py b/hackathon_site/event/migrations/0011_interestsubmission.py new file mode 100644 index 0000000..294df88 --- /dev/null +++ b/hackathon_site/event/migrations/0011_interestsubmission.py @@ -0,0 +1,32 @@ +# Generated by Django 3.2.15 on 2026-06-26 18:36 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('event', '0010_team_credits'), + ] + + operations = [ + migrations.CreateModel( + name='InterestSubmission', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('first_name', models.CharField(max_length=30)), + ('last_name', models.CharField(max_length=30)), + ('email', models.EmailField(max_length=254)), + ('age', models.PositiveIntegerField(choices=[(None, ''), (17, '17'), (18, '18'), (19, '19'), (20, '20'), (21, '21'), (22, '22'), (23, '22+')])), + ('phone_number', models.CharField(max_length=20, validators=[django.core.validators.RegexValidator('^\\+?\\d[\\d\\s()-]{7,}$', message='Enter a valid phone number.')])), + ('school', models.CharField(max_length=255)), + ('study_level', models.CharField(choices=[(None, ''), ('post-secondary-2-years', '2 year Undergraduate University or community college program'), ('post-secondary-3-or-more-years', '3+ year Undergraduate University program'), ('graduate', 'Graduate University (Masters, Professional, Doctoral, etc)'), ('other', 'Other')], help_text='Current level of study', max_length=50)), + ('country', models.CharField(choices=[(None, ''), ('Afghanistan', 'Afghanistan'), ('Albania', 'Albania'), ('Algeria', 'Algeria'), ('Andorra', 'Andorra'), ('Angola', 'Angola'), ('Antigua and Barbuda', 'Antigua and Barbuda'), ('Argentina', 'Argentina'), ('Armenia', 'Armenia'), ('Australia', 'Australia'), ('Austria', 'Austria'), ('Azerbaijan', 'Azerbaijan'), ('Bahamas', 'Bahamas'), ('Bahrain', 'Bahrain'), ('Bangladesh', 'Bangladesh'), ('Barbados', 'Barbados'), ('Belarus', 'Belarus'), ('Belgium', 'Belgium'), ('Belize', 'Belize'), ('Benin', 'Benin'), ('Bhutan', 'Bhutan'), ('Bolivia', 'Bolivia'), ('Bosnia and Herzegovina', 'Bosnia and Herzegovina'), ('Botswana', 'Botswana'), ('Brazil', 'Brazil'), ('Brunei', 'Brunei'), ('Bulgaria', 'Bulgaria'), ('Burkina Faso', 'Burkina Faso'), ('Burundi', 'Burundi'), ('Cabo Verde', 'Cabo Verde'), ('Cambodia', 'Cambodia'), ('Cameroon', 'Cameroon'), ('Canada', 'Canada'), ('Central African Republic', 'Central African Republic'), ('Chad', 'Chad'), ('Chile', 'Chile'), ('China', 'China'), ('Colombia', 'Colombia'), ('Comoros', 'Comoros'), ('Congo (Brazzaville)', 'Congo (Brazzaville)'), ('Congo (Kinshasa)', 'Congo (Kinshasa)'), ('Costa Rica', 'Costa Rica'), ("Côte d'Ivoire", "Côte d'Ivoire"), ('Croatia', 'Croatia'), ('Cuba', 'Cuba'), ('Cyprus', 'Cyprus'), ('Czechia', 'Czechia'), ('Denmark', 'Denmark'), ('Djibouti', 'Djibouti'), ('Dominica', 'Dominica'), ('Dominican Republic', 'Dominican Republic'), ('Ecuador', 'Ecuador'), ('Egypt', 'Egypt'), ('El Salvador', 'El Salvador'), ('Equatorial Guinea', 'Equatorial Guinea'), ('Eritrea', 'Eritrea'), ('Estonia', 'Estonia'), ('Eswatini', 'Eswatini'), ('Ethiopia', 'Ethiopia'), ('Fiji', 'Fiji'), ('Finland', 'Finland'), ('France', 'France'), ('Gabon', 'Gabon'), ('Gambia', 'Gambia'), ('Georgia', 'Georgia'), ('Germany', 'Germany'), ('Ghana', 'Ghana'), ('Greece', 'Greece'), ('Grenada', 'Grenada'), ('Guatemala', 'Guatemala'), ('Guinea', 'Guinea'), ('Guinea-Bissau', 'Guinea-Bissau'), ('Guyana', 'Guyana'), ('Haiti', 'Haiti'), ('Honduras', 'Honduras'), ('Hungary', 'Hungary'), ('Iceland', 'Iceland'), ('India', 'India'), ('Indonesia', 'Indonesia'), ('Iran', 'Iran'), ('Iraq', 'Iraq'), ('Ireland', 'Ireland'), ('Israel', 'Israel'), ('Italy', 'Italy'), ('Jamaica', 'Jamaica'), ('Japan', 'Japan'), ('Jordan', 'Jordan'), ('Kazakhstan', 'Kazakhstan'), ('Kenya', 'Kenya'), ('Kiribati', 'Kiribati'), ('Kosovo', 'Kosovo'), ('Kuwait', 'Kuwait'), ('Kyrgyzstan', 'Kyrgyzstan'), ('Laos', 'Laos'), ('Latvia', 'Latvia'), ('Lebanon', 'Lebanon'), ('Lesotho', 'Lesotho'), ('Liberia', 'Liberia'), ('Libya', 'Libya'), ('Liechtenstein', 'Liechtenstein'), ('Lithuania', 'Lithuania'), ('Luxembourg', 'Luxembourg'), ('Madagascar', 'Madagascar'), ('Malawi', 'Malawi'), ('Malaysia', 'Malaysia'), ('Maldives', 'Maldives'), ('Mali', 'Mali'), ('Malta', 'Malta'), ('Marshall Islands', 'Marshall Islands'), ('Mauritania', 'Mauritania'), ('Mauritius', 'Mauritius'), ('Mexico', 'Mexico'), ('Micronesia', 'Micronesia'), ('Moldova', 'Moldova'), ('Monaco', 'Monaco'), ('Mongolia', 'Mongolia'), ('Montenegro', 'Montenegro'), ('Morocco', 'Morocco'), ('Mozambique', 'Mozambique'), ('Myanmar', 'Myanmar'), ('Namibia', 'Namibia'), ('Nauru', 'Nauru'), ('Nepal', 'Nepal'), ('Netherlands', 'Netherlands'), ('New Zealand', 'New Zealand'), ('Nicaragua', 'Nicaragua'), ('Niger', 'Niger'), ('Nigeria', 'Nigeria'), ('North Korea', 'North Korea'), ('North Macedonia', 'North Macedonia'), ('Norway', 'Norway'), ('Oman', 'Oman'), ('Pakistan', 'Pakistan'), ('Palau', 'Palau'), ('Palestine', 'Palestine'), ('Panama', 'Panama'), ('Papua New Guinea', 'Papua New Guinea'), ('Paraguay', 'Paraguay'), ('Peru', 'Peru'), ('Philippines', 'Philippines'), ('Poland', 'Poland'), ('Portugal', 'Portugal'), ('Qatar', 'Qatar'), ('Romania', 'Romania'), ('Russia', 'Russia'), ('Rwanda', 'Rwanda'), ('Saint Kitts and Nevis', 'Saint Kitts and Nevis'), ('Saint Lucia', 'Saint Lucia'), ('Saint Vincent and the Grenadines', 'Saint Vincent and the Grenadines'), ('Samoa', 'Samoa'), ('San Marino', 'San Marino'), ('Sao Tome and Principe', 'Sao Tome and Principe'), ('Saudi Arabia', 'Saudi Arabia'), ('Senegal', 'Senegal'), ('Serbia', 'Serbia'), ('Seychelles', 'Seychelles'), ('Sierra Leone', 'Sierra Leone'), ('Singapore', 'Singapore'), ('Slovakia', 'Slovakia'), ('Slovenia', 'Slovenia'), ('Solomon Islands', 'Solomon Islands'), ('Somalia', 'Somalia'), ('South Africa', 'South Africa'), ('South Korea', 'South Korea'), ('South Sudan', 'South Sudan'), ('Spain', 'Spain'), ('Sri Lanka', 'Sri Lanka'), ('Sudan', 'Sudan'), ('Suriname', 'Suriname'), ('Sweden', 'Sweden'), ('Switzerland', 'Switzerland'), ('Syria', 'Syria'), ('Taiwan', 'Taiwan'), ('Tajikistan', 'Tajikistan'), ('Tanzania', 'Tanzania'), ('Thailand', 'Thailand'), ('Timor-Leste', 'Timor-Leste'), ('Togo', 'Togo'), ('Tonga', 'Tonga'), ('Trinidad and Tobago', 'Trinidad and Tobago'), ('Tunisia', 'Tunisia'), ('Turkey', 'Turkey'), ('Turkmenistan', 'Turkmenistan'), ('Tuvalu', 'Tuvalu'), ('Uganda', 'Uganda'), ('Ukraine', 'Ukraine'), ('United Arab Emirates', 'United Arab Emirates'), ('United Kingdom', 'United Kingdom'), ('United States', 'United States'), ('Uruguay', 'Uruguay'), ('Uzbekistan', 'Uzbekistan'), ('Vanuatu', 'Vanuatu'), ('Vatican City', 'Vatican City'), ('Venezuela', 'Venezuela'), ('Vietnam', 'Vietnam'), ('Yemen', 'Yemen'), ('Zambia', 'Zambia'), ('Zimbabwe', 'Zimbabwe'), ('Other', 'Other')], max_length=255)), + ('conduct_agree', models.BooleanField(default=False, help_text='I have read and agree to the MLH code of conduct.')), + ('logistics_agree', models.BooleanField(default=False, help_text='I authorize you to share my application/registration information with Major League Hacking for event administration, ranking, and MLH administration in-line with the MLH Privacy Policy. I further agree to the terms of both the MLH Contest Terms and Conditions and the MLH Privacy Policy.')), + ('email_agree', models.BooleanField(blank=True, default=False, help_text='I authorize MLH to send me occasional emails about relevant events, career opportunities, and community announcements.', null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + ), + ] diff --git a/hackathon_site/event/models.py b/hackathon_site/event/models.py index 0bef11d..12bac21 100644 --- a/hackathon_site/event/models.py +++ b/hackathon_site/event/models.py @@ -1,5 +1,6 @@ from django.db import models from django.conf import settings +from django.core import validators from django.contrib.auth import get_user_model import uuid @@ -57,3 +58,138 @@ class UserActivity(models.Model): dinner1 = models.DateTimeField(null=True, blank=True) breakfast2 = models.DateTimeField(null=True, blank=True) lunch2 = models.DateTimeField(null=True, blank=True) + + +# --- Public MLH interest form --------------------------------------------- +# A publicly accessible "register your interest" form shown on the landing +# page while official registration is closed (2027 dates TBD). It collects the +# 8 MLH-required fields + the 3 MLH agreement checkboxes. The field formats and +# checkbox wording mirror the gated registration.Application form so the two +# stay identical for MLH's review. + +# Mirrors registration.models.Application.AGE_CHOICES +INTEREST_AGE_CHOICES = [ + (None, ""), + (17, "17"), + (18, "18"), + (19, "19"), + (20, "20"), + (21, "21"), + (22, "22"), + (23, "22+"), +] + +# Mirrors registration.models.Application.STUDY_LEVEL_CHOICES +INTEREST_STUDY_LEVEL_CHOICES = [ + (None, ""), + ( + "post-secondary-2-years", + "2 year Undergraduate University or community college program", + ), + ("post-secondary-3-or-more-years", "3+ year Undergraduate University program"), + ("graduate", "Graduate University (Masters, Professional, Doctoral, etc)"), + ("other", "Other"), +] + +# ISO 3166-1 list of countries (Country of Residence), per MLH's required format. +_COUNTRY_NAMES = [ + "Afghanistan", "Albania", "Algeria", "Andorra", "Angola", + "Antigua and Barbuda", "Argentina", "Armenia", "Australia", "Austria", + "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus", + "Belgium", "Belize", "Benin", "Bhutan", "Bolivia", + "Bosnia and Herzegovina", "Botswana", "Brazil", "Brunei", "Bulgaria", + "Burkina Faso", "Burundi", "Cabo Verde", "Cambodia", "Cameroon", "Canada", + "Central African Republic", "Chad", "Chile", "China", "Colombia", + "Comoros", "Congo (Brazzaville)", "Congo (Kinshasa)", "Costa Rica", + "Côte d'Ivoire", "Croatia", "Cuba", "Cyprus", "Czechia", "Denmark", + "Djibouti", "Dominica", "Dominican Republic", "Ecuador", "Egypt", + "El Salvador", "Equatorial Guinea", "Eritrea", "Estonia", "Eswatini", + "Ethiopia", "Fiji", "Finland", "France", "Gabon", "Gambia", "Georgia", + "Germany", "Ghana", "Greece", "Grenada", "Guatemala", "Guinea", + "Guinea-Bissau", "Guyana", "Haiti", "Honduras", "Hungary", "Iceland", + "India", "Indonesia", "Iran", "Iraq", "Ireland", "Israel", "Italy", + "Jamaica", "Japan", "Jordan", "Kazakhstan", "Kenya", "Kiribati", + "Kosovo", "Kuwait", "Kyrgyzstan", "Laos", "Latvia", "Lebanon", "Lesotho", + "Liberia", "Libya", "Liechtenstein", "Lithuania", "Luxembourg", + "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", + "Marshall Islands", "Mauritania", "Mauritius", "Mexico", "Micronesia", + "Moldova", "Monaco", "Mongolia", "Montenegro", "Morocco", "Mozambique", + "Myanmar", "Namibia", "Nauru", "Nepal", "Netherlands", "New Zealand", + "Nicaragua", "Niger", "Nigeria", "North Korea", "North Macedonia", + "Norway", "Oman", "Pakistan", "Palau", "Palestine", "Panama", + "Papua New Guinea", "Paraguay", "Peru", "Philippines", "Poland", + "Portugal", "Qatar", "Romania", "Russia", "Rwanda", + "Saint Kitts and Nevis", "Saint Lucia", + "Saint Vincent and the Grenadines", "Samoa", "San Marino", + "Sao Tome and Principe", "Saudi Arabia", "Senegal", "Serbia", + "Seychelles", "Sierra Leone", "Singapore", "Slovakia", "Slovenia", + "Solomon Islands", "Somalia", "South Africa", "South Korea", + "South Sudan", "Spain", "Sri Lanka", "Sudan", "Suriname", "Sweden", + "Switzerland", "Syria", "Taiwan", "Tajikistan", "Tanzania", "Thailand", + "Timor-Leste", "Togo", "Tonga", "Trinidad and Tobago", "Tunisia", + "Turkey", "Turkmenistan", "Tuvalu", "Uganda", "Ukraine", + "United Arab Emirates", "United Kingdom", "United States", "Uruguay", + "Uzbekistan", "Vanuatu", "Vatican City", "Venezuela", "Vietnam", + "Yemen", "Zambia", "Zimbabwe", "Other", +] +INTEREST_COUNTRY_CHOICES = [(None, "")] + [(name, name) for name in _COUNTRY_NAMES] + + +class InterestSubmission(models.Model): + first_name = models.CharField(max_length=30, null=False) + last_name = models.CharField(max_length=30, null=False) + email = models.EmailField(null=False) + age = models.PositiveIntegerField(choices=INTEREST_AGE_CHOICES, null=False) + phone_number = models.CharField( + max_length=20, + null=False, + validators=[ + validators.RegexValidator( + r"^\+?\d[\d\s()-]{7,}$", + message="Enter a valid phone number.", + ) + ], + ) + school = models.CharField(max_length=255, null=False) + study_level = models.CharField( + max_length=50, + help_text="Current level of study", + choices=INTEREST_STUDY_LEVEL_CHOICES, + null=False, + ) + country = models.CharField( + max_length=255, choices=INTEREST_COUNTRY_CHOICES, null=False + ) + + # The 3 MLH checkboxes. Help-text wording mirrors + # registration.models.Application verbatim. + conduct_agree = models.BooleanField( + help_text="I have read and agree to the " + 'MLH code of conduct.', + blank=False, + null=False, + default=False, + ) + logistics_agree = models.BooleanField( + help_text="I authorize you to share my application/registration information with Major League Hacking" + " for event administration, ranking, and MLH administration in-line with the " + 'MLH Privacy Policy. ' + "I further agree to the terms of both the " + 'MLH Contest Terms and Conditions' + " and the " + 'MLH Privacy Policy.', + blank=False, + null=False, + default=False, + ) + email_agree = models.BooleanField( + help_text="I authorize MLH to send me occasional emails about relevant events, career opportunities, and community announcements.", + blank=True, + null=True, + default=False, + ) + + created_at = models.DateTimeField(auto_now_add=True, null=False) + + def __str__(self): + return f"{self.first_name} {self.last_name} <{self.email}>" diff --git a/hackathon_site/event/static/event/js/landing.js b/hackathon_site/event/static/event/js/landing.js index 71f8f55..f9a0557 100644 --- a/hackathon_site/event/static/event/js/landing.js +++ b/hackathon_site/event/static/event/js/landing.js @@ -4,28 +4,48 @@ $(document).scroll(function () { $nav.toggleClass("scrolled", $(this).scrollTop() > $nav.height()); }); +// Pick a readable font colour (dark or light) for a given hex background, so the +// text colour is always derived from the section's actual background. This +// replaces a hard-coded per-index colour list, which silently desynced whenever +// a section was added/removed/reordered (e.g. leaving white text on a white panel). +function contrastFontColor(hex) { + let c = (hex || "").replace("#", ""); + if (c.length === 3) { + c = c.replace(/(.)/g, "$1$1"); // expand shorthand e.g. "fff" -> "ffffff" + } + if (c.length !== 6) { + return "#333"; + } + const r = parseInt(c.substr(0, 2), 16); + const g = parseInt(c.substr(2, 2), 16); + const b = parseInt(c.substr(4, 2), 16); + // Perceived luminance (ITU-R BT.601); bright backgrounds get dark text. + const luminance = 0.299 * r + 0.587 * g + 0.114 * b; + return luminance > 150 ? "#333" : "#FFF"; +} + // Background color changing $(window) .scroll(function () { let $window = $(window), $wrapper = $(".wrapper"), - $colorScrollPanel = $(".colorScroll"), - fontColors = ["#333", "#FFF", "#FFF", "#333", "#333"]; + $colorScrollPanel = $(".colorScroll"); // Change 40% earlier than scroll position so colour is there when you arrive. let scroll = $window.scrollTop() + $window.height() * 0.4; if ($window.scrollTop() < 300) { $wrapper.css("background-color", "#fff"); } - $colorScrollPanel.each(function (i) { + $colorScrollPanel.each(function () { let $this = $(this); if ( $this.position().top <= scroll && $this.position().top + $this.height() > scroll ) { - $wrapper.css("background-color", $(this).attr("data-background-color")); - $colorScrollPanel.css("color", fontColors[i]); + let bg = $this.attr("data-background-color"); + $wrapper.css("background-color", bg); + $colorScrollPanel.css("color", contrastFontColor(bg)); } }); }) diff --git a/hackathon_site/event/tests.py b/hackathon_site/event/tests.py index ee92215..1098a02 100644 --- a/hackathon_site/event/tests.py +++ b/hackathon_site/event/tests.py @@ -9,7 +9,7 @@ from django.urls import reverse from rest_framework import status -from event.models import Profile, User, Team as EventTeam +from event.models import Profile, User, Team as EventTeam, InterestSubmission from hackathon_site.tests import SetupUserMixin from registration.models import Team as RegistrationTeam, Application @@ -134,6 +134,67 @@ def test_no_apply_button_when_registration_closed(self, mock_is_registration_ope response = self.client.get(self.view) self.assertNotContains(response, reverse("registration:signup")) + def test_interest_form_renders(self): + """The public MLH interest form and its checkboxes render on the page.""" + response = self.client.get(self.view) + self.assertContains(response, "Register Your Interest") + self.assertContains(response, "MLH code of conduct") + self.assertContains(response, 'name="conduct_agree"') + self.assertContains(response, 'name="logistics_agree"') + self.assertContains(response, 'name="email_agree"') + + +class InterestSubmissionViewTestCase(TestCase): + """Tests for the public, ungated MLH interest form on the landing page.""" + + def setUp(self): + self.view = reverse("event:index") + self.valid_data = { + "first_name": "Foo", + "last_name": "Bar", + "email": "foo@bar.com", + "age": 18, + "phone_number": "+1 (123) 456-7890", + "school": "University of Toronto", + "study_level": "graduate", + "country": "Canada", + "conduct_agree": "on", + "logistics_agree": "on", + "email_agree": "on", + } + + def test_valid_submission_creates_record_and_redirects(self): + response = self.client.post(self.view, data=self.valid_data) + self.assertRedirects(response, self.view) + self.assertEqual(InterestSubmission.objects.count(), 1) + submission = InterestSubmission.objects.first() + self.assertEqual(submission.email, "foo@bar.com") + # phone number is stripped down to digits on save + self.assertEqual(submission.phone_number, "11234567890") + + def test_missing_required_checkboxes_is_invalid(self): + data = {**self.valid_data} + del data["conduct_agree"] + del data["logistics_agree"] + response = self.client.post(self.view, data=data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(InterestSubmission.objects.count(), 0) + + def test_email_agree_is_optional(self): + data = {**self.valid_data} + del data["email_agree"] + response = self.client.post(self.view, data=data) + self.assertRedirects(response, self.view) + self.assertEqual(InterestSubmission.objects.count(), 1) + self.assertFalse(InterestSubmission.objects.first().email_agree) + + def test_missing_required_field_is_invalid(self): + data = {**self.valid_data} + del data["school"] + response = self.client.post(self.view, data=data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(InterestSubmission.objects.count(), 0) + class DashboardTestCase(SetupUserMixin, TestCase): def setUp(self): diff --git a/hackathon_site/event/views.py b/hackathon_site/event/views.py index 21703c9..8814c0c 100644 --- a/hackathon_site/event/views.py +++ b/hackathon_site/event/views.py @@ -22,6 +22,7 @@ get_curr_sign_in_time, strftime, ) +from event.forms import InterestForm from registration.forms import JoinTeamForm, SignInForm from registration.models import Team as RegistrationTeam, User, Application @@ -35,8 +36,10 @@ def _now(): return datetime.now().replace(tzinfo=settings.TZ_INFO) -class IndexView(TemplateView): +class IndexView(FormView): template_name = "event/landing.html" + form_class = InterestForm + success_url = reverse_lazy("event:index") def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -50,6 +53,15 @@ def get_context_data(self, **kwargs): context["application"] = getattr(self.request.user, "application", None) return context + def form_valid(self, form): + form.save() + messages.success( + self.request, + "Thanks! We've recorded your interest and will email you when " + "registration opens.", + ) + return super().form_valid(form) + class DashboardView(LoginRequiredMixin, FormView): # Form submits should take the user back to the dashboard