mirror of
https://github.com/aljazceru/CTFd.git
synced 2025-12-18 06:24:23 +01:00
1569 submission filter by challenge (#1590)
* Allow the Admin Panel Submissions page to filter by Account IDs, Challenge IDs, and Challenge Names * Deprecate `CTFd.api.v1.helpers.models.build_model_filters` and wrap it to `CTFd.utils.helpers.models.build_model_filters` * Clean up some miscellaneous Submissions code * Closes #1569
This commit is contained in:
@@ -3,6 +3,7 @@ from flask import render_template, request, url_for
|
||||
from CTFd.admin import admin
|
||||
from CTFd.models import Challenges, Submissions
|
||||
from CTFd.utils.decorators import admins_only
|
||||
from CTFd.utils.helpers.models import build_model_filters
|
||||
from CTFd.utils.modes import get_model
|
||||
|
||||
|
||||
@@ -19,12 +20,15 @@ def submissions_listing(submission_type):
|
||||
field = request.args.get("field")
|
||||
page = abs(request.args.get("page", 1, type=int))
|
||||
|
||||
if q:
|
||||
submissions = []
|
||||
if Submissions.__mapper__.has_property(
|
||||
field
|
||||
): # The field exists as an exposed column
|
||||
filters.append(getattr(Submissions, field).like("%{}%".format(q)))
|
||||
filters = build_model_filters(
|
||||
model=Submissions,
|
||||
query=q,
|
||||
field=field,
|
||||
extra_columns={
|
||||
"challenge_name": Challenges.name,
|
||||
"account_id": Submissions.account_id,
|
||||
},
|
||||
)
|
||||
|
||||
Model = get_model()
|
||||
|
||||
@@ -37,7 +41,7 @@ def submissions_listing(submission_type):
|
||||
Submissions.account_id,
|
||||
Submissions.date,
|
||||
Challenges.name.label("challenge_name"),
|
||||
Model.name.label("team_name"),
|
||||
Model.name.label("account_name"),
|
||||
)
|
||||
.filter_by(**filters_by)
|
||||
.filter(*filters)
|
||||
|
||||
@@ -3,7 +3,6 @@ from typing import List
|
||||
from flask import request
|
||||
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.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
|
||||
@@ -13,6 +12,7 @@ from CTFd.models import Awards, Users, db
|
||||
from CTFd.schemas.awards import AwardSchema
|
||||
from CTFd.utils.config import is_teams_mode
|
||||
from CTFd.utils.decorators import admins_only
|
||||
from CTFd.utils.helpers.models import build_model_filters
|
||||
|
||||
awards_namespace = Namespace("awards", description="Endpoint to retrieve Awards")
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ from flask import abort, render_template, request, url_for
|
||||
from flask_restx import Namespace, Resource
|
||||
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.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
|
||||
@@ -44,6 +43,7 @@ from CTFd.utils.decorators.visibility import (
|
||||
check_challenge_visibility,
|
||||
check_score_visibility,
|
||||
)
|
||||
from CTFd.utils.helpers.models import build_model_filters
|
||||
from CTFd.utils.logging import log
|
||||
from CTFd.utils.modes import generate_account_url, get_model
|
||||
from CTFd.utils.security.signing import serialize
|
||||
|
||||
@@ -3,7 +3,6 @@ from typing import List
|
||||
from flask import request
|
||||
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.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
|
||||
@@ -13,6 +12,7 @@ from CTFd.models import Configs, db
|
||||
from CTFd.schemas.config import ConfigSchema
|
||||
from CTFd.utils import set_config
|
||||
from CTFd.utils.decorators import admins_only
|
||||
from CTFd.utils.helpers.models import build_model_filters
|
||||
|
||||
configs_namespace = Namespace("configs", description="Endpoint to retrieve Configs")
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ from typing import List
|
||||
from flask import request
|
||||
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.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
|
||||
@@ -12,6 +11,7 @@ from CTFd.models import Files, db
|
||||
from CTFd.schemas.files import FileSchema
|
||||
from CTFd.utils import uploads
|
||||
from CTFd.utils.decorators import admins_only
|
||||
from CTFd.utils.helpers.models import build_model_filters
|
||||
|
||||
files_namespace = Namespace("files", description="Endpoint to retrieve Files")
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ from typing import List
|
||||
from flask import request
|
||||
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.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
|
||||
@@ -12,6 +11,7 @@ from CTFd.models import Flags, db
|
||||
from CTFd.plugins.flags import FLAG_CLASSES, get_flag_class
|
||||
from CTFd.schemas.flags import FlagSchema
|
||||
from CTFd.utils.decorators import admins_only
|
||||
from CTFd.utils.helpers.models import build_model_filters
|
||||
|
||||
flags_namespace = Namespace("flags", description="Endpoint to retrieve Flags")
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
# This file is no longer used. If you're importing the function from here please update your imports
|
||||
|
||||
from CTFd.utils.helpers.models import build_model_filters as _build_model_filters
|
||||
|
||||
|
||||
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
|
||||
print("CTFd.api.v1.helpers.models.build_model_filters has been deprecated.")
|
||||
print("Please switch to using CTFd.utils.helpers.models.build_model_filters")
|
||||
print(
|
||||
"This function will raise an exception in a future minor release of CTFd and then be removed in a major release."
|
||||
)
|
||||
return _build_model_filters(model, query, field)
|
||||
|
||||
@@ -3,7 +3,6 @@ from typing import List
|
||||
from flask import request
|
||||
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.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
|
||||
@@ -11,6 +10,7 @@ from CTFd.constants import RawEnum
|
||||
from CTFd.models import Hints, HintUnlocks, db
|
||||
from CTFd.schemas.hints import HintSchema
|
||||
from CTFd.utils.decorators import admins_only, authed_only, during_ctf_time_only
|
||||
from CTFd.utils.helpers.models import build_model_filters
|
||||
from CTFd.utils.user import get_current_user, is_admin
|
||||
|
||||
hints_namespace = Namespace("hints", description="Endpoint to retrieve Hints")
|
||||
|
||||
@@ -3,7 +3,6 @@ from typing import List
|
||||
from flask import current_app, request
|
||||
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.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
|
||||
@@ -11,6 +10,7 @@ from CTFd.constants import RawEnum
|
||||
from CTFd.models import Notifications, db
|
||||
from CTFd.schemas.notifications import NotificationSchema
|
||||
from CTFd.utils.decorators import admins_only
|
||||
from CTFd.utils.helpers.models import build_model_filters
|
||||
|
||||
notifications_namespace = Namespace(
|
||||
"notifications", description="Endpoint to retrieve Notifications"
|
||||
|
||||
@@ -3,7 +3,6 @@ from typing import List
|
||||
from flask import request
|
||||
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.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
|
||||
@@ -12,6 +11,7 @@ from CTFd.constants import RawEnum
|
||||
from CTFd.models import Pages, db
|
||||
from CTFd.schemas.pages import PageSchema
|
||||
from CTFd.utils.decorators import admins_only
|
||||
from CTFd.utils.helpers.models import build_model_filters
|
||||
|
||||
pages_namespace = Namespace("pages", description="Endpoint to retrieve Pages")
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ from typing import List
|
||||
|
||||
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.schemas import (
|
||||
@@ -14,6 +13,7 @@ from CTFd.constants import RawEnum
|
||||
from CTFd.models import Submissions, db
|
||||
from CTFd.schemas.submissions import SubmissionSchema
|
||||
from CTFd.utils.decorators import admins_only
|
||||
from CTFd.utils.helpers.models import build_model_filters
|
||||
|
||||
submissions_namespace = Namespace(
|
||||
"submissions", description="Endpoint to retrieve Submission"
|
||||
|
||||
@@ -3,7 +3,6 @@ from typing import List
|
||||
from flask import request
|
||||
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.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
|
||||
@@ -11,6 +10,7 @@ from CTFd.constants import RawEnum
|
||||
from CTFd.models import Tags, db
|
||||
from CTFd.schemas.tags import TagSchema
|
||||
from CTFd.utils.decorators import admins_only
|
||||
from CTFd.utils.helpers.models import build_model_filters
|
||||
|
||||
tags_namespace = Namespace("tags", description="Endpoint to retrieve Tags")
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ from typing import List
|
||||
from flask import abort, request, session
|
||||
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.schemas import (
|
||||
@@ -22,6 +21,7 @@ from CTFd.utils.decorators.visibility import (
|
||||
check_account_visibility,
|
||||
check_score_visibility,
|
||||
)
|
||||
from CTFd.utils.helpers.models import build_model_filters
|
||||
from CTFd.utils.user import get_current_team, get_current_user_type, is_admin
|
||||
|
||||
teams_namespace = Namespace("teams", description="Endpoint to retrieve Teams")
|
||||
|
||||
@@ -3,7 +3,6 @@ from typing import List
|
||||
from flask import request
|
||||
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.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
|
||||
@@ -18,6 +17,7 @@ from CTFd.utils.decorators import (
|
||||
during_ctf_time_only,
|
||||
require_verified_emails,
|
||||
)
|
||||
from CTFd.utils.helpers.models import build_model_filters
|
||||
from CTFd.utils.user import get_current_user
|
||||
|
||||
unlocks_namespace = Namespace("unlocks", description="Endpoint to retrieve Unlocks")
|
||||
|
||||
@@ -3,7 +3,6 @@ from typing import List
|
||||
from flask import abort, request, session
|
||||
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.schemas import (
|
||||
@@ -32,6 +31,7 @@ from CTFd.utils.decorators.visibility import (
|
||||
check_score_visibility,
|
||||
)
|
||||
from CTFd.utils.email import sendmail, user_created_notification
|
||||
from CTFd.utils.helpers.models import build_model_filters
|
||||
from CTFd.utils.security.auth import update_user
|
||||
from CTFd.utils.user import get_current_user, get_current_user_type, is_admin
|
||||
|
||||
|
||||
@@ -8,7 +8,13 @@ from CTFd.forms.fields import SubmitField
|
||||
class SubmissionSearchForm(BaseForm):
|
||||
field = SelectField(
|
||||
"Search Field",
|
||||
choices=[("provided", "Provided"), ("id", "ID")],
|
||||
choices=[
|
||||
("provided", "Provided"),
|
||||
("id", "ID"),
|
||||
("account_id", "Account ID"),
|
||||
("challenge_id", "Challenge ID"),
|
||||
("challenge_name", "Challenge Name"),
|
||||
],
|
||||
default="provided",
|
||||
validators=[InputRequired()],
|
||||
)
|
||||
|
||||
@@ -617,9 +617,7 @@ class Submissions(db.Model):
|
||||
return child_classes[type]
|
||||
|
||||
def __repr__(self):
|
||||
return "<Submission {}, {}, {}, {}>".format(
|
||||
self.team_id, self.challenge_id, self.ip, self.provided
|
||||
)
|
||||
return f"<Submission id={self.id}, challenge_id={self.challenge_id}, ip={self.ip}, provided={self.provided}>"
|
||||
|
||||
|
||||
class Solves(Submissions):
|
||||
|
||||
@@ -29,6 +29,10 @@
|
||||
<i class="btn-fa fas fa-file-alt fa-2x px-2" data-toggle="tooltip" data-placement="top"
|
||||
title="Preview Challenge"></i>
|
||||
</a>
|
||||
<a class="no-decoration" href="{{ url_for('admin.submissions_listing', submission_type='correct', field='challenge_id', q=challenge.id) }}">
|
||||
<i class="btn-fa fas fa-check-circle fa-2x px-2" data-toggle="tooltip" data-placement="top"
|
||||
title="Correct Submissions"></i>
|
||||
</a>
|
||||
<a class="delete-challenge">
|
||||
<i class="btn-fa fas fa-trash-alt fa-2x px-2" data-toggle="tooltip" data-placement="top"
|
||||
title="Delete Challenge"></i>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
{% with form = Forms.submissions.SubmissionSearchForm(field=field, q=q) %}
|
||||
<form method="GET" class="form-inline">
|
||||
<div class="form-group col-md-2">
|
||||
<div class="form-group col-md-2 pr-0">
|
||||
{{ form.field(class="form-control custom-select w-100") }}
|
||||
</div>
|
||||
<div class="form-group col-md-8">
|
||||
@@ -66,7 +66,7 @@
|
||||
</div>
|
||||
</th>
|
||||
<th class="text-center sort-col"><b>ID</b></th>
|
||||
<th class="sort-col"><b>Team</b></th>
|
||||
<th class="sort-col"><b>{{ get_mode_as_word(capitalize=True) }}</b></th>
|
||||
<th class="sort-col"><b>Challenge</b></th>
|
||||
<th class="sort-col"><b>Type</b></th>
|
||||
<th class="sort-col"><b>Provided</b></th>
|
||||
@@ -84,8 +84,10 @@
|
||||
<td class="text-center" id="{{ sub.id }}">
|
||||
{{ sub.id }}
|
||||
</td>
|
||||
<td class="team" id="{{ sub.team_id }}">
|
||||
<a href="{{ generate_account_url(sub.account_id, admin=True) }}">{{ sub.team_name }}</a>
|
||||
<td class="team" id="{{ sub.account_id }}">
|
||||
<a href="{{ generate_account_url(sub.account_id, admin=True) }}">
|
||||
{{ sub.account_name }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="chal" id="{{ sub.challenge_id }}">
|
||||
<a href="{{ url_for('admin.challenges_detail', challenge_id=sub.challenge_id) }}">
|
||||
|
||||
23
CTFd/utils/helpers/models.py
Normal file
23
CTFd/utils/helpers/models.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import sqlalchemy
|
||||
|
||||
|
||||
def build_model_filters(model, query, field, extra_columns=None):
|
||||
if extra_columns is None:
|
||||
extra_columns = {}
|
||||
filters = []
|
||||
if query:
|
||||
# The field exists as an exposed column
|
||||
if model.__mapper__.has_property(field):
|
||||
column = getattr(model, field)
|
||||
|
||||
if type(column.type) == sqlalchemy.sql.sqltypes.Integer:
|
||||
_filter = column.op("==")(query)
|
||||
else:
|
||||
_filter = column.like(f"%{query}%")
|
||||
filters.append(_filter)
|
||||
else:
|
||||
if field in extra_columns:
|
||||
column = extra_columns[field]
|
||||
_filter = column.op("==")(query)
|
||||
filters.append(_filter)
|
||||
return filters
|
||||
Reference in New Issue
Block a user