From 904bba34aed344deb600495adf94db84ee222e93 Mon Sep 17 00:00:00 2001 From: Vishwak Srinivasan Date: Wed, 1 May 2019 00:44:40 +0530 Subject: [PATCH] Add sphinx docs source (#56) Add comments as attributes of classes used instead of actual comments --- .gitignore | 3 + docs/Makefile | 20 ++++++ docs/source/conf.py | 41 +++++++++++ docs/source/forms.rst | 28 ++++++++ docs/source/handler.rst | 5 ++ docs/source/index.rst | 11 +++ docs/source/models.rst | 40 +++++++++++ docs/source/views.rst | 5 ++ judge/forms.py | 78 ++++++++++++-------- judge/handler.py | 153 +++++++++++++++++++++++++++------------- judge/models.py | 114 +++++++++++++++--------------- judge/views.py | 104 +++++++++++++++++++++++++-- 12 files changed, 461 insertions(+), 141 deletions(-) create mode 100644 docs/Makefile create mode 100644 docs/source/conf.py create mode 100644 docs/source/forms.rst create mode 100644 docs/source/handler.rst create mode 100644 docs/source/index.rst create mode 100644 docs/source/models.rst create mode 100644 docs/source/views.rst diff --git a/.gitignore b/.gitignore index 81ed996..96e4759 100644 --- a/.gitignore +++ b/.gitignore @@ -143,3 +143,6 @@ content/contests/ content/tmp/* # End of https://www.gitignore.io/api/django + +# Docs +docs/build diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..014548b --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = PDP +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..2035fa6 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# -- Path setup -------------------------------------------------------------- + +import os +import sys +import django +import sphinx_rtd_theme + +sys.path.insert(0, os.path.abspath('../..')) + +os.environ["DJANGO_SETTINGS_MODULE"] = "pdpjudge.settings" +django.setup() + +# -- Project information ----------------------------------------------------- + +project = 'PDP' +copyright = '2019, Vaibhav Sinha, Prateek Kumar, Vishwak Srinivasan' +author = 'Vaibhav Sinha, Prateek Kumar, Vishwak Srinivasan' + +# The short X.Y version +version = '' +# The full version, including alpha/beta/rc tags +release = '' + +# -- General configuration --------------------------------------------------- + +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.githubpages', +] + +autodoc_member_order = 'bysource' +source_suffix = '.rst' +master_doc = 'index' +language = None + +exclude_patterns = [] +pygments_style = 'sphinx' + +html_theme = 'sphinx_rtd_theme' +html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] diff --git a/docs/source/forms.rst b/docs/source/forms.rst new file mode 100644 index 0000000..5ff5882 --- /dev/null +++ b/docs/source/forms.rst @@ -0,0 +1,28 @@ +Forms and input pre-processing +============================== + +.. automodule:: judge.forms + + .. autoclass:: NewContestForm + :members: + + .. autoclass:: NewProblemForm + :members: + + .. autoclass:: NewSubmissionForm + :members: + + .. autoclass:: NewCommentForm + :members: + + .. autoclass:: AddPersonToContestForm + :members: + + .. autoclass:: AddTestCaseForm + :members: + + .. autoclass:: EditProblemForm + :members: + + .. autoclass:: DeletePersonFromContest + :members: diff --git a/docs/source/handler.rst b/docs/source/handler.rst new file mode 100644 index 0000000..89b508a --- /dev/null +++ b/docs/source/handler.rst @@ -0,0 +1,5 @@ +Handlers and database management +================================ + +.. automodule:: judge.handler + :members: diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..cbe2f3f --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,11 @@ +Welcome to PDP's documentation! +=============================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + models + forms + views + handler diff --git a/docs/source/models.rst b/docs/source/models.rst new file mode 100644 index 0000000..0b7cf08 --- /dev/null +++ b/docs/source/models.rst @@ -0,0 +1,40 @@ +Models and Database Schema +========================== + +.. automodule:: judge.models + + .. autoclass:: Contest + :members: + :exclude-members: DoesNotExist, MultipleObjectsReturned + + .. autoclass:: Problem + :members: + :exclude-members: DoesNotExist, MultipleObjectsReturned + + .. autoclass:: Person + :members: + :exclude-members: DoesNotExist, MultipleObjectsReturned + + .. autoclass:: ContestPerson + :members: + :exclude-members: DoesNotExist, MultipleObjectsReturned + + .. autoclass:: Submission + :members: + :exclude-members: DoesNotExist, MultipleObjectsReturned + + .. autoclass:: TestCase + :members: + :exclude-members: DoesNotExist, MultipleObjectsReturned + + .. autoclass:: SubmissionTestCase + :members: + :exclude-members: DoesNotExist, MultipleObjectsReturned + + .. autoclass:: Comment + :members: + :exclude-members: DoesNotExist, MultipleObjectsReturned + + .. autoclass:: PersonProblemFinalScore + :members: + :exclude-members: DoesNotExist, MultipleObjectsReturned diff --git a/docs/source/views.rst b/docs/source/views.rst new file mode 100644 index 0000000..1ddb9f9 --- /dev/null +++ b/docs/source/views.rst @@ -0,0 +1,5 @@ +Views and page rendering +======================== + +.. automodule:: judge.views + :members: diff --git a/judge/forms.py b/judge/forms.py index f489d06..9b7b205 100644 --- a/judge/forms.py +++ b/judge/forms.py @@ -3,6 +3,9 @@ class MultiEmailField(forms.Field): + """ + Subclass of forms.Field to support a list of email addresses. + """ description = 'Email addresses' def __init__(self, *args, **kwargs): @@ -30,36 +33,37 @@ class NewContestForm(forms.Form): """ Form for creating a new Contest. """ - # Contest Name + contest_name = forms.CharField(label='Contest name', max_length=50, strip=True, widget=forms.TextInput(attrs={'class': 'form-control'}), help_text='Enter the name of the contest.') + """Contest Name""" - # Contest Start Timestamp contest_start = forms.DateTimeField(label='Start Date', widget=forms.DateTimeInput(attrs={'class': 'form-control'}), help_text='Specify when the contest begins.') + """Contest Start Timestamp""" - # Contest Soft End Timestamp contest_soft_end = forms.DateTimeField(label='Soft End Date for contest', widget=forms.DateTimeInput( attrs={'class': 'form-control'}), help_text='Specify after when would you like to \ penalize submissions.') + """Contest Soft End Timestamp""" - # Contest Hard End Timestamp contest_hard_end = forms.DateTimeField(label='Hard End Date for contest', widget=forms.DateTimeInput( attrs={'class': 'form-control'}), help_text='Specify when the contest completely ends.') + """Contest Hard End Timestamp""" - # Contest Penalty factor penalty = forms.DecimalField(label='Penalty', min_value=0.0, max_value=1.0, widget=forms.NumberInput(attrs={'class': 'form-control'}), help_text='Enter a penalty factor between 0 and 1.') + """Contest Penalty factor""" - # Contest is_public property is_public = forms.BooleanField(label='Is this contest public?', required=False) + """Contest is_public property""" def clean(self): cleaned_data = super().clean() @@ -76,72 +80,74 @@ class AddPersonToContestForm(forms.Form): """ Form to add a Person to a Contest. """ - # Email ID of the person + emails = MultiEmailField( label='Emails', widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), help_text='Enter emails seperated using commas') + """Email ID of the person""" class DeletePersonFromContest(forms.Form): """ Form to remove a Person from a Contest. """ - # Email ID of the person + email = forms.EmailField(label='Email', widget=forms.HiddenInput()) + """Email ID of the person""" class NewProblemForm(forms.Form): """ Form for adding a new Problem. """ - # Problem Code Field + code = forms.CharField(label='Code', max_length=10, widget=forms.TextInput( attrs={'class': 'form-control'}), validators=[RegexValidator(r'^[a-z0-9]+$')], help_text='Enter a alphanumeric code in lowercase letters as a \ unique identifier for the problem.') + """Problem Code Field""" - # Problem Name Field name = forms.CharField(label='Title', max_length=50, strip=True, widget=forms.TextInput(attrs={'class': 'form-control'}), help_text='Give a catchy problem name.') + """Problem Name Field""" - # Problem Statement Field statement = forms.CharField(label='Statement', strip=True, widget=forms.HiddenInput(), help_text='Provide a descriptive statement of the problem.') + """Problem Statement Field""" - # Problem Input Format Field input_format = forms.CharField(label='Input Format', strip=True, widget=forms.HiddenInput(), help_text='Give a lucid format for the input that a \ participant should expect.') + """Problem Input Format Field""" - # Problem Output Format Field output_format = forms.CharField(label='Output Format', strip=True, widget=forms.HiddenInput(), help_text='Give a lucid format for the output that a \ participant should follow.') + """Problem Output Format Field""" - # Problem Difficulty Field difficulty = forms.IntegerField(label='Difficulty', widget=forms.NumberInput(attrs={'class': 'form-control'}), min_value=0, max_value=5, initial=0, help_text='Specify a difficulty level between 1 and 5 for \ the problem. If this is unknown, leave it as 0.') + """Problem Difficulty Field""" - # Problem Time limit time_limit = forms.DurationField(label='Time Limit (in seconds)', widget=forms.NumberInput(attrs={'class': 'form-control'}), initial=0, help_text='Specify a time limit in seconds \ for the execution of the program.') + """Problem Time limit""" - # Problem Memory limit memory_limit = forms.IntegerField(label='Memory Limit (in MB)', widget=forms.NumberInput(attrs={'class': 'form-control'}), initial=0, min_value=0, help_text='Specify a memory limit in MB for the execution \ of the program.') + """Problem Memory limit""" - # Problem File Extensions file_exts = forms.CharField(label='Permitted File extensions for submissions', widget=forms.TextInput(attrs={'class': 'form-control'}), max_length=100, required=False, @@ -149,64 +155,66 @@ class NewProblemForm(forms.Form): empty_value='.py,.cpp', help_text='Give a comma separated list of extensions accepted \ for submissions.') + """Problem File Extensions""" - # Problem Starting code starting_code = forms.FileField(label='Starting code', widget=forms.FileInput(attrs={'class': 'form-control-file'}), allow_empty_file=False, required=False, help_text='Upload some starting code to help participants \ get started.') + """Problem Starting code""" - # Problem Max Score max_score = forms.IntegerField(label='Maximum score', widget=forms.NumberInput(attrs={'class': 'form-control'}), initial=10, min_value=0, help_text='Specify the maximum score that passing testcase \ for this problem would award.') + """Problem Max Score""" - # Problem Compilation Script compilation_script = forms.FileField(label='Compilation script', widget=forms.FileInput( attrs={'class': 'form-control-file'}), allow_empty_file=False, required=False, help_text='Upload a custom compilation script.') + """Problem Compilation Script""" - # Problem Test Script test_script = forms.FileField(label='Testing script', widget=forms.FileInput(attrs={'class': 'form-control-file'}), allow_empty_file=False, required=False, help_text='Upload a custom testing script.') + """Problem Test Script""" class EditProblemForm(forms.Form): """ Form for editing an existing problem. """ - # Problem Name Field + name = forms.CharField(label='Title', max_length=50, strip=True, widget=forms.TextInput(attrs={'class': 'form-control'}), help_text='Give a catchy problem name.') + """Problem Name Field""" - # Problem Statement Field statement = forms.CharField(label='Statement', strip=True, widget=forms.HiddenInput(), help_text='Provide a descriptive statement of the problem.') + """Problem Statement Field""" - # Problem Input Format Field input_format = forms.CharField(label='Input Format', strip=True, widget=forms.HiddenInput(), help_text='Give a lucid format for the input that a \ participant should expect.') + """Problem Input Format Field""" - # Problem Output Format Field output_format = forms.CharField(label='Output Format', strip=True, widget=forms.HiddenInput(), help_text='Give a lucid format for the output that a \ participant should follow.') + """Problem Output Format Field""" - # Problem Difficulty Field difficulty = forms.IntegerField(label='Difficulty', widget=forms.NumberInput(attrs={'class': 'form-control'}), initial=0, min_value=0, max_value=5, help_text='Specify a difficulty level between 1 and 5 for \ the problem. If this is unknown, leave it as 0.') + """Problem Difficulty Field""" class NewSubmissionForm(forms.Form): @@ -214,39 +222,47 @@ class NewSubmissionForm(forms.Form): Form to create a new Submission. """ # TODO For now choices are hard coded - # Choices of file type file_type = forms.ChoiceField(label='File type', choices=[ ('.cpp', 'C++'), ('.c', 'C'), ('.py', 'Python'), ]) + """Choices of file type""" - # Submission File submission_file = forms.FileField(label='Choose file', required=True, allow_empty_file=False, widget=forms.FileInput(attrs={'class': 'custom-file-input'}), help_text='Upload your submission.') + """Submission File""" class AddTestCaseForm(forms.Form): """ Form to create a new TestCase """ - # TestCase Type + test_type = forms.ChoiceField(label='Test type', choices=[ ('public', 'Public'), ('private', 'Private') ]) + """TestCase Type""" - # TestCase Input input_file = forms.FileField(label='Input file', allow_empty_file=False, required=True, help_text='Upload input for test case.') + """TestCase Input""" - # TestCase Output output_file = forms.FileField(label='Output file', allow_empty_file=False, required=True, help_text='Upload output for test case.') + """TestCase Output""" class NewCommentForm(forms.Form): + """ + Form to add a new comment + """ + participant_email = forms.EmailField(label='Email', widget=forms.HiddenInput()) + """Email of participant""" + comment = forms.CharField(label='Comment', required=True, widget=forms.Textarea( attrs={'class': 'form-control', 'rows': 2})) + """Comment content""" diff --git a/judge/handler.py b/judge/handler.py index bdb6d81..e0ff4b3 100644 --- a/judge/handler.py +++ b/judge/handler.py @@ -20,7 +20,9 @@ def process_contest(name: str, start_datetime: datetime, soft_end_datetime: date """ Process a New Contest Only penalty can be None in which case Penalty will be set to 0 - Returns: (True, None) or (False, Exception string) + + Returns: + (True, None) or (False, Exception string) """ try: c = models.Contest(name=name, start_datetime=start_datetime, @@ -42,7 +44,9 @@ def delete_contest(contest_id: int) -> Tuple[bool, Optional[str]]: Delete the contest. This will cascade delete in all the tables that have contest as FK. It calls delete_problem for each problem in the contest. - Retuns (True, None) + + Retuns: + (True, None) """ try: c = models.Contest.objects.get(pk=contest_id) @@ -67,7 +71,9 @@ def process_problem(code: str, contest: int, name: str, statement: str, input_fo """ Process a new Problem Nullable [None-able] Fields: start_code, compilation_script, test_script, file_format - Returns: (True, None) or (False, Exception string) + + Returns: + (True, None) or (False, Exception string) """ # Check if the Problem Code has already been taken @@ -132,7 +138,9 @@ def update_problem(code: str, name: str, statement: str, input_format: str, """ Update the fields in problem Pass the code as pk of problem - Returns (True, None) + + Returns: + (True, None) """ try: p = models.Problem.objects.get(pk=code) @@ -156,7 +164,9 @@ def delete_problem(problem_id: str) -> Tuple[bool, Optional[str]]: This will cascade delete in all the tables that have problem as FK. It will also delete all the submissions, testcases and the directory (in problems directory) corresponding to the problem . - Returns (True, None) + + Returns: + (True, None) """ try: problem = models.Problem.objects.get(pk=problem_id) @@ -211,9 +221,11 @@ def process_testcase(problem_id: str, ispublic: bool, """ Process a new Testcase problem is the 'code' (pk) of the problem. - WARNING: This function does not rescore all the submissions and so score will not - change in response to the new testcase. DO NOT CALL THIS FUNCTION ONCE THE - CONTEST HAS STARTED, IT WILL LEAD TO ERRONEOUS SCORES. + + .. warning:: + This function does not rescore all the submissions and so score will not + change in response to the new testcase. DO NOT CALL THIS FUNCTION ONCE THE + CONTEST HAS STARTED, IT WILL LEAD TO ERRONEOUS SCORES. """ try: problem = models.Problem.objects.get(pk=problem_id) @@ -230,10 +242,14 @@ def delete_testcase(testcase_id: str) -> Tuple[bool, Optional[str]]: """ This function deletes the testcase and cascade deletes in all the tables the Fk appears. - WARNING: This function does not rescore all the submissions and so score will not - change in response to the deleted testcase. DO NOT CALL THIS FUNCTION ONCE THE - CONTEST HAS STARTED, IT WILL LEAD TO ERRONEOUS SCORES. - Returns: (True, None) + + .. warning:: + This function does not rescore all the submissions and so score will not + change in response to the deleted testcase. DO NOT CALL THIS FUNCTION ONCE THE + CONTEST HAS STARTED, IT WILL LEAD TO ERRONEOUS SCORES. + + Returns: + (True, None) """ try: inputfile_path = os.path.join( @@ -344,7 +360,9 @@ def add_person_rgx_to_contest(rgx: str, contest: str, In case no persons match the rgx, (False, 'Regex {} did not match any person registered'.format(rgx)) is returned Use regex like cs15btech* to add all persons having emails like cs15btech... - Returns: (True, None) + + Returns: + (True, None) """ pattern = compile(rgx) try: @@ -371,10 +389,15 @@ def add_persons_to_contest(persons: List[str], contest: str, persons is the list of email of persons contest is the pk of the contest permission is False if participant and True is poster - First checks if any of the person exists with an opposing role. If so DO NOT ADD ANYONE - Tnstead return (False, '{} already exists with other permission'.format(p.email)) - Otherwise if not person hhas conflicting permission add all the persons and return (True, None) - This fuction would create records for all the persons who do not already have one irrespective + + .. note:: + First check if any of the person exists with an opposing role. + If so, do not add anyone. Instead return a tuple with False and + and an appropriate message. + Otherwise if person doesn't have conflicting permission, + add all the persons and return (True, None). + + This function would create records for all the persons who do not already have one irrespective of whether anyone has conflict or not. """ try: @@ -409,7 +432,9 @@ def get_personcontest_permission(person: Optional[str], contest: int) -> Optiona Determine the relation between Person and Contest person is the email of the person contest is the pk of the contest - returns False if participant and True is poster None if neither + + Returns: + False if participant and True is poster None if neither """ curr = timezone.now() if person is None: @@ -441,7 +466,9 @@ def delete_personcontest(person: str, contest: str) -> Tuple[bool, Optional[str] """ Delete the record of person and contest in ContestPerson table Passed person is email and contest is the pk - Returns (True, None) + + Returns: + (True, None) """ try: p = models.Person.objects.get(email=person) @@ -467,7 +494,9 @@ def get_personproblem_permission(person: Optional[str], problem: str) -> Optiona Determine the relation between Person and Problem person is the email of the person problem is the code(pk) of the problem - returns False if participant and True is poster None if neither + + Returns: + False if participant and True is poster None if neither """ p = models.Problem.objects.get(pk=problem) if p.contest is None: @@ -479,7 +508,9 @@ def get_posters(contest) -> Tuple[bool, Optional[str]]: """ Return the posters for the contest. contest is the pk of the Contest - Return (True, List of the email of the posters) + + Returns: + (True, List of the email of the posters) """ try: c = models.Contest.objects.get(pk=contest) @@ -495,8 +526,12 @@ def get_participants(contest) -> Tuple[bool, Any]: """ Return the participants for the contest. contest is the pk of the Contest - Returns (True, List of the email of the participants) - Returns (True, []) if contest is public + + Returns: + (True, List of the email of the participants) + + Returns: + (True, []) if contest is public """ try: c = models.Contest.objects.get(pk=contest) @@ -533,19 +568,24 @@ def get_submission_status(person: str, problem: str, submission): """ Get the current status of the submission. Pass email as person and problem code as problem to get a tuple - In case the submission is None, returns: - (True, ({SubmissionID: [(TestcaseID, Verdict, Time_taken, Memory_taken, ispublic, message)]}, - {SubmissionID: (judge_score, ta_score, linter_score, final_score, timestamp, file_type)})) + In case the submission is None, returns (True, (dict1, dict2)) The tuple consists of 2 dictionaries: - First dictionary: Key: Submission ID - Value: list of (TestcaseID, Verdict, Time_taken, - Memory_taken, ispublic, message) - Second dictionary: Key: Submission ID - Value: tuple: (judge_score, ta_score, linter_score, - final_score, timestamp, file_type) + + First dictionary: + Key: Submission ID + + Value: list of (TestcaseID, Verdict, Time_taken, Memory_taken, ispublic, message) + + Second dictionary: + Key: Submission ID + + Value: tuple: (judge_score, ta_score, linter_score, final_score, timestamp, file_type) + In case submission ID is provided: The passed parameters person and problem are ignored and so None is accepted. - Returns: The same dictionaries in a tuple but having only 1 key in both + + Returns: + The same dictionaries in a tuple but having only 1 key in both """ try: if submission is None: @@ -589,11 +629,15 @@ def get_submissions(problem_id: str, person_id: Optional[str]) -> Tuple[bool, An Get all the submissions for this problem by this (or all) persons who attempted. problem is the pk of the Problem. person is the email of the Person or None if you want to retrieve solutions by all participants - Returns (True, {emailofperson: [SubmissionObject1, SubmissionObject2, ...], - emailofperson: [SubmissionObjecti, SubmissionObjectj, ...], - ... ) when person is None - When person is not None returns (True, {emailofperson: - [SubmissionObject1, SubmissionObject2, ...]}) + + Returns: + when person_id is None: + (True, {emailofperson: [SubmissionObject1, SubmissionObject2, ...], \ + emailofperson: [SubmissionObjecti, SubmissionObjectj, ...], \ + ...}) + + when person_id is not None: + (True, {emailofperson: [SubmissionObject1, SubmissionObject2, ...]}) """ try: p = models.Problem.objects.get(code=problem_id) @@ -627,14 +671,17 @@ def get_submissions(problem_id: str, person_id: Optional[str]) -> Tuple[bool, An def get_submission_status_mini(submission: str) -> Tuple[bool, Any]: """ Get the current status of the submission. - Returns: (True, ({TestcaseID: (Verdict, Time_taken, Memory_taken, ispublic, message), ...}, - (judge_score, ta_score, linter_score, final_score, timestamp, file_type))) + + Returns: + (True, (dict1, tuple1)) + The tuple consists of a dictionary and a tuple: - Dictionary: Key: TestcaseID - Value: (Verdict, Time_taken, - Memory_taken, ispublic, message) - Tuple: (judge_score, ta_score, linter_score, - final_score, timestamp, file_type) + Dictionary: + Key: TestcaseID + + Value: (Verdict, Time_taken, Memory_taken, ispublic, message) + Tuple: + (judge_score, ta_score, linter_score, final_score, timestamp, file_type) """ try: s = models.Submission.objects.get(pk=submission) @@ -659,7 +706,9 @@ def get_leaderboard(contest: int) -> Tuple[bool, Any]: """ Returns the current leaderboard for the passed contest Pass contest's pk - Returns (True, [[Rank1Email, ScoreofRank1], [Rank2Email, ScoreofRank2] ... ]) + + Returns: + (True, [[Rank1Email, ScoreofRank1], [Rank2Email, ScoreofRank2] ... ]) """ leaderboard_path = os.path.join('content', 'contests', str(contest)+'.lb') if not os.path.exists(leaderboard_path): @@ -679,7 +728,9 @@ def process_comment(problem: str, person: str, commenter: str, Privately comment 'comment' on the problem for person by commenter. problem is the pk of the Problem. person and commenter are emails of Person. - Returns (True, None) + + Returns: + (True, None) """ try: problem = models.Problem.objects.get(pk=problem) @@ -696,7 +747,9 @@ def process_comment(problem: str, person: str, commenter: str, def get_comments(problem: str, person: str) -> Tuple[bool, Any]: """ Get the private comments on the problem for the person. - Returns (True, [(Commeter, Timestamp, Comment) ... (Sorted in ascending order of time)]) + + Returns: + (True, [(Commeter, Timestamp, Comment) ... (Sorted in chronological order)]) """ try: comments = models.Comment.objects.filter( @@ -713,7 +766,9 @@ def get_csv(contest: str) -> Tuple[bool, Any]: """ Get the csv (in string form) of the current scores of all participants in the contest. Pass pk of the contest - Returns (True, csvstring) + + Returns: + (True, csvstring) """ try: c = models.Contest.objects.get(pk=contest) diff --git a/judge/models.py b/judge/models.py index f485f64..f7585c8 100644 --- a/judge/models.py +++ b/judge/models.py @@ -45,25 +45,24 @@ class Contest(models.Model): Model for Contest. """ - # Contest name [Char] name = models.CharField( max_length=50, default='Unnamed Contest', unique=True) + """Contest name""" - # Start Date and Time for Contest start_datetime = models.DateTimeField() + """Start Date and Time for Contest""" - # "Soft" End Date and Time for Contest soft_end_datetime = models.DateTimeField() + """"Soft" End Date and Time for Contest""" - # "Hard" End Date and Time for Contest hard_end_datetime = models.DateTimeField() + """"Hard" End Date and Time for Contest""" - # Penalty for late-submission penalty = models.FloatField(default=0.0) + """Penalty for late-submission""" - # Is the contest public - # In public Contests everyone except posters can participate public = models.BooleanField() + """Is the contest public?""" def __str__(self): return self.name @@ -73,56 +72,56 @@ class Problem(models.Model): """ Model for a Problem. """ - # Problem code [Char, PrimaryKey] # UNSET is a special problem code which other problems must not use. code = models.CharField(max_length=10, primary_key=True, default='UNSET') + """Problem code""" - # Contest for the problem [Contest] contest = models.ForeignKey(Contest, on_delete=models.CASCADE, null=True) + """Foreign key to contest for the problem""" - # Problem name [Char] name = models.CharField(max_length=50, default='Name not set') + """Problem name""" - # Problem statement [Char] statement = models.TextField(default='The problem statement is empty.') + """Problem statement""" - # Input format [Char] input_format = models.TextField(default='No input format specified.') + """Problem input format""" - # Output format [Char] output_format = models.TextField(default='No output format specified.') + """Problem output format""" - # Difficulty [PositiveInt] difficulty = models.PositiveSmallIntegerField(default=0) + """Problem difficulty""" - # Time Limit [Duration] time_limit = models.DurationField(default=timedelta(seconds=10)) + """Problem time limit""" - # Memory Limit [Int] # Currently this is specified in mega-bytes memory_limit = models.PositiveIntegerField(default=200000) + """Problem memory limit""" - # File format [Char] # Support upto 30 file formats file_format = models.CharField(max_length=100, default='.py,.cpp') + """Accepted file formats for submissions to problem""" - # Start code [File] start_code = models.FileField(upload_to=start_code_name, null=True) + """Problem starting code""" - # Max score [PositiveInt] max_score = models.PositiveSmallIntegerField(default=0) + """Maximum score for a test case for the problem""" - # Compilation script [File] compilation_script = models.FileField( upload_to=partial(compilation_test_upload_location, is_compilation=True), default='./default/compilation_script.sh') + """Problem compilation script""" - # Test script [File] test_script = models.FileField( upload_to=partial(compilation_test_upload_location, is_compilation=False), default='./default/test_script.sh') + """Problem test script""" # Setter solution script [File, Nullable] setter_solution = models.FileField(upload_to=setter_sol_name, null=True) @@ -136,11 +135,11 @@ class Person(models.Model): Model for Person. """ - # Email ID of the Person email = models.EmailField(primary_key=True) + """Email ID of the Person""" - # Rank of the Person rank = models.PositiveIntegerField(default=0) + """Rank of the Person""" def __str__(self): return self.email @@ -153,11 +152,11 @@ class Submission(models.Model): # Self Generated PrimaryKey id = models.CharField(max_length=32, primary_key=True, default=uuid4) - # ForeignKey to Problem problem = models.ForeignKey(Problem, on_delete=models.CASCADE) + """Foreign key to problem for which this is a submission""" - # ForeignKey to Person participant = models.ForeignKey(Person, on_delete=models.CASCADE) + """Foreign key to person who submitted the solution""" # This has to be updated periodically PERMISSIBLE_FILE_TYPES = ( @@ -167,27 +166,27 @@ class Submission(models.Model): ('.cpp', 'CPP'), ) - # File Type [Char] file_type = models.CharField( max_length=5, choices=PERMISSIBLE_FILE_TYPES, default='.none') + """File type of submission""" - # Submission file [File] submission_file = models.FileField(upload_to=submission_upload_location) + """Submission file""" - # Timestamp of submission [Time] timestamp = models.DateTimeField() + """Timestamp of submission""" - # Judge score [Int] judge_score = models.PositiveSmallIntegerField(default=0) + """Judge score""" - # TA score [Int] ta_score = models.PositiveSmallIntegerField(default=0) + """TA score""" - # Final score [Int] - final_score = models.FloatField(default=0.0) - - # Linter score [Int] linter_score = models.FloatField(default=0.0) + """Linter score""" + + final_score = models.FloatField(default=0.0) + """Final score""" class ContestPerson(models.Model): @@ -196,15 +195,15 @@ class ContestPerson(models.Model): This maps how (either as a Participant or Poster) persons have access to the contests. """ - # (FK) Contest ID of the Contest. contest = models.ForeignKey(Contest, on_delete=models.CASCADE) + """Foreign key to contest in which this person is taking part""" - # (FK) Person ID of the Person. person = models.ForeignKey(Person, on_delete=models.CASCADE) + """Foreign key to the actual person""" - # Boolean to determine whether the Person is a Particpant or Poster - # true for Poster and false for Participant + # True for Poster and False for Participant role = models.BooleanField() + """Determines if Person is a Poster or a Participant""" class Meta: unique_together = (('contest', 'person'),) @@ -216,25 +215,25 @@ class TestCase(models.Model): Maintains testcases and mapping between TestCase and Problem. """ - # (FK) Problem ID of the Problem. problem = models.ForeignKey(Problem, on_delete=models.CASCADE) + """Foreign key to problem for which this is a test case""" - # Boolean to determine whether the TestCase is Private or Public - # true for Public and false for Private + # True for Public and False for Private public = models.BooleanField() + """Determines if the test case is a public test case or a private test case""" # Self Generated PrimaryKey id = models.CharField(max_length=32, primary_key=True, default=uuid4) - # Store the inputfile for the testcase. # Sample: ./content/testcase/inputfile_UUID.txt inputfile = models.FileField(upload_to=partial(testcase_upload_location, is_input=True), default='./default/inputfile.txt') + """Input file for the test case""" - # Store the outputfile for the testcase # ./content/testcase/outputfile_UUID.txt outputfile = models.FileField(upload_to=partial(testcase_upload_location, is_input=False), default='./default/outputfile.txt') + """Output file for the test case""" class SubmissionTestCase(models.Model): @@ -254,23 +253,23 @@ class SubmissionTestCase(models.Model): ('RE', 'RUNTIME_ERROR'), ('NA', 'NOT_AVAILABLE')) - # (FK) Submission ID of the Submission. submission = models.ForeignKey(Submission, on_delete=models.CASCADE) + """Foreign key to submission""" - # (FK) testCase ID of the TestCase. testcase = models.ForeignKey(TestCase, on_delete=models.CASCADE) + """Foreign key to test case""" - # Verdict by the judge verdict = models.CharField(max_length=2, choices=VERDICT, default='NA') + """Verdict by the judge""" - # Memory taken by the Submission on this TestCase memory_taken = models.PositiveIntegerField() + """Virtual memory consumed by the submission""" - # Time taken by the Submission on this TestCase time_taken = models.DurationField() + """Time taken by the submission""" - # Message placeholder, used for erroneous submissions message = models.TextField(default='') + """Message placeholder, used for erroneous submissions""" class Meta: unique_together = (('submission', 'testcase'),) @@ -281,39 +280,40 @@ class Comment(models.Model): Model for Comment. """ - # (FK) Problem ID of the Problem. problem = models.ForeignKey(Problem, on_delete=models.CASCADE) + """Foreign key to problem relating to the comment""" - # (FK) Person ID of the Person. person = models.ForeignKey( Person, on_delete=models.CASCADE, related_name='person') + """Foreign key to person""" # Self Generated PrimaryKey id = models.CharField(max_length=32, primary_key=True, default=uuid4) - # (FK) Person ID for the commenter commenter = models.ForeignKey( Person, on_delete=models.CASCADE, related_name='commenter') + """Foreign key to person who commented""" - # Timestamp of the comment timestamp = models.DateTimeField(default=timezone.now) + """Timestamp of the comment""" - # Content of the Comment comment = models.TextField() + """Content of the comment""" class PersonProblemFinalScore(models.Model): """ Model to store the final score assigned to a person for a problem. """ - # (FK) Problem ID of the Problem. + problem = models.ForeignKey(Problem, on_delete=models.CASCADE) + """Foreign key to problem for which the score is saved""" - # (FK) Person ID of the Person. person = models.ForeignKey(Person, on_delete=models.CASCADE) + """Foreign key to person whose submission's score is saved""" - # Final Score [Int] score = models.FloatField(default=0.0) + """Final score saved""" class Meta: unique_together = (('problem', 'person'),) diff --git a/judge/views.py b/judge/views.py index e0a641a..eb89b39 100644 --- a/judge/views.py +++ b/judge/views.py @@ -31,14 +31,23 @@ def _return_file_as_response(path_name): def handler404(request, exception=None): + """ + Renders 404 page + """ return render(request, '404.html', status=404) def handler500(request, exception=None): + """ + Renders 500 page + """ return render(request, '500.html', status=500) def index(request): + """ + Renders the index page + """ context = {} user = _get_user(request) if user is not None: @@ -55,6 +64,9 @@ def index(request): def new_contest(request): + """ + Renders view for the page to create a new contest + """ user = _get_user(request) if user is None: return handler404(request) @@ -85,7 +97,11 @@ def new_contest(request): return render(request, 'judge/new_contest.html', context) -def get_posters(request, contest_id, role=True): +def get_people(request, contest_id, role): + """ + Function to render the page for viewing participants and posters + for a contest based on argument role. + """ user = _get_user(request) perm = handler.get_personcontest_permission( None if user is None else user.email, contest_id) @@ -118,12 +134,27 @@ def get_posters(request, contest_id, role=True): return render(request, 'judge/contest_persons.html', context) +def get_posters(request, contest_id): + """ + Renders the page for posters of a contest. + Dispatches to get_people with role=True. + """ + return get_people(request, contest_id, True) + + def get_participants(request, contest_id): + """ + Renders the page for posters of a contest. + Dispatches to get_people with role=False. + """ return get_posters(request, contest_id, False) -def add_poster(request, contest_id, role=True): - # TODO Have comma seperated values +def add_person(request, contest_id, role): + """ + Function to render the page for adding a person - participant or poster to + a contest. + """ user = _get_user(request) perm = handler.get_personcontest_permission( None if user is None else user.email, contest_id) @@ -147,11 +178,26 @@ def add_poster(request, contest_id, role=True): return render(request, 'judge/contest_add_person.html', context) +def add_poster(request, contest_id): + """ + Renders the page for adding a poster. + Dispatches to add_person with role=True. + """ + return add_person(request, contest_id, True) + + def add_participant(request, contest_id): - return add_poster(request, contest_id, False) + """ + Renders the page for adding a participant. + Dispatches to add_person with role=False. + """ + return add_person(request, contest_id, False) def contest_detail(request, contest_id): + """ + Renders the contest preview page after the contest has been created. + """ contest = get_object_or_404(Contest, pk=contest_id) user = _get_user(request) perm = handler.get_personcontest_permission( @@ -170,6 +216,10 @@ def contest_detail(request, contest_id): def contest_scores_csv(request, contest_id): + """ + Function to provide downloading facility for the CSV of scores + of participants in a contest. + """ user = _get_user(request) perm = handler.get_personcontest_permission( None if user is None else user.email, contest_id) @@ -184,6 +234,9 @@ def contest_scores_csv(request, contest_id): def delete_contest(request, contest_id): + """ + Function to provide the option to delete a contest. + """ user = _get_user(request) perm = handler.get_personcontest_permission( None if user is None else user.email, contest_id) @@ -198,6 +251,9 @@ def delete_contest(request, contest_id): def delete_problem(request, problem_id): + """ + Function to provide the option to delete a problem. + """ user = _get_user(request) problem = get_object_or_404(Problem, pk=problem_id) contest_id = problem.contest.pk @@ -216,6 +272,9 @@ def delete_problem(request, problem_id): def delete_testcase(request, problem_id, testcase_id): + """ + Function to provide the option to delete a test-case of a particular problem. + """ user = _get_user(request) perm = handler.get_personproblem_permission( None if user is None else user.email, problem_id) @@ -233,6 +292,10 @@ def delete_testcase(request, problem_id, testcase_id): def problem_detail(request, problem_id): + """ + Renders the problem preview page after the problem has been created. + This preview will be changed based on the role of the user (poster or participant) + """ problem = get_object_or_404(Problem, pk=problem_id) user = _get_user(request) perm = handler.get_personproblem_permission( @@ -300,6 +363,10 @@ def problem_detail(request, problem_id): def problem_starting_code(request, problem_id: str): + """ + Function to provide downloading facility for the starting code + for a problem. + """ problem = get_object_or_404(Problem, pk=problem_id) user = _get_user(request) perm = handler.get_personproblem_permission(None if user is None else user.email, problem_id) @@ -312,6 +379,10 @@ def problem_starting_code(request, problem_id: str): def problem_compilation_script(request, problem_id: str): + """ + Function to provide downloading facility for the compilation script + for a problem after creating the problem. + """ problem = get_object_or_404(Problem, pk=problem_id) user = _get_user(request) perm = handler.get_personproblem_permission(None if user is None else user.email, problem_id) @@ -324,6 +395,10 @@ def problem_compilation_script(request, problem_id: str): def problem_test_script(request, problem_id: str): + """ + Function to provide downloading facility for the testing script + for a problem after creating the problem. + """ problem = get_object_or_404(Problem, pk=problem_id) user = _get_user(request) perm = handler.get_personproblem_permission(None if user is None else user.email, problem_id) @@ -336,10 +411,16 @@ def problem_test_script(request, problem_id: str): def problem_default_script(request, script_name: str): + """ + Function to provide downloading facility for the default compilation or test script. + """ return _return_file_as_response(os.path.join('judge', 'default', script_name + '.sh')) def new_problem(request, contest_id): + """ + Renders view for the page to create a new problem in a contest. + """ contest = get_object_or_404(Contest, pk=contest_id) user = _get_user(request) perm = handler.get_personcontest_permission( @@ -372,6 +453,9 @@ def new_problem(request, contest_id): def edit_problem(request, problem_id): + """ + Renders view for the page to edit selected fields of a pre-existing problem. + """ problem = get_object_or_404(Problem, pk=problem_id) contest = get_object_or_404(Contest, pk=problem.contest_id) user = _get_user(request) @@ -405,6 +489,11 @@ def edit_problem(request, problem_id): def problem_submissions(request, problem_id: str): + """ + Renders the page where all submissions to a given problem can be seen. + For posters, this renders a set of tables for each participant. + For participants, this renders a table with the scores of their submissions only. + """ user = _get_user(request) perm = handler.get_personproblem_permission( None if user is None else user.email, problem_id) @@ -461,6 +550,9 @@ def problem_submissions(request, problem_id: str): def submission_download(request, submission_id: str): + """ + Function to provide downloading facility of a given submission. + """ user = _get_user(request) submission = get_object_or_404(Submission, pk=submission_id) perm = handler.get_personproblem_permission( @@ -474,6 +566,10 @@ def submission_download(request, submission_id: str): def submission_detail(request, submission_id: str): + """ + Renders the page where a detailed breakdown with respect to judge's + evaluation, additional scores, error messages displayed and so on. + """ user = _get_user(request) submission = get_object_or_404(Submission, pk=submission_id) perm = handler.get_personproblem_permission(