Skip to content
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

feat: Add exercises tags #326

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
56 changes: 53 additions & 3 deletions lms/lmsdb/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,18 @@ def on_notification_saved(
instance.delete_instance()


class ExerciseTagText(BaseModel):
orronai marked this conversation as resolved.
Show resolved Hide resolved
text = TextField(unique=True)
orronai marked this conversation as resolved.
Show resolved Hide resolved

orronai marked this conversation as resolved.
Show resolved Hide resolved
@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()
Expand Down Expand Up @@ -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,
orronai marked this conversation as resolved.
Show resolved Hide resolved
):
user = User.get(User.id == user_id)
exercises = (
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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()
)


orronai marked this conversation as resolved.
Show resolved Hide resolved
class SolutionState(enum.Enum):
CREATED = 'Created'
IN_CHECKING = 'In checking'
Expand Down Expand Up @@ -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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same - we should try not to write such godlike functions

) -> 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 = (
Expand Down
16 changes: 13 additions & 3 deletions lms/lmsweb/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,16 +343,26 @@ def change_last_course_viewed(course_id: int):


@webapp.route('/exercises')
@webapp.route('/exercises/<tag_name>')
orronai marked this conversation as resolved.
Show resolved Hide resolved
@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,
)


Expand Down Expand Up @@ -479,7 +489,7 @@ def comment():

@webapp.route('/send/<int:course_id>/<int:_exercise_number>')
@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)
Expand Down
16 changes: 15 additions & 1 deletion lms/models/solutions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, 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
Expand Down Expand Up @@ -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,
)
orronai marked this conversation as resolved.
Show resolved Hide resolved
):
raise ResourceNotFound(
f'No such tag {tag_name} for course '
f'{current_user.last_course_viewed.name}.', 404,
)
18 changes: 18 additions & 0 deletions lms/static/my.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 6 additions & 1 deletion lms/templates/exercises.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<div id="exercises-page" class="page {{ direction }}">
<div id="exercises-header">
<div id="main-title">
<h1 id="exercises-head">{{ _('Exercises') }}</h1>
<h1 id="exercises-head">{{ _('Exercises') }}{% if tag_name %} - #{{ tag_name }}{% endif %}</h1>
orronai marked this conversation as resolved.
Show resolved Hide resolved
</div>
</div>
<div id="exercises">
Expand All @@ -14,6 +14,11 @@ <h1 id="exercises-head">{{ _('Exercises') }}</h1>
<div class="right-side {{ direction }}-language">
<div class="exercise-number me-3">{{ exercise['exercise_number'] }}</div>
<div class="exercise-name"><div class="ex-title">{{ exercise['exercise_name'] | e }}</div></div>
<div class="exercise-tags ms-1">
{% for tag in exercise.tags %}
<a class="ms-1" id="exercise-tag-link" href="{{ url_for('exercises_page', tag_name=tag.tag) }}">#{{ tag.tag }}</a>
orronai marked this conversation as resolved.
Show resolved Hide resolved
{% endfor %}
</div>
</div>
<div class="left-side">
<div class="comments-count">
Expand Down
2 changes: 1 addition & 1 deletion lms/templates/navbar.html
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
</a>
</li>
{% endif -%}
{%- if not exercises or fetch_archived %}
{%- if not exercises or fetch_archived or tag_name %}
<li class="nav-item">
<a href="/exercises" class="nav-link">
<i class="fa fa-book" aria-hidden="true"></i>
Expand Down
12 changes: 9 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
import pytest

from lms.lmsdb.models import (
ALL_MODELS, Comment, CommentText, Course, Exercise, Note, Notification,
Role, RoleOptions, SharedSolution, Solution, User, UserCourse,
ALL_MODELS, Comment, CommentText, Course, Exercise, ExerciseTag,
ExerciseTagText, Note, Notification, Role, RoleOptions, SharedSolution,
Solution, User, UserCourse,
)
from lms.extractors.base import File
from lms.lmstests.public import celery_app as public_app
Expand Down Expand Up @@ -308,14 +309,19 @@ def create_exercise(
)


def create_exercise_tag(tag_text: str, exercise: Exercise):
new_tag_id = ExerciseTagText.create_tag(text=tag_text).id
return ExerciseTag.create(exercise=exercise, tag=new_tag_id)


def create_shared_solution(solution: Solution) -> SharedSolution:
return SharedSolution.create_new(solution=solution)


def create_note(
creator: User,
user: User,
note_text: CommentText,
note_text: str,
privacy: int,
):
new_note_id = CommentText.create_comment(text=note_text).id
Expand Down
25 changes: 25 additions & 0 deletions tests/test_exercises.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,28 @@ def test_courses_exercises(
template, _ = captured_templates[-1]
assert template.name == 'exercises.html'
assert len(list(Exercise.get_objects(student_user.id))) == 2

@staticmethod
def test_exercise_tags(
orronai marked this conversation as resolved.
Show resolved Hide resolved
student_user: User, course: Course, exercise: Exercise,
):
client = conftest.get_logged_user(username=student_user.username)
conftest.create_usercourse(student_user, course)
client.get(f'course/{course.id}')
conftest.create_exercise_tag('tag1', exercise)
tag_response = client.get('/exercises/tag1')
assert tag_response.status_code == 200

course2 = conftest.create_course(index=1)
exercise2 = conftest.create_exercise(course2, 2)
conftest.create_usercourse(student_user, course2)
conftest.create_exercise_tag('tag2', exercise2)
bad_tag_response = client.get('/exercises/tag2')
assert bad_tag_response.status_code == 404

client.get(f'course/{course2.id}')
success_tag_response = client.get('/exercises/tag2')
assert success_tag_response.status_code == 200

another_bad_tag_response = client.get('/exercises/wrongtag')
assert another_bad_tag_response.status_code == 404