diff --git a/apps/common/fields.py b/apps/common/fields.py index a1e081a03..cc5242d93 100644 --- a/apps/common/fields.py +++ b/apps/common/fields.py @@ -1,8 +1,14 @@ import json from markupsafe import Markup -from wtforms import IntegerField, SelectField, StringField, ValidationError -from wtforms.widgets import Input, HiddenInput +from wtforms import ( + IntegerField, + SelectField, + SelectMultipleField, + StringField, + ValidationError, +) +from wtforms.widgets import Input, HiddenInput, CheckboxInput, ListWidget from wtforms.widgets.html5 import EmailInput from wtforms.widgets.core import html_params from email_validator import validate_email, EmailNotValidError @@ -117,3 +123,8 @@ def __call__(self, field, **kwargs): class StaticField(StringField): widget = StaticWidget() + + +class MultiCheckboxField(SelectMultipleField): + widget = ListWidget(prefix_label=False) + option_widget = CheckboxInput() diff --git a/apps/common/forms.py b/apps/common/forms.py index 5677ed332..67d6ad176 100644 --- a/apps/common/forms.py +++ b/apps/common/forms.py @@ -4,14 +4,14 @@ from flask import current_app as app from flask_wtf import FlaskForm -from wtforms import SelectField, BooleanField +from wtforms import SelectField, BooleanField, ValidationError from wtforms.validators import InputRequired from models.user import UserDiversity from models.cfp_tag import DEFAULT_TAGS, Tag from models.purchase import AdmissionTicket -from .fields import HiddenIntegerField +from .fields import HiddenIntegerField, MultiCheckboxField class Form(FlaskForm): @@ -42,6 +42,31 @@ class Meta(FlaskForm.Meta): AGE_VALUES = ("0-15", "16-25", "26-35", "36-45", "46-55", "56-65", "66+") AGE_CHOICES = tuple(OPT_OUT + [(v, v) for v in AGE_VALUES]) +SEXUALITY_VALUES = ( + "straight-or-heterosexual", + "gay-or-lesbian", + "bisexual-or-pansexual", + "other", +) +SEXUALITY_CHOICES = tuple( + OPT_OUT + [(v, v.capitalize().replace("-", " ")) for v in SEXUALITY_VALUES] +) + + +DISABILITY_CHOICES = tuple( + [ + ("physical", "Physical disability or mobility issue"), + ("vision", "Blindness or a visual impairment not corrected by glasses"), + ("hearing", "Deafness or a serious hearing impairment"), + ("autism-adhd", "Autistic spectrum condition, Asperger's, or ADHD"), + ("long-term", "Long-term illness"), + ("mental-health", "Mental health condition"), + ("other", "Another condition not mentioned here"), + ("none", "None of the above"), + ] +) + + TOPIC_CHOICES = tuple(NULL_SELECTION + [(v, v.capitalize()) for v in DEFAULT_TAGS]) # FIXME these are matchers for transition from freetext diversity form -> select boxes @@ -100,6 +125,8 @@ class DiversityForm(Form): age = SelectField("Age", default=OPT_OUT[0], choices=AGE_CHOICES) gender = SelectField("Gender", default=OPT_OUT[0], choices=GENDER_CHOICES) ethnicity = SelectField("Ethnicity", default=OPT_OUT[0], choices=ETHNICITY_CHOICES) + sexuality = SelectField("Sexuality", default=OPT_OUT[0], choices=SEXUALITY_CHOICES) + disability = MultiCheckboxField("Disability", choices=DISABILITY_CHOICES) # Track CfP reviewer tags cfp_tag_0 = SelectField("Topic 1", choices=TOPIC_CHOICES) @@ -123,6 +150,8 @@ def update_user(self, user): user.diversity.age = self.age.data user.diversity.gender = self.gender.data user.diversity.ethnicity = self.ethnicity.data + user.diversity.sexuality = self.sexuality.data + user.diversity.disability = self.disability.data if self.cfp_tags_required: user.cfp_reviewer_tags = [ @@ -138,6 +167,8 @@ def set_from_user(self, user): self.age.data = guess_age(user.diversity.age) self.gender.data = guess_gender(user.diversity.gender) self.ethnicity.data = guess_ethnicity(user.diversity.ethnicity) + self.sexuality.data = user.diversity.sexuality + self.disability.data = user.diversity.disability if self.cfp_tags_required and user.cfp_reviewer_tags: self.cfp_tag_0.data = user.cfp_reviewer_tags[0].tag @@ -146,6 +177,12 @@ def set_from_user(self, user): return self + def validate_disability(form, field): + if len(field.data) > 1 and "none" in field.data: + raise ValidationError("Cannot select 'no disability' and a disability") + elif len(field.data) > 1 and "" in field.data: + raise ValidationError("Cannot select 'prefer not to say' and a disability") + def validate(self, extra_validators=None): if not super().validate(extra_validators): return False diff --git a/css/_base.scss b/css/_base.scss index 61a83ef59..35b2a9675 100644 --- a/css/_base.scss +++ b/css/_base.scss @@ -226,14 +226,6 @@ div.micro img { color: #333; } -.form-control { - background-color: $form-element; -} - -.form-horizontal .form-group .help-block { - padding-left: 0.5em; -} - legend { margin-bottom: 0px; border-bottom: 1px solid #aaa; @@ -384,17 +376,6 @@ a.collapse-toggle:hover .glyphicon { text-decoration: none; } } } -input[type="checkbox"].big-checkbox { - width: 20px; - height: 20px; - margin-right: 5px; -} -label.big-checkbox { - /* Increase the target area */ - padding: 11px 10px 10px 40px !important; - margin: -4px 0 -10px -40px !important; - vertical-align: 4px; -} /* Modals for the schedules */ .modal { @@ -411,45 +392,6 @@ label.big-checkbox { .role-description { display: none; } -/* Form tweakery */ - -fieldset legend { - margin-bottom: 10px; -} - -input[type=range] { - appearance: none; - border: none; - box-shadow: none; - background: transparent; - margin-bottom: 6px; -} - -input[type=range]::range-track { - appearance: none; - height: 8.4px; - cursor: pointer; - background: $main-link-hover; - border-radius: 3px; -} - -input[type=range]:focus::range-track { - background: lighten($main-link-hover, 5%); -} - -input[type=range]::range-thumb { - appearance: none; - box-shadow: 1px 1px 1px lighten($main-background, 50%); - border: 1px solid $main-background; - height: 30px; - width: 16px; - border-radius: 3px; - background: $highlight-3-background; - cursor: pointer; - margin-top: -11px; -} - - #mailing-list-form #name-field { display: none; } \ No newline at end of file diff --git a/css/_forms.scss b/css/_forms.scss new file mode 100644 index 000000000..4fb19f312 --- /dev/null +++ b/css/_forms.scss @@ -0,0 +1,64 @@ +@use "./_variables.scss" as *; + +.form-control { + background-color: $form-element; +} + +.form-horizontal .form-group .help-block { + padding-left: 0.5em; +} + +input[type="checkbox"] { + width: 20px; + height: 20px; + margin-left: -25px !important; + margin-top: 4px !important; +} + +fieldset legend { + margin-bottom: 10px; +} + +input[type=range] { + appearance: none; + border: none; + box-shadow: none; + background: transparent; + margin-bottom: 6px; +} + +input[type=range]::range-track { + appearance: none; + height: 8.4px; + cursor: pointer; + background: $main-link-hover; + border-radius: 3px; +} + +input[type=range]:focus::range-track { + background: lighten($main-link-hover, 5%); +} + +input[type=range]::range-thumb { + appearance: none; + box-shadow: 1px 1px 1px lighten($main-background, 50%); + border: 1px solid $main-background; + height: 30px; + width: 16px; + border-radius: 3px; + background: $highlight-3-background; + cursor: pointer; + margin-top: -11px; +} + +ul.form-check-input input { + vertical-align: top; +} + +ul.form-check-input li { + list-style-type: none; +} + +ul.form-check-input label { + font-weight: normal; +} \ No newline at end of file diff --git a/css/main.scss b/css/main.scss index 9e0172175..683c9fedd 100644 --- a/css/main.scss +++ b/css/main.scss @@ -11,6 +11,7 @@ @use "./_buttons.scss"; @use "./_video.scss"; @use "./_panel_grid.scss"; +@use "./_forms.scss"; @use "./_about.scss"; @use "./_attendee_content.scss"; diff --git a/migrations/versions/7249f66e2ae0_add_disability_and_sexuality_to_.py b/migrations/versions/7249f66e2ae0_add_disability_and_sexuality_to_.py new file mode 100644 index 000000000..059242604 --- /dev/null +++ b/migrations/versions/7249f66e2ae0_add_disability_and_sexuality_to_.py @@ -0,0 +1,28 @@ +"""add disability and sexuality to diversity table + +Revision ID: 7249f66e2ae0 +Revises: e9c68e8f78c2 +Create Date: 2024-06-03 23:13:28.010124 + +""" + +# revision identifiers, used by Alembic. +revision = '7249f66e2ae0' +down_revision = 'e9c68e8f78c2' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('diversity', sa.Column('disability', sa.String(), nullable=True)) + op.add_column('diversity', sa.Column('sexuality', sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('diversity', 'sexuality') + op.drop_column('diversity', 'disability') + # ### end Alembic commands ### diff --git a/models/user.py b/models/user.py index c9b488e97..fbcb450a9 100644 --- a/models/user.py +++ b/models/user.py @@ -470,6 +470,8 @@ class UserDiversity(BaseModel): age = db.Column(db.String) gender = db.Column(db.String) ethnicity = db.Column(db.String) + disability = db.Column(db.String) + sexuality = db.Column(db.String) @classmethod def get_export_data(cls): @@ -477,6 +479,8 @@ def get_export_data(cls): ages = defaultdict(int) sexes = defaultdict(int) ethnicities = defaultdict(int) + disability = defaultdict(int) + sexuality = defaultdict(int) for row in cls.query: matches = re.findall(r"\b[0-9]{1,3}\b", row.age) @@ -523,11 +527,21 @@ def get_export_data(cls): else: ethnicities["other"] += 1 + # This doesn't need a matcher because it's already encoded + disability[row.disability] += 1 + sexuality[row.sexuality] += 1 + ages.update(bucketise(valid_ages, [0, 15, 25, 35, 45, 55, 65])) data = { "private": { - "diversity": {"age": ages, "sex": sexes, "ethnicity": ethnicities} + "diversity": { + "age": ages, + "sex": sexes, + "ethnicity": ethnicities, + "disability": disability, + "sexuality": sexuality, + } }, "tables": ["diversity"], } diff --git a/templates/_diversityform.html b/templates/_diversityform.html index 6a0a682ae..5a23a9ae4 100644 --- a/templates/_diversityform.html +++ b/templates/_diversityform.html @@ -1,29 +1,31 @@ -{% from "_formhelpers.html" import render_field %} +{% from "_formhelpers.html" import render_field, render_multi_checkbox %} {% macro render_diversity_fields(form, current_user) %} {# Should only be rendered inside a .panel #} - -
-
- If you're comfortable telling us a little bit about yourself, we'd appreciate it. - This information will only be summarised and compared with previous years, to find out how well we're doing with outreach. -
-
-

- {{ render_field(form.age, True) }} - {{ render_field(form.gender, True) }} - {{ render_field(form.ethnicity, True) }} - {% if current_user.has_permission("cfp_reviewer") %} -

- Just so we know that we're reflecting our attendees' interests reasonably well: - please select the three topics that most interest you. Please review - everything you feel able to, this is purely so we can track if there are areas - we need to recruit more reviewers. -

- {{ render_field(form.cfp_tag_0, True) }} - {{ render_field(form.cfp_tag_1, True) }} - {{ render_field(form.cfp_tag_2, True) }} - {% endif %} -
-
+ {% if current_user.has_permission("cfp_reviewer") %} +
+

CfP reviewer survey

+

As you're a reviewer for our Call for Participation, please select the three topics that + most interest you, to help us track the interests of our review panel. Please still + review everything you feel able to.

+ {{ render_field(form.cfp_tag_0, True) }} + {{ render_field(form.cfp_tag_1, True) }} + {{ render_field(form.cfp_tag_2, True) }} +
+ {% endif %} +
+

Diversity survey

+

+ If you're comfortable telling us a little bit about yourself, we'd appreciate it if you + filled in our diversity survey. This information will be summarised and used to track how we + can improve the diversity of the community and the accessibility of EMF. +

+ {{ render_field(form.age, True) }} + {{ render_field(form.gender, True) }} + {{ render_field(form.ethnicity, True) }} + {{ render_field(form.sexuality, True) }} + {% call render_multi_checkbox(form.disability) %} + Please select any of the following that apply to you, or "None of the above": + {% endcall %} +
{% endmacro %} diff --git a/templates/_formhelpers.html b/templates/_formhelpers.html index ce1238a3a..db7bddce0 100644 --- a/templates/_formhelpers.html +++ b/templates/_formhelpers.html @@ -136,5 +136,33 @@ {% endfor %} {% endif %} +{% endmacro %} + +{% macro render_multi_checkbox(field) %} + {% if caller %} + {% set help_text = caller() %} + {% endif %} + + {% if field.errors %} + {# If we have help text, add an aria-describedby attribute to the field. #} + {% do kwargs.update({'aria-describedby': "help-block-" + field.name}) %} + {% endif %} +
+
+ {% if help_text %} +

{{help_text}}

+ {% endif %} +
+ {{ field(class_="form-check-input") }} +
+ {% if field.errors %} + {% for error in field.errors %} +

{{ error }}

+ {% endfor %} + {% endif -%} +
{% endmacro %} + diff --git a/templates/account/details.html b/templates/account/details.html index 07b033fbd..0edc3e7f4 100644 --- a/templates/account/details.html +++ b/templates/account/details.html @@ -14,10 +14,11 @@

Your Details

in the schedule. {% endif %} +
{{ form.hidden_tag() }}
-

+

Account details

@@ -38,7 +39,8 @@

Your Details

{{ render_diversity_fields(form, current_user) }} - {{ form.forward(class_="btn btn-default pull-right", style="margin-bottom: 15px;") }} + {{ form.forward(class_="btn btn-primary pull-right") }}
+
{% endblock %}