From 3e534ef9c78d889c7ea3217ab68d833992f7e959 Mon Sep 17 00:00:00 2001 From: Kevin Chung Date: Thu, 20 Aug 2020 02:41:11 -0400 Subject: [PATCH] Add fields to team forms --- CTFd/forms/teams.py | 195 +++++++++++++++--- CTFd/forms/users.py | 4 +- CTFd/teams.py | 49 ++++- .../admin/templates/modals/teams/create.html | 18 +- .../admin/templates/modals/teams/edit.html | 6 +- .../themes/core/templates/teams/new_team.html | 8 +- CTFd/themes/core/templates/teams/private.html | 6 + tests/users/test_fields.py | 2 +- 8 files changed, 242 insertions(+), 46 deletions(-) diff --git a/CTFd/forms/teams.py b/CTFd/forms/teams.py index e06da7c9..80ccb27e 100644 --- a/CTFd/forms/teams.py +++ b/CTFd/forms/teams.py @@ -4,45 +4,142 @@ from wtforms.validators import InputRequired from CTFd.forms import BaseForm from CTFd.forms.fields import SubmitField +from CTFd.models import TeamFieldEntries, TeamFields from CTFd.utils.countries import SELECT_COUNTRIES_LIST +def build_custom_team_fields( + form_cls, + include_entries=False, + fields_kwargs=None, + field_entries_kwargs=None, + blacklisted_items=("affiliation", "website"), +): + if fields_kwargs is None: + fields_kwargs = {} + if field_entries_kwargs is None: + field_entries_kwargs = {} + + fields = [] + new_fields = TeamFields.query.filter_by(**fields_kwargs).all() + user_fields = {} + + # Only include preexisting values if asked + if include_entries is True: + for f in TeamFieldEntries.query.filter_by(**field_entries_kwargs).all(): + user_fields[f.field_id] = f.value + + for field in new_fields: + if field.name.lower() in blacklisted_items: + continue + + form_field = getattr(form_cls, f"fields[{field.id}]") + + # Add the field_type to the field so we know how to render it + form_field.field_type = field.field_type + + # Only include preexisting values if asked + if include_entries is True: + initial = user_fields.get(field.id, "") + form_field.data = initial + if form_field.render_kw: + form_field.render_kw["data-initial"] = initial + else: + form_field.render_kw = {"data-initial": initial} + + fields.append(form_field) + return fields + + +def attach_custom_team_fields(form_cls, **kwargs): + new_fields = TeamFields.query.filter_by(**kwargs).all() + for field in new_fields: + validators = [] + if field.required: + validators.append(InputRequired()) + + if field.field_type == "text": + input_field = StringField( + field.name, description=field.description, validators=validators + ) + elif field.field_type == "boolean": + input_field = BooleanField( + field.name, description=field.description, validators=validators + ) + + setattr(form_cls, f"fields[{field.id}]", input_field) + + class TeamJoinForm(BaseForm): name = StringField("Team Name", validators=[InputRequired()]) password = PasswordField("Team Password", validators=[InputRequired()]) submit = SubmitField("Join") -class TeamRegisterForm(BaseForm): - name = StringField("Team Name", validators=[InputRequired()]) - password = PasswordField("Team Password", validators=[InputRequired()]) - submit = SubmitField("Create") +def TeamRegisterForm(*args, **kwargs): + class _TeamRegisterForm(BaseForm): + name = StringField("Team Name", validators=[InputRequired()]) + password = PasswordField("Team Password", validators=[InputRequired()]) + submit = SubmitField("Create") + + @property + def extra(self): + return build_custom_team_fields( + self, include_entries=False, blacklisted_items=() + ) + + attach_custom_team_fields(_TeamRegisterForm) + return _TeamRegisterForm(*args, **kwargs) -class TeamSettingsForm(BaseForm): - name = StringField( - "Team Name", description="Your team's public name shown to other competitors" - ) - password = PasswordField( - "New Team Password", description="Set a new team join password" - ) - confirm = PasswordField( - "Confirm Password", - description="Provide your current team password (or your password) to update your team's password", - ) - affiliation = StringField( - "Affiliation", - description="Your team's affiliation publicly shown to other competitors", - ) - website = URLField( - "Website", description="Your team's website publicly shown to other competitors" - ) - country = SelectField( - "Country", - choices=SELECT_COUNTRIES_LIST, - description="Your team's country publicly shown to other competitors", - ) - submit = SubmitField("Submit") +def TeamSettingsForm(*args, **kwargs): + class _TeamSettingsForm(BaseForm): + name = StringField( + "Team Name", + description="Your team's public name shown to other competitors", + ) + password = PasswordField( + "New Team Password", description="Set a new team join password" + ) + confirm = PasswordField( + "Confirm Password", + description="Provide your current team password (or your password) to update your team's password", + ) + affiliation = StringField( + "Affiliation", + description="Your team's affiliation publicly shown to other competitors", + ) + website = URLField( + "Website", + description="Your team's website publicly shown to other competitors", + ) + country = SelectField( + "Country", + choices=SELECT_COUNTRIES_LIST, + description="Your team's country publicly shown to other competitors", + ) + submit = SubmitField("Submit") + + @property + def extra(self): + return build_custom_team_fields( + self, + include_entries=True, + fields_kwargs={"editable": True}, + field_entries_kwargs={"team_id": self.obj.id}, + ) + + def __init__(self, *args, **kwargs): + """ + Custom init to persist the obj parameter to the rest of the form + """ + super().__init__(*args, **kwargs) + obj = kwargs.get("obj") + if obj: + self.obj = obj + + attach_custom_team_fields(_TeamSettingsForm) + return _TeamSettingsForm(*args, **kwargs) class TeamCaptainForm(BaseForm): @@ -82,7 +179,7 @@ class PublicTeamSearchForm(BaseForm): submit = SubmitField("Search") -class TeamCreateForm(BaseForm): +class TeamBaseForm(BaseForm): name = StringField("Team Name", validators=[InputRequired()]) email = EmailField("Email") password = PasswordField("Password") @@ -94,5 +191,41 @@ class TeamCreateForm(BaseForm): submit = SubmitField("Submit") -class TeamEditForm(TeamCreateForm): - pass +def TeamCreateForm(*args, **kwargs): + class _TeamCreateForm(TeamBaseForm): + pass + + @property + def extra(self): + return build_custom_team_fields(self, include_entries=False) + + attach_custom_team_fields(_TeamCreateForm) + + return _TeamCreateForm(*args, **kwargs) + + +def TeamEditForm(*args, **kwargs): + class _TeamEditForm(TeamBaseForm): + pass + + @property + def extra(self): + return build_custom_team_fields( + self, + include_entries=True, + fields_kwargs=None, + field_entries_kwargs={"team_id": self.obj.id}, + ) + + def __init__(self, *args, **kwargs): + """ + Custom init to persist the obj parameter to the rest of the form + """ + super().__init__(*args, **kwargs) + obj = kwargs.get("obj") + if obj: + self.obj = obj + + attach_custom_team_fields(_TeamEditForm) + + return _TeamEditForm(*args, **kwargs) diff --git a/CTFd/forms/users.py b/CTFd/forms/users.py index 55627171..4962aa22 100644 --- a/CTFd/forms/users.py +++ b/CTFd/forms/users.py @@ -76,9 +76,7 @@ def attach_custom_user_fields(form_cls, **kwargs): field.name, description=field.description, validators=validators ) - setattr( - form_cls, f"fields[{field.id}]", input_field, - ) + setattr(form_cls, f"fields[{field.id}]", input_field) class UserSearchForm(BaseForm): diff --git a/CTFd/teams.py b/CTFd/teams.py index 142477c8..e12bc966 100644 --- a/CTFd/teams.py +++ b/CTFd/teams.py @@ -1,7 +1,7 @@ from flask import Blueprint, redirect, render_template, request, url_for from CTFd.cache import clear_team_session, clear_user_session -from CTFd.models import Teams, db +from CTFd.models import TeamFieldEntries, TeamFields, Teams, db from CTFd.utils import config, get_config from CTFd.utils.crypto import verify_password from CTFd.utils.decorators import authed_only, ratelimit @@ -133,14 +133,61 @@ def new(): if not teamname: errors.append("That team name is invalid") + # Process additional user fields + fields = {} + for field in TeamFields.query.all(): + fields[field.id] = field + + entries = {} + for field_id, field in fields.items(): + value = request.form.get(f"fields[{field_id}]", "").strip() + if field.required is True and (value is None or value == ""): + errors.append("Please provide all required fields") + break + + # Handle special casing of existing profile fields + if field.name.lower() == "affiliation": + affiliation = value + break + elif field.name.lower() == "website": + website = value + break + + if field.field_type == "boolean": + entries[field_id] = bool(value) + else: + entries[field_id] = value + + # if website: + # valid_website = validators.validate_url(website) + # else: + # valid_website = True + + # if affiliation: + # valid_affiliation = len(affiliation) < 128 + # else: + # valid_affiliation = True + if errors: return render_template("teams/new_team.html", errors=errors) team = Teams(name=teamname, password=passphrase, captain_id=user.id) + # if website: + # user.website = website + # if affiliation: + # user.affiliation = affiliation + # if country: + # user.country = country + db.session.add(team) db.session.commit() + for field_id, value in entries.items(): + entry = TeamFieldEntries(field_id=field_id, value=value, team_id=team.id) + db.session.add(entry) + db.session.commit() + user.team_id = team.id db.session.commit() diff --git a/CTFd/themes/admin/templates/modals/teams/create.html b/CTFd/themes/admin/templates/modals/teams/create.html index 84fd5aae..787bfa64 100644 --- a/CTFd/themes/admin/templates/modals/teams/create.html +++ b/CTFd/themes/admin/templates/modals/teams/create.html @@ -1,29 +1,33 @@ -{% with form = Forms.teams.TeamEditForm() %} +{% with form = Forms.teams.TeamCreateForm() %} +{% from "admin/macros/forms.html" import render_extra_fields %}
- {{ form.name.label }} + {{ form.name.label }} {{ form.name(class="form-control") }}
- {{ form.email.label }} + {{ form.email.label }} {{ form.email(class="form-control") }}
- {{ form.password.label }} + {{ form.password.label }} {{ form.password(class="form-control") }}
- {{ form.website.label }} + {{ form.website.label }} {{ form.website(class="form-control") }}
- {{ form.affiliation.label }} + {{ form.affiliation.label }} {{ form.affiliation(class="form-control") }}
- {{ form.country.label }} + {{ form.country.label }} {{ form.country(class="form-control custom-select") }}
+ + {{ render_extra_fields(form.extra) }} +
{{ form.hidden(class="form-check-input") }} diff --git a/CTFd/themes/admin/templates/modals/teams/edit.html b/CTFd/themes/admin/templates/modals/teams/edit.html index bca653e4..3823cb9c 100644 --- a/CTFd/themes/admin/templates/modals/teams/edit.html +++ b/CTFd/themes/admin/templates/modals/teams/edit.html @@ -1,4 +1,5 @@ -{% with form = Forms.teams.TeamCreateForm(obj=team) %} +{% with form = Forms.teams.TeamEditForm(obj=team) %} +{% from "admin/macros/forms.html" import render_extra_fields %}
{{ form.name.label }} @@ -24,6 +25,9 @@ {{ form.country.label }} {{ form.country(class="form-control custom-select") }}
+ + {{ render_extra_fields(form.extra) }} +
{{ form.hidden(class="form-check-input") }} diff --git a/CTFd/themes/core/templates/teams/new_team.html b/CTFd/themes/core/templates/teams/new_team.html index f4607d3e..f5da7fe3 100644 --- a/CTFd/themes/core/templates/teams/new_team.html +++ b/CTFd/themes/core/templates/teams/new_team.html @@ -15,15 +15,19 @@ {% include "components/errors.html" %} {% with form = Forms.teams.TeamRegisterForm() %} + {% from "macros/forms.html" import render_extra_fields %}
- {{ form.name.label }} + {{ form.name.label }} {{ form.name(class="form-control") }}
- {{ form.password.label }} + {{ form.password.label }} {{ form.password(class="form-control") }}
+ + {{ render_extra_fields(form.extra) }} +

After creating your team, share the team name and password with your teammates so they can join your team.

diff --git a/CTFd/themes/core/templates/teams/private.html b/CTFd/themes/core/templates/teams/private.html index 45d3e63d..63102f84 100644 --- a/CTFd/themes/core/templates/teams/private.html +++ b/CTFd/themes/core/templates/teams/private.html @@ -15,6 +15,7 @@