Skip to content

feat: Grade marks #320

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Oct 8, 2021
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions lms/lmsdb/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ def _last_status_view_migration() -> bool:
Solution = models.Solution
_migrate_column_in_table_if_needed(Solution, Solution.last_status_view)
_migrate_column_in_table_if_needed(Solution, Solution.last_time_view)
return True


def _uuid_migration() -> bool:
Expand All @@ -252,10 +253,17 @@ def _uuid_migration() -> bool:
return True


def _grade_mark_migration() -> bool:
Solution = models.Solution
_add_not_null_column(Solution, Solution.grade_mark)
return True


def main():
with models.database.connection_context():
if models.database.table_exists(models.Solution.__name__.lower()):
_last_status_view_migration()
_grade_mark_migration()

if models.database.table_exists(models.User.__name__.lower()):
_api_keys_migration()
Expand All @@ -267,6 +275,8 @@ def main():
models.create_basic_roles()
if models.User.select().count() == 0:
models.create_demo_users()
if models.SolutionGradeMark.select().count() == 0:
models.create_basic_grades()

text_fixer.fix_texts()
import_tests.load_tests_from_path('/app_dir/notebooks-tests')
Expand Down
52 changes: 48 additions & 4 deletions lms/lmsdb/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,20 @@ def to_choices(cls: enum.EnumMeta) -> Tuple[Tuple[str, str], ...]:
return tuple((choice.name, choice.value) for choice in choices)


class SolutionGradeMark(BaseModel):
name = CharField()
icon = CharField(null=True)
color = CharField()
order = IntegerField(default=0, index=True, unique=True)

@classmethod
def grades(cls):
return cls.select().order_by(cls.order)

def __str__(self):
return self.name


class Solution(BaseModel):
STATES = SolutionState
STATUS_VIEW = SolutionStatusView
Expand All @@ -389,6 +403,9 @@ class Solution(BaseModel):
index=True,
)
last_time_view = DateTimeField(default=datetime.now, null=True, index=True)
grade_mark = ForeignKeyField(
SolutionGradeMark, backref='solutions', null=True,
)

@property
def solution_files(
Expand Down Expand Up @@ -447,13 +464,18 @@ def view_solution(self) -> None:
def start_checking(self) -> bool:
return self.set_state(Solution.STATES.IN_CHECKING)

def set_state(self, new_state: SolutionState, **kwargs) -> bool:
def set_state(
self, new_state: SolutionState,
grade_mark: Optional[SolutionGradeMark] = None, **kwargs,
) -> bool:
# Optional: filter the old state of the object
# to make sure that no two processes set the state together
requested_solution = (Solution.id == self.id)
updates_dict = {Solution.state.name: new_state.name}
if grade_mark is not None:
updates_dict[Solution.grade_mark.name] = grade_mark
changes = Solution.update(
**{Solution.state.name: new_state.name},
**kwargs,
**updates_dict, **kwargs,
).where(requested_solution)
return changes.execute() == 1

Expand All @@ -475,7 +497,9 @@ def of_user(

solutions = (
cls
.select(cls.exercise, cls.id, cls.state, cls.checker)
.select(
cls.exercise, cls.id, cls.state, cls.checker, cls.grade_mark,
)
.where(cls.exercise.in_(db_exercises), cls.solver == user_id)
.order_by(cls.submission_timestamp.desc())
)
Expand All @@ -487,6 +511,8 @@ def of_user(
exercise['comments_num'] = len(solution.staff_comments)
if solution.is_checked and solution.checker:
exercise['checker'] = solution.checker.fullname
if solution.grade_mark:
exercise['grade_mark'] = solution.grade_mark.name
return tuple(exercises.values())

@property
Expand Down Expand Up @@ -589,10 +615,12 @@ def _base_next_unchecked(cls):

def mark_as_checked(
self,
grade_id: Optional[int] = None,
by: Optional[Union[User, int]] = None,
) -> bool:
return self.set_state(
Solution.STATES.DONE,
SolutionGradeMark.get_or_none(SolutionGradeMark.id == grade_id),
checker=by,
)

Expand Down Expand Up @@ -969,4 +997,20 @@ def create_basic_roles():
Role.create(name=role.value)


def create_basic_grades():
grades_dict = {
'Excellent': {'color': 'green', 'icon': 'star', 'order': 1},
'Nice': {'color': 'blue', 'icon': 'check', 'order': 2},
'Try again': {'color': 'red', 'icon': 'exclamation', 'order': 3},
'Plagiarism': {
'color': 'black', 'icon': 'exclamation-triangle', 'order': 4,
},
}
for grade, values in grades_dict.items():
SolutionGradeMark.create(
name=grade, icon=values.get('icon'), color=values.get('color'),
order=values.get('order'),
)


ALL_MODELS = BaseModel.__subclasses__()
61 changes: 30 additions & 31 deletions lms/lmsweb/translations/he/LC_MESSAGES/messages.po
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# Hebrew translations for PROJECT.
# Hebrew translations for LMS project.
# Copyright (C) 2021 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# This file is distributed under the same license as the project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2021.
#
msgid ""
msgstr ""
"Project-Id-Version: 1.0\n"
"Project-Id-Version: 1.0\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2021-09-29 11:58+0300\n"
"POT-Creation-Date: 2021-10-02 12:46+0300\n"
"PO-Revision-Date: 2021-09-29 11:30+0300\n"
"Last-Translator: Or Ronai\n"
"Language: he\n"
Expand All @@ -18,7 +18,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.9.1\n"

#: lmsdb/models.py:736
#: lmsdb/models.py:764
msgid "Fatal error"
msgstr "כישלון חמור"

Expand Down Expand Up @@ -101,17 +101,17 @@ msgstr "שם המשתמש כבר נמצא בשימוש"
msgid "The email is already in use"
msgstr "האימייל כבר נמצא בשימוש"

#: models/solutions.py:50
#: models/solutions.py:52
#, python-format
msgid "%(solver)s has replied for your \"%(subject)s\" check."
msgstr "%(solver)s הגיב לך על בדיקת תרגיל \"%(subject)s\"."

#: models/solutions.py:57
#: models/solutions.py:59
#, python-format
msgid "%(checker)s replied for \"%(subject)s\"."
msgstr "%(checker)s הגיב לך על תרגיל \"%(subject)s\"."

#: models/solutions.py:69
#: models/solutions.py:75
#, python-format
msgid "Your solution for the \"%(subject)s\" exercise has been checked."
msgstr "הפתרון שלך לתרגיל \"%(subject)s\" נבדק."
Expand Down Expand Up @@ -170,7 +170,7 @@ msgstr "אימות סיסמה"
msgid "Exercises"
msgstr "תרגילים"

#: templates/exercises.html:21 templates/view.html:101
#: templates/exercises.html:21 templates/view.html:113
msgid "Comments for the solution"
msgstr "הערות על התרגיל"

Expand Down Expand Up @@ -289,7 +289,7 @@ msgstr "חמ\"ל תרגילים"
msgid "Name"
msgstr "שם"

#: templates/status.html:13 templates/user.html:41
#: templates/status.html:13 templates/user.html:42
msgid "Checked"
msgstr "נבדק/ו"

Expand Down Expand Up @@ -357,36 +357,39 @@ msgstr "הגשה"
msgid "Checker"
msgstr "בודק"

#: templates/user.html:41
#: templates/user.html:33 templates/view.html:21 templates/view.html:104
msgid "Verbal note"
msgstr "הערה מילולית"

#: templates/user.html:42
msgid "Submitted"
msgstr "הוגש"

#: templates/user.html:41
#: templates/user.html:42
msgid "Not submitted"
msgstr "לא הוגש"

#: templates/user.html:52
#: templates/user.html:54
msgid "Notes"
msgstr "פתקיות"

#: templates/user.html:57 templates/user.html:59
#: templates/user.html:59 templates/user.html:61
msgid "New Note"
msgstr "פתקית חדשה"

#: templates/user.html:63
#: templates/user.html:65
msgid "Related Exercise"
msgstr "תרגיל משויך"

#: templates/user.html:72
#: templates/user.html:74
msgid "Privacy Level"
msgstr "רמת פרטיות"

#: templates/user.html:78
#: templates/user.html:80
msgid "Add Note"
msgstr "הוסף פתקית"

#: templates/view.html:6
#, fuzzy
msgid "Exercise view"
msgstr "שם תרגיל"

Expand All @@ -407,7 +410,6 @@ msgid "This solution is not up to date!"
msgstr "פתרון זה אינו פתרון עדכני!"

#: templates/view.html:15
#, fuzzy, python-format
msgid "Your solution hasn't been checked."
msgstr "הפתרון שלך לתרגיל %(subject)s נבדק."

Expand All @@ -419,42 +421,39 @@ msgstr "חשוב לנו שכל תרגיל יעבור בדיקה של עין אנ
msgid "Presenter"
msgstr "מגיש"

#: templates/view.html:21
#: templates/view.html:24
msgid "Navigate in solution versions"
msgstr "ניווט בגרסאות ההגשה"

#: templates/view.html:27
#, fuzzy
#: templates/view.html:30
msgid "Current page"
msgstr "סיסמה נוכחית"

#: templates/view.html:35
#: templates/view.html:38
msgid "Finish Checking"
msgstr "סיום בדיקה"

#: templates/view.html:75
#: templates/view.html:78
msgid "Automatic Checking"
msgstr "בדיקות אוטומטיות"

#: templates/view.html:82
#, fuzzy
#: templates/view.html:85
msgid "Error"
msgstr "כישלון חמור"

#: templates/view.html:87
#, fuzzy
#: templates/view.html:90
msgid "Staff Error"
msgstr "כישלון חמור"

#: templates/view.html:109
#: templates/view.html:121
msgid "General comments"
msgstr "הערות כלליות"

#: templates/view.html:117
#: templates/view.html:129
msgid "Checker comments"
msgstr "הערות בודק"

#: templates/view.html:127
#: templates/view.html:139
msgid "Done Checking"
msgstr "סיום בדיקה"

Expand Down
8 changes: 7 additions & 1 deletion lms/lmsweb/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,13 @@ def shared_solution(shared_url: str, file_id: Optional[int] = None):
@login_required
@managers_only
def done_checking(exercise_id, solution_id):
is_updated = solutions.mark_as_checked(solution_id, current_user.id)
if request.method == 'POST':
grade_id = request.json.get('grade')
else: # it's a GET
grade_id = request.args.get('grade')
is_updated = solutions.mark_as_checked(
solution_id, current_user.id, grade_id,
)
next_solution = solutions.get_next_unchecked(exercise_id)
next_solution_id = getattr(next_solution, 'id', None)
return jsonify({'success': is_updated, 'next': next_solution_id})
Expand Down
13 changes: 10 additions & 3 deletions lms/models/solutions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
from playhouse.shortcuts import model_to_dict # type: ignore

from lms.extractors.base import File
from lms.lmsdb.models import SharedSolution, Solution, SolutionFile, User
from lms.lmsdb.models import (
SharedSolution, Solution, SolutionFile, SolutionGradeMark, User,
)
from lms.lmstests.public.general import tasks as general_tasks
from lms.lmstests.public.identical_tests import tasks as identical_tests_tasks
from lms.lmsweb import config, routes
Expand Down Expand Up @@ -63,9 +65,13 @@ def get_message_and_addressee(
return msg, addressee


def mark_as_checked(solution_id: int, checker_id: int) -> bool:
def mark_as_checked(
solution_id: int, checker_id: int, grade_id: Optional[int] = None,
) -> bool:
checked_solution: Solution = Solution.get_by_id(solution_id)
is_updated = checked_solution.mark_as_checked(by=checker_id)
is_updated = checked_solution.mark_as_checked(
grade_id=grade_id, by=checker_id,
)
msg = _(
'Your solution for the "%(subject)s" exercise has been checked.',
subject=checked_solution.exercise.subject,
Expand Down Expand Up @@ -136,6 +142,7 @@ def get_view_parameters(
'user_comments':
comments._common_comments(user_id=current_user.id),
'left': Solution.left_in_exercise(solution.exercise),
'grade_marks': SolutionGradeMark.grades(),
}

if viewer_is_solver:
Expand Down
6 changes: 5 additions & 1 deletion lms/static/checker.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
function trackFinished(exerciseId, solutionId, element) {
element.addEventListener('click', () => {
const grade = document.querySelector('input[name="grade-mark"]:checked');
const gradeValue = grade.value;
const xhr = new XMLHttpRequest();
xhr.open('POST', `/checked/${exerciseId}/${solutionId}`, true);
xhr.setRequestHeader('Content-Type', 'application/json');
Expand All @@ -18,7 +20,9 @@ function trackFinished(exerciseId, solutionId, element) {
}
};

xhr.send(JSON.stringify({}));
xhr.send(JSON.stringify({
grade: gradeValue,
}));
});
}

Expand Down
Loading