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 10 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 @@ -291,6 +291,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 @@ -299,13 +300,20 @@ def _uuid_migration() -> bool:
return True


def _assessment_migration() -> bool:
Solution = models.Solution
_add_not_null_column(Solution, Solution.assessment)
return True


def main():
with models.database.connection_context():
if models.database.table_exists(models.Exercise.__name__.lower()):
_add_exercise_course_id_and_number_columns_constraint()

if models.database.table_exists(models.Solution.__name__.lower()):
_last_status_view_migration()
_assessment_migration()

if models.database.table_exists(models.User.__name__.lower()):
_api_keys_migration()
Expand All @@ -318,6 +326,8 @@ def main():
models.create_basic_roles()
if models.User.select().count() == 0:
models.create_demo_users()
if models.SolutionAssessment.select().count() == 0:
models.create_basic_assessments()
if models.Course.select().count() == 0:
course = models.create_basic_course()
_exercise_course_migration(course)
Expand Down
72 changes: 68 additions & 4 deletions lms/lmsdb/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
from lms.lmsdb import database_config
from lms.models.errors import AlreadyExists
from lms.utils import hashing
from lms.utils.colors import get_hex_color
from lms.utils.consts import DEFAULT_ACTIVE_COLOR, DEFAULT_COLOR
from lms.utils.log import log


Expand Down Expand Up @@ -457,6 +459,36 @@ def to_choices(cls: enum.EnumMeta) -> Tuple[Tuple[str, str], ...]:
return tuple((choice.name, choice.value) for choice in choices)


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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. I think we should support different assessments per course. WDYT?
  2. Also: How we deal with a manager removing an assessment? (both 1. completely from the course and 2. de-selecting an evaluation from a specific solution)

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

def __str__(self):
return self.name


@pre_save(sender=SolutionAssessment)
def assessment_on_save_handler(_model_class, instance, created):
"""Change colors to hex."""

try:
instance.color = get_hex_color(instance.color)
except ValueError:
instance.color = DEFAULT_COLOR

try:
instance.active_color = get_hex_color(instance.active_color)
except ValueError:
instance.active_color = DEFAULT_ACTIVE_COLOR


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

@property
def solution_files(
Expand Down Expand Up @@ -539,13 +574,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,
assessment: Optional[SolutionAssessment] = 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 assessment is not None:
updates_dict[Solution.assessment.name] = assessment
changes = Solution.update(
**{Solution.state.name: new_state.name},
**kwargs,
**updates_dict, **kwargs,
).where(requested_solution)
return changes.execute() == 1

Expand All @@ -570,7 +610,9 @@ def of_user(
exercises = Exercise.as_dicts(db_exercises)
solutions = (
cls
.select(cls.exercise, cls.id, cls.state, cls.checker)
.select(
cls.exercise, cls.id, cls.state, cls.checker, cls.assessment,
)
.where(cls.exercise.in_(db_exercises), cls.solver == user_id)
.order_by(cls.submission_timestamp.desc())
)
Expand All @@ -582,6 +624,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.assessment:
exercise['assessment'] = solution.assessment.name
return tuple(exercises.values())

@property
Expand Down Expand Up @@ -684,10 +728,14 @@ def _base_next_unchecked(cls):

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

Expand Down Expand Up @@ -1064,6 +1112,22 @@ def create_basic_roles() -> None:
Role.create(name=role.value)


def create_basic_assessments() -> None:
assessments_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 name, values in assessments_dict.items():
SolutionAssessment.create(
name=name, icon=values.get('icon'), color=values.get('color'),
active_color='white', order=values.get('order'),
)


def create_basic_course() -> Course:
return Course.create(name='Python Course', date=datetime.now())

Expand Down
61 changes: 29 additions & 32 deletions lms/lmsweb/translations/he/LC_MESSAGES/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 1.0\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2021-10-01 10:15+0300\n"
"POT-Creation-Date: 2021-10-03 22:01+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:815
#: lmsdb/models.py:879
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 @@ -224,7 +224,6 @@ msgid "Mark all as read"
msgstr "סמן הכל כנקרא"

#: templates/navbar.html:45
#, fuzzy
msgid "Courses List"
msgstr "רשימת הקורסים"

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

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

Expand Down Expand Up @@ -347,7 +346,6 @@ msgid "Exercises Submitted"
msgstr "תרגילים שהוגשו"

#: templates/user.html:29
#, fuzzy
msgid "Course name"
msgstr "שם קורס"

Expand All @@ -367,36 +365,39 @@ msgstr "הגשה"
msgid "Checker"
msgstr "בודק"

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

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

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

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

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

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

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

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

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

Expand All @@ -417,54 +418,50 @@ msgid "This solution is not up to date!"
msgstr "פתרון זה אינו פתרון עדכני!"

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

#: templates/view.html:15
msgid "It's important for us that all exercises will be checked by human eye."
msgstr "חשוב לנו שכל תרגיל יעבור בדיקה של עין אנושית."

#: templates/view.html:18
msgid "Presenter"
msgid "Solver"
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 @@ -633,7 +633,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':
assessment_id = request.json.get('assessment')
else: # it's a GET
assessment_id = request.args.get('assessment')
is_updated = solutions.mark_as_checked(
solution_id, current_user.id, assessment_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
Loading