diff --git a/README.md b/README.md index b033cbca..fcedbc20 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,29 @@ # Python's Course LMS
- - +
馃憢 Welcome to Python course learning management system. 馃悕 The system objectives - -1. Allow teachers and mentors to input exercises list and provide feedback/comments to students exercises solutions. +1. Allow teachers and mentors to input exercises list and provide feedback/comments + to students exercises solutions. 2. Allow students to load their exercises solutions and get feedback to their work. ## Creating development environment ### Prerequisites -1. Linux based system - either [WSL on windows](https://docs.microsoft.com/en-us/windows/wsl/install-win10) or full blown linux. -2. [Python](https://www.python.org/downloads/release/python-385/) -3. [Docker](https://docs.docker.com/docker-for-windows/install/) and docker-compose. - +1. Linux based system - either [WSL on windows](https://docs.microsoft.com/en-us/windows/wsl/install-win10) + or full blown Linux. +2. Latest Python version +3. Docker ### Minimal setup -This setup is for debug purposes and will use SQLite database and frontend only. +This setup is for debug purposes and will use SQLite database and front-end only. Steps to do: @@ -48,8 +48,8 @@ cd .. flask run # Run in root directory ``` -After logging in, use [localhost admin](https://127.0.0.1:5000/admin) to modify entries in the database. - +After logging in, use [localhost admin](https://127.0.0.1:5000/admin) +to modify entries in the database. ### Full setup @@ -82,25 +82,34 @@ docker exec -it lms_http_1 bash python lmsdb/bootstrap.py ``` -Enter http://127.0.0.1:8080, and the initial credentials should appear in your terminal. :) +Enter [http://127.0.0.1:8080], and the initial credentials should appear in +your terminal. :) -After logging in, use [localhost admin](https://127.0.0.1:8080/admin) to modify entries in the database. +After logging in, use [localhost admin](https://127.0.0.1:8080/admin) to +modify entries in the database. In case you want to enable the mail system: 1. Insert your mail details in the configuration file. 2. Change the `DISABLE_MAIL` line value to False. - ## Code modification check list -## Run flake8 +## Run Flake8 ```bash -# on lms root directory +# on LMS root directory flake8 lms ``` +### Updating localization files + +```bash +. devops/i18n.sh update +``` + +Then go to lms/lmsweb/translations to translate the strings, if needed. + ### Run tests ```bash diff --git a/devops/i18n.sh b/devops/i18n.sh index c16ff7d1..6fbe88df 100644 --- a/devops/i18n.sh +++ b/devops/i18n.sh @@ -1,9 +1,18 @@ #!/bin/bash -x -SCRIPT_FILE_PATH=$(readlink -f "${0}") -SCRIPT_FOLDER=$(dirname "${SCRIPT_FILE_PATH}") -MAIN_FOLDER="${SCRIPT_FOLDER}/.." +original_dir="$(pwd)" +cd "$(dirname "${0:-${BASH_SOURCE[0]}}")" || return +cd .. + + +if [ "${1}" = "update" ]; then + echo "Updating translations" + pybabel extract -F "lms/babel.cfg" -o "lms/lmsweb/translations/messages.pot" "lms" + pybabel update -i "lms/lmsweb/translations/messages.pot" -d "lms/lmsweb/translations" +fi echo "Compiling Flask Babel" -pybabel compile -d "${MAIN_FOLDER}/lms/lmsweb/translations" +pybabel compile -d "lms/lmsweb/translations" + +cd "$original_dir" || return diff --git a/lms/lmsdb/models.py b/lms/lmsdb/models.py index 4b5d5d99..1b6ecca5 100644 --- a/lms/lmsdb/models.py +++ b/lms/lmsdb/models.py @@ -13,8 +13,9 @@ from flask_babel import gettext as _ # type: ignore from flask_login import UserMixin, current_user # type: ignore from peewee import ( # type: ignore - BooleanField, Case, CharField, Check, DateTimeField, ForeignKeyField, - IntegerField, JOIN, ManyToManyField, TextField, UUIDField, fn, + BooleanField, Case, CharField, Check, Database, DateTimeField, + ForeignKeyField, IntegerField, JOIN, ManyToManyField, Select, TextField, + UUIDField, fn, ) from playhouse.signals import ( # type: ignore Model, post_delete, post_save, pre_save, @@ -167,8 +168,85 @@ def public_courses(cls): def public_course_exists(cls): return cls.public_courses().exists() + def get_students(self, fields=None) -> List[int]: + fields = fields or [User.id] + + return ( + self + .select(*fields) + .join(UserCourse) + .join(User, on=(User.id == UserCourse.user_id)) + .join(Role) + .where( + (self.id == UserCourse.course_id) + & (Role.id == Role.get_student_role().id), + ) + ) + + def get_exercise_ids(self) -> List[int]: + return [e.id for e in self.exercise] + + def get_matrix(self, database: Database = database) -> dict: + SolutionAlias = Solution.alias() + fields = [ + SolutionAlias.id.alias('solution_id'), + SolutionAlias.solver.id.alias('solver_id'), + SolutionAlias.exercise.id.alias('exercise_id'), + ] + + query = ( + User + .select(*fields) + .join(Exercise, JOIN.CROSS) + .join(Course, JOIN.LEFT_OUTER, on=(Exercise.course == Course.id)) + .join(SolutionAlias, JOIN.LEFT_OUTER, on=( + (SolutionAlias.exercise == Exercise.id) + & (SolutionAlias.solver == User.id) + )) + .where( + (Exercise.id << self.exercise), + (User.id << self.get_students()), + ) + .group_by(Exercise.id, User.id, SolutionAlias.id) + .having( + (SolutionAlias.id == fn.MAX(SolutionAlias.id)) + | (SolutionAlias.id.is_null(True)), + ) + .alias('solutions_subquery') + ) + + full_query_fields = [ + query.c.solver_id, + query.c.exercise_id, + Solution.id.alias('solution_id'), + User.fullname.alias('checker'), + Solution.state, + Solution.submission_timestamp, + SolutionAssessment.name.alias('assessment'), + SolutionAssessment.icon.alias('assessment_icon'), + ] + + solutions = ( + Select(columns=full_query_fields) + .from_(query) + .join(Solution, JOIN.LEFT_OUTER, on=( + Solution.id == query.c.solution_id + )) + .join(SolutionAssessment, JOIN.LEFT_OUTER, on=( + (Solution.assessment == SolutionAssessment.id) + )) + .join(User, JOIN.LEFT_OUTER, on=(Solution.checker == User.id)) + ) + + query_results = solutions.execute(database) + + return { + (row['exercise_id'], row['solver_id']): row + for row in query_results + } + def __str__(self): - return f'{self.name}: {self.date} - {self.end_date}' + return self.name class User(UserMixin, BaseModel): @@ -181,6 +259,9 @@ class User(UserMixin, BaseModel): last_course_viewed = ForeignKeyField(Course, null=True) uuid = UUIDField(default=uuid4, unique=True) + class Meta: + table_name = "user" + def get_id(self): return str(self.uuid) diff --git a/lms/lmstests/public/unittests/services.py b/lms/lmstests/public/unittests/services.py index 4dad0724..728f6c8a 100644 --- a/lms/lmstests/public/unittests/services.py +++ b/lms/lmstests/public/unittests/services.py @@ -141,7 +141,7 @@ def _handle_failed_to_execute_tests(self, raw_results: bytes) -> None: solution=self._solution, test_name=models.ExerciseTestName.FATAL_TEST_NAME, user_message=fail_user_message, - staff_message=_('Bro, did you check your code?'), + staff_message=_('Woah! Did you check your code?'), ) notifications.send( kind=notifications.NotificationKind.UNITTEST_ERROR, diff --git a/lms/lmsweb/routes.py b/lms/lmsweb/routes.py index 21b17e62..3c12b44e 100644 --- a/lms/lmsweb/routes.py +++ b/lms/lmsweb/routes.py @@ -1,5 +1,6 @@ SOLUTIONS = '/view' STATUS = '/status' +SUBMISSIONS = '/submissions' DOWNLOADS = '/download' SHARED = '/shared' GIT = '/git/