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:
Kevin Chung
2020-08-09 03:40:11 -04:00
committed by GitHub
parent 69b4aafeac
commit f4c9d1e2e8
20 changed files with 76 additions and 34 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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()],
)

View File

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

View File

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

View File

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

View 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