Add fields to team forms (#1615)

* Adds custom fields for teams
* Closes #756
This commit is contained in:
Kevin Chung
2020-08-21 02:49:28 -04:00
committed by GitHub
parent fb454b8262
commit c1672d1506
31 changed files with 1054 additions and 123 deletions

View File

@@ -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)

View File

@@ -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):

View File

@@ -346,7 +346,9 @@ class Users(db.Model):
if admin:
return self.field_entries
return [entry for entry in self.field_entries if entry.field.public]
return [
entry for entry in self.field_entries if entry.field.public and entry.value
]
def get_solves(self, admin=False):
from CTFd.utils import get_config
@@ -467,6 +469,10 @@ class Teams(db.Model):
captain_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="SET NULL"))
captain = db.relationship("Users", foreign_keys=[captain_id])
field_entries = db.relationship(
"TeamFieldEntries", foreign_keys="TeamFieldEntries.team_id", lazy="joined"
)
created = db.Column(db.DateTime, default=datetime.datetime.utcnow)
def __init__(self, **kwargs):
@@ -478,6 +484,10 @@ class Teams(db.Model):
return hash_password(str(plaintext))
@property
def fields(self):
return self.get_fields(admin=False)
@property
def solves(self):
return self.get_solves(admin=False)
@@ -503,6 +513,14 @@ class Teams(db.Model):
else:
return None
def get_fields(self, admin=False):
if admin:
return self.field_entries
return [
entry for entry in self.field_entries if entry.field.public and entry.value
]
def get_solves(self, admin=False):
from CTFd.utils import get_config

View File

@@ -1,6 +1,6 @@
from marshmallow import fields
from CTFd.models import Fields, UserFieldEntries, db, ma
from CTFd.models import Fields, TeamFieldEntries, UserFieldEntries, db, ma
class FieldSchema(ma.ModelSchema):
@@ -22,3 +22,17 @@ class UserFieldEntriesSchema(ma.ModelSchema):
name = fields.Nested(FieldSchema, only=("name"), attribute="field")
description = fields.Nested(FieldSchema, only=("description"), attribute="field")
type = fields.Nested(FieldSchema, only=("field_type"), attribute="field")
class TeamFieldEntriesSchema(ma.ModelSchema):
class Meta:
model = TeamFieldEntries
sqla_session = db.session
include_fk = True
load_only = ("id",)
exclude = ("field", "team", "team_id")
dump_only = ("team_id", "name", "description", "type")
name = fields.Nested(FieldSchema, only=("name"), attribute="field")
description = fields.Nested(FieldSchema, only=("description"), attribute="field")
type = fields.Nested(FieldSchema, only=("field_type"), attribute="field")

View File

@@ -1,7 +1,10 @@
from marshmallow import ValidationError, pre_load, validate
from marshmallow import ValidationError, post_dump, pre_load, validate
from marshmallow.fields import Nested
from marshmallow_sqlalchemy import field_for
from sqlalchemy.orm import load_only
from CTFd.models import Teams, Users, ma
from CTFd.models import TeamFieldEntries, TeamFields, Teams, Users, ma
from CTFd.schemas.fields import TeamFieldEntriesSchema
from CTFd.utils import get_config, string_types
from CTFd.utils.crypto import verify_password
from CTFd.utils.user import get_current_team, get_current_user, is_admin
@@ -44,6 +47,9 @@ class TeamSchema(ma.ModelSchema):
],
)
country = field_for(Teams, "country", validate=[validate_country_code])
fields = Nested(
TeamFieldEntriesSchema, partial=True, many=True, attribute="field_entries"
)
@pre_load
def validate_name(self, data):
@@ -186,6 +192,126 @@ class TeamSchema(ma.ModelSchema):
field_names=["captain_id"],
)
@pre_load
def validate_fields(self, data):
"""
This validator is used to only allow users to update the field entry for their user.
It's not possible to exclude it because without the PK Marshmallow cannot load the right instance
"""
fields = data.get("fields")
if fields is None:
return
current_team = get_current_team()
if is_admin():
team_id = data.get("id")
if team_id:
target_team = Teams.query.filter_by(id=data["id"]).first()
else:
target_team = current_team
# We are editting an existing
if self.view == "admin" and self.instance:
target_team = self.instance
provided_ids = []
for f in fields:
f.pop("id", None)
field_id = f.get("field_id")
# # Check that we have an existing field for this. May be unnecessary b/c the foriegn key should enforce
field = TeamFields.query.filter_by(id=field_id).first_or_404()
# Get the existing field entry if one exists
entry = TeamFieldEntries.query.filter_by(
field_id=field.id, team_id=target_team.id
).first()
if entry:
f["id"] = entry.id
provided_ids.append(entry.id)
# Extremely dirty hack to prevent deleting previously provided data.
# This needs a better soln.
entries = (
TeamFieldEntries.query.options(load_only("id"))
.filter_by(team_id=target_team.id)
.all()
)
for entry in entries:
if entry.id not in provided_ids:
fields.append({"id": entry.id})
else:
provided_ids = []
for f in fields:
# Remove any existing set
f.pop("id", None)
field_id = f.get("field_id")
value = f.get("value")
# # Check that we have an existing field for this. May be unnecessary b/c the foriegn key should enforce
field = TeamFields.query.filter_by(id=field_id).first_or_404()
if field.required is True and value.strip() == "":
raise ValidationError(
f"Field '{field.name}' is required", field_names=["fields"]
)
if field.editable is False:
raise ValidationError(
f"Field '{field.name}' cannot be editted",
field_names=["fields"],
)
# Get the existing field entry if one exists
entry = TeamFieldEntries.query.filter_by(
field_id=field.id, team_id=current_team.id
).first()
if entry:
f["id"] = entry.id
provided_ids.append(entry.id)
# Extremely dirty hack to prevent deleting previously provided data.
# This needs a better soln.
entries = (
TeamFieldEntries.query.options(load_only("id"))
.filter_by(team_id=current_team.id)
.all()
)
for entry in entries:
if entry.id not in provided_ids:
fields.append({"id": entry.id})
@post_dump
def process_fields(self, data):
"""
Handle permissions levels for fields.
This is post_dump to manipulate JSON instead of the raw db object
Admins can see all fields.
Users (self) can see their edittable and public fields
Public (user) can only see public fields
"""
# Gather all possible fields
removed_field_ids = []
fields = TeamFields.query.all()
# Select fields for removal based on current view and properties of the field
for field in fields:
if self.view == "user":
if field.public is False:
removed_field_ids.append(field.id)
elif self.view == "self":
if field.editable is False and field.public is False:
removed_field_ids.append(field.id)
# Rebuild fuilds
fields = data.get("fields")
if fields:
data["fields"] = [
field for field in fields if field["field_id"] not in removed_field_ids
]
views = {
"user": [
"website",
@@ -197,6 +323,7 @@ class TeamSchema(ma.ModelSchema):
"id",
"oauth_id",
"captain_id",
"fields",
],
"self": [
"website",
@@ -210,6 +337,7 @@ class TeamSchema(ma.ModelSchema):
"oauth_id",
"password",
"captain_id",
"fields",
],
"admin": [
"website",
@@ -227,6 +355,7 @@ class TeamSchema(ma.ModelSchema):
"oauth_id",
"password",
"captain_id",
"fields",
],
}
@@ -236,5 +365,6 @@ class TeamSchema(ma.ModelSchema):
kwargs["only"] = self.views[view]
elif isinstance(view, list):
kwargs["only"] = view
self.view = view
super(TeamSchema, self).__init__(*args, **kwargs)

View File

@@ -205,45 +205,55 @@ class UserSchema(ma.ModelSchema):
else:
target_user = current_user
provided_ids = []
for f in fields:
f.pop("id", None)
field_id = f.get("field_id")
# We are editting an existing user
if self.view == "admin" and self.instance:
target_user = self.instance
provided_ids = []
for f in fields:
f.pop("id", None)
field_id = f.get("field_id")
# # Check that we have an existing field for this. May be unnecessary b/c the foriegn key should enforce
field = UserFields.query.filter_by(id=field_id).first_or_404()
# # Check that we have an existing field for this. May be unnecessary b/c the foriegn key should enforce
field = UserFields.query.filter_by(id=field_id).first_or_404()
# Get the existing field entry if one exists
entry = UserFieldEntries.query.filter_by(
field_id=field.id, user_id=target_user.id
).first()
if entry:
f["id"] = entry.id
provided_ids.append(entry.id)
# Get the existing field entry if one exists
entry = UserFieldEntries.query.filter_by(
field_id=field.id, user_id=target_user.id
).first()
if entry:
f["id"] = entry.id
provided_ids.append(entry.id)
# Extremely dirty hack to prevent deleting previously provided data.
# This needs a better soln.
entries = (
UserFieldEntries.query.options(load_only("id"))
.filter_by(user_id=target_user.id)
.all()
)
for entry in entries:
if entry.id not in provided_ids:
fields.append({"id": entry.id})
# Extremely dirty hack to prevent deleting previously provided data.
# This needs a better soln.
entries = (
UserFieldEntries.query.options(load_only("id"))
.filter_by(user_id=target_user.id)
.all()
)
for entry in entries:
if entry.id not in provided_ids:
fields.append({"id": entry.id})
else:
provided_ids = []
for f in fields:
# Remove any existing set
f.pop("id", None)
field_id = f.get("field_id")
value = f.get("value")
# # Check that we have an existing field for this. May be unnecessary b/c the foriegn key should enforce
field = UserFields.query.filter_by(id=field_id).first_or_404()
if field.required is True and value.strip() == "":
raise ValidationError(
f"Field '{field.name}' is required", field_names=["fields"]
)
if field.editable is False:
raise ValidationError(
f"Field {field.name} cannot be editted", field_names=["fields"]
f"Field '{field.name}' cannot be editted",
field_names=["fields"],
)
# Get the existing field entry if one exists

View File

@@ -1,8 +1,8 @@
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.utils import config, get_config
from CTFd.models import TeamFieldEntries, TeamFields, Teams, db
from CTFd.utils import config, get_config, validators
from CTFd.utils.crypto import verify_password
from CTFd.utils.decorators import authed_only, ratelimit
from CTFd.utils.decorators.modes import require_team_mode
@@ -125,6 +125,9 @@ def new():
passphrase = request.form.get("password", "").strip()
errors = get_errors()
website = request.form.get("website")
affiliation = request.form.get("affiliation")
user = get_current_user()
existing_team = Teams.query.filter_by(name=teamname).first()
@@ -133,14 +136,64 @@ 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 valid_website is False:
errors.append("Websites must be a proper URL starting with http or https")
if valid_affiliation is False:
errors.append("Please provide a shorter affiliation")
if errors:
return render_template("teams/new_team.html", errors=errors)
team = Teams(name=teamname, password=passphrase, captain_id=user.id)
if website:
team.website = website
if affiliation:
team.affiliation = affiliation
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()

View File

@@ -20,7 +20,7 @@
v-model.lazy="field.field_type"
>
<option value="text">Text Field</option>
<option value="checkbox">Checkbox</option>
<option value="boolean">Checkbox</option>
</select>
<small class="form-text text-muted"
>Type of field shown to the user</small

View File

@@ -33,7 +33,9 @@ export default {
components: {
Field
},
props: {},
props: {
type: String
},
data: function() {
return {
fields: []
@@ -41,7 +43,7 @@ export default {
},
methods: {
loadFields: function() {
CTFd.fetch("/api/v1/configs/fields?type=user", {
CTFd.fetch(`/api/v1/configs/fields?type=${this.type}`, {
method: "GET",
credentials: "same-origin",
headers: {
@@ -59,7 +61,7 @@ export default {
addField: function() {
this.fields.push({
id: Math.random(),
type: "user",
type: this.type,
field_type: "text",
name: "",
description: "",

View File

@@ -363,9 +363,22 @@ $(() => {
})
.change();
// Insert CommentBox element
// Insert FieldList element for users
const fieldList = Vue.extend(FieldList);
let vueContainer = document.createElement("div");
document.querySelector("#user-field-list").appendChild(vueContainer);
new fieldList({}).$mount(vueContainer);
let userVueContainer = document.createElement("div");
document.querySelector("#user-field-list").appendChild(userVueContainer);
new fieldList({
propsData: {
type: "user"
}
}).$mount(userVueContainer);
// Insert FieldList element for teams
let teamVueContainer = document.createElement("div");
document.querySelector("#team-field-list").appendChild(teamVueContainer);
new fieldList({
propsData: {
type: "team"
}
}).$mount(teamVueContainer);
});

View File

@@ -11,6 +11,19 @@ function createTeam(event) {
event.preventDefault();
const params = $("#team-info-create-form").serializeJSON(true);
params.fields = [];
for (const property in params) {
if (property.match(/fields\[\d+\]/)) {
let field = {};
let id = parseInt(property.slice(7, -1));
field["field_id"] = id;
field["value"] = params[property];
params.fields.push(field);
delete params[property];
}
}
CTFd.fetch("/api/v1/teams", {
method: "POST",
credentials: "same-origin",
@@ -28,15 +41,17 @@ function createTeam(event) {
const team_id = response.data.id;
window.location = CTFd.config.urlRoot + "/admin/teams/" + team_id;
} else {
$("#team-info-form > #results").empty();
$("#team-info-create-form > #results").empty();
Object.keys(response.errors).forEach(function(key, _index) {
$("#team-info-form > #results").append(
$("#team-info-create-form > #results").append(
ezBadge({
type: "error",
body: response.errors[key]
})
);
const i = $("#team-info-form").find("input[name={0}]".format(key));
const i = $("#team-info-create-form").find(
"input[name={0}]".format(key)
);
const input = $(i);
input.addClass("input-filled-invalid");
input.removeClass("input-filled-valid");
@@ -47,7 +62,20 @@ function createTeam(event) {
function updateTeam(event) {
event.preventDefault();
const params = $("#team-info-edit-form").serializeJSON(true);
let params = $("#team-info-edit-form").serializeJSON(true);
params.fields = [];
for (const property in params) {
if (property.match(/fields\[\d+\]/)) {
let field = {};
let id = parseInt(property.slice(7, -1));
field["field_id"] = id;
field["value"] = params[property];
params.fields.push(field);
delete params[property];
}
}
CTFd.fetch("/api/v1/teams/" + window.TEAM_ID, {
method: "PATCH",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -12,6 +12,11 @@
Users
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#team-fields" role="tab" data-toggle="tab">
Teams
</a>
</li>
</ul>
<div class="tab-content">
@@ -23,6 +28,14 @@
<div id="user-field-list" class="pt-3">
</div>
</div>
<div role="tabpanel" class="tab-pane" id="team-fields">
<div class="col-md-12 py-3">
<small>Custom team fields are shown during team creation. Team captains can optionally edit these fields in the team profile.</small>
</div>
<div id="team-field-list" class="pt-3">
</div>
</div>
</div>
</form>
</div>

View File

@@ -1,29 +1,36 @@
{% with form = Forms.teams.TeamEditForm() %}
{% with form = Forms.teams.TeamCreateForm() %}
{% from "admin/macros/forms.html" import render_extra_fields %}
<form id="team-info-create-form" method="POST">
<div class="form-group">
<b>{{ form.name.label }}</b>
{{ form.name.label }}
{{ form.name(class="form-control") }}
</div>
<div class="form-group">
<b>{{ form.email.label }}</b>
{{ form.email.label }}
{{ form.email(class="form-control") }}
</div>
<div class="form-group">
<b>{{ form.password.label }}</b>
{{ form.password.label }}
{{ form.password(class="form-control") }}
</div>
<div class="form-group">
<b>{{ form.website.label }}</b>
{{ form.website.label }}
<small class="float-right text-muted align-text-bottom">Optional</small>
{{ form.website(class="form-control") }}
</div>
<div class="form-group">
<b>{{ form.affiliation.label }}</b>
{{ form.affiliation.label }}
<small class="float-right text-muted align-text-bottom">Optional</small>
{{ form.affiliation(class="form-control") }}
</div>
<div class="form-group">
<b>{{ form.country.label }}</b>
{{ form.country.label }}
<small class="float-right text-muted align-text-bottom">Optional</small>
{{ form.country(class="form-control custom-select") }}
</div>
{{ render_extra_fields(form.extra) }}
<div class="form-group">
<div class="form-check form-check-inline">
{{ form.hidden(class="form-check-input") }}

View File

@@ -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 id="team-info-edit-form" method="POST">
<div class="form-group">
{{ form.name.label }}
@@ -24,6 +25,9 @@
{{ form.country.label }}
{{ form.country(class="form-control custom-select") }}
</div>
{{ render_extra_fields(form.extra) }}
<div class="form-group">
<div class="form-check form-check-inline">
{{ form.hidden(class="form-check-input") }}

View File

@@ -137,6 +137,12 @@
</h3>
{% endif %}
{% for field in team.get_fields(admin=true) %}
<h3 class="d-block">
{{ field.name }}: {{ field.value }}
</h3>
{% endfor %}
<h2 class="text-center">{{ members | length }} members</h2>
<h3 id="team-place" class="text-center">
{% if place %}

View File

@@ -16,13 +16,27 @@ $(() => {
});
}
var form = $("#team-info-form");
let form = $("#team-info-form");
form.submit(function(e) {
e.preventDefault();
$("#results").empty();
var params = $(this).serializeJSON();
var method = "PATCH";
var url = "/api/v1/teams/me";
let params = $(this).serializeJSON();
params.fields = [];
for (const property in params) {
if (property.match(/fields\[\d+\]/)) {
let field = {};
let id = parseInt(property.slice(7, -1));
field["field_id"] = id;
field["value"] = params[property];
params.fields.push(field);
delete params[property];
}
}
let method = "PATCH";
let url = "/api/v1/teams/me";
CTFd.fetch(url, {
method: method,
credentials: "same-origin",
@@ -42,12 +56,12 @@ $(() => {
' <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>\n' +
"</div>";
Object.keys(object.errors).map(function(error) {
var i = form.find("input[name={0}]".format(error));
var input = $(i);
let i = form.find("input[name={0}]".format(error));
let input = $(i);
input.addClass("input-filled-invalid");
input.removeClass("input-filled-valid");
var error_msg = object.errors[error];
var alert = error_template.format(error_msg);
let error_msg = object.errors[error];
let alert = error_template.format(error_msg);
$("#results").append(alert);
});
}

View File

@@ -162,7 +162,7 @@
/***/ (function(module, exports, __webpack_require__) {
;
eval("\n\n__webpack_require__(/*! ../main */ \"./CTFd/themes/core/assets/js/pages/main.js\");\n\n__webpack_require__(/*! ../../utils */ \"./CTFd/themes/core/assets/js/utils.js\");\n\nvar _CTFd = _interopRequireDefault(__webpack_require__(/*! ../../CTFd */ \"./CTFd/themes/core/assets/js/CTFd.js\"));\n\n__webpack_require__(/*! bootstrap/js/dist/modal */ \"./node_modules/bootstrap/js/dist/modal.js\");\n\nvar _jquery = _interopRequireDefault(__webpack_require__(/*! jquery */ \"./node_modules/jquery/dist/jquery.js\"));\n\nvar _ezq = __webpack_require__(/*! ../../ezq */ \"./CTFd/themes/core/assets/js/ezq.js\");\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\n(0, _jquery.default)(function () {\n if (window.team_captain) {\n (0, _jquery.default)(\".edit-team\").click(function () {\n (0, _jquery.default)(\"#team-edit-modal\").modal();\n });\n (0, _jquery.default)(\".edit-captain\").click(function () {\n (0, _jquery.default)(\"#team-captain-modal\").modal();\n });\n }\n\n var form = (0, _jquery.default)(\"#team-info-form\");\n form.submit(function (e) {\n e.preventDefault();\n (0, _jquery.default)(\"#results\").empty();\n var params = (0, _jquery.default)(this).serializeJSON();\n var method = \"PATCH\";\n var url = \"/api/v1/teams/me\";\n\n _CTFd.default.fetch(url, {\n method: method,\n credentials: \"same-origin\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify(params)\n }).then(function (response) {\n if (response.status === 400) {\n response.json().then(function (object) {\n if (!object.success) {\n var error_template = '<div class=\"alert alert-danger alert-dismissable\" role=\"alert\">\\n' + ' <span class=\"sr-only\">Error:</span>\\n' + \" {0}\\n\" + ' <button type=\"button\" class=\"close\" data-dismiss=\"alert\" aria-label=\"Close\"><span aria-hidden=\"true\">×</span></button>\\n' + \"</div>\";\n Object.keys(object.errors).map(function (error) {\n var i = form.find(\"input[name={0}]\".format(error));\n var input = (0, _jquery.default)(i);\n input.addClass(\"input-filled-invalid\");\n input.removeClass(\"input-filled-valid\");\n var error_msg = object.errors[error];\n var alert = error_template.format(error_msg);\n (0, _jquery.default)(\"#results\").append(alert);\n });\n }\n });\n } else if (response.status === 200) {\n response.json().then(function (object) {\n if (object.success) {\n window.location.reload();\n }\n });\n }\n });\n });\n (0, _jquery.default)(\"#team-captain-form\").submit(function (e) {\n e.preventDefault();\n var params = (0, _jquery.default)(\"#team-captain-form\").serializeJSON(true);\n\n _CTFd.default.fetch(\"/api/v1/teams/me\", {\n method: \"PATCH\",\n credentials: \"same-origin\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify(params)\n }).then(function (response) {\n return response.json();\n }).then(function (response) {\n if (response.success) {\n window.location.reload();\n } else {\n (0, _jquery.default)(\"#team-captain-form > #results\").empty();\n Object.keys(response.errors).forEach(function (key, _index) {\n (0, _jquery.default)(\"#team-captain-form > #results\").append((0, _ezq.ezBadge)({\n type: \"error\",\n body: response.errors[key]\n }));\n var i = (0, _jquery.default)(\"#team-captain-form\").find(\"select[name={0}]\".format(key));\n var input = (0, _jquery.default)(i);\n input.addClass(\"input-filled-invalid\");\n input.removeClass(\"input-filled-valid\");\n });\n }\n });\n });\n});\n\n//# sourceURL=webpack:///./CTFd/themes/core/assets/js/pages/teams/private.js?");
eval("\n\n__webpack_require__(/*! ../main */ \"./CTFd/themes/core/assets/js/pages/main.js\");\n\n__webpack_require__(/*! ../../utils */ \"./CTFd/themes/core/assets/js/utils.js\");\n\nvar _CTFd = _interopRequireDefault(__webpack_require__(/*! ../../CTFd */ \"./CTFd/themes/core/assets/js/CTFd.js\"));\n\n__webpack_require__(/*! bootstrap/js/dist/modal */ \"./node_modules/bootstrap/js/dist/modal.js\");\n\nvar _jquery = _interopRequireDefault(__webpack_require__(/*! jquery */ \"./node_modules/jquery/dist/jquery.js\"));\n\nvar _ezq = __webpack_require__(/*! ../../ezq */ \"./CTFd/themes/core/assets/js/ezq.js\");\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\n(0, _jquery.default)(function () {\n if (window.team_captain) {\n (0, _jquery.default)(\".edit-team\").click(function () {\n (0, _jquery.default)(\"#team-edit-modal\").modal();\n });\n (0, _jquery.default)(\".edit-captain\").click(function () {\n (0, _jquery.default)(\"#team-captain-modal\").modal();\n });\n }\n\n var form = (0, _jquery.default)(\"#team-info-form\");\n form.submit(function (e) {\n e.preventDefault();\n (0, _jquery.default)(\"#results\").empty();\n var params = (0, _jquery.default)(this).serializeJSON();\n params.fields = [];\n\n for (var property in params) {\n if (property.match(/fields\\[\\d+\\]/)) {\n var field = {};\n var id = parseInt(property.slice(7, -1));\n field[\"field_id\"] = id;\n field[\"value\"] = params[property];\n params.fields.push(field);\n delete params[property];\n }\n }\n\n var method = \"PATCH\";\n var url = \"/api/v1/teams/me\";\n\n _CTFd.default.fetch(url, {\n method: method,\n credentials: \"same-origin\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify(params)\n }).then(function (response) {\n if (response.status === 400) {\n response.json().then(function (object) {\n if (!object.success) {\n var error_template = '<div class=\"alert alert-danger alert-dismissable\" role=\"alert\">\\n' + ' <span class=\"sr-only\">Error:</span>\\n' + \" {0}\\n\" + ' <button type=\"button\" class=\"close\" data-dismiss=\"alert\" aria-label=\"Close\"><span aria-hidden=\"true\">×</span></button>\\n' + \"</div>\";\n Object.keys(object.errors).map(function (error) {\n var i = form.find(\"input[name={0}]\".format(error));\n var input = (0, _jquery.default)(i);\n input.addClass(\"input-filled-invalid\");\n input.removeClass(\"input-filled-valid\");\n var error_msg = object.errors[error];\n var alert = error_template.format(error_msg);\n (0, _jquery.default)(\"#results\").append(alert);\n });\n }\n });\n } else if (response.status === 200) {\n response.json().then(function (object) {\n if (object.success) {\n window.location.reload();\n }\n });\n }\n });\n });\n (0, _jquery.default)(\"#team-captain-form\").submit(function (e) {\n e.preventDefault();\n var params = (0, _jquery.default)(\"#team-captain-form\").serializeJSON(true);\n\n _CTFd.default.fetch(\"/api/v1/teams/me\", {\n method: \"PATCH\",\n credentials: \"same-origin\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify(params)\n }).then(function (response) {\n return response.json();\n }).then(function (response) {\n if (response.success) {\n window.location.reload();\n } else {\n (0, _jquery.default)(\"#team-captain-form > #results\").empty();\n Object.keys(response.errors).forEach(function (key, _index) {\n (0, _jquery.default)(\"#team-captain-form > #results\").append((0, _ezq.ezBadge)({\n type: \"error\",\n body: response.errors[key]\n }));\n var i = (0, _jquery.default)(\"#team-captain-form\").find(\"select[name={0}]\".format(key));\n var input = (0, _jquery.default)(i);\n input.addClass(\"input-filled-invalid\");\n input.removeClass(\"input-filled-valid\");\n });\n }\n });\n });\n});\n\n//# sourceURL=webpack:///./CTFd/themes/core/assets/js/pages/teams/private.js?");
/***/ })

File diff suppressed because one or more lines are too long

View File

@@ -15,15 +15,19 @@
{% include "components/errors.html" %}
{% with form = Forms.teams.TeamRegisterForm() %}
{% from "macros/forms.html" import render_extra_fields %}
<form method="POST">
<div class="form-group">
{{ form.name.label }}
<b>{{ form.name.label }}</b>
{{ form.name(class="form-control") }}
</div>
<div class="form-group">
{{ form.password.label }}
<b>{{ form.password.label }}</b>
{{ form.password(class="form-control") }}
</div>
{{ render_extra_fields(form.extra) }}
<div class="row pt-3">
<div class="col-md-12">
<p>After creating your team, share the team name and password with your teammates so they can join your team.</p>

View File

@@ -15,6 +15,7 @@
</div>
<div class="modal-body clearfix">
{% with form = Forms.teams.TeamSettingsForm(obj=team) %}
{% from "macros/forms.html" import render_extra_fields %}
<form id="team-info-form" method="POST">
<div class="form-group">
<b>{{ form.name.label }}</b>
@@ -58,6 +59,11 @@
{{ form.country.description }}
</small>
</div>
<hr>
{{ render_extra_fields(form.extra) }}
<div id="results">
</div>
@@ -120,6 +126,11 @@
</span>
</h3>
{% endif %}
{% for field in team.fields %}
<h3 class="d-block">
{{ field.name }}: {{ field.value }}
</h3>
{% endfor %}
<h2 id="team-place" class="text-center">
{# This intentionally hides the team's place when scores are hidden because this can be their internal profile
and we don't want to leak their place in the CTF. #}

View File

@@ -25,6 +25,11 @@
</span>
</h3>
{% endif %}
{% for field in team.fields %}
<h3 class="d-block">
{{ field.name }}: {{ field.value }}
</h3>
{% endfor %}
<h2 id="team-place" class="text-center">
{# This intentionally hides the team's place when scores are hidden because this can be their internal profile
and we don't want to leak their place in the CTF. #}

View File

@@ -1,10 +1,12 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from CTFd.models import Users
from tests.helpers import (
create_ctfd,
destroy_ctfd,
gen_field,
gen_team,
login_as_user,
register_user,
)
@@ -37,3 +39,56 @@ def test_admin_view_fields():
assert "CustomField3" in resp
assert "CustomField4" in resp
destroy_ctfd(app)
def test_admin_view_team_fields():
app = create_ctfd(user_mode="teams")
with app.app_context():
register_user(app)
team = gen_team(app.db)
user = Users.query.filter_by(id=2).first()
user.team_id = team.id
app.db.session.commit()
gen_field(
app.db,
name="CustomField1",
type="team",
required=True,
public=True,
editable=True,
)
gen_field(
app.db,
name="CustomField2",
type="team",
required=False,
public=True,
editable=True,
)
gen_field(
app.db,
name="CustomField3",
type="team",
required=False,
public=False,
editable=True,
)
gen_field(
app.db,
name="CustomField4",
type="team",
required=False,
public=False,
editable=False,
)
with login_as_user(app, name="admin") as admin:
# Admins should see all team fields regardless of public or editable
r = admin.get("/admin/teams/1")
resp = r.get_data(as_text=True)
assert "CustomField1" in resp
assert "CustomField2" in resp
assert "CustomField3" in resp
assert "CustomField4" in resp
destroy_ctfd(app)

View File

@@ -1,11 +1,12 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from CTFd.models import Fields, UserFieldEntries
from CTFd.models import Fields, TeamFieldEntries, Teams, UserFieldEntries, Users
from tests.helpers import (
create_ctfd,
destroy_ctfd,
gen_field,
gen_team,
login_as_user,
register_user,
)
@@ -251,3 +252,165 @@ def test_partial_field_update():
)
destroy_ctfd(app)
def test_api_team_self_fields_permissions():
app = create_ctfd(user_mode="teams")
with app.app_context():
register_user(app)
team = gen_team(app.db)
user = Users.query.filter_by(id=2).first()
user.team_id = team.id
app.db.session.commit()
team = Teams.query.filter_by(id=1).first()
team.captain_id = 2
app.db.session.commit()
gen_field(
app.db, name="CustomField1", type="team", public=False, editable=False
)
gen_field(app.db, name="CustomField2", type="team", public=True, editable=True)
app.db.session.add(
TeamFieldEntries(type="team", value="CustomValue1", team_id=1, field_id=1)
)
app.db.session.add(
TeamFieldEntries(type="team", value="CustomValue2", team_id=1, field_id=2)
)
app.db.session.commit()
assert len(team.field_entries) == 2
with login_as_user(app) as user, login_as_user(app, name="admin") as admin:
r = user.get("/api/v1/teams/me")
resp = r.get_json()
assert resp["data"]["fields"] == [
{
"value": "CustomValue2",
"name": "CustomField2",
"description": "CustomFieldDescription",
"type": "text",
"field_id": 2,
}
]
assert len(resp["data"]["fields"]) == 1
# Admin gets data and should see all fields
r = admin.get("/api/v1/teams/1")
resp = r.get_json()
assert len(resp["data"]["fields"]) == 2
r = user.patch(
"/api/v1/teams/me",
json={
"fields": [
{"field_id": 1, "value": "NewCustomValue1"},
{"field_id": 2, "value": "NewCustomValue2"},
]
},
)
assert r.get_json() == {
"success": False,
"errors": {"fields": ["Field 'CustomField1' cannot be editted"]},
}
assert r.status_code == 400
assert (
TeamFieldEntries.query.filter_by(id=1).first().value == "CustomValue1"
)
assert (
TeamFieldEntries.query.filter_by(id=2).first().value == "CustomValue2"
)
# After making the field public the user should see both fields
field = Fields.query.filter_by(id=1).first()
field.public = True
app.db.session.commit()
r = user.get("/api/v1/teams/me")
resp = r.get_json()
assert len(resp["data"]["fields"]) == 2
# Captain should be able to edit their values after it's made editable
field = Fields.query.filter_by(id=1).first()
field.editable = True
app.db.session.commit()
r = user.patch(
"/api/v1/teams/me",
json={
"fields": [
{"field_id": 1, "value": "NewCustomValue1"},
{"field_id": 2, "value": "NewCustomValue2"},
]
},
)
print(r.get_json())
assert r.status_code == 200
assert (
TeamFieldEntries.query.filter_by(id=1).first().value
== "NewCustomValue1"
)
assert (
TeamFieldEntries.query.filter_by(id=2).first().value
== "NewCustomValue2"
)
destroy_ctfd(app)
def test_team_partial_field_update():
app = create_ctfd(user_mode="teams")
with app.app_context():
register_user(app)
team = gen_team(app.db)
user = Users.query.filter_by(id=2).first()
user.team_id = team.id
team = Teams.query.filter_by(id=1).first()
team.captain_id = 2
app.db.session.commit()
gen_field(app.db, name="CustomField1", type="team")
gen_field(app.db, name="CustomField2", type="team")
with login_as_user(app) as user:
r = user.patch(
"/api/v1/teams/me",
json={
"fields": [
{"field_id": 1, "value": "CustomValue1"},
{"field_id": 2, "value": "CustomValue2"},
]
},
)
assert r.status_code == 200
assert TeamFieldEntries.query.count() == 2
r = user.patch(
"/api/v1/teams/me",
json={"fields": [{"field_id": 2, "value": "NewCustomValue2"}]},
)
assert r.status_code == 200
assert TeamFieldEntries.query.count() == 2
assert (
TeamFieldEntries.query.filter_by(field_id=1, team_id=1).first().value
== "CustomValue1"
)
assert (
TeamFieldEntries.query.filter_by(field_id=2, team_id=1).first().value
== "NewCustomValue2"
)
with login_as_user(app, name="admin") as admin:
r = admin.patch(
"/api/v1/teams/1",
json={"fields": [{"field_id": 2, "value": "AdminNewCustomValue2"}]},
)
assert r.status_code == 200
assert TeamFieldEntries.query.count() == 2
assert (
TeamFieldEntries.query.filter_by(field_id=1, team_id=1).first().value
== "CustomValue1"
)
assert (
TeamFieldEntries.query.filter_by(field_id=2, team_id=1).first().value
== "AdminNewCustomValue2"
)
destroy_ctfd(app)

251
tests/teams/test_fields.py Normal file
View File

@@ -0,0 +1,251 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from CTFd.models import TeamFieldEntries, Teams, Users
from tests.helpers import (
create_ctfd,
destroy_ctfd,
gen_field,
gen_team,
login_as_user,
register_user,
)
def test_new_fields_show_on_pages():
app = create_ctfd(user_mode="teams")
with app.app_context():
register_user(app)
team = gen_team(app.db)
user = Users.query.filter_by(id=2).first()
user.team_id = team.id
team = Teams.query.filter_by(id=1).first()
team.captain_id = 2
app.db.session.commit()
gen_field(app.db, name="CustomField1", type="team")
with login_as_user(app) as client:
r = client.get("/teams/new")
assert "CustomField1" in r.get_data(as_text=True)
assert "CustomFieldDescription" in r.get_data(as_text=True)
r = client.get("/team")
assert "CustomField1" in r.get_data(as_text=True)
assert "CustomFieldDescription" in r.get_data(as_text=True)
r = client.patch(
"/api/v1/teams/me",
json={"fields": [{"field_id": 1, "value": "CustomFieldEntry"}]},
)
resp = r.get_json()
assert resp["success"] is True
assert resp["data"]["fields"][0]["value"] == "CustomFieldEntry"
assert resp["data"]["fields"][0]["description"] == "CustomFieldDescription"
assert resp["data"]["fields"][0]["name"] == "CustomField1"
assert resp["data"]["fields"][0]["field_id"] == 1
r = client.get("/team")
resp = r.get_data(as_text=True)
assert "CustomField1" in resp
assert "CustomFieldEntry" in resp
r = client.get("/teams/1")
resp = r.get_data(as_text=True)
assert "CustomField1" in resp
assert "CustomFieldEntry" in resp
destroy_ctfd(app)
def test_team_fields_required_on_creation():
app = create_ctfd(user_mode="teams")
with app.app_context():
register_user(app)
gen_field(app.db, type="team")
with app.app_context():
with login_as_user(app) as client:
assert Teams.query.count() == 0
r = client.get("/teams/new")
resp = r.get_data(as_text=True)
assert "CustomField" in resp
assert "CustomFieldDescription" in resp
with client.session_transaction() as sess:
data = {
"name": "team",
"password": "password",
"nonce": sess.get("nonce"),
}
r = client.post("/teams/new", data=data)
assert "Please provide all required fields" in r.get_data(as_text=True)
assert Teams.query.count() == 0
with client.session_transaction() as sess:
data = {
"name": "team",
"password": "password",
"fields[1]": "CustomFieldEntry",
"nonce": sess.get("nonce"),
}
r = client.post("/teams/new", data=data)
assert r.status_code == 302
assert Teams.query.count() == 1
entry = TeamFieldEntries.query.filter_by(id=1).first()
assert entry.team_id == 1
assert entry.value == "CustomFieldEntry"
destroy_ctfd(app)
def test_team_fields_properties():
app = create_ctfd(user_mode="teams")
with app.app_context():
register_user(app)
team = gen_team(app.db)
user = Users.query.filter_by(id=2).first()
user.team_id = team.id
team = Teams.query.filter_by(id=1).first()
team.captain_id = 2
app.db.session.commit()
gen_field(
app.db,
name="CustomField1",
type="team",
required=True,
public=True,
editable=True,
)
gen_field(
app.db,
name="CustomField2",
type="team",
required=False,
public=True,
editable=True,
)
gen_field(
app.db,
name="CustomField3",
type="team",
required=False,
public=False,
editable=True,
)
gen_field(
app.db,
name="CustomField4",
type="team",
required=False,
public=False,
editable=False,
)
with login_as_user(app) as client:
r = client.get("/teams/new")
resp = r.get_data(as_text=True)
assert "CustomField1" in resp
assert "CustomField2" in resp
assert "CustomField3" in resp
assert "CustomField4" in resp
r = client.get("/team")
resp = r.get_data(as_text=True)
assert "CustomField1" in resp
assert "CustomField2" in resp
assert "CustomField3" in resp
assert "CustomField4" not in resp
r = client.patch(
"/api/v1/teams/me",
json={
"fields": [
{"field_id": 1, "value": "CustomFieldEntry1"},
{"field_id": 2, "value": "CustomFieldEntry2"},
{"field_id": 3, "value": "CustomFieldEntry3"},
{"field_id": 4, "value": "CustomFieldEntry4"},
]
},
)
resp = r.get_json()
assert resp == {
"success": False,
"errors": {"fields": ["Field 'CustomField4' cannot be editted"]},
}
r = client.patch(
"/api/v1/teams/me",
json={
"fields": [
{"field_id": 1, "value": "CustomFieldEntry1"},
{"field_id": 2, "value": "CustomFieldEntry2"},
{"field_id": 3, "value": "CustomFieldEntry3"},
]
},
)
assert r.status_code == 200
r = client.get("/team")
resp = r.get_data(as_text=True)
assert "CustomField1" in resp
assert "CustomField2" in resp
assert (
"CustomField3" in resp
) # This is here because /team contains team settings
assert "CustomField4" not in resp
r = client.get("/teams/1")
resp = r.get_data(as_text=True)
assert "CustomField1" in resp
assert "CustomField2" in resp
assert "CustomField3" not in resp
assert "CustomField4" not in resp
destroy_ctfd(app)
def test_teams_boolean_checkbox_field():
app = create_ctfd(user_mode="teams")
with app.app_context():
register_user(app)
gen_field(
app.db,
name="CustomField1",
type="team",
field_type="boolean",
required=False,
)
with login_as_user(app) as client:
r = client.get("/teams/new")
resp = r.get_data(as_text=True)
# We should have rendered a checkbox input
assert "checkbox" in resp
with client.session_transaction() as sess:
data = {
"name": "team",
"password": "password",
"nonce": sess.get("nonce"),
"fields[1]": "y",
}
client.post("/teams/new", data=data)
assert Teams.query.count() == 1
assert TeamFieldEntries.query.count() == 1
assert TeamFieldEntries.query.filter_by(id=1).first().value is True
with login_as_user(app) as client:
r = client.get("/team")
resp = r.get_data(as_text=True)
assert "CustomField1" in resp
assert "checkbox" in resp
r = client.patch(
"/api/v1/teams/me", json={"fields": [{"field_id": 1, "value": False}]}
)
assert r.status_code == 200
assert TeamFieldEntries.query.count() == 1
assert TeamFieldEntries.query.filter_by(id=1).first().value is False
destroy_ctfd(app)

View File

@@ -130,7 +130,7 @@ def test_fields_properties():
resp = r.get_json()
assert resp == {
"success": False,
"errors": {"fields": ["Field CustomField4 cannot be editted"]},
"errors": {"fields": ["Field 'CustomField4' cannot be editted"]},
}
r = client.patch(
@@ -166,25 +166,24 @@ def test_boolean_checkbox_field():
with app.app_context():
gen_field(app.db, name="CustomField1", field_type="boolean", required=False)
with app.app_context():
with app.test_client() as client:
r = client.get("/register")
resp = r.get_data(as_text=True)
with app.test_client() as client:
r = client.get("/register")
resp = r.get_data(as_text=True)
# We should have rendered a checkbox input
assert "checkbox" in resp
# We should have rendered a checkbox input
assert "checkbox" in resp
with client.session_transaction() as sess:
data = {
"name": "user",
"email": "user@ctfd.io",
"password": "password",
"nonce": sess.get("nonce"),
"fields[1]": "y",
}
client.post("/register", data=data)
with client.session_transaction() as sess:
assert sess["id"]
with client.session_transaction() as sess:
data = {
"name": "user",
"email": "user@ctfd.io",
"password": "password",
"nonce": sess.get("nonce"),
"fields[1]": "y",
}
client.post("/register", data=data)
with client.session_transaction() as sess:
assert sess["id"]
assert UserFieldEntries.query.count() == 1
assert UserFieldEntries.query.filter_by(id=1).first().value is True
@@ -196,7 +195,7 @@ def test_boolean_checkbox_field():
assert "checkbox" in resp
r = client.patch(
"/api/v1/users/me", json={"fields": [{"field_id": 1, "value": False}]},
"/api/v1/users/me", json={"fields": [{"field_id": 1, "value": False}]}
)
assert r.status_code == 200
assert UserFieldEntries.query.count() == 1