Skip to content

Commit abaa5b7

Browse files
authored
Merge pull request #137 from profcomff/iss14/query-mark-lecturer
2 parents 03358a0 + 7df7169 commit abaa5b7

File tree

6 files changed

+308
-99
lines changed

6 files changed

+308
-99
lines changed

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ format:
1414
autoflake -r --in-place --remove-all-unused-imports ./migrations
1515
isort ./migrations
1616
black ./migrations
17+
autoflake -r --in-place --remove-all-unused-imports ./tests
18+
isort ./tests
19+
black ./tests
1720

1821
db:
1922
docker run -d -p 5432:5432 -e POSTGRES_HOST_AUTH_METHOD=trust --name db-rating_api postgres:15

rating_api/routes/lecturer.py

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,21 @@
22

33
from auth_lib.fastapi import UnionAuth
44
from fastapi import APIRouter, Depends, Query
5+
from fastapi_filter import FilterDepends
56
from fastapi_sqlalchemy import db
67
from sqlalchemy import and_
78

89
from rating_api.exceptions import AlreadyExists, ObjectNotFound
910
from rating_api.models import Comment, Lecturer, LecturerUserComment, ReviewStatus
1011
from rating_api.schemas.base import StatusResponseModel
11-
from rating_api.schemas.models import CommentGet, LecturerGet, LecturerGetAll, LecturerPatch, LecturerPost
12+
from rating_api.schemas.models import (
13+
CommentGet,
14+
LecturerGet,
15+
LecturerGetAll,
16+
LecturerPatch,
17+
LecturerPost,
18+
LecturersFilter,
19+
)
1220
from rating_api.utils.mark import calc_weighted_mark
1321

1422

@@ -76,16 +84,11 @@ async def get_lecturer(id: int, info: list[Literal["comments", "mark"]] = Query(
7684

7785
@lecturer.get("", response_model=LecturerGetAll)
7886
async def get_lecturers(
87+
lecturer_filter=FilterDepends(LecturersFilter),
7988
limit: int = 10,
8089
offset: int = 0,
8190
info: list[Literal["comments", "mark"]] = Query(default=[]),
82-
order_by: str = Query(
83-
enum=["mark_weighted", "mark_kindness", "mark_freebie", "mark_clarity", "mark_general", "last_name"],
84-
default="mark_weighted",
85-
),
86-
subject: str = Query(''),
87-
name: str = Query(''),
88-
asc_order: bool = False,
91+
mark: float = Query(default=None, ge=-2, le=2),
8992
) -> LecturerGetAll:
9093
"""
9194
`limit` - максимальное количество возвращаемых преподавателей
@@ -95,6 +98,13 @@ async def get_lecturers(
9598
`order_by` - возможные значения `"mark_weighted", "mark_kindness", "mark_freebie", "mark_clarity", "mark_general", "last_name"`.
9699
Если передано `'last_name'` - возвращается список преподавателей отсортированных по алфавиту по фамилиям
97100
Если передано `'mark_...'` - возвращается список преподавателей отсортированных по конкретной оценке
101+
Если передано просто так (или с '+' в начале параметра), то сортирует по возрастанию
102+
С '-' в начале -- по убыванию.
103+
104+
*Пример запросов с этим параметром*:
105+
- `...?order_by=-mark_kindness`
106+
- `...?order_by=mark_freebie`
107+
- `...?order_by=+mark_freebie` (эквивалентно 2ому пункту)
98108
99109
`info` - возможные значения `'comments'`, `'mark'`.
100110
Если передано `'comments'`, то возвращаются одобренные комментарии к преподавателю.
@@ -107,30 +117,17 @@ async def get_lecturers(
107117
`name`
108118
Поле для ФИО. Если передано `name` - возвращает всех преподователей, для которых нашлись совпадения с переданной строкой
109119
110-
`asc_order`
111-
Если передано true, сортировать в порядке возрастания
112-
Иначе - в порядке убывания
120+
`mark`
121+
Поле для оценки. Если передано, то возвращает только тех преподавателей, для которых средняя общая оценка ('general_mark')
122+
больше, чем переданный 'mark'.
113123
"""
114-
lecturers_query = (
115-
Lecturer.query(session=db.session)
116-
.outerjoin(Lecturer.comments)
117-
.group_by(Lecturer.id)
118-
.filter(Lecturer.search_by_subject(subject))
119-
.filter(Lecturer.search_by_name(name))
120-
.order_by(
121-
*(
122-
Lecturer.order_by_mark(order_by, asc_order)
123-
if "mark" in order_by
124-
else Lecturer.order_by_name(order_by, asc_order)
125-
)
126-
)
124+
lecturers_query = lecturer_filter.filter(
125+
Lecturer.query(session=db.session).outerjoin(Lecturer.comments).group_by(Lecturer.id)
127126
)
128-
127+
lecturers_query = lecturer_filter.sort(lecturers_query)
129128
lecturers = lecturers_query.offset(offset).limit(limit).all()
130129
lecturers_count = lecturers_query.group_by(Lecturer.id).count()
131130

132-
if not lecturers:
133-
raise ObjectNotFound(Lecturer, 'all')
134131
result = LecturerGetAll(limit=limit, offset=offset, total=lecturers_count)
135132
if "mark" in info:
136133
mean_mark_general = Lecturer.mean_mark_general()
@@ -143,6 +140,12 @@ async def get_lecturers(
143140
for comment in db_lecturer.comments
144141
if comment.review_status is ReviewStatus.APPROVED
145142
]
143+
if (
144+
mark is not None
145+
and approved_comments
146+
and sum(comment.mark_general for comment in approved_comments) / len(approved_comments) <= mark
147+
):
148+
continue
146149
if "comments" in info and approved_comments:
147150
lecturer_to_result.comments = sorted(
148151
approved_comments, key=lambda comment: comment.create_ts, reverse=True
@@ -166,6 +169,8 @@ async def get_lecturers(
166169
if approved_comments:
167170
lecturer_to_result.subjects = list({comment.subject for comment in approved_comments})
168171
result.lecturers.append(lecturer_to_result)
172+
if len(result.lecturers) == 0:
173+
raise ObjectNotFound(Lecturer, 'all')
169174
return result
170175

171176

rating_api/schemas/models.py

Lines changed: 71 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import datetime
2+
from typing import List
23
from uuid import UUID
34

4-
from pydantic import field_validator
5+
from fastapi import Query
6+
from fastapi_filter.contrib.sqlalchemy import Filter
7+
from pydantic import ValidationInfo, field_validator
58

69
from rating_api.exceptions import WrongMark
7-
from rating_api.models import ReviewStatus
10+
from rating_api.models import Lecturer, ReviewStatus
811
from rating_api.schemas.base import Base
912

1013

@@ -24,57 +27,13 @@ class CommentGet(Base):
2427
dislike_count: int
2528

2629

27-
class CommentGetWithStatus(Base):
28-
uuid: UUID
29-
user_id: int | None = None
30-
create_ts: datetime.datetime
31-
update_ts: datetime.datetime
32-
subject: str | None = None
33-
text: str
34-
mark_kindness: int
35-
mark_freebie: int
36-
mark_clarity: int
37-
mark_general: float
38-
lecturer_id: int
30+
class CommentGetWithStatus(CommentGet):
3931
review_status: ReviewStatus
40-
like_count: int
41-
dislike_count: int
4232

4333

44-
class CommentGetWithAllInfo(Base):
45-
uuid: UUID
46-
user_id: int | None = None
47-
create_ts: datetime.datetime
48-
update_ts: datetime.datetime
49-
subject: str | None = None
50-
text: str
51-
mark_kindness: int
52-
mark_freebie: int
53-
mark_clarity: int
54-
mark_general: float
55-
lecturer_id: int
34+
class CommentGetWithAllInfo(CommentGet):
5635
review_status: ReviewStatus
5736
approved_by: int | None = None
58-
like_count: int
59-
dislike_count: int
60-
61-
62-
class CommentPost(Base):
63-
subject: str
64-
text: str
65-
create_ts: datetime.datetime | None = None
66-
update_ts: datetime.datetime | None = None
67-
mark_kindness: int
68-
mark_freebie: int
69-
mark_clarity: int
70-
is_anonymous: bool = True
71-
72-
@field_validator('mark_kindness', 'mark_freebie', 'mark_clarity')
73-
@classmethod
74-
def validate_mark(cls, value):
75-
if value not in [-2, -1, 0, 1, 2]:
76-
raise WrongMark()
77-
return value
7837

7938

8039
class CommentUpdate(Base):
@@ -92,22 +51,16 @@ def validate_mark(cls, value):
9251
return value
9352

9453

95-
class CommentImport(Base):
96-
lecturer_id: int
97-
subject: str | None = None
98-
text: str
54+
class CommentPost(CommentUpdate):
9955
create_ts: datetime.datetime | None = None
10056
update_ts: datetime.datetime | None = None
101-
mark_kindness: int
102-
mark_freebie: int
103-
mark_clarity: int
57+
is_anonymous: bool = True
10458

105-
@field_validator('mark_kindness', 'mark_freebie', 'mark_clarity')
106-
@classmethod
107-
def validate_mark(cls, value):
108-
if value not in [-2, -1, 0, 1, 2]:
109-
raise WrongMark()
110-
return value
59+
60+
class CommentImport(CommentUpdate):
61+
lecturer_id: int
62+
create_ts: datetime.datetime | None = None
63+
update_ts: datetime.datetime | None = None
11164

11265

11366
class CommentImportAll(Base):
@@ -123,16 +76,10 @@ class CommentGetAll(Base):
12376

12477
class CommentGetAllWithStatus(Base):
12578
comments: list[CommentGetWithStatus] = []
126-
limit: int
127-
offset: int
128-
total: int
12979

13080

13181
class CommentGetAllWithAllInfo(Base):
13282
comments: list[CommentGetWithAllInfo] = []
133-
limit: int
134-
offset: int
135-
total: int
13683

13784

13885
class LecturerUserCommentPost(Base):
@@ -171,9 +118,63 @@ class LecturerPost(Base):
171118
timetable_id: int | None = None
172119

173120

174-
class LecturerPatch(Base):
121+
class LecturerPatch(LecturerPost):
175122
first_name: str | None = None
176123
last_name: str | None = None
177124
middle_name: str | None = None
178-
avatar_link: str | None = None
179-
timetable_id: int | None = None
125+
126+
127+
class LecturersFilter(Filter):
128+
subject: str = ''
129+
name: str = ''
130+
order_by: List[str] = [
131+
'mark_weighted',
132+
]
133+
134+
@field_validator("*", mode="before", check_fields=False)
135+
def validate_order_by(cls, value, field: ValidationInfo):
136+
return value
137+
138+
@field_validator('order_by', mode='before')
139+
def check_order_param(cls, value: str) -> str:
140+
"""Проверяет, что значение поля (без +/-) входит в список возможных."""
141+
allowed_ordering = {
142+
"mark_weighted",
143+
"mark_kindness",
144+
"mark_freebie",
145+
"mark_clarity",
146+
"mark_general",
147+
"last_name",
148+
}
149+
cleaned_value = value.replace("+", "").replace("-", "")
150+
if cleaned_value in allowed_ordering:
151+
return value
152+
else:
153+
raise ValueError(f'"order_by"-field must contain value from {allowed_ordering}.')
154+
155+
def filter(self, query: Query) -> Query:
156+
if self.subject:
157+
query = query.filter(self.Constants.model.search_by_subject(self.subject))
158+
if self.name:
159+
query = query.filter(self.Constants.model.search_by_name(self.name))
160+
return query
161+
162+
def sort(self, query: Query) -> Query:
163+
if not self.ordering_values:
164+
return query
165+
elif len(self.ordering_values) > 1:
166+
raise ValueError('order_by (хотя бы пока что) поддерживает лишь один параметр для сортировки!')
167+
168+
for field_name in self.ordering_values:
169+
direction = True
170+
if field_name.startswith("-"):
171+
direction = False
172+
field_name = field_name.replace("-", "").replace("+", "")
173+
if field_name.startswith('mark_'):
174+
query = query.order_by(*self.Constants.model.order_by_mark(field_name, direction))
175+
else:
176+
query = query.order_by(*self.Constants.model.order_by_name(field_name, direction))
177+
return query
178+
179+
class Constants(Filter.Constants):
180+
model = Lecturer

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ auth-lib-profcomff[fastapi]
33
aiohttp
44
fastapi
55
fastapi-sqlalchemy
6+
fastapi-filter[sqlalchemy]
67
gunicorn
78
logging-profcomff
89
psycopg2-binary

tests/conftest.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ def lecturers_with_comments(dbsession, lecturers):
221221
with 6 comments to non-deleted lecturers 4 approved and one dismissed and one pending.
222222
Two of them have alike names.
223223
Two of them have a different user_id.
224+
One of them have a different subject.
224225
"""
225226
comments_data = [
226227
(lecturers[0].id, 9990, 'test_subject', ReviewStatus.APPROVED, 1, 1, 1),
@@ -240,7 +241,7 @@ def lecturers_with_comments(dbsession, lecturers):
240241
(lecturers[2].id, 9990, 'test_subject2', ReviewStatus.DISMISSED, 2, 2, 2),
241242
(lecturers[2].id, 9990, 'test_subject2', ReviewStatus.PENDING, -2, -2, -2),
242243
(lecturers[2].id, 9991, 'test_subject11', ReviewStatus.APPROVED, 1, 1, 1),
243-
(lecturers[2].id, 9992, 'test_subject12', ReviewStatus.APPROVED, 0, 0, 0),
244+
(lecturers[2].id, 9992, 'test_subject13', ReviewStatus.APPROVED, 0, 0, 0),
244245
]
245246

246247
comments = []

0 commit comments

Comments
 (0)