From 1204de35cb423a6a6c115f11778fe9f5bbc566a4 Mon Sep 17 00:00:00 2001 From: Or Ronai Date: Tue, 5 Oct 2021 15:55:53 +0300 Subject: [PATCH 1/6] feat: Add exercises tags - Added tags per course - Added for the exercises.html template the tags - Added the tables of the tags - Added a test --- lms/lmsdb/models.py | 56 ++++++++++++++++++++++++++++++++++-- lms/lmsweb/views.py | 21 ++++++++++---- lms/models/solutions.py | 16 ++++++++++- lms/static/my.css | 18 ++++++++++++ lms/templates/exercises.html | 7 ++++- lms/templates/navbar.html | 2 +- tests/conftest.py | 12 ++++++-- tests/test_exercises.py | 25 ++++++++++++++++ 8 files changed, 143 insertions(+), 14 deletions(-) diff --git a/lms/lmsdb/models.py b/lms/lmsdb/models.py index cf3c8bc1..853c10f1 100644 --- a/lms/lmsdb/models.py +++ b/lms/lmsdb/models.py @@ -346,6 +346,18 @@ def on_notification_saved( instance.delete_instance() +class ExerciseTagText(BaseModel): + text = TextField(unique=True) + + @classmethod + def create_tag(cls, text: str) -> 'ExerciseTagText': + instance, _ = cls.get_or_create(**{cls.text.name: html.escape(text)}) + return instance + + def __str__(self): + return self.text + + class Exercise(BaseModel): subject = CharField() date = DateTimeField() @@ -378,7 +390,7 @@ def is_number_exists(cls, number: int) -> bool: @classmethod def get_objects( cls, user_id: int, fetch_archived: bool = False, - from_all_courses: bool = False, + from_all_courses: bool = False, exercise_tag: Optional[str] = None, ): user = User.get(User.id == user_id) exercises = ( @@ -394,6 +406,13 @@ def get_objects( exercises = exercises.where( UserCourse.course == user.last_course_viewed, ) + if exercise_tag: + exercises = ( + exercises + .join(ExerciseTag) + .join(ExerciseTagText) + .where(ExerciseTagText.text == exercise_tag) + ) if not fetch_archived: exercises = exercises.where(cls.is_archived == False) # NOQA: E712 return exercises @@ -408,6 +427,7 @@ def as_dict(self) -> Dict[str, Any]: 'exercise_number': self.number, 'course_id': self.course.id, 'course_name': self.course.name, + 'tags': ExerciseTag.by_exercise(self), } @staticmethod @@ -426,6 +446,36 @@ def exercise_number_save_handler(model_class, instance, created): instance.number = model_class.get_highest_number() + 1 +class ExerciseTag(BaseModel): + exercise = ForeignKeyField(Exercise) + tag = ForeignKeyField(ExerciseTagText) + date = DateTimeField(default=datetime.now) + + @classmethod + def by_exercise( + cls, exercise: Exercise, + ) -> Union[Iterable['ExerciseTag'], 'ExerciseTag']: + return ( + cls + .select() + .where(cls.exercise == exercise) + .order_by(cls.date) + ) + + @classmethod + def is_course_tag_exists(cls, course: Course, tag_name: str): + return ( + cls + .select() + .join(ExerciseTagText) + .where(ExerciseTagText.text == tag_name) + .switch() + .join(Exercise) + .where(Exercise.course == course) + .exists() + ) + + class SolutionState(enum.Enum): CREATED = 'Created' IN_CHECKING = 'In checking' @@ -561,11 +611,11 @@ def test_results(self) -> Iterable[dict]: @classmethod def of_user( cls, user_id: int, with_archived: bool = False, - from_all_courses: bool = False, + from_all_courses: bool = False, exercise_tag: Optional[str] = None, ) -> Iterable[Dict[str, Any]]: db_exercises = Exercise.get_objects( user_id=user_id, fetch_archived=with_archived, - from_all_courses=from_all_courses, + from_all_courses=from_all_courses, exercise_tag=exercise_tag, ) exercises = Exercise.as_dicts(db_exercises) solutions = ( diff --git a/lms/lmsweb/views.py b/lms/lmsweb/views.py index cc5c9689..31fc0fe9 100644 --- a/lms/lmsweb/views.py +++ b/lms/lmsweb/views.py @@ -15,8 +15,9 @@ from werkzeug.utils import redirect from lms.lmsdb.models import ( - ALL_MODELS, Comment, Course, Note, Role, RoleOptions, SharedSolution, - Solution, SolutionFile, User, UserCourse, database, + ALL_MODELS, Comment, Course, ExerciseTag, ExerciseTagText, Note, Role, + RoleOptions, SharedSolution, Solution, SolutionFile, User, UserCourse, + database, ) from lms.lmsweb import babel, limiter, routes, webapp from lms.lmsweb.admin import ( @@ -343,16 +344,26 @@ def change_last_course_viewed(course_id: int): @webapp.route('/exercises') +@webapp.route('/exercises/') @login_required -def exercises_page(): +def exercises_page(tag_name: Optional[str] = None): fetch_archived = bool(request.args.get('archived')) - exercises = Solution.of_user(current_user.id, fetch_archived) + try: + solutions.check_tag_name(tag_name) + except LmsError as e: + error_message, status_code = e.args + return fail(status_code, error_message) + + exercises = Solution.of_user( + current_user.id, fetch_archived, exercise_tag=tag_name, + ) is_manager = current_user.role.is_manager return render_template( 'exercises.html', exercises=exercises, is_manager=is_manager, fetch_archived=fetch_archived, + tag_name=tag_name, ) @@ -479,7 +490,7 @@ def comment(): @webapp.route('/send//') @login_required -def send(course_id: int, _exercise_number: Optional[int]): +def send(course_id: int, _exercise_number: Optional[int] = None): if not UserCourse.is_user_registered(current_user.id, course_id): return fail(403, "You aren't allowed to watch this page.") return render_template('upload.html', course_id=course_id) diff --git a/lms/models/solutions.py b/lms/models/solutions.py index d61d5e15..de803b1b 100644 --- a/lms/models/solutions.py +++ b/lms/models/solutions.py @@ -8,7 +8,9 @@ from playhouse.shortcuts import model_to_dict # type: ignore from lms.extractors.base import File -from lms.lmsdb.models import SharedSolution, Solution, SolutionFile, User +from lms.lmsdb.models import ( + ExerciseTag, ExerciseTagText, SharedSolution, Solution, SolutionFile, User, +) from lms.lmstests.public.general import tasks as general_tasks from lms.lmstests.public.identical_tests import tasks as identical_tests_tasks from lms.lmsweb import config, routes @@ -205,3 +207,15 @@ def get_files_tree(files: Iterable[SolutionFile]) -> List[Dict[str, Any]]: for file in file_details: del file['fullpath'] return file_details + + +def check_tag_name(tag_name: Optional[str]) -> None: + if ( + tag_name is not None and not ExerciseTag.is_course_tag_exists( + current_user.last_course_viewed, tag_name, + ) + ): + raise ResourceNotFound( + f'No such tag {tag_name} for course ' + f'{current_user.last_course_viewed.name}.', 404, + ) diff --git a/lms/static/my.css b/lms/static/my.css index a637d906..d76d66a5 100644 --- a/lms/static/my.css +++ b/lms/static/my.css @@ -177,6 +177,24 @@ a { line-height: 3rem; } +.exercise-tags { + align-items: center; + display: flex; + font-size: 1em; + height: 3rem; + justify-content: center; + text-align: center; + white-space: normal; +} + +#exercise-tag-link { + color: #919191; +} + +#exercise-tag-link:hover { + color: #646464; +} + .exercise-send { display: flex; flex-direction: row; diff --git a/lms/templates/exercises.html b/lms/templates/exercises.html index 0b08335f..8a0bf292 100644 --- a/lms/templates/exercises.html +++ b/lms/templates/exercises.html @@ -5,7 +5,7 @@
-

{{ _('Exercises') }}

+

{{ _('Exercises') }}{% if tag_name %} - #{{ tag_name }}{% endif %}

@@ -14,6 +14,11 @@

{{ _('Exercises') }}

{{ exercise['exercise_number'] }}
{{ exercise['exercise_name'] | e }}
+
+ {% for tag in exercise.tags %} + #{{ tag.tag }} + {% endfor %} +
diff --git a/lms/templates/navbar.html b/lms/templates/navbar.html index d2e49864..d027c660 100644 --- a/lms/templates/navbar.html +++ b/lms/templates/navbar.html @@ -66,7 +66,7 @@ {% endif -%} - {%- if not exercises or fetch_archived %} + {%- if not exercises or fetch_archived or tag_name %}