From 1e360c1d3b512222af0f2f56485624eaa80a8015 Mon Sep 17 00:00:00 2001 From: pivovarovma Date: Wed, 9 Apr 2025 19:29:18 +0000 Subject: [PATCH 1/5] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20approved=5Fby?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../versions/8206267a15ff_approved_by.py | 29 +++++++++++++++++++ rating_api/models/db.py | 1 + rating_api/routes/comment.py | 6 ++-- rating_api/schemas/models.py | 2 ++ tests/test_routes/test_comment.py | 4 +-- 5 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 migrations/versions/8206267a15ff_approved_by.py diff --git a/migrations/versions/8206267a15ff_approved_by.py b/migrations/versions/8206267a15ff_approved_by.py new file mode 100644 index 0000000..f66dcad --- /dev/null +++ b/migrations/versions/8206267a15ff_approved_by.py @@ -0,0 +1,29 @@ +"""approved_by + +Revision ID: 8206267a15ff +Revises: 5cf69f1026d9 +Create Date: 2025-04-09 18:30:18.038265 + +""" + +import sqlalchemy as sa +from alembic import op + + +# revision identifiers, used by Alembic. +revision = '8206267a15ff' +down_revision = '5cf69f1026d9' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('comment', sa.Column('approved_by', sa.Integer(), nullable=False)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('comment', 'approved_by') + # ### end Alembic commands ### diff --git a/rating_api/models/db.py b/rating_api/models/db.py index 61b6c63..ca4ca5b 100644 --- a/rating_api/models/db.py +++ b/rating_api/models/db.py @@ -109,6 +109,7 @@ class Comment(BaseDbModel): mark_kindness: Mapped[int] = mapped_column(Integer, nullable=False) mark_freebie: Mapped[int] = mapped_column(Integer, nullable=False) mark_clarity: Mapped[int] = mapped_column(Integer, nullable=False) + approved_by: Mapped[int] = mapped_column(Integer, nullable=False) lecturer_id: Mapped[int] = mapped_column(Integer, ForeignKey("lecturer.id")) lecturer: Mapped[Lecturer] = relationship( "Lecturer", diff --git a/rating_api/routes/comment.py b/rating_api/routes/comment.py index c973ddd..5d155ee 100644 --- a/rating_api/routes/comment.py +++ b/rating_api/routes/comment.py @@ -235,8 +235,8 @@ async def get_comments( @comment.patch("/{uuid}/review", response_model=CommentGet) async def review_comment( uuid: UUID, + user=Depends(UnionAuth(scopes=["rating.comment.review"], auto_error=False, allow_none=True)), review_status: Literal[ReviewStatus.APPROVED, ReviewStatus.DISMISSED] = ReviewStatus.DISMISSED, - _=Depends(UnionAuth(scopes=["rating.comment.review"], allow_none=False, auto_error=True)), ) -> CommentGet: """ Scopes: `["rating.comment.review"]` @@ -251,7 +251,9 @@ async def review_comment( if not check_comment: raise ObjectNotFound(Comment, uuid) - return CommentGet.model_validate(Comment.update(session=db.session, id=uuid, review_status=review_status)) + return CommentGet.model_validate( + Comment.update(session=db.session, id=uuid, review_status=review_status, approved_by=user.get("id")) + ) @comment.patch("/{uuid}", response_model=CommentGet) diff --git a/rating_api/schemas/models.py b/rating_api/schemas/models.py index c591fdf..8efe254 100644 --- a/rating_api/schemas/models.py +++ b/rating_api/schemas/models.py @@ -20,6 +20,7 @@ class CommentGet(Base): mark_clarity: int mark_general: float lecturer_id: int + approved_by: int class CommentGetWithStatus(Base): @@ -35,6 +36,7 @@ class CommentGetWithStatus(Base): mark_general: float lecturer_id: int review_status: ReviewStatus + approved_by: int class CommentPost(Base): diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index 69292a4..e61ec58 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -172,7 +172,7 @@ }, 0, status.HTTP_200_OK, - ), + ) ], ) def test_create_comment(client, dbsession, lecturers, body, lecturer_n, response_status): @@ -243,7 +243,7 @@ def test_comments_by_user_id(client, lecturers_with_comments, user_id, response_ @pytest.mark.parametrize( - 'review_status, response_status,is_reviewed', + 'review_status, response_status, is_reviewed', [ ("approved", status.HTTP_200_OK, True), ("approved", status.HTTP_200_OK, False), From bd3e4e78415d14dfe18bf454236f74a5b4a1663f Mon Sep 17 00:00:00 2001 From: pivovarovma Date: Sat, 12 Apr 2025 07:57:54 +0000 Subject: [PATCH 2/5] =?UTF-8?q?=D0=91=D1=8B=D0=BB=D0=B8=20=D1=81=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D0=B0=D0=BD=D1=8B=20=D1=81=D0=BE=D0=BE=D1=82=D1=81?= =?UTF-8?q?=D0=B2=D0=B5=D1=82=D1=81=D0=B2=D1=83=D1=8E=D1=89=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/review_code.py | 390 ++++++++---------- ...oved_by.py => dd44854aa12a_approved_by.py} | 6 +- rating_api/routes/comment.py | 16 +- rating_api/schemas/models.py | 16 +- tests/test_routes/test_comment.py | 2 +- 5 files changed, 212 insertions(+), 218 deletions(-) rename migrations/versions/{8206267a15ff_approved_by.py => dd44854aa12a_approved_by.py} (86%) diff --git a/.github/workflows/review_code.py b/.github/workflows/review_code.py index b53793f..8b7a69c 100644 --- a/.github/workflows/review_code.py +++ b/.github/workflows/review_code.py @@ -1,11 +1,13 @@ +import json import os +import re +import subprocess import sys -import json + import requests -import subprocess -import re from mistralai.client import MistralClient + client = MistralClient(api_key=os.environ.get("MISTRAL_API_KEY")) pr_number = os.environ.get("PR_NUMBER") @@ -14,28 +16,28 @@ base_sha = os.environ.get("BASE_SHA") head_sha = os.environ.get("HEAD_SHA") -result = subprocess.run( - f"git diff --name-only {base_sha} {head_sha}", - shell=True, - capture_output=True, - text=True -) -files = [f for f in result.stdout.strip().split("\n") if f.endswith(('.py', '.js', '.ts', '.go', '.java', '.cs', '.cpp', '.h', '.c'))] +result = subprocess.run(f"git diff --name-only {base_sha} {head_sha}", shell=True, capture_output=True, text=True) +files = [ + f + for f in result.stdout.strip().split("\n") + if f.endswith(('.py', '.js', '.ts', '.go', '.java', '.cs', '.cpp', '.h', '.c')) +] if not files: print("Нет файлов для ревью") sys.exit(0) + def parse_diff(diff_text): changes = [] current_hunk = None lines = diff_text.split('\n') file_path = None - + for line in lines: if line.startswith('diff --git'): file_path = line.split(' ')[2][2:] - + elif line.startswith('@@'): hunk_info = line.split('@@')[1].strip() matches = re.match(r'-(\d+)(?:,\d+)? \+(\d+)(?:,\d+)?', hunk_info) @@ -47,49 +49,45 @@ def parse_diff(diff_text): 'old_start': old_start, 'new_start': new_start, 'lines': [], - 'context': hunk_info + 'context': hunk_info, } changes.append(current_hunk) - + elif current_hunk is not None: current_hunk['lines'].append(line) - + return changes + def parse_line_comments(review_text): line_comments = [] - + pattern = r'СТРОКА (\d+)(?:-(\d+))?: (.*?)(?=\nСТРОКА|\n\n|$)' matches = re.finditer(pattern, review_text, re.DOTALL) - + for match in matches: start_line = int(match.group(1)) end_line = int(match.group(2)) if match.group(2) else start_line comment = match.group(3).strip() - - line_comments.append({ - 'start_line': start_line, - 'end_line': end_line, - 'comment': comment - }) - + + line_comments.append({'start_line': start_line, 'end_line': end_line, 'comment': comment}) + return line_comments + def get_commit_id(): commits_url = f"https://api.github.com/repos/{repository}/pulls/{pr_number}/commits" - headers = { - "Authorization": f"token {github_token}", - "Accept": "application/vnd.github.v3+json" - } - + headers = {"Authorization": f"token {github_token}", "Accept": "application/vnd.github.v3+json"} + response = requests.get(commits_url, headers=headers) if response.status_code == 200: commits = response.json() if commits: return commits[-1]['sha'] - + return head_sha + def extract_file_content(file_path): try: with open(file_path, 'r', encoding='utf-8') as f: @@ -98,205 +96,199 @@ def extract_file_content(file_path): print(f"Ошибка при чтении файла {file_path}: {e}") return [] + def get_diff_hunk_for_position(patch, position): lines = patch.split('\n') if 0 <= position < len(lines): start_idx = position while start_idx > 0 and not lines[start_idx].startswith('@@'): start_idx -= 1 - + if start_idx < 0 or not lines[start_idx].startswith('@@'): return None - + end_idx = position while end_idx < len(lines) and not (end_idx > position and lines[end_idx].startswith('@@')): end_idx += 1 - + hunk_lines = lines[start_idx:end_idx] return '\n'.join(hunk_lines) - + return None + def validate_position(patch, position): if position <= 0: return False - + lines = patch.split('\n') if position >= len(lines): return False - + diff_hunk = get_diff_hunk_for_position(patch, position) if not diff_hunk: return False - + if position < len(lines) and lines[position].startswith('-'): return False - + return True + def find_position_by_content(patch, content, line_num, vicinity=2): lines = patch.split('\n') content = content.strip() - + if not content: return None - + for i, line in enumerate(lines): if (line.startswith('+') or line.startswith(' ')) and content in line.strip(): if get_diff_hunk_for_position(patch, i): return i - + for i, line in enumerate(lines): if line.startswith('+') or line.startswith(' '): content_parts = content.split() if content_parts and any(part in line for part in content_parts if len(part) > 3): if get_diff_hunk_for_position(patch, i): return i - + return None + def create_review_with_comments(file_comments, commit_id): """Создает ревью с комментариями к конкретным строкам кода""" review_url = f"https://api.github.com/repos/{repository}/pulls/{pr_number}/reviews" - headers = { - "Authorization": f"token {github_token}", - "Accept": "application/vnd.github.v3+json" - } - + headers = {"Authorization": f"token {github_token}", "Accept": "application/vnd.github.v3+json"} + pr_url = f"https://api.github.com/repos/{repository}/pulls/{pr_number}" pr_response = requests.get(pr_url, headers=headers) pr_info = {} if pr_response.status_code == 200: pr_info = pr_response.json() - + files_url = f"https://api.github.com/repos/{repository}/pulls/{pr_number}/files" files_response = requests.get(files_url, headers=headers) pr_files = {} - + if files_response.status_code == 200: for file_info in files_response.json(): pr_files[file_info['filename']] = file_info - + review_comments = [] total_comments = 0 placed_comments = 0 - + file_first_positions = {} file_diff_hunks = {} - + for file_path, file_info in pr_files.items(): patch = file_info.get('patch', '') - + if patch: lines = patch.split('\n') if len(lines) > 0: file_first_positions[file_path] = 1 - + for i, line in enumerate(lines): if line.startswith('+'): file_first_positions[file_path] = i + 1 break - + file_diff_hunks[file_path] = get_diff_hunk_for_position(patch, file_first_positions[file_path]) else: file_first_positions[file_path] = 1 file_diff_hunks[file_path] = None - + for file_path, comments in file_comments.items(): total_comments += len(comments) - + print(f"Обрабатываем комментарии для файла: {file_path}") if file_path not in pr_files: print(f"Файл {file_path} не найден в PR") continue - + patch = pr_files[file_path].get('patch', '') - + if not patch: print(f"Отсутствует patch для файла {file_path}, добавляем комментарии в общий список") file_level_comments = [] for comment in comments: file_level_comments.append(f"**Комментарий к строке {comment['start_line']}**: {comment['comment']}") - + if file_level_comments: - review_comments.append({ - "path": file_path, - "position": 1, - "body": "\n\n".join(file_level_comments) - }) + review_comments.append({"path": file_path, "position": 1, "body": "\n\n".join(file_level_comments)}) placed_comments += 1 continue - + pr_files[file_path]['parsed_patch'] = patch - + diff_result = subprocess.run( - f"git diff {base_sha} {head_sha} -- {file_path}", - shell=True, - capture_output=True, - text=True + f"git diff {base_sha} {head_sha} -- {file_path}", shell=True, capture_output=True, text=True ) full_diff = diff_result.stdout - + file_content = extract_file_content(file_path) - + line_position_maps = {} - + line_position_map_git = {} line_num = 0 position = 0 for line in full_diff.split('\n'): position += 1 - + if line.startswith('@@'): hunk_info = line.split('@@')[1].strip() matches = re.match(r'-(\d+)(?:,\d+)? \+(\d+)(?:,\d+)?', hunk_info) if matches: line_num = int(matches.group(2)) - 1 - + if line.startswith('+'): line_num += 1 line_position_map_git[line_num] = position elif line.startswith(' '): line_num += 1 - + line_position_maps['git'] = line_position_map_git - + line_position_map_api = {} line_num = 0 position = 0 for line in patch.split('\n'): position += 1 - + if line.startswith('@@'): matches = re.match(r'-(\d+)(?:,\d+)? \+(\d+)(?:,\d+)?', line.split('@@')[1].strip()) if matches: line_num = int(matches.group(2)) - 1 - + if line.startswith('+'): line_num += 1 line_position_map_api[line_num] = position elif line.startswith(' '): line_num += 1 - + line_position_maps['api'] = line_position_map_api - + line_content_map = {} if file_content: for i, line in enumerate(file_content): - line_content_map[i+1] = line.strip() - + line_content_map[i + 1] = line.strip() + position_hunk_map = {} for pos in range(len(patch.split('\n'))): hunk = get_diff_hunk_for_position(patch, pos) if hunk: position_hunk_map[pos] = hunk - + file_level_comments = [] file_comments_added = 0 - + valid_positions = set() position_hunk_mapping = {} - + lines = patch.split('\n') for pos, line in enumerate(lines): if not line.startswith('-'): @@ -305,14 +297,14 @@ def create_review_with_comments(file_comments, commit_id): hunk = get_diff_hunk_for_position(patch, pos) if hunk: position_hunk_mapping[pos] = hunk - + for comment in comments: start_line = comment['start_line'] comment_body = comment['comment'] position_found = False position = None diff_hunk = None - + for map_name, position_map in line_position_maps.items(): if start_line in position_map: position = position_map[start_line] @@ -320,30 +312,36 @@ def create_review_with_comments(file_comments, commit_id): diff_hunk = position_hunk_mapping.get(position) if diff_hunk: position_found = True - print(f"Найдена позиция для строки {start_line} в карте {map_name}: {position} с валидным diff_hunk") + print( + f"Найдена позиция для строки {start_line} в карте {map_name}: {position} с валидным diff_hunk" + ) break else: - print(f"Найдена позиция {position} для строки {start_line} в карте {map_name}, но она невалидна") - + print( + f"Найдена позиция {position} для строки {start_line} в карте {map_name}, но она невалидна" + ) + if not position_found and file_content and 0 < start_line <= len(file_content): target_line = file_content[start_line - 1].rstrip() context_line = target_line.strip() - + if context_line: position = find_position_by_content(patch, context_line, start_line) if position is not None and position in valid_positions: diff_hunk = position_hunk_mapping.get(position) if diff_hunk: position_found = True - print(f"Найдена позиция для строки {start_line} через точное совпадение контекста: {position} с валидным diff_hunk") - + print( + f"Найдена позиция для строки {start_line} через точное совпадение контекста: {position} с валидным diff_hunk" + ) + if not position_found: context_lines = [] for offset in range(-5, 6): idx = start_line - 1 + offset if 0 <= idx < len(file_content): context_lines.append(file_content[idx].strip()) - + for i, context in enumerate(context_lines): if context and offset != 0: position = find_position_by_content(patch, context, start_line - 5 + i) @@ -351,14 +349,16 @@ def create_review_with_comments(file_comments, commit_id): diff_hunk = position_hunk_mapping.get(position) if diff_hunk: position_found = True - print(f"Найдена позиция для строки {start_line} через окружающий контекст (строка {start_line - 5 + i}): {position} с валидным diff_hunk") + print( + f"Найдена позиция для строки {start_line} через окружающий контекст (строка {start_line - 5 + i}): {position} с валидным diff_hunk" + ) break - + if not position_found and valid_positions: nearest_line = None nearest_position = None min_distance = float('inf') - + for line_num, pos in line_position_map_api.items(): if pos in valid_positions: distance = abs(line_num - start_line) @@ -366,14 +366,16 @@ def create_review_with_comments(file_comments, commit_id): min_distance = distance nearest_line = line_num nearest_position = pos - + if nearest_position and min_distance <= 5: position = nearest_position diff_hunk = position_hunk_mapping.get(position) if diff_hunk: position_found = True - print(f"Найдена ближайшая валидная позиция для строки {start_line} (строка {nearest_line}): {position} с diff_hunk") - + print( + f"Найдена ближайшая валидная позиция для строки {start_line} (строка {nearest_line}): {position} с diff_hunk" + ) + if not position_found: for pos in sorted(valid_positions): diff_hunk = position_hunk_mapping.get(pos) @@ -382,46 +384,47 @@ def create_review_with_comments(file_comments, commit_id): position_found = True print(f"Используем первую валидную позицию {position} для строки {start_line} с diff_hunk") break - + if position_found and position is not None and diff_hunk: - review_comments.append({ - "path": file_path, - "position": position, - "body": comment_body, - "diff_hunk": diff_hunk - }) + review_comments.append( + {"path": file_path, "position": position, "body": comment_body, "diff_hunk": diff_hunk} + ) placed_comments += 1 file_comments_added += 1 print(f"✅ Успешно определена позиция {position} с diff_hunk для строки {start_line}") else: - print(f"❌ Не удалось определить валидную позицию для строки {start_line} в файле {file_path}, добавлен комментарий к файлу") + print( + f"❌ Не удалось определить валидную позицию для строки {start_line} в файле {file_path}, добавлен комментарий к файлу" + ) file_level_comments.append(f"**Комментарий к строке {start_line}**: {comment_body}") - + if file_level_comments: if file_comments_added == 0: first_position = file_first_positions.get(file_path, 1) first_hunk = file_diff_hunks.get(file_path) - + comment_data = { "path": file_path, "position": first_position, - "body": "# Комментарии к файлу\n\n" + "\n\n".join(file_level_comments) + "body": "# Комментарии к файлу\n\n" + "\n\n".join(file_level_comments), } - + if first_hunk: comment_data["diff_hunk"] = first_hunk - + review_comments.append(comment_data) placed_comments += 1 else: for comment in review_comments: if comment["path"] == file_path: - comment["body"] = comment["body"] + "\n\n# Дополнительные комментарии\n\n" + "\n\n".join(file_level_comments) + comment["body"] = ( + comment["body"] + "\n\n# Дополнительные комментарии\n\n" + "\n\n".join(file_level_comments) + ) break - + print(f"Всего комментариев: {total_comments}") print(f"Размещено комментариев: {placed_comments}") - + if not review_comments: print("Нет комментариев для добавления") return False @@ -430,13 +433,15 @@ def create_review_with_comments(file_comments, commit_id): for comment in review_comments: if "path" not in comment or "position" not in comment or comment["position"] is None: - print(f"Пропускаем невалидный комментарий к файлу {comment.get('path', 'неизвестный')}: отсутствует позиция") + print( + f"Пропускаем невалидный комментарий к файлу {comment.get('path', 'неизвестный')}: отсутствует позиция" + ) continue - + if comment["path"] not in pr_files: print(f"Пропускаем невалидный комментарий к файлу {comment['path']}: файл не найден в PR") continue - + if "diff_hunk" not in comment and comment["path"] in pr_files and pr_files[comment["path"]].get('patch'): hunk = get_diff_hunk_for_position(pr_files[comment["path"]]['parsed_patch'], comment["position"]) if hunk: @@ -444,63 +449,49 @@ def create_review_with_comments(file_comments, commit_id): else: print(f"Пропускаем комментарий к файлу {comment['path']}: не удалось найти diff_hunk") continue - + if "diff_hunk" in comment: del comment["diff_hunk"] - + valid_review_comments.append(comment) - + if not valid_review_comments: print("После валидации не осталось валидных комментариев, создаем общий комментарий") summary = "# Комментарии к коду\n\n" - + for file_path, comments in file_comments.items(): summary += f"## Файл: {file_path}\n\n" for comment in comments: summary += f"**Строка {comment['start_line']}**: {comment['comment']}\n\n" summary += "---\n\n" - - review_data = { - "commit_id": commit_id, - "event": "COMMENT", - "body": summary - } - + + review_data = {"commit_id": commit_id, "event": "COMMENT", "body": summary} + response = requests.post( - f"https://api.github.com/repos/{repository}/pulls/{pr_number}/reviews", - headers=headers, - json=review_data + f"https://api.github.com/repos/{repository}/pulls/{pr_number}/reviews", headers=headers, json=review_data ) - + if response.status_code not in [200, 201]: print(f"Ошибка при создании общего комментария: {response.status_code} - {response.text}") return False else: print("Общий комментарий к PR успешно создан.") return True - - review_data = { - "commit_id": commit_id, - "event": "COMMENT", - "comments": valid_review_comments - } - + + review_data = {"commit_id": commit_id, "event": "COMMENT", "comments": valid_review_comments} + print(f"Отправляем запрос на создание ревью с {len(valid_review_comments)} комментариями") for i, comment in enumerate(valid_review_comments): print(f"Комментарий {i+1}: файл={comment['path']}, позиция={comment['position']}") - + if len(valid_review_comments) > 3: print("Много комментариев, отправляем по одному для увеличения вероятности успеха") successful_comments = 0 failed_comments = [] - + for i, comment in enumerate(valid_review_comments): - single_review_data = { - "commit_id": commit_id, - "event": "COMMENT", - "comments": [comment] - } - + single_review_data = {"commit_id": commit_id, "event": "COMMENT", "comments": [comment]} + single_response = requests.post(review_url, headers=headers, json=single_review_data) if single_response.status_code in [200, 201]: successful_comments += 1 @@ -508,58 +499,52 @@ def create_review_with_comments(file_comments, commit_id): else: failed_comments.append(comment) print(f"Ошибка при создании комментария {i+1}: {single_response.status_code} - {single_response.text}") - + if successful_comments > 0: print(f"Успешно создано {successful_comments} из {len(valid_review_comments)} комментариев") - + if failed_comments: print(f"Создаем общий комментарий для {len(failed_comments)} неудачных комментариев") summary = "# Дополнительные комментарии\n\n" - + for comment in failed_comments: file_path = comment.get("path", "неизвестный файл") body = comment.get("body", "") summary += f"## Файл: {file_path}\n\n{body}\n\n---\n\n" - - review_data = { - "commit_id": commit_id, - "event": "COMMENT", - "body": summary - } - + + review_data = {"commit_id": commit_id, "event": "COMMENT", "body": summary} + response = requests.post( f"https://api.github.com/repos/{repository}/pulls/{pr_number}/reviews", headers=headers, - json=review_data + json=review_data, ) - + if response.status_code not in [200, 201]: - print(f"Ошибка при создании общего комментария для неудачных комментариев: {response.status_code} - {response.text}") + print( + f"Ошибка при создании общего комментария для неудачных комментариев: {response.status_code} - {response.text}" + ) else: print("Общий комментарий для неудачных комментариев успешно создан.") - + return True else: print("Не удалось создать ни один комментарий, создаем общий комментарий") summary = "# Комментарии к коду\n\n" - + for comment in valid_review_comments: file_path = comment.get("path", "неизвестный файл") body = comment.get("body", "") summary += f"## Файл: {file_path}\n\n{body}\n\n---\n\n" - - review_data = { - "commit_id": commit_id, - "event": "COMMENT", - "body": summary - } - + + review_data = {"commit_id": commit_id, "event": "COMMENT", "body": summary} + response = requests.post( f"https://api.github.com/repos/{repository}/pulls/{pr_number}/reviews", headers=headers, - json=review_data + json=review_data, ) - + if response.status_code not in [200, 201]: print(f"Ошибка при создании общего комментария: {response.status_code} - {response.text}") return False @@ -570,62 +555,56 @@ def create_review_with_comments(file_comments, commit_id): response = requests.post(review_url, headers=headers, json=review_data) if response.status_code not in [200, 201]: print(f"Ошибка при создании ревью: {response.status_code} - {response.text}") - + print("Пробуем создать общий комментарий к PR...") summary = "# Комментарии к коду\n\n" - + for comment in valid_review_comments: file_path = comment.get("path", "неизвестный файл") body = comment.get("body", "") summary += f"## Файл: {file_path}\n\n{body}\n\n---\n\n" - - review_data = { - "commit_id": commit_id, - "event": "COMMENT", - "body": summary - } - + + review_data = {"commit_id": commit_id, "event": "COMMENT", "body": summary} + response = requests.post( f"https://api.github.com/repos/{repository}/pulls/{pr_number}/reviews", headers=headers, - json=review_data + json=review_data, ) - + if response.status_code not in [200, 201]: print(f"Ошибка при создании общего комментария: {response.status_code} - {response.text}") return False else: print("Общий комментарий к PR успешно создан.") return True - + return False - + print(f"Ревью успешно создано с {len(valid_review_comments)} комментариями") return True + all_file_comments = {} full_review = "## Ревью кода с помощью Mistral AI\n\n" for file_path in files: if not os.path.exists(file_path): continue - + diff_result = subprocess.run( - f"git diff {base_sha} {head_sha} -- {file_path}", - shell=True, - capture_output=True, - text=True + f"git diff {base_sha} {head_sha} -- {file_path}", shell=True, capture_output=True, text=True ) diff = diff_result.stdout - + if not diff.strip(): continue - + changes = parse_diff(diff) - + if not changes: continue - + prompt = f"""# Задача: Экспертное ревью кода для Pull Request ## Файл для анализа @@ -723,21 +702,16 @@ def create_review_with_comments(file_comments, commit_id): Добавь 2-3 предложения с пояснением оценки и общими рекомендациями. """ - + try: - chat_response = client.chat( - model="mistral-large-latest", - messages=[ - {"role": "user", "content": prompt} - ] - ) - + chat_response = client.chat(model="mistral-large-latest", messages=[{"role": "user", "content": prompt}]) + review_text = chat_response.choices[0].message.content - + line_comments = parse_line_comments(review_text) if line_comments: all_file_comments[file_path] = line_comments - + full_review += f"### Ревью для файла: `{file_path}`\n\n{review_text}\n\n---\n\n" except Exception as e: print(f"Ошибка при анализе {file_path}: {e}") @@ -750,4 +724,4 @@ def create_review_with_comments(file_comments, commit_id): commit_id = get_commit_id() create_review_with_comments(all_file_comments, commit_id) else: - print("Не найдено комментариев к строкам кода") + print("Не найдено комментариев к строкам кода") diff --git a/migrations/versions/8206267a15ff_approved_by.py b/migrations/versions/dd44854aa12a_approved_by.py similarity index 86% rename from migrations/versions/8206267a15ff_approved_by.py rename to migrations/versions/dd44854aa12a_approved_by.py index f66dcad..d50b9f4 100644 --- a/migrations/versions/8206267a15ff_approved_by.py +++ b/migrations/versions/dd44854aa12a_approved_by.py @@ -1,8 +1,8 @@ """approved_by -Revision ID: 8206267a15ff +Revision ID: dd44854aa12a Revises: 5cf69f1026d9 -Create Date: 2025-04-09 18:30:18.038265 +Create Date: 2025-04-12 07:55:31.393429 """ @@ -11,7 +11,7 @@ # revision identifiers, used by Alembic. -revision = '8206267a15ff' +revision = 'dd44854aa12a' down_revision = '5cf69f1026d9' branch_labels = None depends_on = None diff --git a/rating_api/routes/comment.py b/rating_api/routes/comment.py index 5d155ee..7948dfe 100644 --- a/rating_api/routes/comment.py +++ b/rating_api/routes/comment.py @@ -23,6 +23,7 @@ CommentGet, CommentGetAll, CommentGetAllWithStatus, + CommentGetWithAllInfo, CommentGetWithStatus, CommentImportAll, CommentPost, @@ -232,10 +233,10 @@ async def get_comments( return result -@comment.patch("/{uuid}/review", response_model=CommentGet) +@comment.patch("/{uuid}/review", response_model=Union[CommentGetAll, CommentGetWithAllInfo]) async def review_comment( uuid: UUID, - user=Depends(UnionAuth(scopes=["rating.comment.review"], auto_error=False, allow_none=True)), + user=Depends(UnionAuth(scopes=["rating.comment.review"], auto_error=True, allow_none=True)), review_status: Literal[ReviewStatus.APPROVED, ReviewStatus.DISMISSED] = ReviewStatus.DISMISSED, ) -> CommentGet: """ @@ -251,9 +252,14 @@ async def review_comment( if not check_comment: raise ObjectNotFound(Comment, uuid) - return CommentGet.model_validate( - Comment.update(session=db.session, id=uuid, review_status=review_status, approved_by=user.get("id")) - ) + if "rating.comment.review" in [scope['name'] for scope in user.get('session_scopes')]: + result = CommentGetWithAllInfo.model_validate( + Comment.update(session=db.session, id=uuid, review_status=review_status, approved_by=user.get("id")) + ) + else: + result = CommentGet.model_validate(Comment.update(session=db.session, id=uuid, review_status=review_status)) + + return result @comment.patch("/{uuid}", response_model=CommentGet) diff --git a/rating_api/schemas/models.py b/rating_api/schemas/models.py index 8efe254..5c35d62 100644 --- a/rating_api/schemas/models.py +++ b/rating_api/schemas/models.py @@ -20,7 +20,6 @@ class CommentGet(Base): mark_clarity: int mark_general: float lecturer_id: int - approved_by: int class CommentGetWithStatus(Base): @@ -36,6 +35,21 @@ class CommentGetWithStatus(Base): mark_general: float lecturer_id: int review_status: ReviewStatus + + +class CommentGetWithAllInfo(Base): + uuid: UUID + user_id: int | None = None + create_ts: datetime.datetime + update_ts: datetime.datetime + subject: str | None = None + text: str + mark_kindness: int + mark_freebie: int + mark_clarity: int + mark_general: float + lecturer_id: int + review_status: ReviewStatus approved_by: int diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index e61ec58..eaeaae4 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -172,7 +172,7 @@ }, 0, status.HTTP_200_OK, - ) + ), ], ) def test_create_comment(client, dbsession, lecturers, body, lecturer_n, response_status): From e4d332434dca77194a769870f3c04a472e02a65c Mon Sep 17 00:00:00 2001 From: pivovarovma Date: Sat, 12 Apr 2025 08:29:50 +0000 Subject: [PATCH 3/5] =?UTF-8?q?=D0=95=D1=89=D0=B5=20=D0=BE=D0=B4=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=BF=D0=BE=D1=80=D1=86=D0=B8=D1=8F=20=D0=B8=D0=B7?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...oved_by.py => 572423875c05_approved_by.py} | 14 ++++++------ rating_api/models/db.py | 2 +- rating_api/routes/comment.py | 22 +++++++------------ rating_api/schemas/models.py | 9 +++++++- 4 files changed, 24 insertions(+), 23 deletions(-) rename migrations/versions/{dd44854aa12a_approved_by.py => 572423875c05_approved_by.py} (54%) diff --git a/migrations/versions/dd44854aa12a_approved_by.py b/migrations/versions/572423875c05_approved_by.py similarity index 54% rename from migrations/versions/dd44854aa12a_approved_by.py rename to migrations/versions/572423875c05_approved_by.py index d50b9f4..289e9fd 100644 --- a/migrations/versions/dd44854aa12a_approved_by.py +++ b/migrations/versions/572423875c05_approved_by.py @@ -1,8 +1,8 @@ """approved_by -Revision ID: dd44854aa12a -Revises: 5cf69f1026d9 -Create Date: 2025-04-12 07:55:31.393429 +Revision ID: 572423875c05 +Revises: dd44854aa12a +Create Date: 2025-04-12 08:24:55.812306 """ @@ -11,19 +11,19 @@ # revision identifiers, used by Alembic. -revision = 'dd44854aa12a' -down_revision = '5cf69f1026d9' +revision = '572423875c05' +down_revision = 'dd44854aa12a' branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('comment', sa.Column('approved_by', sa.Integer(), nullable=False)) + op.alter_column('comment', 'approved_by', existing_type=sa.INTEGER(), nullable=True) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('comment', 'approved_by') + op.alter_column('comment', 'approved_by', existing_type=sa.INTEGER(), nullable=False) # ### end Alembic commands ### diff --git a/rating_api/models/db.py b/rating_api/models/db.py index ca4ca5b..2f01068 100644 --- a/rating_api/models/db.py +++ b/rating_api/models/db.py @@ -109,7 +109,7 @@ class Comment(BaseDbModel): mark_kindness: Mapped[int] = mapped_column(Integer, nullable=False) mark_freebie: Mapped[int] = mapped_column(Integer, nullable=False) mark_clarity: Mapped[int] = mapped_column(Integer, nullable=False) - approved_by: Mapped[int] = mapped_column(Integer, nullable=False) + approved_by: Mapped[int] = mapped_column(Integer, nullable=True) lecturer_id: Mapped[int] = mapped_column(Integer, ForeignKey("lecturer.id")) lecturer: Mapped[Lecturer] = relationship( "Lecturer", diff --git a/rating_api/routes/comment.py b/rating_api/routes/comment.py index 7948dfe..8ca1aa2 100644 --- a/rating_api/routes/comment.py +++ b/rating_api/routes/comment.py @@ -22,9 +22,8 @@ from rating_api.schemas.models import ( CommentGet, CommentGetAll, - CommentGetAllWithStatus, + CommentGetAllWithAllInfo, CommentGetWithAllInfo, - CommentGetWithStatus, CommentImportAll, CommentPost, CommentUpdate, @@ -166,7 +165,7 @@ async def get_comment(uuid: UUID) -> CommentGet: return CommentGet.model_validate(comment) -@comment.get("", response_model=Union[CommentGetAll, CommentGetAllWithStatus]) +@comment.get("", response_model=Union[CommentGetAll, CommentGetAllWithAllInfo]) async def get_comments( limit: int = 10, offset: int = 0, @@ -197,8 +196,8 @@ async def get_comments( if not comments: raise ObjectNotFound(Comment, 'all') if "rating.comment.review" in [scope['name'] for scope in user.get('session_scopes')] or user.get('id') == user_id: - result = CommentGetAllWithStatus(limit=limit, offset=offset, total=len(comments)) - comment_validator = CommentGetWithStatus + result = CommentGetAllWithAllInfo(limit=limit, offset=offset, total=len(comments)) + comment_validator = CommentGetWithAllInfo else: result = CommentGetAll(limit=limit, offset=offset, total=len(comments)) comment_validator = CommentGet @@ -233,7 +232,7 @@ async def get_comments( return result -@comment.patch("/{uuid}/review", response_model=Union[CommentGetAll, CommentGetWithAllInfo]) +@comment.patch("/{uuid}/review", response_model=CommentGet) async def review_comment( uuid: UUID, user=Depends(UnionAuth(scopes=["rating.comment.review"], auto_error=True, allow_none=True)), @@ -252,14 +251,9 @@ async def review_comment( if not check_comment: raise ObjectNotFound(Comment, uuid) - if "rating.comment.review" in [scope['name'] for scope in user.get('session_scopes')]: - result = CommentGetWithAllInfo.model_validate( - Comment.update(session=db.session, id=uuid, review_status=review_status, approved_by=user.get("id")) - ) - else: - result = CommentGet.model_validate(Comment.update(session=db.session, id=uuid, review_status=review_status)) - - return result + return CommentGet.model_validate( + Comment.update(session=db.session, id=uuid, review_status=review_status, approved_by=user.get("id")) + ) @comment.patch("/{uuid}", response_model=CommentGet) diff --git a/rating_api/schemas/models.py b/rating_api/schemas/models.py index 5c35d62..af01659 100644 --- a/rating_api/schemas/models.py +++ b/rating_api/schemas/models.py @@ -50,7 +50,7 @@ class CommentGetWithAllInfo(Base): mark_general: float lecturer_id: int review_status: ReviewStatus - approved_by: int + approved_by: int | None = None class CommentPost(Base): @@ -122,6 +122,13 @@ class CommentGetAllWithStatus(Base): total: int +class CommentGetAllWithAllInfo(Base): + comments: list[CommentGetWithAllInfo] = [] + limit: int + offset: int + total: int + + class LecturerUserCommentPost(Base): lecturer_id: int user_id: int From bad9e9cc8b1e47258b0db0179258bb1082c60580 Mon Sep 17 00:00:00 2001 From: pivovarovma Date: Sat, 12 Apr 2025 09:12:01 +0000 Subject: [PATCH 4/5] =?UTF-8?q?=D0=95=D1=89=D0=B5=20=D0=BD=D0=B5=D0=BC?= =?UTF-8?q?=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../versions/572423875c05_approved_by.py | 29 ------------------- .../versions/dd44854aa12a_approved_by.py | 24 +++++++++++++++ rating_api/routes/comment.py | 9 ++++-- 3 files changed, 31 insertions(+), 31 deletions(-) delete mode 100644 migrations/versions/572423875c05_approved_by.py create mode 100644 migrations/versions/dd44854aa12a_approved_by.py diff --git a/migrations/versions/572423875c05_approved_by.py b/migrations/versions/572423875c05_approved_by.py deleted file mode 100644 index 289e9fd..0000000 --- a/migrations/versions/572423875c05_approved_by.py +++ /dev/null @@ -1,29 +0,0 @@ -"""approved_by - -Revision ID: 572423875c05 -Revises: dd44854aa12a -Create Date: 2025-04-12 08:24:55.812306 - -""" - -import sqlalchemy as sa -from alembic import op - - -# revision identifiers, used by Alembic. -revision = '572423875c05' -down_revision = 'dd44854aa12a' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('comment', 'approved_by', existing_type=sa.INTEGER(), nullable=True) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('comment', 'approved_by', existing_type=sa.INTEGER(), nullable=False) - # ### end Alembic commands ### diff --git a/migrations/versions/dd44854aa12a_approved_by.py b/migrations/versions/dd44854aa12a_approved_by.py new file mode 100644 index 0000000..c48d3e1 --- /dev/null +++ b/migrations/versions/dd44854aa12a_approved_by.py @@ -0,0 +1,24 @@ +"""approved_by + +Revision ID: dd44854aa12a +Revises: 5cf69f1026d9 +Create Date: 2025-04-12 07:55:31.393429 + +""" + +import sqlalchemy as sa +from alembic import op + + +revision = 'dd44854aa12a' +down_revision = '5cf69f1026d9' +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column('comment', sa.Column('approved_by', sa.Integer(), nullable=True)) + + +def downgrade(): + op.drop_column('comment', 'approved_by') diff --git a/rating_api/routes/comment.py b/rating_api/routes/comment.py index 8ca1aa2..bf17a39 100644 --- a/rating_api/routes/comment.py +++ b/rating_api/routes/comment.py @@ -24,6 +24,8 @@ CommentGetAll, CommentGetAllWithAllInfo, CommentGetWithAllInfo, + CommentGetWithStatus, + CommentGetAllWithStatus, CommentImportAll, CommentPost, CommentUpdate, @@ -165,7 +167,7 @@ async def get_comment(uuid: UUID) -> CommentGet: return CommentGet.model_validate(comment) -@comment.get("", response_model=Union[CommentGetAll, CommentGetAllWithAllInfo]) +@comment.get("", response_model=Union[CommentGetAll, CommentGetAllWithAllInfo, CommentGetAllWithStatus]) async def get_comments( limit: int = 10, offset: int = 0, @@ -195,9 +197,12 @@ async def get_comments( comments = Comment.query(session=db.session).all() if not comments: raise ObjectNotFound(Comment, 'all') - if "rating.comment.review" in [scope['name'] for scope in user.get('session_scopes')] or user.get('id') == user_id: + if "rating.comment.review" in [scope['name'] for scope in user.get('session_scopes')]: result = CommentGetAllWithAllInfo(limit=limit, offset=offset, total=len(comments)) comment_validator = CommentGetWithAllInfo + elif user.get('id') == user_id: + result = CommentGetAllWithStatus(limit=limit, offset=offset, total=len(comments)) + comment_validator = CommentGetWithStatus else: result = CommentGetAll(limit=limit, offset=offset, total=len(comments)) comment_validator = CommentGet From 98a5a49b46923943a387a7a1ac184b998bbd6857 Mon Sep 17 00:00:00 2001 From: pivovarovma Date: Sat, 12 Apr 2025 09:19:44 +0000 Subject: [PATCH 5/5] =?UTF-8?q?=D0=94=D0=BB=D1=8F=20=D0=BB=D0=B8=D1=81?= =?UTF-8?q?=D1=82=D0=B8=D0=BD=D0=B3=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rating_api/routes/comment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rating_api/routes/comment.py b/rating_api/routes/comment.py index bf17a39..137953d 100644 --- a/rating_api/routes/comment.py +++ b/rating_api/routes/comment.py @@ -23,9 +23,9 @@ CommentGet, CommentGetAll, CommentGetAllWithAllInfo, + CommentGetAllWithStatus, CommentGetWithAllInfo, CommentGetWithStatus, - CommentGetAllWithStatus, CommentImportAll, CommentPost, CommentUpdate,