Add API searching and filtering (#1515)

* Works on #1318 
* Adds searching and filtering to most of the bulk API endpoints
* Adds documentation on the GET parameters used to conduct searches
This commit is contained in:
Kevin Chung
2020-06-28 14:08:54 -04:00
committed by GitHub
parent a769e2c91f
commit efb9831d2a
14 changed files with 394 additions and 44 deletions

View File

@@ -3,9 +3,12 @@ from typing import List
from flask import request from flask import request
from flask_restx import Namespace, Resource from flask_restx import Namespace, Resource
from CTFd.api.v1.helpers.models import build_model_filters
from CTFd.api.v1.helpers.request import validate_args
from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic
from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
from CTFd.cache import clear_standings from CTFd.cache import clear_standings
from CTFd.constants import RawEnum
from CTFd.models import Awards, Users, db from CTFd.models import Awards, Users, db
from CTFd.schemas.awards import AwardSchema from CTFd.schemas.awards import AwardSchema
from CTFd.utils.config import is_teams_mode from CTFd.utils.config import is_teams_mode
@@ -35,6 +38,55 @@ awards_namespace.schema_model(
@awards_namespace.route("") @awards_namespace.route("")
class AwardList(Resource): class AwardList(Resource):
@admins_only
@awards_namespace.doc(
description="Endpoint to list Award objects in bulk",
responses={
200: ("Success", "AwardListSuccessResponse"),
400: (
"An error occured processing the provided or stored data",
"APISimpleErrorResponse",
),
},
)
@validate_args(
{
"user_id": (int, None),
"team_id": (int, None),
"type": (str, None),
"value": (int, None),
"category": (int, None),
"icon": (int, None),
"q": (str, None),
"field": (
RawEnum(
"AwardFields",
{
"name": "name",
"description": "description",
"category": "category",
"icon": "icon",
},
),
None,
),
},
location="query",
)
def get(self, query_args):
q = query_args.pop("q", None)
field = str(query_args.pop("field", None))
filters = build_model_filters(model=Awards, query=q, field=field)
awards = Awards.query.filter_by(**query_args).filter(*filters).all()
schema = AwardSchema(many=True)
response = schema.dump(awards)
if response.errors:
return {"success": False, "errors": response.errors}, 400
return {"success": True, "data": response.data}
@admins_only @admins_only
@awards_namespace.doc( @awards_namespace.doc(
description="Endpoint to create an Award object", description="Endpoint to create an Award object",

View File

@@ -5,9 +5,12 @@ from flask import abort, render_template, request, url_for
from flask_restx import Namespace, Resource from flask_restx import Namespace, Resource
from sqlalchemy.sql import and_ from sqlalchemy.sql import and_
from CTFd.api.v1.helpers.models import build_model_filters
from CTFd.api.v1.helpers.request import validate_args
from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic
from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
from CTFd.cache import clear_standings from CTFd.cache import clear_standings
from CTFd.constants import RawEnum
from CTFd.models import ChallengeFiles as ChallengeFilesModel from CTFd.models import ChallengeFiles as ChallengeFilesModel
from CTFd.models import ( from CTFd.models import (
Challenges, Challenges,
@@ -86,19 +89,56 @@ class ChallengeList(Resource):
), ),
}, },
) )
def get(self): @validate_args(
{
"name": (str, None),
"max_attempts": (int, None),
"value": (int, None),
"category": (str, None),
"type": (str, None),
"state": (str, None),
"q": (str, None),
"field": (
RawEnum(
"ChallengeFields",
{
"name": "name",
"description": "description",
"category": "category",
"type": "type",
"state": "state",
},
),
None,
),
},
location="query",
)
def get(self, query_args):
# Build filtering queries
q = query_args.pop("q", None)
field = str(query_args.pop("field", None))
filters = build_model_filters(model=Challenges, query=q, field=field)
# This can return None (unauth) if visibility is set to public # This can return None (unauth) if visibility is set to public
user = get_current_user() user = get_current_user()
# Admins can request to see everything # Admins can request to see everything
if is_admin() and request.args.get("view") == "admin": if is_admin() and request.args.get("view") == "admin":
challenges = Challenges.query.order_by(Challenges.value).all() challenges = (
Challenges.query.filter_by(**query_args)
.filter(*filters)
.order_by(Challenges.value)
.all()
)
solve_ids = set([challenge.id for challenge in challenges]) solve_ids = set([challenge.id for challenge in challenges])
else: else:
challenges = ( challenges = (
Challenges.query.filter( Challenges.query.filter(
and_(Challenges.state != "hidden", Challenges.state != "locked") and_(Challenges.state != "hidden", Challenges.state != "locked")
) )
.filter_by(**query_args)
.filter(*filters)
.order_by(Challenges.value) .order_by(Challenges.value)
.all() .all()
) )

View File

@@ -3,9 +3,12 @@ from typing import List
from flask import request from flask import request
from flask_restx import Namespace, Resource from flask_restx import Namespace, Resource
from CTFd.api.v1.helpers.models import build_model_filters
from CTFd.api.v1.helpers.request import validate_args
from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic
from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
from CTFd.cache import clear_config, clear_standings from CTFd.cache import clear_config, clear_standings
from CTFd.constants import RawEnum
from CTFd.models import Configs, db from CTFd.models import Configs, db
from CTFd.schemas.config import ConfigSchema from CTFd.schemas.config import ConfigSchema
from CTFd.utils import get_config, set_config from CTFd.utils import get_config, set_config
@@ -46,8 +49,21 @@ class ConfigList(Resource):
), ),
}, },
) )
def get(self): @validate_args(
configs = Configs.query.all() {
"key": (str, None),
"value": (str, None),
"q": (str, None),
"field": (RawEnum("ConfigFields", {"key": "key", "value": "value"}), None),
},
location="query",
)
def get(self, query_args):
q = query_args.pop("q", None)
field = str(query_args.pop("field", None))
filters = build_model_filters(model=Configs, query=q, field=field)
configs = Configs.query.filter_by(**query_args).filter(*filters).all()
schema = ConfigSchema(many=True) schema = ConfigSchema(many=True)
response = schema.dump(configs) response = schema.dump(configs)
if response.errors: if response.errors:

View File

@@ -3,8 +3,11 @@ from typing import List
from flask import request from flask import request
from flask_restx import Namespace, Resource from flask_restx import Namespace, Resource
from CTFd.api.v1.helpers.models import build_model_filters
from CTFd.api.v1.helpers.request import validate_args
from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic
from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
from CTFd.constants import RawEnum
from CTFd.models import Files, db from CTFd.models import Files, db
from CTFd.schemas.files import FileSchema from CTFd.schemas.files import FileSchema
from CTFd.utils import uploads from CTFd.utils import uploads
@@ -45,9 +48,24 @@ class FilesList(Resource):
), ),
}, },
) )
def get(self): @validate_args(
file_type = request.args.get("type") {
files = Files.query.filter_by(type=file_type).all() "type": (str, None),
"location": (str, None),
"q": (str, None),
"field": (
RawEnum("FileFields", {"type": "type", "location": "location"}),
None,
),
},
location="query",
)
def get(self, query_args):
q = query_args.pop("q", None)
field = str(query_args.pop("field", None))
filters = build_model_filters(model=Files, query=q, field=field)
files = Files.query.filter_by(**query_args).filter(*filters).all()
schema = FileSchema(many=True) schema = FileSchema(many=True)
response = schema.dump(files) response = schema.dump(files)

View File

@@ -3,8 +3,11 @@ from typing import List
from flask import request from flask import request
from flask_restx import Namespace, Resource from flask_restx import Namespace, Resource
from CTFd.api.v1.helpers.models import build_model_filters
from CTFd.api.v1.helpers.request import validate_args
from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic
from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
from CTFd.constants import RawEnum
from CTFd.models import Flags, db from CTFd.models import Flags, db
from CTFd.plugins.flags import FLAG_CLASSES, get_flag_class from CTFd.plugins.flags import FLAG_CLASSES, get_flag_class
from CTFd.schemas.flags import FlagSchema from CTFd.schemas.flags import FlagSchema
@@ -45,8 +48,28 @@ class FlagList(Resource):
), ),
}, },
) )
def get(self): @validate_args(
flags = Flags.query.all() {
"challenge_id": (int, None),
"type": (str, None),
"content": (str, None),
"data": (str, None),
"q": (str, None),
"field": (
RawEnum(
"FlagFields", {"type": "type", "content": "content", "data": "data"}
),
None,
),
},
location="query",
)
def get(self, query_args):
q = query_args.pop("q", None)
field = str(query_args.pop("field", None))
filters = build_model_filters(model=Flags, query=q, field=field)
flags = Flags.query.filter_by(**query_args).filter(*filters).all()
schema = FlagSchema(many=True) schema = FlagSchema(many=True)
response = schema.dump(flags) response = schema.dump(flags)
if response.errors: if response.errors:

View File

@@ -0,0 +1,7 @@
def build_model_filters(model, query, field):
filters = []
if query:
# The field exists as an exposed column
if model.__mapper__.has_property(field):
filters.append(getattr(model, field).like("%{}%".format(query)))
return filters

View File

@@ -3,8 +3,11 @@ from typing import List
from flask import request from flask import request
from flask_restx import Namespace, Resource from flask_restx import Namespace, Resource
from CTFd.api.v1.helpers.models import build_model_filters
from CTFd.api.v1.helpers.request import validate_args
from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic
from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
from CTFd.constants import RawEnum
from CTFd.models import Hints, HintUnlocks, db from CTFd.models import Hints, HintUnlocks, db
from CTFd.schemas.hints import HintSchema from CTFd.schemas.hints import HintSchema
from CTFd.utils.decorators import admins_only, authed_only, during_ctf_time_only from CTFd.utils.decorators import admins_only, authed_only, during_ctf_time_only
@@ -45,8 +48,26 @@ class HintList(Resource):
), ),
}, },
) )
def get(self): @validate_args(
hints = Hints.query.all() {
"type": (str, None),
"challenge_id": (int, None),
"content": (str, None),
"cost": (int, None),
"q": (str, None),
"field": (
RawEnum("HintFields", {"type": "type", "content": "content"}),
None,
),
},
location="query",
)
def get(self, query_args):
q = query_args.pop("q", None)
field = str(query_args.pop("field", None))
filters = build_model_filters(model=Hints, query=q, field=field)
hints = Hints.query.filter_by(**query_args).filter(*filters).all()
response = HintSchema(many=True).dump(hints) response = HintSchema(many=True).dump(hints)
if response.errors: if response.errors:

View File

@@ -3,8 +3,11 @@ from typing import List
from flask import current_app, request from flask import current_app, request
from flask_restx import Namespace, Resource from flask_restx import Namespace, Resource
from CTFd.api.v1.helpers.models import build_model_filters
from CTFd.api.v1.helpers.request import validate_args
from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic
from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
from CTFd.constants import RawEnum
from CTFd.models import Notifications, db from CTFd.models import Notifications, db
from CTFd.schemas.notifications import NotificationSchema from CTFd.schemas.notifications import NotificationSchema
from CTFd.utils.decorators import admins_only from CTFd.utils.decorators import admins_only
@@ -46,8 +49,28 @@ class NotificantionList(Resource):
), ),
}, },
) )
def get(self): @validate_args(
notifications = Notifications.query.all() {
"title": (str, None),
"content": (str, None),
"user_id": (int, None),
"team_id": (int, None),
"q": (str, None),
"field": (
RawEnum("NotificationFields", {"title": "title", "content": "content"}),
None,
),
},
location="query",
)
def get(self, query_args):
q = query_args.pop("q", None)
field = str(query_args.pop("field", None))
filters = build_model_filters(model=Notifications, query=q, field=field)
notifications = (
Notifications.query.filter_by(**query_args).filter(*filters).all()
)
schema = NotificationSchema(many=True) schema = NotificationSchema(many=True)
result = schema.dump(notifications) result = schema.dump(notifications)
if result.errors: if result.errors:

View File

@@ -3,10 +3,12 @@ from typing import List
from flask import request from flask import request
from flask_restx import Namespace, Resource from flask_restx import Namespace, Resource
from CTFd.api.v1.helpers.models import build_model_filters
from CTFd.api.v1.helpers.request import validate_args from CTFd.api.v1.helpers.request import validate_args
from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic
from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
from CTFd.cache import clear_pages from CTFd.cache import clear_pages
from CTFd.constants import RawEnum
from CTFd.models import Pages, db from CTFd.models import Pages, db
from CTFd.schemas.pages import PageSchema from CTFd.schemas.pages import PageSchema
from CTFd.utils.decorators import admins_only from CTFd.utils.decorators import admins_only
@@ -59,11 +61,23 @@ class PageList(Resource):
"draft": (bool, None), "draft": (bool, None),
"hidden": (bool, None), "hidden": (bool, None),
"auth_required": (bool, None), "auth_required": (bool, None),
"q": (str, None),
"field": (
RawEnum(
"PageFields",
{"title": "title", "route": "route", "content": "content"},
),
None,
),
}, },
location="query", location="query",
) )
def get(self, query): def get(self, query_args):
pages = Pages.query.filter_by(**query).all() q = query_args.pop("q", None)
field = str(query_args.pop("field", None))
filters = build_model_filters(model=Pages, query=q, field=field)
pages = Pages.query.filter_by(**query_args).filter(*filters).all()
schema = PageSchema(exclude=["content"], many=True) schema = PageSchema(exclude=["content"], many=True)
response = schema.dump(pages) response = schema.dump(pages)
if response.errors: if response.errors:

View File

@@ -1,8 +1,8 @@
from typing import List from typing import List
from flask import request
from flask_restx import Namespace, Resource from flask_restx import Namespace, Resource
from CTFd.api.v1.helpers.models import build_model_filters
from CTFd.api.v1.helpers.request import validate_args from CTFd.api.v1.helpers.request import validate_args
from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic
from CTFd.api.v1.schemas import ( from CTFd.api.v1.schemas import (
@@ -10,6 +10,7 @@ from CTFd.api.v1.schemas import (
PaginatedAPIListSuccessResponse, PaginatedAPIListSuccessResponse,
) )
from CTFd.cache import clear_standings from CTFd.cache import clear_standings
from CTFd.constants import RawEnum
from CTFd.models import Submissions, db from CTFd.models import Submissions, db
from CTFd.schemas.submissions import SubmissionSchema from CTFd.schemas.submissions import SubmissionSchema
from CTFd.utils.decorators import admins_only from CTFd.utils.decorators import admins_only
@@ -52,20 +53,44 @@ class SubmissionsList(Resource):
), ),
}, },
) )
def get(self): @validate_args(
args = request.args.to_dict() {
schema = SubmissionSchema(many=True) "challenge_id": (int, None),
pagination_args = { "user_id": (int, None),
"per_page": int(args.pop("per_page", 50)), "team_id": (int, None),
"page": int(args.pop("page", 1)), "ip": (str, None),
} "provided": (str, None),
if args: "type": (str, None),
submissions = Submissions.query.filter_by(**args).paginate( "q": (str, None),
**pagination_args, max_per_page=100 "field": (
RawEnum(
"SubmissionFields",
{
"challenge_id": "challenge_id",
"user_id": "user_id",
"team_id": "team_id",
"ip": "ip",
"provided": "provided",
"type": "type",
},
),
None,
),
},
location="query",
) )
else: def get(self, query_args):
submissions = Submissions.query.paginate( q = query_args.pop("q", None)
**pagination_args, max_per_page=100 field = str(query_args.pop("field", None))
filters = build_model_filters(model=Submissions, query=q, field=field)
args = query_args
schema = SubmissionSchema(many=True)
submissions = (
Submissions.query.filter_by(**args)
.filter(*filters)
.paginate(max_per_page=100)
) )
response = schema.dump(submissions.items) response = schema.dump(submissions.items)

View File

@@ -3,8 +3,11 @@ from typing import List
from flask import request from flask import request
from flask_restx import Namespace, Resource from flask_restx import Namespace, Resource
from CTFd.api.v1.helpers.models import build_model_filters
from CTFd.api.v1.helpers.request import validate_args
from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic
from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
from CTFd.constants import RawEnum
from CTFd.models import Tags, db from CTFd.models import Tags, db
from CTFd.schemas.tags import TagSchema from CTFd.schemas.tags import TagSchema
from CTFd.utils.decorators import admins_only from CTFd.utils.decorators import admins_only
@@ -42,9 +45,26 @@ class TagList(Resource):
), ),
}, },
) )
def get(self): @validate_args(
# TODO: Filter by challenge_id {
tags = Tags.query.all() "challenge_id": (int, None),
"value": (str, None),
"q": (str, None),
"field": (
RawEnum(
"TagFields", {"challenge_id": "challenge_id", "value": "value"}
),
None,
),
},
location="query",
)
def get(self, query_args):
q = query_args.pop("q", None)
field = str(query_args.pop("field", None))
filters = build_model_filters(model=Tags, query=q, field=field)
tags = Tags.query.filter_by(**query_args).filter(*filters).all()
schema = TagSchema(many=True) schema = TagSchema(many=True)
response = schema.dump(tags) response = schema.dump(tags)

View File

@@ -4,12 +4,15 @@ from typing import List
from flask import abort, request, session from flask import abort, request, session
from flask_restx import Namespace, Resource from flask_restx import Namespace, Resource
from CTFd.api.v1.helpers.models import build_model_filters
from CTFd.api.v1.helpers.request import validate_args
from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic
from CTFd.api.v1.schemas import ( from CTFd.api.v1.schemas import (
APIDetailedSuccessResponse, APIDetailedSuccessResponse,
PaginatedAPIListSuccessResponse, PaginatedAPIListSuccessResponse,
) )
from CTFd.cache import clear_standings, clear_team_session, clear_user_session from CTFd.cache import clear_standings, clear_team_session, clear_user_session
from CTFd.constants import RawEnum
from CTFd.models import Awards, Submissions, Teams, Unlocks, Users, db from CTFd.models import Awards, Submissions, Teams, Unlocks, Users, db
from CTFd.schemas.awards import AwardSchema from CTFd.schemas.awards import AwardSchema
from CTFd.schemas.submissions import SubmissionSchema from CTFd.schemas.submissions import SubmissionSchema
@@ -57,12 +60,44 @@ class TeamList(Resource):
), ),
}, },
) )
def get(self): @validate_args(
{
"affiliation": (str, None),
"country": (str, None),
"bracket": (str, None),
"q": (str, None),
"field": (
RawEnum(
"TeamFields",
{
"name": "name",
"website": "website",
"country": "country",
"bracket": "bracket",
"affiliation": "affiliation",
},
),
None,
),
},
location="query",
)
def get(self, query_args):
q = query_args.pop("q", None)
field = str(query_args.pop("field", None))
filters = build_model_filters(model=Teams, query=q, field=field)
if is_admin() and request.args.get("view") == "admin": if is_admin() and request.args.get("view") == "admin":
teams = Teams.query.filter_by().paginate(per_page=50, max_per_page=100) teams = (
Teams.query.filter_by(**query_args)
.filter(*filters)
.paginate(per_page=50, max_per_page=100)
)
else: else:
teams = Teams.query.filter_by(hidden=False, banned=False).paginate( teams = (
per_page=50, max_per_page=100 Teams.query.filter_by(hidden=False, banned=False, **query_args)
.filter(*filters)
.paginate(per_page=50, max_per_page=100)
) )
user_type = get_current_user_type(fallback="user") user_type = get_current_user_type(fallback="user")

View File

@@ -3,9 +3,12 @@ from typing import List
from flask import request from flask import request
from flask_restx import Namespace, Resource from flask_restx import Namespace, Resource
from CTFd.api.v1.helpers.models import build_model_filters
from CTFd.api.v1.helpers.request import validate_args
from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic
from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
from CTFd.cache import clear_standings from CTFd.cache import clear_standings
from CTFd.constants import RawEnum
from CTFd.models import Unlocks, db, get_class_by_tablename from CTFd.models import Unlocks, db, get_class_by_tablename
from CTFd.schemas.awards import AwardSchema from CTFd.schemas.awards import AwardSchema
from CTFd.schemas.unlocks import UnlockSchema from CTFd.schemas.unlocks import UnlockSchema
@@ -53,10 +56,28 @@ class UnlockList(Resource):
), ),
}, },
) )
def get(self): @validate_args(
hints = Unlocks.query.all() {
"user_id": (int, None),
"team_id": (int, None),
"target": (int, None),
"type": (str, None),
"q": (str, None),
"field": (
RawEnum("UnlockFields", {"target": "target", "type": "type"}),
None,
),
},
location="query",
)
def get(self, query_args):
q = query_args.pop("q", None)
field = str(query_args.pop("field", None))
filters = build_model_filters(model=Unlocks, query=q, field=field)
unlocks = Unlocks.query.filter_by(**query_args).filter(*filters).all()
schema = UnlockSchema() schema = UnlockSchema()
response = schema.dump(hints) response = schema.dump(unlocks)
if response.errors: if response.errors:
return {"success": False, "errors": response.errors}, 400 return {"success": False, "errors": response.errors}, 400

View File

@@ -3,12 +3,15 @@ from typing import List
from flask import abort, request from flask import abort, request
from flask_restx import Namespace, Resource from flask_restx import Namespace, Resource
from CTFd.api.v1.helpers.models import build_model_filters
from CTFd.api.v1.helpers.request import validate_args
from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic
from CTFd.api.v1.schemas import ( from CTFd.api.v1.schemas import (
APIDetailedSuccessResponse, APIDetailedSuccessResponse,
PaginatedAPIListSuccessResponse, PaginatedAPIListSuccessResponse,
) )
from CTFd.cache import clear_standings, clear_user_session from CTFd.cache import clear_standings, clear_user_session
from CTFd.constants import RawEnum
from CTFd.models import ( from CTFd.models import (
Awards, Awards,
Notifications, Notifications,
@@ -69,12 +72,44 @@ class UserList(Resource):
), ),
}, },
) )
def get(self): @validate_args(
{
"affiliation": (str, None),
"country": (str, None),
"bracket": (str, None),
"q": (str, None),
"field": (
RawEnum(
"UserFields",
{
"name": "name",
"website": "website",
"country": "country",
"bracket": "bracket",
"affiliation": "affiliation",
},
),
None,
),
},
location="query",
)
def get(self, query_args):
q = query_args.pop("q", None)
field = str(query_args.pop("field", None))
filters = build_model_filters(model=Users, query=q, field=field)
if is_admin() and request.args.get("view") == "admin": if is_admin() and request.args.get("view") == "admin":
users = Users.query.filter_by().paginate(per_page=50, max_per_page=100) users = (
Users.query.filter_by(**query_args)
.filter(*filters)
.paginate(per_page=50, max_per_page=100)
)
else: else:
users = Users.query.filter_by(banned=False, hidden=False).paginate( users = (
per_page=50, max_per_page=100 Users.query.filter_by(banned=False, hidden=False, **query_args)
.filter(*filters)
.paginate(per_page=50, max_per_page=100)
) )
response = UserSchema(view="user", many=True).dump(users.items) response = UserSchema(view="user", many=True).dump(users.items)