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
Changes from all 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
@@ -300,6 +300,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:
@@ -308,13 +309,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()
@@ -330,6 +338,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)
101 changes: 90 additions & 11 deletions lms/lmsdb/models.py
Original file line number Diff line number Diff line change
@@ -26,6 +26,10 @@
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_ASSESSMENT_BUTTON_ACTIVE_COLOR, DEFAULT_ASSESSMENT_BUTTON_COLOR,
)
from lms.utils.log import log


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

@classmethod
def get_highest_number(cls):
return cls.select(fn.MAX(cls.number)).scalar()
def get_highest_number(cls, course: Course):
return (
cls
.select(fn.MAX(cls.number))
.where(cls.course == course)
.group_by(cls.course)
.scalar()
)

@classmethod
def is_number_exists(cls, number: int) -> bool:
return cls.select().where(cls.number == number).exists()
def is_number_exists(cls, course: Course, number: int) -> bool:
return (
cls
.select()
.where(cls.course == course, cls.number == number)
.exists()
)

@classmethod
def get_objects(
@@ -446,9 +461,8 @@ def __str__(self):
@pre_save(sender=Exercise)
def exercise_number_save_handler(model_class, instance, created):
"""Change the exercise number to the highest consecutive number."""

if model_class.is_number_exists(instance.number):
instance.number = model_class.get_highest_number() + 1
if model_class.is_number_exists(instance.course, instance.number):
instance.number = model_class.get_highest_number(instance.course) + 1


class SolutionState(enum.Enum):
@@ -482,6 +496,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)
course = ForeignKeyField(Course, backref='assessments')

@classmethod
def get_assessments(cls, course: Course):
return cls.select().where(cls.course == course).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_ASSESSMENT_BUTTON_COLOR

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


class Solution(BaseModel):
STATES = SolutionState
STATUS_VIEW = SolutionStatusView
@@ -506,6 +550,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(
@@ -564,13 +611,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

@@ -595,7 +647,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())
)
@@ -607,6 +661,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
@@ -709,10 +765,15 @@ def _base_next_unchecked(cls):

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

@@ -1089,6 +1150,24 @@ 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,
},
}
courses = Course.select()
for course in courses:
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'), course=course,
)


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

Loading