diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..7c9270922 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index abffbd4d1..f3c4fa0f4 100644 --- a/.gitignore +++ b/.gitignore @@ -196,3 +196,6 @@ typings/ # devcontainer .devcontainer/data + +# MacOS file +.DS_Store diff --git a/backend/.DS_Store b/backend/.DS_Store new file mode 100644 index 000000000..9d2e2a117 Binary files /dev/null and b/backend/.DS_Store differ diff --git a/backend/contest/migrations/0013_auto_20220302_1413.py b/backend/contest/migrations/0013_auto_20220302_1413.py new file mode 100644 index 000000000..5727aedff --- /dev/null +++ b/backend/contest/migrations/0013_auto_20220302_1413.py @@ -0,0 +1,49 @@ +# Generated by Django 3.2.12 on 2022-03-02 05:13 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contest', '0012_rename_total_score_acmcontestrank_total_penalty'), + ] + + operations = [ + migrations.AddField( + model_name='contest', + name='constraints', + field=models.JSONField(default=list), + ), + migrations.AddField( + model_name='contest', + name='requirements', + field=models.JSONField(default=list), + ), + migrations.AddField( + model_name='contest', + name='scoring', + field=models.TextField(default='ACM-ICPC style'), + ), + migrations.CreateModel( + name='ContestPrize', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('color', models.TextField()), + ('name', models.TextField()), + ('reward', models.TextField()), + ('contest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contest.contest')), + ], + ), + migrations.AddField( + model_name='acmcontestrank', + name='prize', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='contest.contestprize'), + ), + migrations.AddField( + model_name='oicontestrank', + name='prize', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='contest.contestprize'), + ), + ] diff --git a/backend/contest/models.py b/backend/contest/models.py index 7ca53bd6f..6f7b7e28c 100644 --- a/backend/contest/models.py +++ b/backend/contest/models.py @@ -7,10 +7,16 @@ from account.models import User from utils.models import RichTextField +from account.models import AdminType + class Contest(models.Model): title = models.TextField() description = RichTextField() + requirements = JSONField(default=list) + constraints = JSONField(default=list) + # allowed_groups = models.ManyToManyField(Group, blank=True) + scoring = models.TextField(default="ACM-ICPC style") # show real time rank or cached rank real_time_rank = models.BooleanField() password = models.TextField(null=True) @@ -55,10 +61,18 @@ class Meta: ordering = ("-start_time",) +class ContestPrize(models.Model): + contest = models.ForeignKey(Contest, on_delete=models.CASCADE) + color = models.TextField() + name = models.TextField() + reward = models.TextField() + + class AbstractContestRank(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) contest = models.ForeignKey(Contest, on_delete=models.CASCADE) submission_number = models.IntegerField(default=0) + prize = models.ForeignKey(ContestPrize, on_delete=models.SET_NULL, blank=True, null=True) class Meta: abstract = True @@ -73,6 +87,17 @@ class ACMContestRank(AbstractContestRank): # key is problem id submission_info = JSONField(default=dict) + @property + def rank(self): + qs_contest = Contest.objects.get(id=self.contest.id) + if qs_contest.status == ContestStatus.CONTEST_ENDED: + qs_participants = ACMContestRank.objects.filter(contest=self.contest.id, user__admin_type=AdminType.REGULAR_USER, user__is_disabled=False).\ + select_related("user").order_by("-accepted_number", "total_penalty", "total_time") + for i in range(qs_participants.count()): + if qs_participants[i].user.id == self.user.id: + return i+1 + return -1 + class Meta: db_table = "acm_contest_rank" unique_together = (("user", "contest"),) diff --git a/backend/contest/serializers.py b/backend/contest/serializers.py index 356cddaee..8aa1b7185 100644 --- a/backend/contest/serializers.py +++ b/backend/contest/serializers.py @@ -106,3 +106,11 @@ class ACMContesHelperSerializer(serializers.Serializer): problem_id = serializers.CharField() rank_id = serializers.IntegerField() checked = serializers.BooleanField() + + +class ProfileContestSerializer(serializers.Serializer): + id = serializers.IntegerField() + title = serializers.CharField(max_length=128) + start_time = serializers.DateTimeField() + rank = serializers.IntegerField() + percentage = serializers.FloatField() diff --git a/backend/contest/tests.py b/backend/contest/tests.py index 26a9bd28b..42463584b 100644 --- a/backend/contest/tests.py +++ b/backend/contest/tests.py @@ -5,7 +5,9 @@ from utils.api.tests import APITestCase -from .models import ContestAnnouncement, ContestRuleType, Contest +from .models import ContestAnnouncement, ContestRuleType, Contest, ACMContestRank +from submission.models import Submission +from problem.models import Problem, ProblemIOMode DEFAULT_CONTEST_DATA = {"title": "test title", "description": "test description", "start_time": timezone.localtime(timezone.now()), @@ -15,6 +17,34 @@ "allowed_ip_ranges": [], "visible": True, "real_time_rank": True} +DEFAULT_PROBLEM_DATA = {"_id": "A-110", "title": "test", "description": "

test

", "input_description": "test", + "output_description": "test", "time_limit": 1000, "memory_limit": 256, "difficulty": "Level1", + "visible": True, "languages": ["C", "C++", "Java", "Python2"], "template": {}, + "samples": [{"input": "test", "output": "test"}], "spj": False, "spj_language": "C", + "spj_code": "", "spj_compile_ok": True, "test_case_id": "499b26290cc7994e0b497212e842ea85", + "test_case_score": [{"output_name": "1.out", "input_name": "1.in", "output_size": 0, + "stripped_output_md5": "d41d8cd98f00b204e9800998ecf8427e", + "input_size": 0, "score": 0}], + "io_mode": {"io_mode": ProblemIOMode.standard, "input": "input.txt", "output": "output.txt"}, + "share_submission": False, + "rule_type": "ACM", "hint": "

test

", "source": "test"} + +DEFAULT_SUBMISSION_DATA = { + "problem_id": "1", + "user_id": 1, + "username": "test", + "code": "xxxxxxxxxxxxxx", + "result": -2, + "info": {}, + "language": "C", + "statistic_info": {} +} + + +DEFAULT_ACMCONTESTRANK_DATA = {"submission_number": 1, "accepted_number": 1, "total_time": 123, "total_penalty": 123, + "submission_info": {"1": {"is_ac": True, "ac_time": 123, "penalty": 123, "problem_submission": 1}}, + "contest": 1} + class ContestAdminAPITest(APITestCase): def setUp(self): @@ -148,3 +178,36 @@ def test_get_contest_announcement_list(self): contest_id = self.create_contest_announcements() response = self.client.get(self.url, data={"contest_id": contest_id}) self.assertSuccess(response) + + +class UserContestAPITest(APITestCase): + def setUp(self): + # create contest + admin = self.create_admin() + self.contest = Contest.objects.create(created_by=admin, **DEFAULT_CONTEST_DATA) + + # create problem in contest + data = copy.deepcopy(DEFAULT_PROBLEM_DATA) + data["contest_id"] = self.contest.id + self.problem = Problem.objects.create(created_by=admin, **data) + + # user submit problem + user = self.create_user("test", "test123") + data = copy.deepcopy(DEFAULT_SUBMISSION_DATA) + data["contest_id"] = self.contest.id + data["problem_id"] = self.problem.id + data["user_id"] = user.id + self.submission = Submission.objects.create(**data) + + # create ACMContestRank + data = copy.deepcopy(DEFAULT_ACMCONTESTRANK_DATA) + data["user"] = user + data["contest"] = self.contest + self.rank = ACMContestRank.objects.create(**data) + + self.url = self.reverse("contest_user_api") + + # test UserContestAPI : can user get contest info which he participated and rank? + def test_get_participated_contest_list(self): + response = self.client.get(self.url) + self.assertSuccess(response) diff --git a/backend/contest/urls/oj.py b/backend/contest/urls/oj.py index 9e82b81bd..6a3f768d3 100644 --- a/backend/contest/urls/oj.py +++ b/backend/contest/urls/oj.py @@ -3,6 +3,7 @@ from ..views.oj import ContestAnnouncementListAPI from ..views.oj import ContestPasswordVerifyAPI, ContestAccessAPI from ..views.oj import ContestListAPI, ContestAPI, ContestRankAPI +from ..views.oj import UserContestAPI urlpatterns = [ path("contests/", ContestListAPI.as_view(), name="contest_list_api"), @@ -11,4 +12,5 @@ path("contest/announcement/", ContestAnnouncementListAPI.as_view(), name="contest_announcement_api"), path("contest/access/", ContestAccessAPI.as_view(), name="contest_access_api"), path("contest/rank/", ContestRankAPI.as_view(), name="contest_rank_api"), + path("contest/user/", UserContestAPI.as_view(), name="contest_user_api"), ] diff --git a/backend/contest/views/oj.py b/backend/contest/views/oj.py index 9e82647e8..efef1f806 100644 --- a/backend/contest/views/oj.py +++ b/backend/contest/views/oj.py @@ -14,6 +14,7 @@ from ..serializers import ContestAnnouncementSerializer from ..serializers import ContestSerializer, ContestPasswordVerifySerializer from ..serializers import ACMContestRankSerializer +from ..serializers import ProfileContestSerializer class ContestAnnouncementListAPI(APIView): @@ -229,3 +230,29 @@ def get(self, request): page_qs["results"] = serializer(page_qs["results"], many=True, is_contest_admin=is_contest_admin).data page_qs["results"].append(self.contest.id) return self.success(page_qs) + + +class UserContestAPI(APIView): + def get(self, request): + user = request.user + # queryset for all problems information which user submitted + qs_problems = ACMContestRank.objects.filter(user=user.id, user__admin_type=AdminType.REGULAR_USER, user__is_disabled=False) + contests = [] + for problem in qs_problems: + contest_id = problem.contest.id + try: + contest = Contest.objects.get(id=contest_id, visible=True) + contest = ContestSerializer(contest).data + total_participants = ACMContestRank.objects.filter(contest=contest_id, user__admin_type=AdminType.REGULAR_USER, user__is_disabled=False).count() + contest["rank"] = problem.rank + contest["percentage"] = round(contest["rank"]/total_participants*100, 2) + contests.append(contest) + except Contest.DoesNotExist: + return self.error("Contest does not exist") + + # priority + priority = request.GET.get("priority") + if priority: + contests = sorted(contests, key=lambda c: c[priority]) + + return self.success(ProfileContestSerializer(contests, many=True).data) diff --git a/frontend/package.json b/frontend/package.json index ac7b53b15..f4040bebf 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,6 +16,7 @@ "axios": "^0.24.0", "bootstrap-vue": "^2.21.2", "browser-detect": "^0.2.28", + "chart.js": "^3.7.0", "core-js": "^3.19.3", "highlight.js": "10.7.3", "iview": "2", diff --git a/frontend/src/pages/oj/api.js b/frontend/src/pages/oj/api.js index a74f161c6..7a9ceb18f 100644 --- a/frontend/src/pages/oj/api.js +++ b/frontend/src/pages/oj/api.js @@ -213,6 +213,13 @@ export default { } }) }, + getUserContestInfo (offset, limit, params) { + params.limit = limit + params.offset = offset + return ajax('contest/user/', 'get', { + params + }) + }, submitCode (data) { return ajax('submission/', 'post', { data diff --git a/frontend/src/pages/oj/components/Pagination.vue b/frontend/src/pages/oj/components/Pagination.vue new file mode 100644 index 000000000..3475c767a --- /dev/null +++ b/frontend/src/pages/oj/components/Pagination.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/frontend/src/pages/oj/components/user/ProfileContest.vue b/frontend/src/pages/oj/components/user/ProfileContest.vue index 1af2953b0..aa04ac2b6 100644 --- a/frontend/src/pages/oj/components/user/ProfileContest.vue +++ b/frontend/src/pages/oj/components/user/ProfileContest.vue @@ -1,38 +1,181 @@ + diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 83d425e71..e08c4cbd5 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2752,6 +2752,11 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== +chart.js@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-3.7.0.tgz#7a19c93035341df801d613993c2170a1fcf1d882" + integrity sha512-31gVuqqKp3lDIFmzpKIrBeum4OpZsQjSIAqlOpgjosHDJZlULtvwLEZKtEhIAZc7JMPaHlYMys40Qy9Mf+1AAg== + check-types@^8.0.3: version "8.0.3" resolved "https://registry.yarnpkg.com/check-types/-/check-types-8.0.3.tgz#3356cca19c889544f2d7a95ed49ce508a0ecf552"