mirror of
https://github.com/aljazceru/CTFd.git
synced 2026-02-14 02:34:23 +01:00
Add fields to team forms (#1615)
* Adds custom fields for teams * Closes #756
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: "",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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
@@ -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>
|
||||
@@ -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") }}
|
||||
|
||||
@@ -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") }}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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. #}
|
||||
|
||||
@@ -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. #}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
251
tests/teams/test_fields.py
Normal 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)
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user