From 992ee2053c22fd2589b2b4e688816eae6d1cd2de Mon Sep 17 00:00:00 2001 From: Dariiiii Date: Thu, 12 Mar 2026 23:53:04 +0300 Subject: [PATCH 1/3] anti-plagiarism check prototype --- .../report_checks/anti_plagiarism_check.py | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 app/main/checks/report_checks/anti_plagiarism_check.py diff --git a/app/main/checks/report_checks/anti_plagiarism_check.py b/app/main/checks/report_checks/anti_plagiarism_check.py new file mode 100644 index 00000000..e01fe8d0 --- /dev/null +++ b/app/main/checks/report_checks/anti_plagiarism_check.py @@ -0,0 +1,71 @@ +import re +from ..base_check import BaseReportCriterion, answer + +class AntiPlagiarismCheck(BaseReportCriterion): + label = "Проверка на заимствования" + _description = '' + id = 'anti_plagiarism_check' + + def __init__(self, file_info, originality_threshold=70): + super().__init__(file_info) + self.chapters = [] + self.originality_threshold = originality_threshold + + def late_init(self): + self.chapters = self.file.make_chapters(self.file_type['report_type']) + + def check(self): + if self.file.page_counter() < 4: + return answer(False, "В отчете недостаточно страниц. Нечего проверять.") + self.late_init() + text = "" + result_str = "" + for chapter in self.chapters: + if 'список использованных источников' in chapter['text'].lower(): + break + for child in chapter['child']: + text += " " + child['text'] # заменить на нужную для метода шинглов структуру + result = self.run_antiplagiarism(text) + originality_percent = result["originality"] + borrowed_fragments = result["fragments"] + user = 'admin' # уточнить, как получить роль пользователя + if originality_percent < self.originality_threshold: + result_str += f"Обнаружены заимствования в тексте отчета. Уникальность работы составляет {originality_percent}%.

" + if user == 'admin': + result_str += f"Ниже приведены фраменты, содержащие заимстования.
" + for i, fragment in enumerate(borrowed_fragments, start=1): + result_str += (f"Заимствованный фрагмент №{i} находится на странице {fragment['page_in_doc']}." + f"Источник: {fragment['source']}, страница {fragment['page_in_source']}." + f"Совпадение {fragment['percent']}%
" + f"Текст фрагмента:
" + f"{fragment['text']}
" + f"Подробнее: Ссылка на страницу сравнения

" # добавить реальную ссылку + ) + return answer(False, result_str) + return answer(True, f"Уникальность текста {originality_percent}%. Проверка пройдена.") + + def run_antiplagiarism(self, text): + # вызов реального алгоритма антиплагиата + originality = 65 + fragments = [ + { + "text": "пример заимствованного текста", + "source": "doc_id", + "percent": 90, + "page_in_doc": 5, + "page_in_source": 8 + + }, + { + "text": "еще один заимствованный фрагмент", + "source": "doc_id2", + "percent": 70, + "page_in_doc": 7, + "page_in_source": 9 + } + ] + + return { + "originality": originality, + "fragments": fragments + } \ No newline at end of file From 761d736a41c28c679a9bb29cd3548dfc44f97da8 Mon Sep 17 00:00:00 2001 From: Dariiiii Date: Fri, 27 Mar 2026 15:37:32 +0300 Subject: [PATCH 2/3] Add anti-plagiarism comparison page --- app/main/check_packs/pack_config.py | 1 + app/main/checks/report_checks/__init__.py | 3 +- .../report_checks/anti_plagiarism_check.py | 10 +- app/routes/anti_plagiarism.py | 56 ++++++++ app/server.py | 2 + app/templates/anti_plagiarism.html | 49 +++++++ assets/scripts/anti_plagiarism.js | 123 ++++++++++++++++++ assets/styles/anti_plagiarism.css | 48 +++++++ webpack.config.js | 1 + 9 files changed, 288 insertions(+), 5 deletions(-) create mode 100644 app/routes/anti_plagiarism.py create mode 100644 app/templates/anti_plagiarism.html create mode 100644 assets/scripts/anti_plagiarism.js create mode 100644 assets/styles/anti_plagiarism.css diff --git a/app/main/check_packs/pack_config.py b/app/main/check_packs/pack_config.py index 91e08134..e3c3c6f7 100644 --- a/app/main/check_packs/pack_config.py +++ b/app/main/check_packs/pack_config.py @@ -50,6 +50,7 @@ ["empty_task_page_check"], ["water_in_the_text_check"], ["report_task_tracker"], + ["anti_plagiarism_check"], ] DEFAULT_TYPE = 'pres' diff --git a/app/main/checks/report_checks/__init__.py b/app/main/checks/report_checks/__init__.py index 7b1b974b..188bc58f 100644 --- a/app/main/checks/report_checks/__init__.py +++ b/app/main/checks/report_checks/__init__.py @@ -34,4 +34,5 @@ from .task_tracker import ReportTaskTracker from .paragraphs_count_check import ReportParagraphsCountCheck from .template_name import ReportTemplateNameCheck -from .decimal_places import ReportDecimalPlacesCheck \ No newline at end of file +from .decimal_places import ReportDecimalPlacesCheck +from .anti_plagiarism_check import AntiPlagiarismCheck \ No newline at end of file diff --git a/app/main/checks/report_checks/anti_plagiarism_check.py b/app/main/checks/report_checks/anti_plagiarism_check.py index e01fe8d0..3e824f6f 100644 --- a/app/main/checks/report_checks/anti_plagiarism_check.py +++ b/app/main/checks/report_checks/anti_plagiarism_check.py @@ -1,6 +1,7 @@ import re from ..base_check import BaseReportCriterion, answer + class AntiPlagiarismCheck(BaseReportCriterion): label = "Проверка на заимствования" _description = '' @@ -30,7 +31,7 @@ def check(self): borrowed_fragments = result["fragments"] user = 'admin' # уточнить, как получить роль пользователя if originality_percent < self.originality_threshold: - result_str += f"Обнаружены заимствования в тексте отчета. Уникальность работы составляет {originality_percent}%.

" + result_str += f"Обнаружены заимствования в тексте отчета. Уникальность работы составляет {originality_percent}%.

" if user == 'admin': result_str += f"Ниже приведены фраменты, содержащие заимстования.
" for i, fragment in enumerate(borrowed_fragments, start=1): @@ -39,11 +40,12 @@ def check(self): f"Совпадение {fragment['percent']}%
" f"Текст фрагмента:
" f"{fragment['text']}
" - f"Подробнее: Ссылка на страницу сравнения

" # добавить реальную ссылку + f"Подробнее: перейти к сравнению

" # добавить реальную ссылку ) return answer(False, result_str) return answer(True, f"Уникальность текста {originality_percent}%. Проверка пройдена.") - + def run_antiplagiarism(self, text): # вызов реального алгоритма антиплагиата originality = 65 @@ -68,4 +70,4 @@ def run_antiplagiarism(self, text): return { "originality": originality, "fragments": fragments - } \ No newline at end of file + } diff --git a/app/routes/anti_plagiarism.py b/app/routes/anti_plagiarism.py new file mode 100644 index 00000000..d2ce05f6 --- /dev/null +++ b/app/routes/anti_plagiarism.py @@ -0,0 +1,56 @@ +import bson +from bson import ObjectId + +from flask import Blueprint, render_template, request, url_for +from flask_login import current_user, login_required + +from app.db import db_methods +from app.root_logger import get_root_logger + +anti_plagiarism = Blueprint('anti_plagiarism', __name__, template_folder='templates', static_folder='static') +logger = get_root_logger('web') + + +@anti_plagiarism.route("/", methods=["GET"]) +@login_required +def anti_plagiarism_page(_id): + try: + oid = ObjectId(_id) + except bson.errors.InvalidId: + logger.error('_id exception:', exc_info=True) + return render_template("./404.html") + + check = db_methods.get_check(oid) + if check is None: + logger.info("Запрошенная проверка не найдена: " + _id) + return render_template("./404.html") + + if not (current_user.is_admin or current_user.username == check.user or check.user == "api_access_token"): + return "У вас нет прав на просмотр результатов чужих проверок", 403 + + source_check = check + source_check_id = request.args.get("source_check_id") + if source_check_id: + try: + source_oid = ObjectId(source_check_id) + except bson.errors.InvalidId: + logger.error('source_check_id exception:', exc_info=True) + return render_template("./404.html") + source_check = db_methods.get_check(source_oid) + if source_check is None: + logger.info("Запрошенная проверка не найдена: " + source_check_id) + return render_template("./404.html") + if not (current_user.is_admin or current_user.username == source_check.user or source_check.user == "api_access_token"): + return "У вас нет прав на просмотр результатов чужих проверок", 403 + + fragments = [] + + student_pdf_url = url_for("get_pdf.get_pdf_main", _id=check.conv_pdf_fs_id) + source_pdf_url = url_for("get_pdf.get_pdf_main", _id=source_check.conv_pdf_fs_id) + + return render_template( + "anti_plagiarism.html", + fragments=fragments, + student_pdf_url=student_pdf_url, + source_pdf_url=source_pdf_url, + ) diff --git a/app/server.py b/app/server.py index 986dcd48..a42d38b6 100644 --- a/app/server.py +++ b/app/server.py @@ -51,6 +51,7 @@ from routes.version import version from routes.capacity import capacity from routes.profile import profile +from routes.anti_plagiarism import anti_plagiarism from server_consts import UPLOAD_FOLDER @@ -87,6 +88,7 @@ app.register_blueprint(version, url_prefix='/version') app.register_blueprint(capacity, url_prefix='/capacity') app.register_blueprint(profile, url_prefix='/profile') +app.register_blueprint(anti_plagiarism, url_prefix='/anti_plagiarism') app.logger.addHandler(get_logging_stdout_handler()) app.logger.propagate = False diff --git a/app/templates/anti_plagiarism.html b/app/templates/anti_plagiarism.html new file mode 100644 index 00000000..5ff8588e --- /dev/null +++ b/app/templates/anti_plagiarism.html @@ -0,0 +1,49 @@ +{% extends "root.html" %} + +{% block title %}Сравнение документов{% endblock %} + +{% block main %} +
+

+ Проверка на заимствования +

+ +
+
+
Документ студента
+
+ + + + Страница из + + +
+
+ +
+
+ +
+
Документ для сравнения
+
+ + + + Страница из + + +
+
+ +
+
+
+
+{% endblock %} +{% block script %} + + +{% endblock script %} diff --git a/assets/scripts/anti_plagiarism.js b/assets/scripts/anti_plagiarism.js new file mode 100644 index 00000000..bf3dd15b --- /dev/null +++ b/assets/scripts/anti_plagiarism.js @@ -0,0 +1,123 @@ +import '../styles/anti_plagiarism.css'; +import * as pdfjsLib from 'pdfjs-dist'; +import pdfjsWorker from "pdfjs-dist/build/pdf.worker.entry"; + +$(function(){ + if ($("#student-pdf-download").length === 0 || $("#source-pdf-download").length === 0) { + return; + } + + pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker; + + function initPdfViewer(canvasId, canvasFrameId, prevBtnId, nextBtnId, pageNumId, pageCountId, pdfUrl) { + let pdfDoc = null; + let pageNum = 1; + let pageIsRendering = false; + let pageNumIsPending = null; + + const canvas = document.getElementById(canvasId); + const canvasFrame = document.getElementById(canvasFrameId); + const ctx = canvas.getContext('2d'); + + const renderPage = num => { + pageIsRendering = true; + + pdfDoc.getPage(num).then(page => { + const baseViewport = page.getViewport({scale: 1}); + const targetWidth = canvasFrame.clientWidth || baseViewport.width; + const scale = targetWidth / baseViewport.width; + const viewport = page.getViewport({scale: scale}); + + canvas.height = viewport.height; + canvas.width = viewport.width; + + const renderCtx = { + canvasContext: ctx, + viewport + }; + + page.render(renderCtx).promise.then(() => { + pageIsRendering = false; + + if (pageNumIsPending !== null) { + renderPage(pageNumIsPending); + pageNumIsPending = null; + } + }); + + document.getElementById(pageNumId).textContent = num; + }); + }; + + const queueRenderPage = num => { + if (pageIsRendering) { + pageNumIsPending = num; + } else { + renderPage(num); + } + }; + + const showPrevPage = () => { + if (pageNum <= 1) { + return; + } + pageNum--; + queueRenderPage(pageNum); + }; + + const showNextPage = () => { + if (pageNum >= pdfDoc.numPages) { + return; + } + pageNum++; + queueRenderPage(pageNum); + }; + + const handleResize = () => { + if (!pdfDoc) { + return; + } + queueRenderPage(pageNum); + }; + + pdfjsLib + .getDocument(pdfUrl) + .promise.then(pdfDoc_ => { + pdfDoc = pdfDoc_; + document.getElementById(pageCountId).textContent = pdfDoc.numPages; + renderPage(pageNum); + }); + + $('#' + prevBtnId).click(showPrevPage); + $('#' + nextBtnId).click(showNextPage); + window.addEventListener('resize', handleResize); + + if (window.ResizeObserver) { + const resizeObserver = new ResizeObserver(handleResize); + resizeObserver.observe(canvasFrame); + } + } + + const studentPdfUrl = $("#student-pdf-download").attr('href'); + const sourcePdfUrl = $("#source-pdf-download").attr('href'); + + initPdfViewer( + 'student-canvas', + 'student-canvas-frame', + 'student-prev-page', + 'student-next-page', + 'student-page-num', + 'student-page-count', + studentPdfUrl + ); + + initPdfViewer( + 'source-canvas', + 'source-canvas-frame', + 'source-prev-page', + 'source-next-page', + 'source-page-num', + 'source-page-count', + sourcePdfUrl + ); +}); diff --git a/assets/styles/anti_plagiarism.css b/assets/styles/anti_plagiarism.css new file mode 100644 index 00000000..3461d584 --- /dev/null +++ b/assets/styles/anti_plagiarism.css @@ -0,0 +1,48 @@ +#anti_plagiarism_holder { + overflow: auto; +} + +#results_title { + margin-top: 1rem; +} + +.pdf_compare_container { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 2rem; + margin-bottom: 1rem; +} + +.pdf_block { + width: 600px; + max-width: 100%; +} + +.pdf_block .title { + margin-bottom: 1rem; + text-align: center; +} + +.pdf_controls { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.pdf_canvas_frame { + width: 100%; + aspect-ratio: 210 / 297; + overflow: hidden; + background: var(--bright-color); + border: 1px solid var(--border-color); +} + +canvas { + display: block; + width: 100%; + height: auto; + background: var(--bright-color); +} diff --git a/webpack.config.js b/webpack.config.js index 161f4314..20b3baa8 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -19,6 +19,7 @@ module.exports = { profile: ['core-js/stable', 'regenerator-runtime/runtime', './assets/scripts/general_imports.js', './assets/scripts/profile.js'], results: ['core-js/stable', 'regenerator-runtime/runtime', './assets/scripts/general_imports.js', './assets/scripts/results.js'], signup: ['core-js/stable', 'regenerator-runtime/runtime', './assets/scripts/general_imports.js', './assets/scripts/signup.js'], + anti_plagiarism: ['core-js/stable', 'regenerator-runtime/runtime', './assets/scripts/general_imports.js', './assets/scripts/anti_plagiarism.js'], upload: ['core-js/stable', 'regenerator-runtime/runtime', './assets/scripts/general_imports.js', './assets/scripts/upload.js'], user_list: ['core-js/stable', 'regenerator-runtime/runtime', './assets/scripts/general_imports.js', './assets/scripts/user_list.js'], version: ['core-js/stable', 'regenerator-runtime/runtime', './assets/scripts/general_imports.js', './assets/scripts/version.js'], From 1e33a537864d08ec48ced0ac1e4af159fd233f86 Mon Sep 17 00:00:00 2001 From: Dariiiii Date: Fri, 24 Apr 2026 13:30:13 +0300 Subject: [PATCH 3/3] update page access permissions --- .../report_checks/anti_plagiarism_check.py | 32 +++++++++-------- app/routes/anti_plagiarism.py | 34 ++++++++----------- app/routes/results.py | 12 +++++++ 3 files changed, 43 insertions(+), 35 deletions(-) diff --git a/app/main/checks/report_checks/anti_plagiarism_check.py b/app/main/checks/report_checks/anti_plagiarism_check.py index 3e824f6f..fcf83ee0 100644 --- a/app/main/checks/report_checks/anti_plagiarism_check.py +++ b/app/main/checks/report_checks/anti_plagiarism_check.py @@ -20,7 +20,7 @@ def check(self): return answer(False, "В отчете недостаточно страниц. Нечего проверять.") self.late_init() text = "" - result_str = "" + user_result_str = "" for chapter in self.chapters: if 'список использованных источников' in chapter['text'].lower(): break @@ -29,21 +29,23 @@ def check(self): result = self.run_antiplagiarism(text) originality_percent = result["originality"] borrowed_fragments = result["fragments"] - user = 'admin' # уточнить, как получить роль пользователя if originality_percent < self.originality_threshold: - result_str += f"Обнаружены заимствования в тексте отчета. Уникальность работы составляет {originality_percent}%.

" - if user == 'admin': - result_str += f"Ниже приведены фраменты, содержащие заимстования.
" - for i, fragment in enumerate(borrowed_fragments, start=1): - result_str += (f"Заимствованный фрагмент №{i} находится на странице {fragment['page_in_doc']}." - f"Источник: {fragment['source']}, страница {fragment['page_in_source']}." - f"Совпадение {fragment['percent']}%
" - f"Текст фрагмента:
" - f"{fragment['text']}
" - f"Подробнее: перейти к сравнению

" # добавить реальную ссылку - ) - return answer(False, result_str) + user_result_str += f"Обнаружены заимствования в тексте отчета. Уникальность работы составляет {originality_percent}%.
" + admin_result_str = user_result_str + admin_result_str += f"Ниже приведены фраменты, содержащие заимстования.

" + for i, fragment in enumerate(borrowed_fragments, start=1): + source_check_id = str(fragment.get('source', '')) + admin_result_str += (f"Заимствованный фрагмент №{i} находится на странице {fragment['page_in_doc']}. " + f"Источник: {fragment['source']}, страница {fragment['page_in_source']}." + f"Совпадение {fragment['percent']}%
" + f"Текст фрагмента:
" + f"{fragment['text']}
" + f"Подробнее: " + f"перейти к сравнению

" + ) + return answer(False, user_result_str, admin_result_str) return answer(True, f"Уникальность текста {originality_percent}%. Проверка пройдена.") def run_antiplagiarism(self, text): diff --git a/app/routes/anti_plagiarism.py b/app/routes/anti_plagiarism.py index d2ce05f6..a990e50c 100644 --- a/app/routes/anti_plagiarism.py +++ b/app/routes/anti_plagiarism.py @@ -1,7 +1,7 @@ import bson from bson import ObjectId -from flask import Blueprint, render_template, request, url_for +from flask import Blueprint, render_template, url_for from flask_login import current_user, login_required from app.db import db_methods @@ -11,9 +11,9 @@ logger = get_root_logger('web') -@anti_plagiarism.route("/", methods=["GET"]) +@anti_plagiarism.route("//", methods=["GET"]) @login_required -def anti_plagiarism_page(_id): +def anti_plagiarism_page(_id, source_check_id): try: oid = ObjectId(_id) except bson.errors.InvalidId: @@ -25,23 +25,17 @@ def anti_plagiarism_page(_id): logger.info("Запрошенная проверка не найдена: " + _id) return render_template("./404.html") - if not (current_user.is_admin or current_user.username == check.user or check.user == "api_access_token"): - return "У вас нет прав на просмотр результатов чужих проверок", 403 - - source_check = check - source_check_id = request.args.get("source_check_id") - if source_check_id: - try: - source_oid = ObjectId(source_check_id) - except bson.errors.InvalidId: - logger.error('source_check_id exception:', exc_info=True) - return render_template("./404.html") - source_check = db_methods.get_check(source_oid) - if source_check is None: - logger.info("Запрошенная проверка не найдена: " + source_check_id) - return render_template("./404.html") - if not (current_user.is_admin or current_user.username == source_check.user or source_check.user == "api_access_token"): - return "У вас нет прав на просмотр результатов чужих проверок", 403 + try: + source_oid = ObjectId(_id) + except bson.errors.InvalidId: + logger.error('source_check_id exception:', exc_info=True) + return render_template("./404.html") + source_check = db_methods.get_check(source_oid) + if source_check is None: + logger.info("Запрошенная проверка не найдена: " + source_check_id) + return render_template("./404.html") + if not (current_user.is_admin): + return "У вас нет прав на просмотр", 403 fragments = [] diff --git a/app/routes/results.py b/app/routes/results.py index 18a08c24..a32106c2 100644 --- a/app/routes/results.py +++ b/app/routes/results.py @@ -16,6 +16,17 @@ logger = get_root_logger('web') +def _prepare_verdicts_for_view(check, is_admin): + if not isinstance(check.enabled_checks, list): + return check + + for criterion_info in check.enabled_checks: + verdict = criterion_info.get('verdict') + if isinstance(verdict, (list, tuple)) and len(verdict) > 1: + criterion_info['verdict'] = [verdict[1] if is_admin else verdict[0]] + return check + + @results_bp.route("/", methods=["GET"]) @login_required def results_main(_id): @@ -30,6 +41,7 @@ def results_main(_id): if current_user.is_admin or current_user.username == check.user or check.user == "api_access_token": # show processing time for user avg_process_time = None if check.is_ended else db_methods.get_average_processing_time() + check = _prepare_verdicts_for_view(check, current_user.is_admin) return render_template("./results.html", navi_upload=True, results=check, columns=TABLE_COLUMNS, avg_process_time=avg_process_time, stats=format_check(check.pack()))