Skip to content

Commit 6d7611f

Browse files
author
szhang45
committed
Add editor support and dev tools
- Add new frontend editor in Svelte for submissions - Add development tools and examples in dev/ directory - Update problem models and tasks for better editor integration - Add new Celery tasks and improve submission handling - Update templates for enhanced UI/UX - Add utilities for rankings and contests - Update dependencies and gitignore - Migrate database schema changes
1 parent d5c8126 commit 6d7611f

42 files changed

Lines changed: 1366 additions & 465 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
.vscode
21
__pycache__
32
.venv
43
.ruff_cache
54
.env
65
.first_log
76
collected_static
87
media
9-
./filler_data.py
8+
./filler_data.py
9+
./filler_data.pydb.sqlite3
10+

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ TJ Computer Team Members: You can find all information about in-houses and the c
44
This grader website can be accessed at https://tjctgrader.org/.
55
Please contact us through tjctgrader@gmail.com if you have any questions or concerns.
66

7+
This repository contains most of the website that deals with how pages are rendered to users. The part of the website that runs code and judges it can be found at this repository: https://github.com/TJ-Computer-Team/coderunner.
8+
79
---
810
Current Developers: Samuel Chow, Samuel Zhang
911

autograder/apps/contests/utils.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def get_standings(cid):
1414
pid_index = {p.id: i for i, p in enumerate(problems)}
1515
start, end = contest.start, contest.end
1616

17-
users = GraderUser.objects.filter(is_staff=False)
17+
users = GraderUser.objects.all()
1818
stats = {
1919
u.id: {
2020
"id": u.id,
@@ -39,16 +39,18 @@ def get_standings(cid):
3939
if user_data is None or prob_idx is None:
4040
continue
4141

42-
if user_data["problems"][prob_idx] <= 0:
43-
user_data["problems"][prob_idx] -= 1
44-
4542
if s.verdict in ("Accepted", "AC"):
46-
if user_data["problems"][prob_idx] < 0:
43+
if user_data["problems"][prob_idx] == 0:
4744
user_data["solved"] += problems[prob_idx].points
4845
minutes = int((s.timestamp - start).total_seconds() / 60)
49-
50-
user_data["problems"][prob_idx] = abs(user_data["problems"][prob_idx])
51-
user_data["penalty"] += minutes + 5 * user_data["problems"][prob_idx]
46+
# Add time and wrong-submission penalty
47+
user_data["penalty"] += minutes - 10 * abs(
48+
min(0, user_data["problems"][prob_idx])
49+
)
50+
user_data["problems"][prob_idx] = 1
51+
else:
52+
if user_data["problems"][prob_idx] == 0:
53+
user_data["problems"][prob_idx] -= 1
5254

5355
# Filter and sort
5456
standings = [

autograder/apps/contests/views.py

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from django.core.paginator import Paginator
33
from django.conf import settings
44
from django.utils import timezone
5-
from django.http import HttpResponse
65
from ..oauth.decorators import login_required, admin_required
76
from .models import Contest
87
from ..problems.models import Problem
@@ -17,8 +16,6 @@
1716
@login_required
1817
def contests_view(request):
1918
contests = Contest.objects.filter(tjioi=settings.TJIOI_MODE).order_by("-start")
20-
if not request.user.is_staff:
21-
contests = contests.filter(start__lte=timezone.now())
2219
context = {"contests": contests}
2320
return render(request, "contest/contests.html", context)
2421

@@ -27,15 +24,18 @@ def contests_view(request):
2724
def contest_view(request, cid):
2825
contest = get_object_or_404(Contest, pk=cid)
2926

30-
problems = list(Problem.objects.filter(contest=contest).order_by("id"))
27+
problems = list(Problem.objects.filter(contest=contest).order_by('id'))
3128
if problems is None:
3229
problems = []
3330

31+
now = timezone.now()
32+
contest_start = contest.start
3433
contest_end = contest.end
3534
time_message = contest_end
3635
time_type = "end"
37-
if timezone.now() < contest.start:
38-
return HttpResponse("Contest has not started yet", status=403)
36+
if now < contest_start:
37+
time_type = "start"
38+
time_message = contest_start
3939

4040
ordered = []
4141
for problem in problems:
@@ -74,18 +74,20 @@ def contest_view(request, cid):
7474
ordered[pind]["solves"] += 1
7575
ordered[pind]["users"].append(ind)
7676

77-
context = {
78-
"not_empty": "yes" if ordered else "no",
79-
"title": contest.name,
80-
"problems": ordered,
81-
"user": request.user.id,
82-
"cid": contest.id,
83-
"timeStatus": time_message,
84-
"timeType": time_type,
85-
"editorial": getattr(contest, "editorial", None),
86-
"contest_over": timezone.now() > contest.end,
87-
}
88-
return render(request, "contest/contest.html", context)
77+
if ordered:
78+
context = {
79+
"title": contest.name,
80+
"problems": ordered,
81+
"user": request.user.id,
82+
"cid": contest.id,
83+
"timeStatus": time_message,
84+
"timeType": time_type,
85+
"editorial": getattr(contest, "editorial", None),
86+
"contest_over": timezone.now() > contest.end,
87+
}
88+
return render(request, "contest/contest.html", context)
89+
else:
90+
return redirect("contests:contests")
8991

9092

9193
@login_required
@@ -94,9 +96,6 @@ def contest_standings_view(request, cid):
9496
problems = Problem.objects.filter(contest_id=cid)
9597
contest = get_object_or_404(Contest, id=cid)
9698

97-
if timezone.now() < contest.start:
98-
return HttpResponse("Contest has not started yet", status=403)
99-
10099
context = {
101100
"title": standings["title"],
102101
"cid": cid,
@@ -112,8 +111,6 @@ def contest_standings_view(request, cid):
112111
@login_required
113112
def contest_status_view(request, cid, mine_only, page):
114113
contest = get_object_or_404(Contest, id=cid)
115-
if timezone.now() < contest.start:
116-
return HttpResponse("Contest has not started yet", status=403)
117114
subs = Submission.objects.filter(contest=contest)
118115
if not request.user.is_staff:
119116
subs = subs.filter(usr__is_staff=False)

autograder/apps/index/signals.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from django.db.models.signals import post_save
22
from django.dispatch import receiver
33
from .models import GraderUser
4-
from ..rankings.tasks import update_codeforces_rating
4+
from decimal import Decimal
5+
from ..rankings.utils import get_codeforces_rating
56
import threading
67
import logging
78

@@ -10,7 +11,7 @@
1011

1112

1213
@receiver(post_save, sender=GraderUser)
13-
def handle_user_updates(sender, instance, created, update_fields=None, **kwargs):
14+
def update_rating(sender, instance, created, **kwargs):
1415
if getattr(_signal_lock, "in_signal", False):
1516
return
1617

@@ -23,12 +24,31 @@ def handle_user_updates(sender, instance, created, update_fields=None, **kwargs)
2324
}
2425

2526
new_usaco_rating = usaco_map.get(instance.usaco_division, 800)
27+
new_cf_rating = get_codeforces_rating(instance)
28+
if new_cf_rating is None:
29+
new_cf_rating = instance.cf_rating
30+
vals = [new_usaco_rating, new_cf_rating, instance.inhouse]
31+
vals.sort()
32+
new_index = (
33+
Decimal("0.2") * vals[0] + Decimal("0.35") * vals[1] + Decimal("0.45") * vals[2]
34+
)
35+
36+
fields_to_update = []
2637
if instance.usaco_rating != new_usaco_rating:
2738
instance.usaco_rating = new_usaco_rating
39+
fields_to_update.append("usaco_rating")
40+
41+
if instance.cf_rating != new_cf_rating:
42+
instance.cf_rating = new_cf_rating
43+
fields_to_update.append("cf_rating")
44+
45+
if instance.index != new_index:
46+
instance.index = new_index
47+
fields_to_update.append("index")
48+
49+
if fields_to_update:
2850
try:
2951
_signal_lock.in_signal = True
30-
instance.save(update_fields=["usaco_rating"])
52+
instance.save(update_fields=fields_to_update)
3153
finally:
3254
_signal_lock.in_signal = False
33-
34-
update_codeforces_rating.delay(instance.id)

autograder/apps/index/views.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ def profile_view(request):
5353

5454
cfh = request.user.cf_handle
5555

56-
context = {"cf_handle": cfh if cfh and len(cfh) > 0 else ""}
56+
context = {
57+
"cf_handle": cfh if cfh and len(cfh) > 0 else ""
58+
}
5759
return render(request, "index/profile.html", context=context)
5860

5961

autograder/apps/problems/migrations/0010_remove_problem_solution.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44

55

66
class Migration(migrations.Migration):
7+
78
dependencies = [
8-
("problems", "0009_alter_problem_testcases_zip"),
9+
('problems', '0009_alter_problem_testcases_zip'),
910
]
1011

1112
operations = [
1213
migrations.RemoveField(
13-
model_name="problem",
14-
name="solution",
14+
model_name='problem',
15+
name='solution',
1516
),
1617
]

autograder/apps/problems/migrations/0011_alter_problem_testcases_zip.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44

55

66
class Migration(migrations.Migration):
7+
78
dependencies = [
8-
("problems", "0010_remove_problem_solution"),
9+
('problems', '0010_remove_problem_solution'),
910
]
1011

1112
operations = [
1213
migrations.AlterField(
13-
model_name="problem",
14-
name="testcases_zip",
15-
field=models.FileField(blank=True, upload_to="problem_testcases/"),
14+
model_name='problem',
15+
name='testcases_zip',
16+
field=models.FileField(blank=True, upload_to='problem_testcases/'),
1617
),
1718
]
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
from django.db.models.signals import post_save
22
from django.dispatch import receiver
33
from .models import Problem
4-
from ...coderunner.files import add_problem_to_coderunner, add_tests_to_coderunner
4+
from .tasks import add_problem_to_coderunner_task, add_tests_to_coderunner_task
55
import logging
66

77
logger = logging.getLogger(__name__)
88

99

1010
@receiver(post_save, sender=Problem)
1111
def add_problem_folder(sender, instance, created, **kwargs):
12-
add_problem_to_coderunner(instance.id)
12+
add_problem_to_coderunner_task.delay(instance.id)
1313

1414

1515
@receiver(post_save, sender=Problem)
1616
def add_problem_tests(sender, instance, created, **kwargs):
17-
add_tests_to_coderunner(instance.id)
17+
add_tests_to_coderunner_task.delay(instance.id)

autograder/apps/problems/tasks.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import os
2+
import logging
3+
import requests
4+
import zipfile
5+
import tempfile
6+
from urllib.parse import urlencode
7+
from celery import shared_task
8+
from django.conf import settings
9+
from ..problems.models import Problem
10+
11+
logger = logging.getLogger(__name__)
12+
13+
14+
@shared_task(bind=True, max_retries=3, default_retry_delay=30)
15+
def add_problem_to_coderunner_task(self, problem_id: int):
16+
logger.info(f"Registering problem {problem_id} with the coderunner.")
17+
coderunner_url = settings.CODERUNNER_URL + "addProblem"
18+
try:
19+
response = requests.post(
20+
coderunner_url,
21+
data=urlencode({"pid": problem_id}),
22+
headers={"Content-Type": "application/x-www-form-urlencoded"},
23+
)
24+
response.raise_for_status()
25+
logger.info(f"Successfully added problem {problem_id} to coderunner.")
26+
except requests.exceptions.RequestException as exc:
27+
logger.warning(f"Network error for problem {problem_id}: {exc}. Retrying...")
28+
raise self.retry(exc=exc)
29+
except Exception:
30+
logger.exception(
31+
f"An unexpected error occurred while adding problem {problem_id}."
32+
)
33+
raise
34+
35+
36+
@shared_task(bind=True, max_retries=3, default_retry_delay=30)
37+
def add_tests_to_coderunner_task(self, problem_id: int):
38+
logger.info(f"Starting test case upload for problem {problem_id}.")
39+
try:
40+
problem = Problem.objects.get(id=problem_id)
41+
if not problem.testcases_zip or not hasattr(problem.testcases_zip, "path"):
42+
logger.warning(f"No zip file found for problem {problem_id}.")
43+
return
44+
45+
with tempfile.TemporaryDirectory() as tmpdir:
46+
with zipfile.ZipFile(problem.testcases_zip.path, "r") as zip_ref:
47+
zip_ref.extractall(tmpdir)
48+
names = zip_ref.namelist()
49+
50+
upload_url = settings.CODERUNNER_URL + "addTest"
51+
headers = {"Content-Type": "application/x-www-form-urlencoded"}
52+
testcases = {}
53+
54+
for name in names:
55+
full_path = os.path.join(tmpdir, name)
56+
if not os.path.isfile(full_path):
57+
continue
58+
59+
base, ext = os.path.splitext(name)
60+
if not base.isdigit() or ext not in [".in", ".out"]:
61+
logger.error(f"Invalid file name: {name}")
62+
return
63+
64+
tid = int(base)
65+
if tid not in testcases:
66+
testcases[tid] = {}
67+
with open(full_path, "r") as f:
68+
content = f.read()
69+
if ext == ".in":
70+
testcases[tid]["test"] = content
71+
else:
72+
testcases[tid]["out"] = content
73+
74+
for tid, tc in testcases.items():
75+
payload = {
76+
"pid": problem_id,
77+
"tid": tid,
78+
"test": tc["test"],
79+
"out": tc["out"],
80+
}
81+
response = requests.post(
82+
upload_url, data=urlencode(payload), headers=headers
83+
)
84+
if response.status_code != 200:
85+
logger.error(
86+
f"Failed to upload test {tid} for problem {problem_id}: {response.text}"
87+
)
88+
else:
89+
logger.info(f"Uploaded test {tid} for problem {problem_id}")
90+
91+
except Problem.DoesNotExist:
92+
logger.error(f"Problem {problem_id} not found for test upload.")
93+
except requests.exceptions.RequestException as exc:
94+
logger.warning(
95+
f"Network error during test upload for {problem_id}: {exc}. Retrying..."
96+
)
97+
raise self.retry(exc=exc)
98+
except Exception:
99+
logger.exception(
100+
f"An unexpected error occurred during test upload for {problem_id}."
101+
)
102+
raise

0 commit comments

Comments
 (0)