From 2d49c8dd9aab76767c7181fd0bc3e2f6a8edc0b3 Mon Sep 17 00:00:00 2001 From: Kevin Chung Date: Mon, 27 Apr 2020 15:04:29 -0400 Subject: [PATCH] Switch to using a Flask SQLAlchemy pagination object for submission searching --- CTFd/admin/submissions.py | 38 ++++++++++++-------- CTFd/themes/admin/assets/js/styles.js | 18 ++++++++++ CTFd/themes/admin/static/js/core.dev.js | 2 +- CTFd/themes/admin/templates/submissions.html | 19 +++++----- 4 files changed, 53 insertions(+), 24 deletions(-) diff --git a/CTFd/admin/submissions.py b/CTFd/admin/submissions.py index f409a814..13fce04f 100644 --- a/CTFd/admin/submissions.py +++ b/CTFd/admin/submissions.py @@ -1,4 +1,4 @@ -from flask import render_template, request +from flask import render_template, request, url_for from CTFd.admin import admin from CTFd.models import Challenges, Submissions @@ -10,16 +10,21 @@ from CTFd.utils.modes import get_model @admin.route("/admin/submissions/") @admins_only def submissions_listing(submission_type): - filters = {} + filters_by = {} if submission_type: - filters["type"] = submission_type + filters_by["type"] = submission_type + filters = [] - curr_page = abs(int(request.args.get("page", 1, type=int))) - results_per_page = 50 - page_start = results_per_page * (curr_page - 1) - page_end = results_per_page * (curr_page - 1) + results_per_page - sub_count = Submissions.query.filter_by(**filters).count() - page_count = int(sub_count / results_per_page) + (sub_count % results_per_page > 0) + q = request.args.get("q") + field = request.args.get("field") + page = 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))) Model = get_model() @@ -34,18 +39,23 @@ def submissions_listing(submission_type): Challenges.name.label("challenge_name"), Model.name.label("team_name"), ) - .filter_by(**filters) + .filter_by(**filters_by) + .filter(*filters) .join(Challenges) .join(Model) .order_by(Submissions.date.desc()) - .slice(page_start, page_end) - .all() + .paginate(page=page, per_page=50) ) + args = dict(request.args) + args.pop('page', 1) + return render_template( "admin/submissions.html", submissions=submissions, - page_count=page_count, - curr_page=curr_page, + prev_page=url_for(request.endpoint, type=submission_type, **args, page=submissions.prev_num), + next_page=url_for(request.endpoint, type=submission_type, **args, page=submissions.next_num), type=submission_type, + q=q, + field=field, ) diff --git a/CTFd/themes/admin/assets/js/styles.js b/CTFd/themes/admin/assets/js/styles.js index 6a7e2a3c..be73b5e5 100644 --- a/CTFd/themes/admin/assets/js/styles.js +++ b/CTFd/themes/admin/assets/js/styles.js @@ -80,6 +80,24 @@ export default () => { window.location.href = url.toString(); }); + $(".page-prev").click(function(e) { + e.preventDefault(); + let url = new URL(window.location); + let page = url.searchParams.get("page"); + page = page ? page : 1; + url.searchParams.set("page", --page); + window.location.href = url.toString(); + }); + + $(".page-next").click(function(e) { + e.preventDefault(); + let url = new URL(window.location); + let page = url.searchParams.get("page"); + page = page ? page : 1; + url.searchParams.set("page", ++page); + window.location.href = url.toString(); + }); + makeSortableTables(); $('[data-toggle="tooltip"]').tooltip(); }); diff --git a/CTFd/themes/admin/static/js/core.dev.js b/CTFd/themes/admin/static/js/core.dev.js index 28236410..4b85aa5a 100644 --- a/CTFd/themes/admin/static/js/core.dev.js +++ b/CTFd/themes/admin/static/js/core.dev.js @@ -20,7 +20,7 @@ eval("\n\nvar _CTFd = _interopRequireDefault(__webpack_require__(/*! core/CTFd * /***/ (function(module, exports, __webpack_require__) { ; -eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = void 0;\n\n__webpack_require__(/*! bootstrap/dist/js/bootstrap.bundle */ \"./node_modules/bootstrap/dist/js/bootstrap.bundle.js\");\n\nvar _utils = __webpack_require__(/*! core/utils */ \"./CTFd/themes/core/assets/js/utils.js\");\n\nvar _jquery = _interopRequireDefault(__webpack_require__(/*! jquery */ \"./node_modules/jquery/dist/jquery.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nvar _default = function _default() {\n // TODO: This is kind of a hack to mimic a React-like state construct.\n // It should be removed once we have a real front-end framework in place.\n (0, _jquery.default)(\":input\").each(function () {\n (0, _jquery.default)(this).data(\"initial\", (0, _jquery.default)(this).val());\n });\n (0, _jquery.default)(\".form-control\").bind({\n focus: function focus() {\n (0, _jquery.default)(this).addClass(\"input-filled-valid\");\n },\n blur: function blur() {\n if ((0, _jquery.default)(this).val() === \"\") {\n (0, _jquery.default)(this).removeClass(\"input-filled-valid\");\n }\n }\n });\n (0, _jquery.default)(\".modal\").on(\"show.bs.modal\", function (e) {\n (0, _jquery.default)(\".form-control\").each(function () {\n if ((0, _jquery.default)(this).val()) {\n (0, _jquery.default)(this).addClass(\"input-filled-valid\");\n }\n });\n });\n (0, _jquery.default)(function () {\n (0, _jquery.default)(\".form-control\").each(function () {\n if ((0, _jquery.default)(this).val()) {\n (0, _jquery.default)(this).addClass(\"input-filled-valid\");\n }\n });\n (0, _jquery.default)(\"tr[data-href]\").click(function () {\n var sel = getSelection().toString();\n\n if (!sel) {\n var href = (0, _jquery.default)(this).attr(\"data-href\");\n\n if (href) {\n window.location = href;\n }\n }\n\n return false;\n });\n (0, _jquery.default)(\"[data-checkbox]\").click(function (e) {\n if ((0, _jquery.default)(e.target).is(\"input[type=checkbox]\")) {\n e.stopImmediatePropagation();\n return;\n }\n\n var checkbox = (0, _jquery.default)(this).find(\"input[type=checkbox]\"); // Doing it this way with an event allows data-checkbox-all to work\n\n checkbox.click();\n e.stopImmediatePropagation();\n });\n (0, _jquery.default)(\"[data-checkbox-all]\").on(\"click change\", function (e) {\n var checked = (0, _jquery.default)(this).prop(\"checked\");\n var idx = (0, _jquery.default)(this).index() + 1;\n (0, _jquery.default)(this).closest(\"table\").find(\"tr td:nth-child(\".concat(idx, \") input[type=checkbox]\")).prop(\"checked\", checked);\n e.stopImmediatePropagation();\n });\n (0, _jquery.default)(\"tr[data-href] a, tr[data-href] button\").click(function (e) {\n // TODO: This is a hack to allow modal close buttons to work\n if (!(0, _jquery.default)(this).attr(\"data-dismiss\")) {\n e.stopPropagation();\n }\n });\n (0, _jquery.default)(\".page-select\").change(function () {\n var url = new URL(window.location);\n url.searchParams.set(\"page\", this.value);\n window.location.href = url.toString();\n });\n (0, _utils.makeSortableTables)();\n (0, _jquery.default)('[data-toggle=\"tooltip\"]').tooltip();\n });\n};\n\nexports.default = _default;\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/styles.js?"); +eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = void 0;\n\n__webpack_require__(/*! bootstrap/dist/js/bootstrap.bundle */ \"./node_modules/bootstrap/dist/js/bootstrap.bundle.js\");\n\nvar _utils = __webpack_require__(/*! core/utils */ \"./CTFd/themes/core/assets/js/utils.js\");\n\nvar _jquery = _interopRequireDefault(__webpack_require__(/*! jquery */ \"./node_modules/jquery/dist/jquery.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nvar _default = function _default() {\n // TODO: This is kind of a hack to mimic a React-like state construct.\n // It should be removed once we have a real front-end framework in place.\n (0, _jquery.default)(\":input\").each(function () {\n (0, _jquery.default)(this).data(\"initial\", (0, _jquery.default)(this).val());\n });\n (0, _jquery.default)(\".form-control\").bind({\n focus: function focus() {\n (0, _jquery.default)(this).addClass(\"input-filled-valid\");\n },\n blur: function blur() {\n if ((0, _jquery.default)(this).val() === \"\") {\n (0, _jquery.default)(this).removeClass(\"input-filled-valid\");\n }\n }\n });\n (0, _jquery.default)(\".modal\").on(\"show.bs.modal\", function (e) {\n (0, _jquery.default)(\".form-control\").each(function () {\n if ((0, _jquery.default)(this).val()) {\n (0, _jquery.default)(this).addClass(\"input-filled-valid\");\n }\n });\n });\n (0, _jquery.default)(function () {\n (0, _jquery.default)(\".form-control\").each(function () {\n if ((0, _jquery.default)(this).val()) {\n (0, _jquery.default)(this).addClass(\"input-filled-valid\");\n }\n });\n (0, _jquery.default)(\"tr[data-href]\").click(function () {\n var sel = getSelection().toString();\n\n if (!sel) {\n var href = (0, _jquery.default)(this).attr(\"data-href\");\n\n if (href) {\n window.location = href;\n }\n }\n\n return false;\n });\n (0, _jquery.default)(\"[data-checkbox]\").click(function (e) {\n if ((0, _jquery.default)(e.target).is(\"input[type=checkbox]\")) {\n e.stopImmediatePropagation();\n return;\n }\n\n var checkbox = (0, _jquery.default)(this).find(\"input[type=checkbox]\"); // Doing it this way with an event allows data-checkbox-all to work\n\n checkbox.click();\n e.stopImmediatePropagation();\n });\n (0, _jquery.default)(\"[data-checkbox-all]\").on(\"click change\", function (e) {\n var checked = (0, _jquery.default)(this).prop(\"checked\");\n var idx = (0, _jquery.default)(this).index() + 1;\n (0, _jquery.default)(this).closest(\"table\").find(\"tr td:nth-child(\".concat(idx, \") input[type=checkbox]\")).prop(\"checked\", checked);\n e.stopImmediatePropagation();\n });\n (0, _jquery.default)(\"tr[data-href] a, tr[data-href] button\").click(function (e) {\n // TODO: This is a hack to allow modal close buttons to work\n if (!(0, _jquery.default)(this).attr(\"data-dismiss\")) {\n e.stopPropagation();\n }\n });\n (0, _jquery.default)(\".page-select\").change(function () {\n var url = new URL(window.location);\n url.searchParams.set(\"page\", this.value);\n window.location.href = url.toString();\n });\n (0, _jquery.default)(\".page-prev\").click(function (e) {\n e.preventDefault();\n var url = new URL(window.location);\n var page = url.searchParams.get(\"page\");\n page = page ? page : 1;\n url.searchParams.set(\"page\", --page);\n window.location.href = url.toString();\n });\n (0, _jquery.default)(\".page-next\").click(function (e) {\n e.preventDefault();\n var url = new URL(window.location);\n var page = url.searchParams.get(\"page\");\n page = page ? page : 1;\n url.searchParams.set(\"page\", ++page);\n window.location.href = url.toString();\n });\n (0, _utils.makeSortableTables)();\n (0, _jquery.default)('[data-toggle=\"tooltip\"]').tooltip();\n });\n};\n\nexports.default = _default;\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/styles.js?"); /***/ }), diff --git a/CTFd/themes/admin/templates/submissions.html b/CTFd/themes/admin/templates/submissions.html index 5daa84c2..31960981 100644 --- a/CTFd/themes/admin/templates/submissions.html +++ b/CTFd/themes/admin/templates/submissions.html @@ -16,7 +16,8 @@
{% if q and field %} -

Searching for submissions with {{field}} matching {{q}}

+
Searching for submissions with {{ field }} matching {{ q }}
+
Page {{ submissions.page }} of {{ submissions.total }} results
{% endif %}
@@ -71,7 +72,7 @@ - {% for sub in submissions %} + {% for sub in submissions.items %}
@@ -102,19 +103,19 @@ {% endfor %} - {% if page_count > 1 %} + {% if submissions.pages > 1 %}
Page
- {% if curr_page != 1 %} - <<< + {% if submissions.page != 1 %} + <<< {% endif %} - {% if curr_page != page_count %} - >>> + {% if submissions.next_num %} + >>> {% endif %}
{% endif %}