Skip to content

Commit

Permalink
feat: Add matrix checker page, exercises x users per course (#351)
Browse files Browse the repository at this point in the history
* feat: Add matrix checker page, exercises x users per course
* dev: Add tool to update i18n
* chore: Small formatting fixes
  • Loading branch information
yammesicka authored Mar 24, 2024
1 parent bca9068 commit 1356ad2
Show file tree
Hide file tree
Showing 19 changed files with 644 additions and 184 deletions.
39 changes: 24 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
# Python's Course LMS

<p align="center">
<img title="BSD-3 Clause" src="https://img.shields.io/github/license/PythonFreeCourse/LMS.svg">
<img title="Travis (.com) branch" src="https://img.shields.io/travis/com/PythonFreeCourse/LMS/master.svg">
<img title="BSD-3 Clause" src="https://img.shields.io/github/license/PythonFreeCourse/LMS.svg" alt="LMS License is BSD-3 Clause">
</p>

👋 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:

Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down
17 changes: 13 additions & 4 deletions devops/i18n.sh
Original file line number Diff line number Diff line change
@@ -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
87 changes: 84 additions & 3 deletions lms/lmsdb/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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):
Expand All @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion lms/lmstests/public/unittests/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions lms/lmsweb/routes.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
SOLUTIONS = '/view'
STATUS = '/status'
SUBMISSIONS = '/submissions'
DOWNLOADS = '/download'
SHARED = '/shared'
GIT = '/git/<int:course_id>/<int:exercise_number>.git'
Loading

0 comments on commit 1356ad2

Please sign in to comment.