Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion hackathon_site/event/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
55 changes: 55 additions & 0 deletions hackathon_site/event/forms.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import re

from django import forms
from django.contrib.auth.forms import (
PasswordChangeForm as _PasswordChangeForm,
PasswordResetForm as _PasswordResetForm,
SetPasswordForm as _SetPasswordForm,
AuthenticationForm as _AuthenticationForm,
)

from event.models import InterestSubmission


class PasswordChangeForm(_PasswordChangeForm):
def __init__(self, user, *args, **kwargs):
Expand All @@ -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
52 changes: 52 additions & 0 deletions hackathon_site/event/jinja2/event/form_macros.html
Original file line number Diff line number Diff line change
@@ -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="") %}
<div class="input-field row col {{ size }}" id="{{ id }}">
{{ field }}
{% if show_label %}
{% if show_help_text_as_label %}
<label for="{{ field.id_for_label }}">{{ field.help_text|safe or field.label}}</label>
{% else %}
{{ field.label_tag() }}
{% endif %}
{% endif %}
{% if show_help_text %}
<span class="helper-text">{{ field.help_text|safe }}</span>
{% endif %}
{% if field.errors %}
<span class="formFieldError col s12">
{% for error in field.errors %}
{{ error }}
<br />
{% endfor %}
</span>
{% endif %}
</div>
{% endmacro %}

{% macro render_checkbox_field(field, size="s12") %}
<div class="row col {{ size }}" style="padding-top: 0.5em; padding-bottom: 0.5em;">
<label>
<input
type="checkbox"
class="filled-in"
{% if field.value() %}checked="checked" {% endif %}
name="{{ field.name }}"
id="{{ field.id_for_label }}">
<span>{{ field.help_text|safe }}</span>
</label>
{% if field.errors %}
<span class="formFieldError col s12">
{% for error in field.errors %}
{{ error }}
{% endfor %}
</span>
{% endif %}
</div>
{% endmacro %}
183 changes: 182 additions & 1 deletion hackathon_site/event/jinja2/event/landing.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{% extends "event/base.html" %}
{% from "event/form_macros.html" import render_field, render_checkbox_field %}

{% block nav_background_color %}{% endblock %}

Expand All @@ -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') }}");
</script>
<!-- Select2 CSS (MLH interest form) -->
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-beta.1/dist/css/select2.min.css" rel="stylesheet"/>
<link rel="stylesheet" type="text/css" href="{{ static('event/styles/css/select2.css') }}"/>
<style>
/* Fade each section's text colour in step with the wrapper's 0.3s background
fade, so the text never momentarily mismatches its background (e.g. white
text appearing before the blue background has finished fading in). */
.colorScroll {
transition: color 0.3s ease-out;
}
/* Interest form collapsible: centre the title + arrow and flip the arrow when open. */
#interest .collapsible-header {
justify-content: center;
align-items: center;
cursor: pointer;
}
#interest .interest-title {
margin: 0.5rem 0;
}
#interest .interest-toggle {
font-size: 1.4rem;
font-weight: 600;
}
#interest .interest-arrow {
margin-left: 0.5rem;
transition: transform 0.2s ease;
}
#interest .active .interest-arrow {
transform: rotate(180deg);
}
</style>
{% endblock %}

{% block nav_links %}
Expand Down Expand Up @@ -46,7 +78,7 @@ <h5>
</h5>
<button
class="btn-small waves-effect waves-light colorBtn"
onclick="window.open('http://mlh.io/code-of-conduct', '_blank')">
onclick="window.open('https://github.com/MLH/mlh-policies/blob/main/code-of-conduct.md', '_blank')">
View MLH Code of Conduct
</button>
</div>
Expand Down Expand Up @@ -107,6 +139,74 @@ <h3 class="center" id="countdownTitle"></h3>
</div>
<br /><br /><br />

<div class="container colorScroll" data-background-color="#fff">
<div class="section scrollspy" id="interest">
{% set msgs = get_messages(request) %}
{# Open the panel automatically when there's feedback to show. #}
{% set open_form = form.errors or msgs %}
<div class="row">
<div class="col s12 center">
<h3 class="center interest-title">Register Your Interest</h3>
<p class="center">
MakeUofT 2027 is on its way! Leave your info below and we'll email you as soon as applications open.
</p>
</div>
<div class="col s12">
<ul class="collapsible expandable">
<li {% if open_form %}class="active"{% endif %}>
<div class="collapsible-header">
<span class="interest-toggle">Sign up for updates</span>
<i class="material-icons interest-arrow">expand_more</i>
</div>
<div class="collapsible-body">
{% if msgs %}
{% for message in msgs %}
<p class="center primaryText">{{ message }}</p>
{% endfor %}
{% endif %}

<form action="{{ url('event:index') }}" method="post">
{{ csrf_input }}
<div class="row">
{% if form.non_field_errors() %}
<div class="col s12">
{% for error in form.non_field_errors() %}
<span class="formError">{{ error }}</span>
{% endfor %}
</div>
{% endif %}

{{ render_field(form.first_name, size="s12 m6") }}
{{ render_field(form.last_name, size="s12 m6") }}
{{ render_field(form.email, size="s12 m6") }}
{{ render_field(form.phone_number, size="s12 m6") }}
{{ render_field(form.age, size="s12 m6 select2-wrapper") }}
{{ render_field(form.study_level, show_help_text_as_label=True, size="s12 m6 select2-wrapper") }}
{{ render_field(form.school, size="s12 m6 select2-wrapper") }}
{{ render_field(form.country, size="s12 m6 select2-wrapper") }}

<div class="col s12">
<p>We are currently in the process of partnering with MLH. The following 3 checkboxes are for this partnership. If we do not end up partnering with MLH, your information will not be shared.</p>
<br/>
</div>

{{ render_checkbox_field(form.conduct_agree) }}
{{ render_checkbox_field(form.logistics_agree) }}
{{ render_checkbox_field(form.email_agree) }}

<div class="input-field col s12 center">
<button type="submit" class="btn-small waves-effect waves-light colorBtn">Submit</button>
</div>
</div>
</form>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>

<div class="container colorScroll" data-background-color="#035E8A">
<div class="section scrollspy" id="schedule">
<div class="row schedule">
Expand Down Expand Up @@ -399,4 +499,85 @@ <h3 class="row center">Contact Us</h3>
<script src="{{ static('event/js/sponsors-data.js') }}"></script>
<script src="{{ static('event/js/sponsors-component.js') }}"></script>
<script src="{{ static('event/js/landing.js') }}"></script>

<!-- Select2 JS for the MLH interest form -->
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-beta.1/dist/js/select2.min.js"></script>
<script>
$(document).ready(function () {
const selects = $("#interest select");
// width:'100%' so Select2 sizes from CSS, not the (initially hidden,
// zero-width) collapsible body it lives in.
selects.not(".select2-school-select").select2({width: "100%"});
$(".select2-school-select").select2({tags: true, width: "100%"}); // Allow custom school options

// Properly style any pre-filled selects
$.each(selects, (i, e) => {
if (e.value) {
$(e).siblings("label").addClass("label-active");
}
});

selects.on("select2:opening", function () {
$(this).siblings("label").addClass("label-active");
});

selects.on("select2:closing", function (e) {
if (!this.value) {
$(this).siblings("label").removeClass("label-active");
}
// steal focus during close - only capture once and stop propagation
// https://stackoverflow.com/a/49261426/3882202
$(e.target).data("select2").$selection.one("focus focusin", function (e) {
e.stopPropagation();
});
});

// on first focus (bubbles up to document), open the menu
$(document).on('focus', '#interest .select2-selection.select2-selection--single', function () {
$(this).closest(".select2-container").siblings('select:enabled').select2('open');
});

{% if open_form %}
// Expand the interest form when there are validation errors or a
// success message so the feedback isn't hidden behind the collapsed panel.
$("#interest .collapsible").collapsible("open", 0);
{% endif %}
});

$.get("https://raw.githubusercontent.com/MLH/mlh-policies/master/schools.csv", (data) => {
/* Populate the school list with MLH's list of recognized schools.
* Because `tags: true` is set, applicants can enter a custom option as well. */
let schools = data.split("\n").slice(1);
let select2Data = [];

for (let school of schools) {
school = school.trim().replace(/["]/g, "");
select2Data.push({
id: school,
text: school
});
}

let select2Elem = $(".select2-school-select");

select2Elem.select2({
data: select2Data,
tags: true,
width: "100%",
});

{% if form.school.value() is not none and form.school.value() %}
const selectedSchool = "{{ form.school.value() }}";

if (select2Elem.find(`option[value='${selectedSchool}']`).length) {
select2Elem.val(selectedSchool).trigger("change");
} else {
const newOption = new Option(selectedSchool, selectedSchool, true, true);
select2Elem.append(newOption).trigger("change");
}

select2Elem.siblings("label").addClass("label-active");
{% endif %}
});
</script>
{% endblock %}
Loading
Loading