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/0014_auto_20220302_1508.py b/backend/contest/migrations/0014_auto_20220302_1508.py new file mode 100644 index 000000000..e77580b74 --- /dev/null +++ b/backend/contest/migrations/0014_auto_20220302_1508.py @@ -0,0 +1,49 @@ +# Generated by Django 3.2.12 on 2022-03-02 06:08 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contest', '0013_contestannouncement_problem'), + ] + + 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 d605b2c6c..2f8333bec 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 010714192..eecdd0714 100644 --- a/backend/contest/serializers.py +++ b/backend/contest/serializers.py @@ -112,3 +112,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 c7f5a2e86..099adfdbd 100644 --- a/backend/contest/tests.py +++ b/backend/contest/tests.py @@ -5,9 +5,9 @@ from utils.api.tests import APITestCase -from .models import ContestAnnouncement, ContestRuleType, Contest - -from problem.models import ProblemIOMode +from .models import ContestAnnouncement, ContestRuleType, Contest, ACMContestRank +from submission.models import Submission +from problem.models import Problem, ProblemIOMode, ProblemTag DEFAULT_PROBLEM_DATA = {"_id": "A-110", "title": "test", "description": "

test

", "input_description": "test", "output_description": "test", "time_limit": 1000, "memory_limit": 256, "difficulty": "Level1", @@ -29,6 +29,22 @@ "allowed_ip_ranges": [], "visible": True, "real_time_rank": True} +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): @@ -167,3 +183,45 @@ 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 + tags = data.pop("tags") + problem = Problem.objects.create(created_by=admin, **data) + + for item in tags: + try: + tag = ProblemTag.objects.get(name=item) + except ProblemTag.DoesNotExist: + tag = ProblemTag.objects.create(name=item) + problem.tags.add(tag) + + self.problem = problem + # 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/backend/oj/settings.py b/backend/oj/settings.py index 5ccec1c56..11154a0a2 100644 --- a/backend/oj/settings.py +++ b/backend/oj/settings.py @@ -45,6 +45,7 @@ 'contest', 'utils', 'submission', + 'temperature', 'options', 'judge', 'assignment', diff --git a/backend/oj/urls.py b/backend/oj/urls.py index 1b5b9f61a..e37b147c0 100644 --- a/backend/oj/urls.py +++ b/backend/oj/urls.py @@ -25,4 +25,5 @@ path("api/lecture/professor/", include("course.urls.professor")), path("api/lecture/", include("assignment.urls.student")), path("api/lecture/professor/", include("assignment.urls.professor")), + path("api/", include("temperature.urls")), ] diff --git a/backend/submission/migrations/0017_auto_20220302_1508.py b/backend/submission/migrations/0017_auto_20220302_1508.py new file mode 100644 index 000000000..a4a912204 --- /dev/null +++ b/backend/submission/migrations/0017_auto_20220302_1508.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.12 on 2022-03-02 06:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('submission', '0016_auto_20211226_1458'), + ] + + operations = [ + migrations.AddField( + model_name='submission', + name='code_length', + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name='submission', + name='title', + field=models.TextField(null=True), + ), + ] diff --git a/backend/submission/models.py b/backend/submission/models.py index 809b0d7f3..0f27489d9 100644 --- a/backend/submission/models.py +++ b/backend/submission/models.py @@ -29,10 +29,12 @@ class Submission(models.Model): contest = models.ForeignKey(Contest, null=True, on_delete=models.CASCADE) problem = models.ForeignKey(Problem, on_delete=models.CASCADE) assignment = models.ForeignKey(Assignment, null=True, on_delete=models.CASCADE, related_name="submissions") + title = models.TextField(null=True) create_time = models.DateTimeField(auto_now_add=True) user_id = models.IntegerField(db_index=True) username = models.TextField() code = models.TextField() + code_length = models.IntegerField(default=0) result = models.IntegerField(db_index=True, default=JudgeStatus.PENDING) # Judgment details returned from JudgeServer info = JSONField(default=dict) diff --git a/backend/submission/urls.py b/backend/submission/urls.py index 11134cd03..dd88cac4a 100644 --- a/backend/submission/urls.py +++ b/backend/submission/urls.py @@ -2,6 +2,7 @@ from .views import (SubmissionAPI, SubmissionListAPI, ContestSubmissionListAPI, AssignmentSubmissionListAPI, AssignmentSubmissionListProfessorAPI, SubmissionExistsAPI, EditSubmissionScoreAPI) +from .views import ProfileSubmissionListAPI urlpatterns = [ path("submission/", SubmissionAPI.as_view(), name="submission_api"), @@ -11,4 +12,5 @@ path("assignment_submissions/", AssignmentSubmissionListAPI.as_view(), name="assignment_submission_list_api"), path("assignment_submissions_professor/", AssignmentSubmissionListProfessorAPI.as_view(), name="assignment_submission_list_professor_api"), path("edit_submission_score/", EditSubmissionScoreAPI.as_view(), name="edit_submission_score_api"), + path("profile_submissions/", ProfileSubmissionListAPI.as_view(), name="profile_submission_list_api"), ] diff --git a/backend/submission/views.py b/backend/submission/views.py index 3d2b5186f..827f25e4f 100644 --- a/backend/submission/views.py +++ b/backend/submission/views.py @@ -10,6 +10,7 @@ # from judge.dispatcher import JudgeDispatcher from problem.models import Problem, ProblemRuleType from account.models import User, AdminType +from django.db.models import Q from utils.api import APIView, validate_serializer from utils.constants import AssignmentStatus from utils.cache import cache @@ -86,7 +87,9 @@ def post(self, request): username=request.user.username, language=data["language"], code=data["code"], + code_length=len(data["code"].encode("utf-8")), problem_id=problem.id, + title=problem.title, ip=request.session["ip"], contest_id=data.get("contest_id"), assignment_id=data.get("assignment_id")) @@ -473,3 +476,22 @@ def get(self, request): return self.success(request.user.is_authenticated and Submission.objects.filter(problem_id=request.GET["problem_id"], user_id=request.user.id).exists()) + + +class ProfileSubmissionListAPI(APIView): + def get(self, request): + submissions = Submission.objects.filter(user_id=request.user.id) + # keyword search, 문제 번호로 검색 안됨 + keyword = request.GET.get("keyword", "").strip() + if keyword: + submissions = submissions.filter(Q(title__icontains=keyword)) + # result search + result = request.GET.get("result") + if result: + if result == "AC": + submissions = submissions.filter(result=0) + elif result == "NAC": + submissions = submissions.exclude(result=0) + data = self.paginate_data(request, submissions) + data["results"] = SubmissionListSerializer(data["results"], many=True, user=request.user).data + return self.success(data) diff --git a/backend/temperature/__init__.py b/backend/temperature/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/temperature/admin.py b/backend/temperature/admin.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/temperature/apps.py b/backend/temperature/apps.py new file mode 100644 index 000000000..120c14451 --- /dev/null +++ b/backend/temperature/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class TemperatureConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'temperature' diff --git a/backend/temperature/migrations/0001_initial.py b/backend/temperature/migrations/0001_initial.py new file mode 100644 index 000000000..6c85433ed --- /dev/null +++ b/backend/temperature/migrations/0001_initial.py @@ -0,0 +1,47 @@ +# Generated by Django 3.2.12 on 2022-03-23 09:15 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('problem', '0018_auto_20211226_1458'), + ] + + operations = [ + migrations.CreateModel( + name='Temperature', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('temperature', models.IntegerField(default=0)), + ('date', models.DateField(auto_now_add=True)), + ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'temperature', + 'ordering': ('-date',), + 'unique_together': {('user', 'date')}, + }, + ), + migrations.CreateModel( + name='SolvedProblem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('score', models.IntegerField()), + ('solved_time', models.DateField(auto_now_add=True)), + ('problem', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='problem.problem')), + ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'solvedproblem', + 'ordering': ('-solved_time',), + 'unique_together': {('user', 'problem')}, + }, + ), + ] diff --git a/backend/temperature/migrations/__init__.py b/backend/temperature/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/temperature/models.py b/backend/temperature/models.py new file mode 100644 index 000000000..713f1cab4 --- /dev/null +++ b/backend/temperature/models.py @@ -0,0 +1,51 @@ +from django.db import models +from problem.models import Problem +from account.models import User + + +class JudgeStatus: + COMPILE_ERROR = -2 + WRONG_ANSWER = -1 + ACCEPTED = 0 + CPU_TIME_LIMIT_EXCEEDED = 1 + REAL_TIME_LIMIT_EXCEEDED = 2 + MEMORY_LIMIT_EXCEEDED = 3 + RUNTIME_ERROR = 4 + SYSTEM_ERROR = 5 + PENDING = 6 + JUDGING = 7 + PARTIALLY_ACCEPTED = 8 + + +class ProblemScore: + scores = { + "Level1": 8, + "Level2": 16, + "Level3": 32, + "Level4": 64, + "Level5": 128, + "Level6": 256, + "Level7": 512, + } + + +class Temperature(models.Model): + user = models.ForeignKey(User, null=True, on_delete=models.CASCADE) + temperature = models.IntegerField(default=0) + date = models.DateField(auto_now_add=True) + + class Meta: + db_table = "temperature" + unique_together = (("user", "date"),) + ordering = ["-date", "-temperature"] + + +class SolvedProblem(models.Model): + user = models.ForeignKey(User, null=True, on_delete=models.CASCADE) + problem = models.ForeignKey(Problem, null=True, on_delete=models.CASCADE) + solved_time = models.DateField(auto_now_add=True) + + class Meta: + db_table = "solvedproblem" + unique_together = (("user", "problem"),) + ordering = ("-solved_time",) diff --git a/backend/temperature/serializers.py b/backend/temperature/serializers.py new file mode 100644 index 000000000..307502be2 --- /dev/null +++ b/backend/temperature/serializers.py @@ -0,0 +1,5 @@ +from utils.api import serializers + + +class CreateTemperatureSerializer(serializers.Serializer): + id = serializers.CharField() diff --git a/backend/temperature/tests.py b/backend/temperature/tests.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/temperature/urls.py b/backend/temperature/urls.py new file mode 100644 index 000000000..37c2378b3 --- /dev/null +++ b/backend/temperature/urls.py @@ -0,0 +1,11 @@ +from django.urls import path + +from .views import TemperatureAPI, RankAPI, TemperatureListAPI, SolvedProblemListAPI, RankListAPI + +urlpatterns = [ + path("temperature/", TemperatureAPI.as_view(), name="temperature_api"), + path("rank/", RankAPI.as_view(), name="rank_api"), + path("profile_temperatures/", TemperatureListAPI.as_view(), name="profile_temperature_list_api"), + path("profile_solvedproblems/", SolvedProblemListAPI.as_view(), name="profile_solvedproblems_list_api"), + path("profile_ranks/", RankListAPI.as_view(), name="profile_rank_list_api") +] diff --git a/backend/temperature/views.py b/backend/temperature/views.py new file mode 100644 index 000000000..5d034fa58 --- /dev/null +++ b/backend/temperature/views.py @@ -0,0 +1,70 @@ +from utils.api import APIView, validate_serializer +from .models import ProblemScore, Temperature, SolvedProblem +from problem.models import Problem +from .serializers import CreateTemperatureSerializer +import datetime + +def createTemperature(user, date): + if Temperature.objects.filter(user=user, date=date).exists(): return + solved_problem = SolvedProblem.objects.filter(user=user) + solved_problem = list(solved_problem) + temp = 0 + for t in solved_problem: + temp += ProblemScore.scores[t.problem.difficulty] / (2 ** ((date - t.solved_time) // 7)) + Temperature.objects.create(user=user, temperature=temp) + +class TemperatureAPI(APIView): + def post(self, request): + id = request.GET.get("id") + if not id: + return self.error("Parameter error, id is required") + difficulty = Problem.objects.get(id=id).difficulty + date = datetime.date.today() + createTemperature(request.user, date) + user_temp = Temperature.objects.get(user=request.user, date=date) + user_temp_qs = Temperature.objects.filter(user=request.user, date=date) + solved_problem = SolvedProblem.objects.filter(user=request.user, problem=Problem.objects.get(id=id)) + if not solved_problem: + SolvedProblem.objects.create(user=request.user, + problem=Problem.objects.get(id=id)) + user_temp_qs.update(temperature=user_temp.temperature+ProblemScore.scores[difficulty]) + return self.success() + + def get(self, request): + date = datetime.date.today() + createTemperature(request.user, date) + user_temp = Temperature.objects.get(user=request.user, date=date) + if not user_temp: + return self.error("DB error, Data doesn't exist") + else: + data = { "temperature": user_temp.temperature } + return self.success(data) + +class RankAPI(APIView): + def get(self, request): + date = datetime.date.today() + for idx, temp in enumerate(list(Temperature.objects.filter(date=date).order_by("-temperature"))): + if temp.user_id == request.user.id: + rank = idx + 1 + break + data = { + "rank": rank + } + return self.success(data) +class TemperatureListAPI(APIView): + def get(self, request): + user_temps = Temperature.objects.get(user=request.user) + data = user_temps + return self.success(data) + +class SolvedProblemListAPI(APIView): + def get(self, request): + solved_problems = SolvedProblem.objects.filter(user_id=request.user.id) + data = solved_problems + return self.success(data) +class RankListAPI(APIView): + def get(self, request): + data = [] + solved_problem = SolvedProblem.objects.filter(user=request.user) + solved_problem = list(solved_problem) + return self.success(data) diff --git a/frontend/package.json b/frontend/package.json index d7c0261d0..ca0457a92 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,6 +22,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 b0656be2f..8715162c4 100644 --- a/frontend/src/pages/oj/api.js +++ b/frontend/src/pages/oj/api.js @@ -208,6 +208,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 @@ -227,6 +234,13 @@ export default { params }) }, + getProfileSubmissionList (offset, limit, params) { + params.limit = limit + params.offset = offset + return ajax('profile_submissions/', 'get', { + params + }) + }, getSubmission (id) { return ajax('submission/', 'get', { params: { @@ -314,6 +328,16 @@ export default { return ajax('assignment_submissions/', 'get', { params }) + }, + temperature (problemID) { + return ajax('temperature/', 'post', { + params: { + id: problemID + } + }) + }, + getTemperature () { + return ajax('temperature/', 'get') } } diff --git a/frontend/src/pages/oj/components/Header.vue b/frontend/src/pages/oj/components/Header.vue index 61ad0ff39..8a0dd5d5a 100644 --- a/frontend/src/pages/oj/components/Header.vue +++ b/frontend/src/pages/oj/components/Header.vue @@ -28,6 +28,7 @@ @@ -77,6 +78,11 @@ export default { } else { window.open('/admin/') } + }, + async goProfile () { + await this.$router.push({ + name: 'profile' + }) } }, computed: { 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 new file mode 100644 index 000000000..aa04ac2b6 --- /dev/null +++ b/frontend/src/pages/oj/components/user/ProfileContest.vue @@ -0,0 +1,181 @@ + + + + + + diff --git a/frontend/src/pages/oj/components/user/ProfileGroup.vue b/frontend/src/pages/oj/components/user/ProfileGroup.vue new file mode 100644 index 000000000..f54f94b97 --- /dev/null +++ b/frontend/src/pages/oj/components/user/ProfileGroup.vue @@ -0,0 +1,270 @@ + + + + + diff --git a/frontend/src/pages/oj/components/user/ProfileHistory.vue b/frontend/src/pages/oj/components/user/ProfileHistory.vue new file mode 100644 index 000000000..8a253d51a --- /dev/null +++ b/frontend/src/pages/oj/components/user/ProfileHistory.vue @@ -0,0 +1,128 @@ + + + + + + diff --git a/frontend/src/pages/oj/components/user/ProfileSubmission.vue b/frontend/src/pages/oj/components/user/ProfileSubmission.vue new file mode 100644 index 000000000..1038a6eaf --- /dev/null +++ b/frontend/src/pages/oj/components/user/ProfileSubmission.vue @@ -0,0 +1,494 @@ + + + + + diff --git a/frontend/src/pages/oj/components/user/TabSplitInTwo.vue b/frontend/src/pages/oj/components/user/TabSplitInTwo.vue new file mode 100644 index 000000000..f24a6efc8 --- /dev/null +++ b/frontend/src/pages/oj/components/user/TabSplitInTwo.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/frontend/src/pages/oj/components/user/UserInfo.vue b/frontend/src/pages/oj/components/user/UserInfo.vue new file mode 100644 index 000000000..5d7f19cef --- /dev/null +++ b/frontend/src/pages/oj/components/user/UserInfo.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/frontend/src/pages/oj/router/routes.js b/frontend/src/pages/oj/router/routes.js index 3bccb2b51..c2b99e860 100644 --- a/frontend/src/pages/oj/router/routes.js +++ b/frontend/src/pages/oj/router/routes.js @@ -21,7 +21,8 @@ import { LectureAssignmentList, LectureAssignmentDetail, LectureQna, - LectureQnaDetail + LectureQnaDetail, + Profile } from '../views' export default [ @@ -116,9 +117,9 @@ export default [ meta: { title: 'Contest Ranking' } }, { - name: 'profile-setting', + name: 'setting', path: '/setting', - meta: { requiresAuth: true, title: 'Profile Settings' }, + meta: { requiresAuth: true, title: 'settings' }, component: ProfileSetting }, { @@ -163,6 +164,12 @@ export default [ meta: { title: 'Lecture QnA Detail' }, component: LectureQnaDetail }, + { + name: 'profile', + path: '/profile', + meta: { requiresAuth: true, title: 'My Profile' }, + component: Profile + }, { path: '*', meta: { title: '404' }, diff --git a/frontend/src/pages/oj/views/index.js b/frontend/src/pages/oj/views/index.js index 06f6554cb..5ba68ebbf 100644 --- a/frontend/src/pages/oj/views/index.js +++ b/frontend/src/pages/oj/views/index.js @@ -12,6 +12,7 @@ import LectureAssignmentList from './lecture/LectureAssignmentList.vue' import LectureAssignmentDetail from './lecture/LectureAssignmentDetail.vue' import LectureQna from './lecture/LectureQnA.vue' import LectureQnaDetail from './lecture/LectureQnADetail.vue' +import Profile from './user/Profile.vue' // Grouping Components in the Same Chunk const Problem = () => import(/* webpackChunkName: "Problem" */ '@oj/views/problem/Problem.vue') @@ -32,7 +33,8 @@ export { Logout, ProblemList, Announcement, AnnouncementList, Problem, ApplyResetPassword, ResetPassword, EmailAuth, ProfileSetting, ContestList, ContestDetail, ContestProblemList, ContestRanking, Register, - LectureList, LectureDashboard, LectureAssignmentList, LectureAssignmentDetail, LectureQna, LectureQnaDetail + LectureList, LectureDashboard, LectureAssignmentList, LectureAssignmentDetail, LectureQna, LectureQnaDetail, + Profile } /* 구성 요소 내보내기는 두 가지 범주로 나뉩니다. * 하나는 일반적으로 직접 내보내기에 사용되며 diff --git a/frontend/src/pages/oj/views/problem/Problem.vue b/frontend/src/pages/oj/views/problem/Problem.vue index dec066679..1c981d35c 100644 --- a/frontend/src/pages/oj/views/problem/Problem.vue +++ b/frontend/src/pages/oj/views/problem/Problem.vue @@ -391,7 +391,10 @@ export default { theme: 'material', theme_list: ['solarized', 'monokai', 'material'], - overlayShow: false + overlayShow: false, + + // temperature + checked: false } }, async mounted () { @@ -571,6 +574,10 @@ export default { try { const res = await api.getSubmission(id) this.result = res.data.data + if (!this.checked && this.result.result === 0) { + this.checked = true + await api.temperature(this.problem.id) + } if (Object.keys(res.data.data.statistic_info).length !== 0) { this.submitting = false this.submitted = false diff --git a/frontend/src/pages/oj/views/user/Profile.vue b/frontend/src/pages/oj/views/user/Profile.vue new file mode 100644 index 000000000..ee4d9267f --- /dev/null +++ b/frontend/src/pages/oj/views/user/Profile.vue @@ -0,0 +1,93 @@ + + + + + diff --git a/frontend/src/styles/common.scss b/frontend/src/styles/common.scss index 866f9918a..64c59de1c 100644 --- a/frontend/src/styles/common.scss +++ b/frontend/src/styles/common.scss @@ -21,7 +21,7 @@ body { } .section-title { - font-size: 21px; + font-size: 22px; font-weight: 500; padding-top: 10px; padding-bottom: 20px; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 06bc012ba..a5efc8933 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2945,6 +2945,11 @@ check-more-types@^2.24.0: resolved "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600" integrity sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA= +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"