Skip to content

Commit f3ed156

Browse files
authored
feat: Grade marks (#320)
* feat: Add grades mark - Added grades table - Added backend and frontend implementaion - Added tests - Added the grade to the user`s table - Fixed translations - Added active color column - Added pre_save colors - Changed the exercise number pre_save - Assessments are now per course - Allow unselecting choice on second click
1 parent 8a41e38 commit f3ed156

File tree

15 files changed

+350
-60
lines changed

15 files changed

+350
-60
lines changed

Diff for: lms/lmsdb/bootstrap.py

+10
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ def _last_status_view_migration() -> bool:
300300
Solution = models.Solution
301301
_migrate_column_in_table_if_needed(Solution, Solution.last_status_view)
302302
_migrate_column_in_table_if_needed(Solution, Solution.last_time_view)
303+
return True
303304

304305

305306
def _uuid_migration() -> bool:
@@ -308,13 +309,20 @@ def _uuid_migration() -> bool:
308309
return True
309310

310311

312+
def _assessment_migration() -> bool:
313+
Solution = models.Solution
314+
_add_not_null_column(Solution, Solution.assessment)
315+
return True
316+
317+
311318
def main():
312319
with models.database.connection_context():
313320
if models.database.table_exists(models.Exercise.__name__.lower()):
314321
_add_exercise_course_id_and_number_columns_constraint()
315322

316323
if models.database.table_exists(models.Solution.__name__.lower()):
317324
_last_status_view_migration()
325+
_assessment_migration()
318326

319327
if models.database.table_exists(models.User.__name__.lower()):
320328
_api_keys_migration()
@@ -330,6 +338,8 @@ def main():
330338
models.create_basic_roles()
331339
if models.User.select().count() == 0:
332340
models.create_demo_users()
341+
if models.SolutionAssessment.select().count() == 0:
342+
models.create_basic_assessments()
333343
if models.Course.select().count() == 0:
334344
course = models.create_basic_course()
335345
_exercise_course_migration(course)

Diff for: lms/lmsdb/models.py

+90-11
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
from lms.lmsdb import database_config
2727
from lms.models.errors import AlreadyExists
2828
from lms.utils import hashing
29+
from lms.utils.colors import get_hex_color
30+
from lms.utils.consts import (
31+
DEFAULT_ASSESSMENT_BUTTON_ACTIVE_COLOR, DEFAULT_ASSESSMENT_BUTTON_COLOR,
32+
)
2933
from lms.utils.log import log
3034

3135

@@ -393,12 +397,23 @@ def open_for_new_solutions(self) -> bool:
393397
return datetime.now() < self.due_date and not self.is_archived
394398

395399
@classmethod
396-
def get_highest_number(cls):
397-
return cls.select(fn.MAX(cls.number)).scalar()
400+
def get_highest_number(cls, course: Course):
401+
return (
402+
cls
403+
.select(fn.MAX(cls.number))
404+
.where(cls.course == course)
405+
.group_by(cls.course)
406+
.scalar()
407+
)
398408

399409
@classmethod
400-
def is_number_exists(cls, number: int) -> bool:
401-
return cls.select().where(cls.number == number).exists()
410+
def is_number_exists(cls, course: Course, number: int) -> bool:
411+
return (
412+
cls
413+
.select()
414+
.where(cls.course == course, cls.number == number)
415+
.exists()
416+
)
402417

403418
@classmethod
404419
def get_objects(
@@ -446,9 +461,8 @@ def __str__(self):
446461
@pre_save(sender=Exercise)
447462
def exercise_number_save_handler(model_class, instance, created):
448463
"""Change the exercise number to the highest consecutive number."""
449-
450-
if model_class.is_number_exists(instance.number):
451-
instance.number = model_class.get_highest_number() + 1
464+
if model_class.is_number_exists(instance.course, instance.number):
465+
instance.number = model_class.get_highest_number(instance.course) + 1
452466

453467

454468
class SolutionState(enum.Enum):
@@ -482,6 +496,36 @@ def to_choices(cls: enum.EnumMeta) -> Tuple[Tuple[str, str], ...]:
482496
return tuple((choice.name, choice.value) for choice in choices)
483497

484498

499+
class SolutionAssessment(BaseModel):
500+
name = CharField()
501+
icon = CharField(null=True)
502+
color = CharField()
503+
active_color = CharField()
504+
order = IntegerField(default=0, index=True)
505+
course = ForeignKeyField(Course, backref='assessments')
506+
507+
@classmethod
508+
def get_assessments(cls, course: Course):
509+
return cls.select().where(cls.course == course).order_by(cls.order)
510+
511+
def __str__(self):
512+
return self.name
513+
514+
515+
@pre_save(sender=SolutionAssessment)
516+
def assessment_on_save_handler(_model_class, instance, created):
517+
"""Change colors to hex."""
518+
try:
519+
instance.color = get_hex_color(instance.color)
520+
except ValueError:
521+
instance.color = DEFAULT_ASSESSMENT_BUTTON_COLOR
522+
523+
try:
524+
instance.active_color = get_hex_color(instance.active_color)
525+
except ValueError:
526+
instance.active_color = DEFAULT_ASSESSMENT_BUTTON_ACTIVE_COLOR
527+
528+
485529
class Solution(BaseModel):
486530
STATES = SolutionState
487531
STATUS_VIEW = SolutionStatusView
@@ -506,6 +550,9 @@ class Solution(BaseModel):
506550
index=True,
507551
)
508552
last_time_view = DateTimeField(default=datetime.now, null=True, index=True)
553+
assessment = ForeignKeyField(
554+
SolutionAssessment, backref='solutions', null=True,
555+
)
509556

510557
@property
511558
def solution_files(
@@ -564,13 +611,18 @@ def view_solution(self) -> None:
564611
def start_checking(self) -> bool:
565612
return self.set_state(Solution.STATES.IN_CHECKING)
566613

567-
def set_state(self, new_state: SolutionState, **kwargs) -> bool:
614+
def set_state(
615+
self, new_state: SolutionState,
616+
assessment: Optional[SolutionAssessment] = None, **kwargs,
617+
) -> bool:
568618
# Optional: filter the old state of the object
569619
# to make sure that no two processes set the state together
570620
requested_solution = (Solution.id == self.id)
621+
updates_dict = {Solution.state.name: new_state.name}
622+
if assessment is not None:
623+
updates_dict[Solution.assessment.name] = assessment
571624
changes = Solution.update(
572-
**{Solution.state.name: new_state.name},
573-
**kwargs,
625+
**updates_dict, **kwargs,
574626
).where(requested_solution)
575627
return changes.execute() == 1
576628

@@ -595,7 +647,9 @@ def of_user(
595647
exercises = Exercise.as_dicts(db_exercises)
596648
solutions = (
597649
cls
598-
.select(cls.exercise, cls.id, cls.state, cls.checker)
650+
.select(
651+
cls.exercise, cls.id, cls.state, cls.checker, cls.assessment,
652+
)
599653
.where(cls.exercise.in_(db_exercises), cls.solver == user_id)
600654
.order_by(cls.submission_timestamp.desc())
601655
)
@@ -607,6 +661,8 @@ def of_user(
607661
exercise['comments_num'] = len(solution.staff_comments)
608662
if solution.is_checked and solution.checker:
609663
exercise['checker'] = solution.checker.fullname
664+
if solution.assessment:
665+
exercise['assessment'] = solution.assessment.name
610666
return tuple(exercises.values())
611667

612668
@property
@@ -709,10 +765,15 @@ def _base_next_unchecked(cls):
709765

710766
def mark_as_checked(
711767
self,
768+
assessment_id: Optional[int] = None,
712769
by: Optional[Union[User, int]] = None,
713770
) -> bool:
771+
assessment = SolutionAssessment.get_or_none(
772+
SolutionAssessment.id == assessment_id,
773+
)
714774
return self.set_state(
715775
Solution.STATES.DONE,
776+
assessment=assessment,
716777
checker=by,
717778
)
718779

@@ -1089,6 +1150,24 @@ def create_basic_roles() -> None:
10891150
Role.create(name=role.value)
10901151

10911152

1153+
def create_basic_assessments() -> None:
1154+
assessments_dict = {
1155+
'Excellent': {'color': 'green', 'icon': 'star', 'order': 1},
1156+
'Nice': {'color': 'blue', 'icon': 'check', 'order': 2},
1157+
'Try again': {'color': 'red', 'icon': 'exclamation', 'order': 3},
1158+
'Plagiarism': {
1159+
'color': 'black', 'icon': 'exclamation-triangle', 'order': 4,
1160+
},
1161+
}
1162+
courses = Course.select()
1163+
for course in courses:
1164+
for name, values in assessments_dict.items():
1165+
SolutionAssessment.create(
1166+
name=name, icon=values.get('icon'), color=values.get('color'),
1167+
active_color='white', order=values.get('order'), course=course,
1168+
)
1169+
1170+
10921171
def create_basic_course() -> Course:
10931172
return Course.create(name='Python Course', date=datetime.now())
10941173

0 commit comments

Comments
 (0)