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 %}
+
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])