1318 pagination users teams submissions (#1513)

* Paginate only the `/api/v1/users`, `/api/v1/teams`, and `/api/v1/submissions` endpoints.
* Add a `PaginatedAPIListSuccessResponse` class with a customized `apidoc` method to hack in the pagination scheme
* Works on #1318
This commit is contained in:
Kevin Chung
2020-06-28 01:39:45 -04:00
committed by GitHub
parent dea6b122b7
commit a769e2c91f
4 changed files with 124 additions and 19 deletions

View File

@@ -1,4 +1,4 @@
from typing import Any, List, Optional
from typing import Any, Dict, List, Optional
from pydantic import BaseModel
@@ -55,6 +55,51 @@ class APIListSuccessResponse(APIDetailedSuccessResponse):
return schema
class PaginatedAPIListSuccessResponse(APIListSuccessResponse):
meta: Dict[str, Any]
@classmethod
def apidoc(cls):
"""
Helper to inline references from the generated schema
"""
schema = cls.schema()
schema["properties"]["meta"] = {
"title": "Meta",
"type": "object",
"properties": {
"pagination": {
"title": "Pagination",
"type": "object",
"properties": {
"page": {"title": "Page", "type": "integer"},
"next": {"title": "Next", "type": "integer"},
"prev": {"title": "Prev", "type": "integer"},
"pages": {"title": "Pages", "type": "integer"},
"per_page": {"title": "Per Page", "type": "integer"},
"total": {"title": "Total", "type": "integer"},
},
"required": ["page", "next", "prev", "pages", "per_page", "total"],
}
},
"required": ["pagination"],
}
try:
key = schema["properties"]["data"]["items"]["$ref"]
ref = key.split("/").pop()
definition = schema["definitions"][ref]
schema["properties"]["data"]["items"] = definition
del schema["definitions"][ref]
if bool(schema["definitions"]) is False:
del schema["definitions"]
except KeyError:
pass
return schema
class APISimpleErrorResponse(BaseModel):
success: bool = False
errors: Optional[List[str]]

View File

@@ -5,7 +5,10 @@ from flask_restx import Namespace, Resource
from CTFd.api.v1.helpers.request import validate_args
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,
PaginatedAPIListSuccessResponse,
)
from CTFd.cache import clear_standings
from CTFd.models import Submissions, db
from CTFd.schemas.submissions import SubmissionSchema
@@ -23,7 +26,7 @@ class SubmissionDetailedSuccessResponse(APIDetailedSuccessResponse):
data: SubmissionModel
class SubmissionListSuccessResponse(APIListSuccessResponse):
class SubmissionListSuccessResponse(PaginatedAPIListSuccessResponse):
data: List[SubmissionModel]
@@ -52,17 +55,38 @@ class SubmissionsList(Resource):
def get(self):
args = request.args.to_dict()
schema = SubmissionSchema(many=True)
pagination_args = {
"per_page": int(args.pop("per_page", 50)),
"page": int(args.pop("page", 1)),
}
if args:
submissions = Submissions.query.filter_by(**args).all()
submissions = Submissions.query.filter_by(**args).paginate(
**pagination_args, max_per_page=100
)
else:
submissions = Submissions.query.all()
submissions = Submissions.query.paginate(
**pagination_args, max_per_page=100
)
response = schema.dump(submissions)
response = schema.dump(submissions.items)
if response.errors:
return {"success": False, "errors": response.errors}, 400
return {"success": True, "data": response.data}
return {
"meta": {
"pagination": {
"page": submissions.page,
"next": submissions.next_num,
"prev": submissions.prev_num,
"pages": submissions.pages,
"per_page": submissions.per_page,
"total": submissions.total,
}
},
"success": True,
"data": response.data,
}
@admins_only
@submissions_namespace.doc(

View File

@@ -5,7 +5,10 @@ from flask import abort, request, session
from flask_restx import Namespace, Resource
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,
PaginatedAPIListSuccessResponse,
)
from CTFd.cache import clear_standings, clear_team_session, clear_user_session
from CTFd.models import Awards, Submissions, Teams, Unlocks, Users, db
from CTFd.schemas.awards import AwardSchema
@@ -28,7 +31,7 @@ class TeamDetailedSuccessResponse(APIDetailedSuccessResponse):
data: TeamModel
class TeamListSuccessResponse(APIListSuccessResponse):
class TeamListSuccessResponse(PaginatedAPIListSuccessResponse):
data: List[TeamModel]
@@ -56,19 +59,34 @@ class TeamList(Resource):
)
def get(self):
if is_admin() and request.args.get("view") == "admin":
teams = Teams.query.filter_by()
teams = Teams.query.filter_by().paginate(per_page=50, max_per_page=100)
else:
teams = Teams.query.filter_by(hidden=False, banned=False)
teams = Teams.query.filter_by(hidden=False, banned=False).paginate(
per_page=50, max_per_page=100
)
user_type = get_current_user_type(fallback="user")
view = copy.deepcopy(TeamSchema.views.get(user_type))
view.remove("members")
response = TeamSchema(view=view, many=True).dump(teams)
response = TeamSchema(view=view, many=True).dump(teams.items)
if response.errors:
return {"success": False, "errors": response.errors}, 400
return {"success": True, "data": response.data}
return {
"meta": {
"pagination": {
"page": teams.page,
"next": teams.next_num,
"prev": teams.prev_num,
"pages": teams.pages,
"per_page": teams.per_page,
"total": teams.total,
}
},
"success": True,
"data": response.data,
}
@admins_only
@teams_namespace.doc(

View File

@@ -4,7 +4,10 @@ from flask import abort, request
from flask_restx import Namespace, Resource
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,
PaginatedAPIListSuccessResponse,
)
from CTFd.cache import clear_standings, clear_user_session
from CTFd.models import (
Awards,
@@ -40,7 +43,7 @@ class UserDetailedSuccessResponse(APIDetailedSuccessResponse):
data: UserModel
class UserListSuccessResponse(APIListSuccessResponse):
class UserListSuccessResponse(PaginatedAPIListSuccessResponse):
data: List[UserModel]
@@ -68,16 +71,31 @@ class UserList(Resource):
)
def get(self):
if is_admin() and request.args.get("view") == "admin":
users = Users.query.filter_by()
users = Users.query.filter_by().paginate(per_page=50, max_per_page=100)
else:
users = Users.query.filter_by(banned=False, hidden=False)
users = Users.query.filter_by(banned=False, hidden=False).paginate(
per_page=50, max_per_page=100
)
response = UserSchema(view="user", many=True).dump(users)
response = UserSchema(view="user", many=True).dump(users.items)
if response.errors:
return {"success": False, "errors": response.errors}, 400
return {"success": True, "data": response.data}
return {
"meta": {
"pagination": {
"page": users.page,
"next": users.next_num,
"prev": users.prev_num,
"pages": users.pages,
"per_page": users.per_page,
"total": users.total,
}
},
"success": True,
"data": response.data,
}
@users_namespace.doc()
@admins_only