diff --git a/src/application.py b/src/application.py index 48a1711..a467922 100644 --- a/src/application.py +++ b/src/application.py @@ -184,7 +184,7 @@ def get_asset(filename): @login_required def dl_file(problem_id): problem = db.execute("SELECT * FROM problems WHERE id=?", problem_id) - if len(problem) == 0 or (problem[0]["draft"] and not + if len(problem) == 0 or (problem[0]["status"] == PROBLEM_STAT["DRAFT"] and not check_perm(["ADMIN", "SUPERADMIN", "PROBLEM_MANAGER", "CONTENT_MANAGER"])): return abort(404) return send_from_directory("dl/", f"{problem_id}.zip", as_attachment=True) @@ -202,7 +202,7 @@ def dl_contest(contest_id, problem_id): return abort(404) problem = db.execute(("SELECT * FROM contest_problems WHERE contest_id=? " "AND problem_id=?"), contest_id, problem_id) - if len(problem) == 0 or (problem[0]["draft"] and + if len(problem) == 0 or (problem[0]["status"] == PROBLEM_STAT["DRAFT"] and not check_perm(["ADMIN", "SUPERADMIN"])): return abort(404) return send_from_directory("dl/", f"{contest_id}/{problem_id}.zip", @@ -654,20 +654,20 @@ def problems(): data = db.execute( ("SELECT problems.*, COUNT(DISTINCT problem_solved.user_id) AS sols " "FROM problems LEFT JOIN problem_solved ON " - "problems.id=problem_solved.problem_id WHERE (draft=0 AND category=?)" + "problems.id=problem_solved.problem_id WHERE (status=0 AND category=?)" "GROUP BY problems.id ORDER BY id ASC LIMIT 50 OFFSET ?"), category, page) length = db.execute(("SELECT COUNT(*) AS cnt FROM problems WHERE " - "draft=0 AND category=?"), category)[0]["cnt"] + "status=0 AND category=?"), category)[0]["cnt"] else: data = db.execute( ("SELECT problems.*, COUNT(DISTINCT problem_solved.user_id) AS sols " "FROM problems LEFT JOIN problem_solved ON " - "problems.id=problem_solved.problem_id WHERE draft=0 " + "problems.id=problem_solved.problem_id WHERE status=0 " "GROUP BY problems.id ORDER BY id ASC LIMIT 50 OFFSET ?"), page) - length = db.execute("SELECT COUNT(*) AS cnt FROM problems WHERE draft=0")[0]["cnt"] # noqa E501 + length = db.execute("SELECT COUNT(*) AS cnt FROM problems WHERE status=0")[0]["cnt"] # noqa E501 - categories = db.execute("SELECT DISTINCT category FROM problems WHERE draft=0") + categories = db.execute("SELECT DISTINCT category FROM problems WHERE status=0") categories.sort(key=lambda x: x['category']) is_ongoing_contest = len(db.execute( @@ -680,6 +680,49 @@ def problems(): is_ongoing_contest=is_ongoing_contest) +@app.route('/problems/archived') +def archived_problems(): + page = request.args.get("page") + if not page: + page = "1" + page = (int(page) - 1) * 50 + + category = request.args.get("category") + if not category: + category = None + + solved = set() + if session.get("user_id"): + solved_db = db.execute("SELECT problem_id FROM problem_solved WHERE user_id=:uid", + uid=session["user_id"]) + for row in solved_db: + solved.add(row["problem_id"]) + + if category is not None: + data = db.execute( + ("SELECT problems.*, COUNT(DISTINCT problem_solved.user_id) AS sols " + "FROM problems LEFT JOIN problem_solved ON " + "problems.id=problem_solved.problem_id WHERE (status=2 AND category=?)" + "GROUP BY problems.id ORDER BY id ASC LIMIT 50 OFFSET ?"), + category, page) + length = db.execute(("SELECT COUNT(*) AS cnt FROM problems WHERE " + "status=0 AND category=?"), category)[0]["cnt"] + else: + data = db.execute( + ("SELECT problems.*, COUNT(DISTINCT problem_solved.user_id) AS sols " + "FROM problems LEFT JOIN problem_solved ON " + "problems.id=problem_solved.problem_id WHERE status=2 " + "GROUP BY problems.id ORDER BY id ASC LIMIT 50 OFFSET ?"), page) + length = db.execute("SELECT COUNT(*) AS cnt FROM problems WHERE status=2")[0]["cnt"] # noqa E501 + + categories = db.execute("SELECT DISTINCT category FROM problems WHERE status=2") + categories.sort(key=lambda x: x['category']) + + return render_template('problem/archived_list.html', + data=data, solved=solved, length=-(-length // 50), + categories=categories, selected=category) + + @app.route("/problems/create", methods=["GET", "POST"]) @perm_required(["ADMIN", "SUPERADMIN", "PROBLEM_MANAGER", "CONTENT_MANAGER"]) def create_problem(): @@ -722,11 +765,10 @@ def create_problem(): # Create & ensure problem doesn't already exist try: - db.execute(("INSERT INTO problems (id, name, point_value, category, flag, draft, " - "flag_hint, instanced) VALUES (:id, :name, :point_value, :category, " - ":flag, :draft, :fhint, :inst)"), - id=problem_id, name=name, point_value=point_value, category=category, - flag=flag, draft=draft, fhint=flag_hint, inst=instanced) + db.execute(("INSERT INTO problems (id, name, point_value, category, flag, " + "status, flag_hint, instanced) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"), + problem_id, name, point_value, category, flag, draft, flag_hint, + instanced) except ValueError: flash('A problem with this name or ID already exists', 'danger') return render_template("problem/create.html"), 400 @@ -757,8 +799,8 @@ def draft_problems(): page = "1" page = (int(page) - 1) * 50 - data = db.execute("SELECT * FROM problems WHERE draft=1 LIMIT 50 OFFSET ?", page) - length = db.execute("SELECT COUNT(*) AS cnt FROM problems WHERE draft=1")[0]["cnt"] + data = db.execute("SELECT * FROM problems WHERE status=1 LIMIT 50 OFFSET ?", page) + length = db.execute("SELECT COUNT(*) AS cnt FROM problems WHERE status=1")[0]["cnt"] return render_template('problem/draft_problems.html', data=data, length=-(-length // 50)) diff --git a/src/assets/css/style.css b/src/assets/css/style.css index 4812469..b9123df 100644 --- a/src/assets/css/style.css +++ b/src/assets/css/style.css @@ -206,6 +206,7 @@ td { .pagination { overflow-x: auto; + margin-bottom: 0; } .hidden { diff --git a/src/helpers.py b/src/helpers.py index 62acf37..518adbe 100644 --- a/src/helpers.py +++ b/src/helpers.py @@ -25,6 +25,12 @@ "CONTENT_MANAGER": 3, } +PROBLEM_STAT = { + "PUBLISHED": 0, + "DRAFT": 1, + "ARCHIVED": 2, +} + def sha256sum(string): return hashlib.sha256(string.encode("utf-8")).hexdigest() diff --git a/src/migrate.py b/src/migrate.py index bcf8c19..fed4892 100644 --- a/src/migrate.py +++ b/src/migrate.py @@ -16,6 +16,40 @@ db = cs50.SQL("sqlite:///database.db") db.execute("BEGIN") + +db.execute("""CREATE TABLE 'problems_migration' ( + 'id' varchar(64) NOT NULL UNIQUE, + 'name' varchar(256) NOT NULL, + 'point_value' integer NOT NULL DEFAULT(0), + 'category' varchar(64), + 'flag' varchar(256) NOT NULL, + 'status' integer NOT NULL DEFAULT(0), + 'flag_hint' varchar(256) NOT NULL DEFAULT(''), + 'instanced' boolean NOT NULL DEFAULT(0) +)""") +db.execute("INSERT INTO problems_migration SELECT id, name, point_value, category, flag, draft, flag_hint, instanced FROM problems") +db.execute("DROP TABLE problems") +db.execute("ALTER TABLE problems_migration RENAME TO problems") + +db.execute("""CREATE TABLE 'contest_problems_migration' ( + 'contest_id' varchar(32) NOT NULL, + 'problem_id' varchar(64) NOT NULL, + 'name' varchar(256) NOT NULL, + 'point_value' integer NOT NULL DEFAULT(0), + 'category' varchar(64), + 'flag' varchar(256) NOT NULL, + 'status' integer NOT NULL DEFAULT(0), + 'score_min' integer NOT NULL DEFAULT(0), + 'score_max' integer NOT NULL DEFAULT(0), + 'score_users' integer NOT NULL DEFAULT(-1), + 'flag_hint' varchar(256) NOT NULL DEFAULT(''), + 'instanced' boolean NOT NULL DEFAULT(0), + UNIQUE(contest_id, problem_id) ON CONFLICT ABORT +)""") +db.execute("INSERT INTO contest_problems_migration SELECT contest_id, problem_id, name, point_value, category, flag, draft, score_min, score_max, score_users, flag_hint, instanced FROM contest_problems") +db.execute("DROP TABLE contest_problems") +db.execute("ALTER TABLE contest_problems_migration RENAME TO contest_problems") + db.execute("COMMIT") with open('settings.py', 'a') as f: diff --git a/src/schema.sql b/src/schema.sql index 7a54ced..10d4f5e 100644 --- a/src/schema.sql +++ b/src/schema.sql @@ -32,7 +32,7 @@ CREATE TABLE 'problems' ( 'point_value' integer NOT NULL DEFAULT(0), 'category' varchar(64), 'flag' varchar(256) NOT NULL, - 'draft' boolean NOT NULL DEFAULT(0), + 'status' integer NOT NULL DEFAULT(0), -- see helpers.py 'flag_hint' varchar(256) NOT NULL DEFAULT(''), 'instanced' boolean NOT NULL DEFAULT(0) ); @@ -75,7 +75,7 @@ CREATE TABLE 'contest_problems' ( 'point_value' integer NOT NULL DEFAULT(0), 'category' varchar(64), 'flag' varchar(256) NOT NULL, - 'draft' boolean NOT NULL DEFAULT(0), + 'status' integer NOT NULL DEFAULT(0), -- 0: published, 1: draft 'score_min' integer NOT NULL DEFAULT(0), 'score_max' integer NOT NULL DEFAULT(0), 'score_users' integer NOT NULL DEFAULT(-1), diff --git a/src/templates/contest/contest_problem.html b/src/templates/contest/contest_problem.html index 4d60c7b..859e59c 100644 --- a/src/templates/contest/contest_problem.html +++ b/src/templates/contest/contest_problem.html @@ -163,7 +163,7 @@

Live Instance


Edit problem
Download problem - {% if data["draft"] %} + {% if data["status"] == 1 %}
Publish problem @@ -201,7 +201,7 @@

Live Instance

document.getElementById("hint").classList.toggle("hidden"); }); -{% if data["draft"] %} +{% if data["status"] == 1 %} +{% endblock %} diff --git a/src/templates/problem/problem.html b/src/templates/problem/problem.html index 8cd8b85..d8b179c 100644 --- a/src/templates/problem/problem.html +++ b/src/templates/problem/problem.html @@ -20,6 +20,7 @@

+
{% endif %} @@ -165,9 +166,12 @@

Live Instance


Edit problem
Download problem
Delete problem - {% if data["draft"] %} -
Publish draft - {% endif %} +
Change problem type to {% endif %} @@ -205,17 +209,14 @@

Live Instance

.setAttribute("value", "Are you sure you want to delete this problem? " + "Click here to confirm."); }); - -{% endif %} -{% if data["draft"] %} - {% endif %} diff --git a/src/templates/problem/problems.html b/src/templates/problem/problems.html index 1f265fe..10e8cfe 100644 --- a/src/templates/problem/problems.html +++ b/src/templates/problem/problems.html @@ -32,6 +32,7 @@

Problems

{% endfor %} +
View archived problems
diff --git a/src/tests/test_dl.py b/src/tests/test_dl.py index c3db9b0..6e3498f 100644 --- a/src/tests/test_dl.py +++ b/src/tests/test_dl.py @@ -108,7 +108,9 @@ def test_dl(client, database): }, follow_redirects=True) assert result.status_code == 200 - result = client.post('/problem/helloworldtesting/publish', follow_redirects=True) + result = client.post('/problem/helloworldtesting/changestat', data={ + 'status': 'PUBLISHED' + }, follow_redirects=True) assert result.status_code == 200 client.post('/login', data={'username': 'normal_user', 'password': 'CTFOJadmin'}) diff --git a/src/tests/test_problems.py b/src/tests/test_problems.py index 69eba3e..98f962d 100644 --- a/src/tests/test_problems.py +++ b/src/tests/test_problems.py @@ -89,10 +89,23 @@ def test_problem(client, database): assert result.status_code == 200 assert b'1 Point' in result.data + # Archive + result = client.post('/problem/helloworldtesting/changestat', data={ + 'status': 'ARCHIVED' + }, follow_redirects=True) + assert result.status_code == 200 + assert b'ARCHIVED' in result.data + + result = client.get('/problems/archived') + assert result.status_code == 200 + assert b'helloworldtesting' in result.data + # Publish - result = client.post('/problem/helloworldtesting/publish', follow_redirects=True) + result = client.post('/problem/helloworldtesting/changestat', data={ + 'status': 'PUBLISHED' + }, follow_redirects=True) assert result.status_code == 200 - assert b'published' in result.data + assert b'PUBLISHED' in result.data # Editorial result = client.get('/problem/helloworldtesting/editeditorial') diff --git a/src/views/api.py b/src/views/api.py index 3179edc..42b0078 100644 --- a/src/views/api.py +++ b/src/views/api.py @@ -35,7 +35,8 @@ def problem(): problem_id = request.args["id"] data = db.execute("SELECT * FROM problems WHERE id=:pid", pid=problem_id) - if len(data) == 0 or (data[0]["draft"] and not api_perm(["ADMIN", "SUPERADMIN", "PROBLEM_MANAGER", "CONTENT_MANAGER"])): + if len(data) == 0 or (data[0]["status"] == PROBLEM_STAT["DRAFT"] and + not api_perm(["ADMIN", "SUPERADMIN", "PROBLEM_MANAGER", "CONTENT_MANAGER"])): return json_fail("Problem not found", 404) description = read_file(f"metadata/problems/{problem_id}/description.md") @@ -72,7 +73,7 @@ def check_instancer_perms(id): has_perm = api_perm(["ADMIN", "SUPERADMIN", "PROBLEM_MANAGER", "CONTENT_MANAGER"]) data = db.execute("SELECT * FROM problems WHERE id=:pid", pid=problem_id) - if len(data) == 0 or (data[0]["draft"] and not has_perm): + if len(data) == 0 or (data[0]["status"] == PROBLEM_STAT["DRAFT"] and not has_perm): return ("Problem not found", 404) if not data[0]["instanced"]: # Check if the problem is instanced return ("This problem is not instanced", 400) @@ -181,7 +182,7 @@ def contest_problem(): data = db.execute(("SELECT * FROM contest_problems WHERE " "contest_id=:cid AND problem_id=:pid"), cid=contest_id, pid=problem_id) - if len(data) == 0 or (data[0]["draft"] and not has_perm): + if len(data) == 0 or (data[0]["status"] == PROBLEM_STAT["DRAFT"] and not has_perm): return json_fail("Problem not found", 404) description = read_file(f"metadata/contests/{contest_id}/{problem_id}/description.md") diff --git a/src/views/contest.py b/src/views/contest.py index a62e5d2..35cb5ef 100644 --- a/src/views/contest.py +++ b/src/views/contest.py @@ -58,7 +58,7 @@ def contest(contest_id): data = [] info = db.execute( - ("SELECT * FROM contest_problems WHERE contest_id=:cid AND draft=0 " + ("SELECT * FROM contest_problems WHERE contest_id=:cid AND status=0 " "GROUP BY problem_id ORDER BY problem_id ASC, category ASC;"), cid=contest_id) @@ -198,7 +198,7 @@ def contest_drafts(contest_id): if len(contest_info) != 1: return render_template("contest/contest_noexist.html"), 404 - data = db.execute("SELECT * FROM contest_problems WHERE contest_id=:cid AND draft=1", + data = db.execute("SELECT * FROM contest_problems WHERE contest_id=:cid AND status=1", cid=contest_id) return render_template("contest/draft_problems.html", @@ -222,7 +222,8 @@ def contest_problem(contest_id, problem_id): check = db.execute(("SELECT * FROM contest_problems WHERE contest_id=:cid AND " "problem_id=:pid"), cid=contest_id, pid=problem_id) - if len(check) != 1 or (check[0]["draft"] and not check_perm(["ADMIN", "SUPERADMIN", "CONTENT_MANAGER"])): + if len(check) != 1 or (check[0]["status"] == PROBLEM_STAT["DRAFT"] + and not check_perm(["ADMIN", "SUPERADMIN", "CONTENT_MANAGER"])): return render_template("contest/contest_problem_noexist.html"), 404 # Check if problem is solved @@ -302,7 +303,7 @@ def publish_contest_problem(contest_id, problem_id): return render_template("contest/contest_noexist.html"), 404 r = db.execute( - "UPDATE contest_problems SET draft=0 WHERE problem_id=? AND contest_id=?", + "UPDATE contest_problems SET status=0 WHERE problem_id=? AND contest_id=?", problem_id, contest_id) if r == 0: return render_template("contest/contest_problem_noexist.html"), 404 @@ -596,11 +597,9 @@ def contest_add_problem(contest_id): # Modify problems table try: - db.execute(("INSERT INTO contest_problems VALUES(:cid, :pid, :name, :pv, " - ":category, :flag, :draft, :min, :max, :users, :fhint, :inst)"), - cid=contest_id, pid=problem_id, name=name, pv=max_points, - category=category, flag=flag, draft=draft, min=min_points, - max=max_points, users=users_decay, fhint=flag_hint, inst=instanced) + db.execute(("INSERT INTO contest_problems VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"), + contest_id, problem_id, name, max_points, category, flag, draft, + min_points, max_points, users_decay, flag_hint, instanced) except ValueError: flash('A problem with this ID already exists', 'danger') return render_template("contest/create_problem.html"), 409 @@ -613,7 +612,7 @@ def contest_add_problem(contest_id): # Modify problems table try: db.execute(("INSERT INTO contest_problems(contest_id, problem_id, name, " - "point_value, category, flag, draft, flag_hint, instanced) " + "point_value, category, flag, status, flag_hint, instanced) " "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)"), contest_id, problem_id, name, point_value, category, flag, draft, flag_hint, instanced) diff --git a/src/views/problem.py b/src/views/problem.py index 6102155..d48e850 100644 --- a/src/views/problem.py +++ b/src/views/problem.py @@ -22,26 +22,29 @@ def problem(problem_id): problem_id=problem_id) # Ensure problem exists - if len(data) != 1 or (data[0]["draft"] == 1 and not check_perm(["ADMIN", "SUPERADMIN", "PROBLEM_MANAGER", "CONTENT_MANAGER"])): + if len(data) != 1 or (data[0]["status"] == PROBLEM_STAT["DRAFT"] and + not check_perm(["ADMIN", "SUPERADMIN", "PROBLEM_MANAGER", "CONTENT_MANAGER"])): # noqa return render_template("problem/problem_noexist.html"), 404 data[0]["editorial"] = read_file(f"metadata/problems/{problem_id}/editorial.md") data[0]["solved"] = db.execute(("SELECT COUNT(*) AS cnt FROM problem_solved WHERE " "user_id=? AND problem_id=?"), session["user_id"], problem_id)[0]["cnt"] == 1 + if data[0]["status"] == PROBLEM_STAT["ARCHIVED"]: + flash("This problem is archived. This means that it may no longer be solveable", "warning") if request.method == "GET": - return render_template('problem/problem.html', data=data[0]) + return render_template('problem/problem.html', data=data[0], stats=PROBLEM_STAT) # Reached via POST flag = request.form.get("flag") if not flag: flash('Cannot submit an empty flag', 'danger') - return render_template('problem/problem.html', data=data[0]), 400 + return render_template('problem/problem.html', data=data[0], stats=PROBLEM_STAT), 400 if not verify_flag(flag): flash('Invalid flag', 'danger') - return render_template('problem/problem.html', data=data[0]), 400 + return render_template('problem/problem.html', data=data[0], stats=PROBLEM_STAT), 400 check = data[0]["flag"] == flag db.execute(("INSERT INTO submissions (user_id, problem_id, correct, submitted) " @@ -49,7 +52,7 @@ def problem(problem_id): if not check: flash('The flag you submitted was incorrect', 'danger') - return render_template('problem/problem.html', data=data[0]) + return render_template('problem/problem.html', data=data[0], stats=PROBLEM_STAT) # Add entry into problem solve table try: @@ -64,12 +67,12 @@ def problem(problem_id): data[0]["solved"] = True flash('Congratulations! You have solved this problem!', 'success') - return render_template('problem/problem.html', data=data[0]) + return render_template('problem/problem.html', data=data[0], stats=PROBLEM_STAT) -@api.route('/publish', methods=["POST"]) +@api.route('/changestat', methods=["POST"]) @perm_required(["ADMIN", "SUPERADMIN", "PROBLEM_MANAGER", "CONTENT_MANAGER"]) -def publish_problem(problem_id): +def change_problem_status(problem_id): data = db.execute("SELECT * FROM problems WHERE id=:problem_id", problem_id=problem_id) @@ -77,11 +80,18 @@ def publish_problem(problem_id): if len(data) != 1: return render_template("problem/problem_noexist.html"), 404 - r = db.execute("UPDATE problems SET draft=0 WHERE id=? AND draft=1", problem_id) + new_status = request.form.get("status") + new_stat_code = PROBLEM_STAT.get(new_status) + if new_stat_code is None: + flash("Invalid problem status", "danger") + return redirect("/problem/" + problem_id) + + r = db.execute("UPDATE problems SET status=? WHERE id=?", new_stat_code, problem_id) if r == 1: - logger.info(f"User #{session['user_id']} ({session['username']}) published {problem_id}", # noqa + logger.info((f"User #{session['user_id']} ({session['username']}) made " + "{problem_id} {new_status}"), extra={"section": "problem"}) - flash('Problem successfully published', 'success') + flash('Problem status successfully changed to ' + new_status, 'success') return redirect("/problem/" + problem_id) @@ -95,7 +105,8 @@ def problem_editorial(problem_id): if len(data) == 0: return render_template("problem/problem_noexist.html"), 404 - if data[0]["draft"] == 1 and not check_perm(["ADMIN", "SUPERADMIN", "PROBLEM_MANAGER", "CONTENT_MANAGER"]): + if (data[0]["status"] == PROBLEM_STAT["DRAFT"] and + not check_perm(["ADMIN", "SUPERADMIN", "PROBLEM_MANAGER", "CONTENT_MANAGER"])): # noqa return render_template("problem/problem_noexist.html"), 404 return render_template('problem/problemeditorial.html', data=data[0])