From d62b05c6823684d29b1e736f9c7ce748e762760b Mon Sep 17 00:00:00 2001 From: baydakov-georgiy Date: Sat, 15 Nov 2025 18:55:31 +0300 Subject: [PATCH 01/21] 751_decimal_places_limit --- .../excessive_decimal_places.py | 49 +++++++ .../excessive_decimal_places_check.py | 51 +++++++ app/utils/decimal_checker.py | 126 ++++++++++++++++++ 3 files changed, 226 insertions(+) create mode 100644 app/main/checks/presentation_checks/excessive_decimal_places.py create mode 100644 app/main/checks/report_checks/excessive_decimal_places_check.py create mode 100644 app/utils/decimal_checker.py diff --git a/app/main/checks/presentation_checks/excessive_decimal_places.py b/app/main/checks/presentation_checks/excessive_decimal_places.py new file mode 100644 index 00000000..87bb769b --- /dev/null +++ b/app/main/checks/presentation_checks/excessive_decimal_places.py @@ -0,0 +1,49 @@ +import re +from ..base_check import BasePresCriterion, answer +from utils.decimal_checker import DecimalPlacesChecker, DocumentType + + +class PresExcessiveDecimalPlacesCheck(BasePresCriterion): + description = 'Проверка на избыточное количество десятичных знаков в числах' + label = "Проверка на избыточное количество десятичных знаков" + id = 'pres_excessive_decimal_places_check' + + def __init__(self, file_info, max_decimal_places=2, max_number_of_violations=1): + super().__init__(file_info) + self.max_decimal_places = max_decimal_places + self.max_number_of_violations = max_number_of_violations + self.checker = DecimalPlacesChecker(max_decimal_places, DocumentType.PRESENTATION) + + def check(self): + + detected_slides = {} + total_violations = 0 + + for slide_num, slide_text in enumerate(self.file.get_text_from_slides()): + lines = re.split(r'\n', slide_text) + + slide_violations = self.checker.find_violations_in_lines(lines) + + for line_idx, number_str, decimal_places, line in slide_violations: + total_violations += 1 + + if slide_num not in detected_slides: + detected_slides[slide_num] = [] + + violation_msg = self.checker.format_violation_message( + line_idx, line, number_str, decimal_places + ) + detected_slides[slide_num].append(violation_msg) + + if total_violations > self.max_number_of_violations: + result_str = self.checker.format_failure_message( + total_violations, + detected_slides, + format_page_link_fn=self.format_page_link + ) + result_score = 0 + else: + result_str = self.checker.format_success_message() + result_score = 1 + + return answer(result_score, result_str) diff --git a/app/main/checks/report_checks/excessive_decimal_places_check.py b/app/main/checks/report_checks/excessive_decimal_places_check.py new file mode 100644 index 00000000..796f2952 --- /dev/null +++ b/app/main/checks/report_checks/excessive_decimal_places_check.py @@ -0,0 +1,51 @@ +import re +from ..base_check import BaseReportCriterion, answer +from utils.decimal_checker import DecimalPlacesChecker + + +class ReportExcessiveDecimalPlacesCheck(BaseReportCriterion): + label = "Проверка на избыточное количество десятичных знаков" + description = 'Проверка на избыточное количество десятичных знаков в числах' + id = 'excessive_decimal_places_check' + + def __init__(self, file_info, max_decimal_places=2, max_number_of_violations=2): + super().__init__(file_info) + self.max_decimal_places = max_decimal_places + self.max_number_of_violations = max_number_of_violations + self.checker = DecimalPlacesChecker(max_decimal_places) + + def check(self): + if self.file.page_counter() < 4: + return answer(False, "В отчете недостаточно страниц. Нечего проверять.") + + detected_pages = {} + total_violations = 0 + + for page_num, page_text in self.file.pdf_file.get_text_on_page().items(): + lines = re.split(r'\n', page_text) + + page_violations = self.checker.find_violations_in_lines(lines) + + for line_idx, number_str, decimal_places, line in page_violations: + total_violations += 1 + + if page_num not in detected_pages: + detected_pages[page_num] = [] + + violation_msg = self.checker.format_violation_message( + line_idx, line, number_str, decimal_places + ) + detected_pages[page_num].append(violation_msg) + + if total_violations > self.max_number_of_violations: + result_str = self.checker.format_failure_message( + total_violations, + detected_pages, + format_page_link_fn=self.format_page_link + ) + result_score = 0 + else: + result_str = self.checker.format_success_message() + result_score = 1.0 + + return answer(result_score, result_str) diff --git a/app/utils/decimal_checker.py b/app/utils/decimal_checker.py new file mode 100644 index 00000000..7c649c5f --- /dev/null +++ b/app/utils/decimal_checker.py @@ -0,0 +1,126 @@ +import re +from enum import Enum + +class DocumentType(Enum): + REPORT = 'report' + PRESENTATION = 'pres' + +class DecimalPlacesChecker: + """ + Класс для проверки чисел на избыточное количество десятичных знаков. + Игнорирует IP-адреса, версии ПО и другие составные числа. + + Проверяет: + - Обычные числа с десятичными знаками (например, =3.14159) + Не проверяет: + - IP-адреса (например, 192.168.1.1) + - Версии ПО (например, 1.2.3.4) + - Другие составные числа + """ + + DECIMAL_PATTERN = r'(? 0 else ' ' + + if char_before == '.': + return False + + if end_pos < len(text): + char_after = text[end_pos] + if char_after == '.' and end_pos + 1 < len(text) and text[end_pos + 1].isdigit(): + return False + + return True + + def count_decimal_places(self, number_str): + normalized = number_str.replace(',', '.') + + if '.' in normalized: + decimal_part = normalized.split('.')[1] + return len(decimal_part) + + return 0 + + def has_excessive_decimals(self, number_str): + return self.count_decimal_places(number_str) > self.max_decimal_places + + def find_violations_in_text(self, text): + violations = [] + matches = re.finditer(self.DECIMAL_PATTERN, text) + + for match in matches: + if not self.is_valid_number(match, text): + continue + + number_str = match.group() + decimal_places = self.count_decimal_places(number_str) + + if decimal_places > self.max_decimal_places: + violations.append((number_str, decimal_places, match)) + + return violations + + def find_violations_in_lines(self, lines): + violations = [] + + for line_idx, line in enumerate(lines): + line_violations = self.find_violations_in_text(line) + + for number_str, decimal_places, match in line_violations: + violations.append((line_idx, number_str, decimal_places, line)) + + return violations + + def highlight_number(self, line, number_str): + return line.replace(number_str, f'{number_str}', 1) + + def format_violation_message(self, line_idx, line, number_str, decimal_places): + highlighted_line = self.highlight_number(line, number_str) + return ( + f'Строка {line_idx + 1}: {highlighted_line} ' + f'(найдено {decimal_places} знаков после запятой, ' + f'максимум: {self.max_decimal_places})' + ) + + def format_success_message(self): + if self.type == DocumentType.REPORT: + document_type = "документе" + elif self.type == DocumentType.PRESENTATION: + document_type = "презентации" + return ( + f'Проверка пройдена! Все числа в {document_type} имеют допустимое количество ' + f'десятичных знаков (не более {self.max_decimal_places}).' + ) + + def format_failure_message(self, total_violations, + violations_by_location, + format_page_link_fn=None): + if self.type == DocumentType.REPORT: + location_label = "Страница" + elif self.type == DocumentType.PRESENTATION: + location_label = "Слайд" + result_str = ( + f'Найдены числа с избыточным количеством десятичных знаков!
' + f'Максимально допустимое количество знаков после запятой: {self.max_decimal_places}
' + f'Всего нарушений: {total_violations}

' + ) + + for location_num, violations in violations_by_location.items(): + if format_page_link_fn: + location_str = f'{location_label} {format_page_link_fn([location_num])}' + else: + location_str = f'{location_label} №{location_num}' + + result_str += f'{location_str}:
' + result_str += '
'.join(violations) + result_str += '

' + + return result_str From 5a9f252576be127fa6482311870926a6a8558a85 Mon Sep 17 00:00:00 2001 From: baydakov-georgiy Date: Sat, 15 Nov 2025 19:07:32 +0300 Subject: [PATCH 02/21] fixed doc-string --- app/utils/decimal_checker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/utils/decimal_checker.py b/app/utils/decimal_checker.py index 7c649c5f..47d9dd13 100644 --- a/app/utils/decimal_checker.py +++ b/app/utils/decimal_checker.py @@ -11,7 +11,7 @@ class DecimalPlacesChecker: Игнорирует IP-адреса, версии ПО и другие составные числа. Проверяет: - - Обычные числа с десятичными знаками (например, =3.14159) + - Обычные числа с десятичными знаками (например, -3.14159) Не проверяет: - IP-адреса (например, 192.168.1.1) - Версии ПО (например, 1.2.3.4) From ecee0d7eff38c78f82f2bb0d1dd0c133bb254000 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Mon, 8 Dec 2025 11:53:45 +0300 Subject: [PATCH 03/21] update main_character_check --- app/main/checks/report_checks/main_character_check.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/app/main/checks/report_checks/main_character_check.py b/app/main/checks/report_checks/main_character_check.py index c596e999..47914a6f 100644 --- a/app/main/checks/report_checks/main_character_check.py +++ b/app/main/checks/report_checks/main_character_check.py @@ -82,12 +82,6 @@ def extract_table_contents(self, table): contents.append("|".join(row_text)) return contents - def calculate_find_value(self, table, index): - count = int((len(table) - index - 2) / 2) - if count >= 0: - return count - return 0 - def check_table(self, check_list, table, table_num): for item in check_list: for i, line in enumerate(table): @@ -105,10 +99,7 @@ def check_table(self, check_list, table, table_num): continue elif item["key"] in ["Зав. кафедрой", "Консультант"] and item["found_key"] > 0: - if item["key"] == "Консультант": - if item["found_key"] == 1: - item["find"] += self.calculate_find_value(table, i) for value in item["value"]: - if re.search(value, line): + if "Руководитель" not in line and re.search(value, line): # исключаем из поиска строки с рукодителем item["found_value"] += 1 item["logs"] += f"'{item['key']}': значение компоненты '{value}' найдено в строке '{line}' в таблице №{table_num}
" From 266f9198fdf61fb46d6bac733c5f46c5382f898e Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Mon, 8 Dec 2025 11:54:16 +0300 Subject: [PATCH 04/21] add recheck test --- tests/test_recheck.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 tests/test_recheck.py diff --git a/tests/test_recheck.py b/tests/test_recheck.py new file mode 100644 index 00000000..4fc8135c --- /dev/null +++ b/tests/test_recheck.py @@ -0,0 +1,36 @@ +import time +from basic_selenium_test import BasicSeleniumTest +from selenium.webdriver.common.by import By +from selenium.webdriver.support.wait import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + + +class RecheckTestSelenium(BasicSeleniumTest): + + def test_recheck_file(self): + check_id = self.open_statistic() + if check_id: + URL = self.get_url(f"/recheck/{check_id}") + self.get_driver().get(URL) + obj = WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located((By.ID, "results_title")) + ) + if "Производится проверка файла" in obj.text: + start_time = time.time() + max_time = 240 + while (time.time() - start_time) < max_time: + time.sleep(10) + try: + obj = WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located((By.ID, "results_table")) + ) + if obj is not None: + self.assertNotEquals(obj, None) + return + except: + continue + self.fail("Result of check is not found") + else: + self.fail("No checking status after /recheck") + else: + self.skipTest("No check in system for testing recheck") From 5d82440f5d0806b2ef7f2149481aed72ef24e4c9 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Wed, 10 Dec 2025 15:05:19 +0300 Subject: [PATCH 05/21] Update style_check_settings.py --- app/main/checks/report_checks/style_check_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/main/checks/report_checks/style_check_settings.py b/app/main/checks/report_checks/style_check_settings.py index 7c3a61b5..36912a9f 100644 --- a/app/main/checks/report_checks/style_check_settings.py +++ b/app/main/checks/report_checks/style_check_settings.py @@ -12,7 +12,7 @@ class StyleCheckSettings: HEADER_2_REGEX = "^()([\\w\\s]+)\\.$" STD_BANNED_WORDS = ['мы', 'моя', 'мои', 'моё', 'наш', 'наши', 'аттач', 'билдить', 'бинарник', 'валидный', 'дебаг', 'деплоить', 'десктопное', 'железо', - 'исходники', 'картинка', 'консольное', 'конфиг', 'кусок', 'либа', 'лог', 'мануал', 'машина', + 'исходники', 'картинка', 'консольное', 'конфиг', 'кусок', 'либа', 'лог', 'мануал', 'отнаследованный', 'парсинг', 'пост', 'распаковать', 'сбоит', 'скачать', 'склонировать', 'скрипт', 'тестить', 'тул', 'тула', 'тулза', 'фиксить', 'флажок', 'флаг', 'юзкейс', 'продакт', 'продакшн', 'прод', 'фидбек', 'дедлайн', 'дэдлайн', 'оптимально', 'оптимальный', 'надежный', 'интуитивный', From fe9f6493d8adc5ab58b19c65b6856cd067b45f25 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Wed, 17 Dec 2025 08:32:21 +0300 Subject: [PATCH 06/21] add warned_words for banned_words_check --- .../report_checks/banned_words_check.py | 45 +++++++++++-------- .../report_checks/style_check_settings.py | 23 +++++++--- 2 files changed, 44 insertions(+), 24 deletions(-) diff --git a/app/main/checks/report_checks/banned_words_check.py b/app/main/checks/report_checks/banned_words_check.py index 351b403e..58c67cd3 100644 --- a/app/main/checks/report_checks/banned_words_check.py +++ b/app/main/checks/report_checks/banned_words_check.py @@ -5,12 +5,13 @@ class ReportBannedWordsCheck(BaseReportCriterion): label = "Проверка наличия запретных слов в тексте отчёта" - description = 'Запрещено упоминание слова "мы"' + description = 'Запрещено упоминание определенных "опасных" слов' id = 'banned_words_check' def __init__(self, file_info, headers_map=None): super().__init__(file_info) self.words = [] + self.warned_words = [] self.min_count = 0 self.max_count = 0 if headers_map: @@ -21,12 +22,14 @@ def __init__(self, file_info, headers_map=None): def late_init(self): self.headers_main = self.file.get_main_headers(self.file_type['report_type']) if self.headers_main in StyleCheckSettings.CONFIGS.get(self.config): - self.words = [morph.normal_forms(word)[0] for word in StyleCheckSettings.CONFIGS.get(self.config)[self.headers_main]['banned_words']] + self.words = {morph.normal_forms(word)[0] for word in StyleCheckSettings.CONFIGS.get(self.config)[self.headers_main]['banned_words']} + self.warned_words = {morph.normal_forms(word)[0] for word in StyleCheckSettings.CONFIGS.get(self.config)[self.headers_main]['warned_words']} self.min_count = StyleCheckSettings.CONFIGS.get(self.config)[self.headers_main]['min_count_for_banned_words_check'] self.max_count = StyleCheckSettings.CONFIGS.get(self.config)[self.headers_main]['max_count_for_banned_words_check'] else: if 'any_header' in StyleCheckSettings.CONFIGS.get(self.config): - self.words = [morph.normal_forms(word)[0] for word in StyleCheckSettings.CONFIGS.get(self.config)['any_header']['banned_words']] + self.words = {morph.normal_forms(word)[0] for word in StyleCheckSettings.CONFIGS.get(self.config)['any_header']['banned_words']} + self.warned_words = {morph.normal_forms(word)[0] for word in StyleCheckSettings.CONFIGS.get(self.config)['any_header']['warned_words']} self.min_count = StyleCheckSettings.CONFIGS.get(self.config)['any_header']['min_count_for_banned_words_check'] self.max_count = StyleCheckSettings.CONFIGS.get(self.config)['any_header']['max_count_for_banned_words_check'] @@ -34,29 +37,35 @@ def check(self): if self.file.page_counter() < 4: return answer(False, "В отчете недостаточно страниц. Нечего проверять.") self.late_init() - detected_lines = {} result_str = f'Запрещенные слова: {"; ".join(self.words)}
' - count = 0 + banned_counter = {'words': self.words, 'detected_lines': {}, 'count': 0} + warned_counter = {'words': self.warned_words,'detected_lines': {}, 'count': 0} for k, v in self.file.pdf_file.get_text_on_page().items(): lines_on_page = re.split(r'\n', v) for index, line in enumerate(lines_on_page): - words_on_line = re.split(r'[^\w-]+', line) - words_on_line = [morph.normal_forms(word)[0] for word in words_on_line] - count_banned_words = set(words_on_line).intersection(self.words) - if count_banned_words: - count += len(count_banned_words) - if k not in detected_lines.keys(): - detected_lines[k] = [] - detected_lines[k].append(f'Строка {index + 1}: {line} [{"; ".join(count_banned_words)}]') - if len(detected_lines): + words_on_line = {morph.normal_forms(word)[0] for word in re.split(r'[^\w-]+', line)} + for counter in (banned_counter, warned_counter): + count_banned_words = words_on_line.intersection(counter['words']) + if count_banned_words: + counter['count'] += len(count_banned_words) + if k not in counter['detected_lines'].keys(): + counter['detected_lines'][k] = [] + counter['detected_lines'][k].append(f'Строка {index + 1}: {line} [{"; ".join(count_banned_words)}]') + if len(banned_counter['detected_lines']): result_str += 'Обнаружены запретные слова!

' - for k, v in detected_lines.items(): - result_str += f'Страница №{k}:
{"
".join(detected_lines[k])}

' + for k, v in banned_counter['detected_lines'].items(): + result_str += f'Страница №{k}:
{"
".join(banned_counter['detected_lines'][k])}

' else: result_str = 'Пройдена!' + + if len(warned_counter['detected_lines']): + result_str += f'

Обнаружены потенциально опасные слова (не влияют на результат проверки)!
Обратите внимание, что их использование возможно только в подтвержденных случаях: {"; ".join(self.warned_words)}

' + for k, v in warned_counter['detected_lines'].items(): + result_str += f'Страница №{k}:
{"
".join(warned_counter['detected_lines'][k])}

' + result_score = 1 - if count > self.min_count: - if count <= self.max_count: + if banned_counter['count'] > self.min_count: + if banned_counter['count'] <= self.max_count: result_score = 0.5 else: result_score = 0 diff --git a/app/main/checks/report_checks/style_check_settings.py b/app/main/checks/report_checks/style_check_settings.py index 36912a9f..40a7cd21 100644 --- a/app/main/checks/report_checks/style_check_settings.py +++ b/app/main/checks/report_checks/style_check_settings.py @@ -10,14 +10,15 @@ class StyleCheckSettings: HEADER_REGEX = "^\\D+.+$" HEADER_1_REGEX = "^()([\\w\\s]+)$" HEADER_2_REGEX = "^()([\\w\\s]+)\\.$" - STD_BANNED_WORDS = ['мы', 'моя', 'мои', 'моё', 'наш', 'наши', + STD_BANNED_WORDS = ('мы', 'моя', 'мои', 'моё', 'наш', 'наши', 'аттач', 'билдить', 'бинарник', 'валидный', 'дебаг', 'деплоить', 'десктопное', 'железо', 'исходники', 'картинка', 'консольное', 'конфиг', 'кусок', 'либа', 'лог', 'мануал', 'отнаследованный', 'парсинг', 'пост', 'распаковать', 'сбоит', 'скачать', 'склонировать', 'скрипт', 'тестить', 'тул', 'тула', 'тулза', 'фиксить', 'флажок', 'флаг', 'юзкейс', 'продакт', 'продакшн', - 'прод', 'фидбек', 'дедлайн', 'дэдлайн', 'оптимально', 'оптимальный', 'надежный', 'интуитивный', + 'прод', 'фидбек', 'дедлайн', 'дэдлайн', 'оптимально', 'надежный', 'интуитивный', 'хороший', 'плохой', 'идеальный', 'быстро', 'медленно', 'какой-нибудь', 'некоторый', 'почти' - ] # TODO: list of "warning" words + ) + STD_WARNED_WORDS = ('машина', 'оптимальный') # TODO: list of "warning" words STD_MIN_LIT_REF = 1 STD_MAX_LIT_REF = 1000 #just in case for future edit HEADER_1_STYLE = { @@ -101,6 +102,7 @@ class StyleCheckSettings: "unify_regex": APPENDIX_UNIFY_REGEX, "regex": APPENDIX_REGEX, "banned_words": STD_BANNED_WORDS, + "warned_words": STD_WARNED_WORDS, 'min_count_for_banned_words_check': 3, 'max_count_for_banned_words_check': 6, 'min_ref_for_literature_references_check': STD_MIN_LIT_REF, @@ -125,6 +127,7 @@ class StyleCheckSettings: "unify_regex": None, "regex": HEADER_REGEX, "banned_words": STD_BANNED_WORDS, + "warned_words": STD_WARNED_WORDS, 'min_count_for_banned_words_check': 3, 'max_count_for_banned_words_check': 6, 'min_ref_for_literature_references_check': STD_MIN_LIT_REF, @@ -149,7 +152,8 @@ class StyleCheckSettings: "ПЛАН РАБОТЫ НА ВЕСЕННИЙ СЕМЕСТР", "ОТЗЫВ РУКОВОДИТЕЛЯ", "СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ"], "unify_regex": None, "regex": HEADER_REGEX, - "banned_words": STD_BANNED_WORDS + ['доработать', 'доработка', 'переписать', 'рефакторинг', 'исправление'] + "banned_words": STD_BANNED_WORDS + ('доработать', 'доработка', 'переписать', 'рефакторинг', 'исправление'), + "warned_words": STD_WARNED_WORDS }, } @@ -162,7 +166,8 @@ class StyleCheckSettings: "ПЛАН РАБОТЫ НА ОСЕННИЙ СЕМЕСТР", "СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ"], "unify_regex": None, "regex": HEADER_REGEX, - "banned_words": STD_BANNED_WORDS + ['доработать', 'доработка', 'переписать', 'рефакторинг', 'исправление'] + "banned_words": STD_BANNED_WORDS + ('доработать', 'доработка', 'переписать', 'рефакторинг', 'исправление'), + "warned_words": STD_WARNED_WORDS }, } @@ -175,7 +180,8 @@ class StyleCheckSettings: "ПЛАН РАБОТЫ НА ВЕСЕННИЙ СЕМЕСТР", "ОТЗЫВ РУКОВОДИТЕЛЯ", "СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ"], "unify_regex": None, "regex": HEADER_REGEX, - "banned_words": STD_BANNED_WORDS + ['доработать', 'доработка', 'переписать', 'рефакторинг', 'исправление'] + "banned_words": STD_BANNED_WORDS + ('доработать', 'доработка', 'переписать', 'рефакторинг', 'исправление'), + "warned_words": STD_WARNED_WORDS }, } @@ -193,6 +199,7 @@ class StyleCheckSettings: "unify_regex": None, "regex": HEADER_REGEX, "banned_words": STD_BANNED_WORDS, + "warned_words": STD_WARNED_WORDS, 'min_count_for_banned_words_check': 3, 'max_count_for_banned_words_check': 6, }, @@ -208,6 +215,7 @@ class StyleCheckSettings: "unify_regex": None, "regex": HEADER_REGEX, "banned_words": STD_BANNED_WORDS, + "warned_words": STD_WARNED_WORDS, 'min_count_for_banned_words_check': 3, 'max_count_for_banned_words_check': 6, } @@ -230,6 +238,7 @@ class StyleCheckSettings: "unify_regex": None, "regex": HEADER_REGEX, "banned_words": STD_BANNED_WORDS, + "warned_words": STD_WARNED_WORDS, 'min_ref_for_literature_references_check': 1, 'mах_ref_for_literature_references_check': 1000, #just for future possible edit 'min_count_for_banned_words_check': 2, @@ -249,6 +258,7 @@ class StyleCheckSettings: "unify_regex": None, "regex": HEADER_REGEX, "banned_words": STD_BANNED_WORDS, + "warned_words": STD_WARNED_WORDS, 'min_ref_for_literature_references_check': 3, 'mах_ref_for_literature_references_check': 1000, #just for future possible edit 'min_count_for_banned_words_check': 2, @@ -268,6 +278,7 @@ class StyleCheckSettings: "unify_regex": None, "regex": HEADER_REGEX, "banned_words": STD_BANNED_WORDS, + "warned_words": STD_WARNED_WORDS, 'min_ref_for_literature_references_check': 5, 'mах_ref_for_literature_references_check': 1000, #just for future possible edit 'min_count_for_banned_words_check': 2, From d7121dddde13bda3fde6dd5098f393a4ef69da06 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Fri, 26 Dec 2025 12:08:34 +0300 Subject: [PATCH 07/21] add login_required and author check for result page --- app/routes/results.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/app/routes/results.py b/app/routes/results.py index 91cc5b7f..95f4f508 100644 --- a/app/routes/results.py +++ b/app/routes/results.py @@ -3,6 +3,7 @@ from time import time from flask import Blueprint, Response, render_template +from flask_login import current_user, login_required from wsgiref.handlers import format_date_time as format_date from app.db import db_methods @@ -16,6 +17,7 @@ @results_bp.route("/", methods=["GET"]) +@login_required def results_main(_id): try: oid = ObjectId(_id) @@ -24,11 +26,15 @@ def results_main(_id): return render_template("./404.html") check = db_methods.get_check(oid) if check is not None: - # show processing time for user - avg_process_time = None if check.is_ended else db_methods.get_average_processing_time() - return render_template("./results.html", navi_upload=True, results=check, - columns=TABLE_COLUMNS, avg_process_time=avg_process_time, - stats=format_check(check.pack())) + # show check only for author or admin + if current_user.is_admin or current_user.username == check.user: + # show processing time for user + avg_process_time = None if check.is_ended else db_methods.get_average_processing_time() + return render_template("./results.html", navi_upload=True, results=check, + columns=TABLE_COLUMNS, avg_process_time=avg_process_time, + stats=format_check(check.pack())) + else: + return "У вас нет прав на просмотр результатов чужих проверок", 403 else: logger.info("Запрошенная проверка не найдена: " + _id) return render_template("./404.html") From 125ea65bf22dd12c9ac557e7156902e7dfaff323 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Fri, 26 Dec 2025 12:11:51 +0300 Subject: [PATCH 08/21] little update for 404 page --- app/templates/404.html | 2 +- assets/styles/404.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/templates/404.html b/app/templates/404.html index bf8140dd..ad648890 100644 --- a/app/templates/404.html +++ b/app/templates/404.html @@ -4,7 +4,7 @@ {% block main %}
- Страница не найдена! +

Запрашиваемый ресурс не найден

diff --git a/assets/styles/404.css b/assets/styles/404.css index c833cc62..384902ba 100644 --- a/assets/styles/404.css +++ b/assets/styles/404.css @@ -1,3 +1,3 @@ #middle-container { - background-color: black; + background-color: rgb(255, 255, 255); } From 452a797789219bdf26c8190bbb2b706c36ef34a2 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Wed, 28 Jan 2026 15:30:09 +0300 Subject: [PATCH 09/21] Update results.py Show 'api_access_token' results to all --- app/routes/results.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/routes/results.py b/app/routes/results.py index 95f4f508..18a08c24 100644 --- a/app/routes/results.py +++ b/app/routes/results.py @@ -26,8 +26,8 @@ def results_main(_id): return render_template("./404.html") check = db_methods.get_check(oid) if check is not None: - # show check only for author or admin - if current_user.is_admin or current_user.username == check.user: + # show check only for author or admin or api_access_token + 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() return render_template("./results.html", navi_upload=True, results=check, From 77aeae21ee202dd3f194e05721cadf7533c38c08 Mon Sep 17 00:00:00 2001 From: LapshinAE0 Date: Wed, 28 Jan 2026 17:26:00 +0300 Subject: [PATCH 10/21] check 3 label is done --- app/main/check_packs/pack_config.py | 1 + app/main/checks/report_checks/__init__.py | 1 + .../report_checks/check_chapters_3_level.py | 51 +++++++++++++++++++ 3 files changed, 53 insertions(+) create mode 100644 app/main/checks/report_checks/check_chapters_3_level.py diff --git a/app/main/check_packs/pack_config.py b/app/main/check_packs/pack_config.py index 91e08134..1c722a79 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"], + ["report_3_level_in_content_check"], ] DEFAULT_TYPE = 'pres' diff --git a/app/main/checks/report_checks/__init__.py b/app/main/checks/report_checks/__init__.py index 30f22617..7f1eed4b 100644 --- a/app/main/checks/report_checks/__init__.py +++ b/app/main/checks/report_checks/__init__.py @@ -34,3 +34,4 @@ from .task_tracker import ReportTaskTracker from .paragraphs_count_check import ReportParagraphsCountCheck from .template_name import ReportTemplateNameCheck +from .check_chapters_3_level import ReportСhaptersLevel3ContentCheck diff --git a/app/main/checks/report_checks/check_chapters_3_level.py b/app/main/checks/report_checks/check_chapters_3_level.py new file mode 100644 index 00000000..709aff9c --- /dev/null +++ b/app/main/checks/report_checks/check_chapters_3_level.py @@ -0,0 +1,51 @@ +from ..base_check import BaseReportCriterion, answer + +class ReportСhaptersLevel3ContentCheck(BaseReportCriterion): + label = "Проверка содержания на наличия объктов 3 уровня" + description = "В содержании не должно быть объектов третьего уровня" + id = 'report_3_level_in_content_check' + + def __init__(self, file_info): + super().__init__(file_info) + + + def check(self): + try: + headers = self.file.make_chapters(self.file_type['report_type']) + + if not headers: + return answer(False, "Не найдено ни одного заголовка.") + + level_3_count = 0 + bool_content_find = False + for header in headers: + if header["text"].upper() == "СОДЕРЖАНИЕ": + bool_content_find = True + level_3_count = self._count_level_3_headers(header["child"]) + break + + if not bool_content_find: + return answer(False, "Не найдено заголовка 'Содержание'") + + if level_3_count > 0: + result_str = f"Найдено {level_3_count} заголовков 3 уровня и выше. " + result_str += "Содержание должно содержать только заголовки 1 и 2 уровня.
" + return answer(False, result_str) + + return answer(True, "Все заголовки соответствуют требованиям (1-2 уровень)") + + except Exception as e: + return answer(False, f"Ошибка при проверке: {str(e)}") + + def _count_level_3_headers(self, content): + count = 0 + + for header in content: + if self._is_level_3_or_higher(header): + count += 1 + count += self._count_level_3_headers(header["child"]) + + return count + + def _is_level_3_or_higher(self, header): + return header["level"] >= 3 From 4dc44cb3ff81a045bf9e09135cfdc25d18540c22 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Mon, 9 Feb 2026 17:34:17 +0300 Subject: [PATCH 11/21] rm captcha logic --- .env_example | 2 -- README.md | 2 -- app/routes/upload.py | 2 +- app/server.py | 2 -- app/settings.py | 3 --- app/templates/upload.html | 3 --- assets/scripts/upload.js | 15 ++------------- requirements.txt | 1 - 8 files changed, 3 insertions(+), 27 deletions(-) diff --git a/.env_example b/.env_example index 653025ea..0e58b0a1 100644 --- a/.env_example +++ b/.env_example @@ -1,5 +1,3 @@ -RECAPTCHA_SITE_KEY=123 -RECAPTCHA_SECRET_KEY=123 SECRET_KEY=123 ADMIN_PASSWORD=admin SIGNUP_PAGE_ENABLED=False diff --git a/README.md b/README.md index 8aa9524e..cd28d245 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,6 @@ ## Environment - To `.env` in root: ``` -RECAPTCHA_SITE_KEY=... -RECAPTCHA_SECRET_KEY=... SECRET_KEY=... ADMIN_PASSWORD=... SIGNUP_PAGE_ENABLED=... diff --git a/app/routes/upload.py b/app/routes/upload.py index 521b3d16..b81ba313 100644 --- a/app/routes/upload.py +++ b/app/routes/upload.py @@ -15,7 +15,7 @@ @login_required def upload_main(): if request.method == "POST": - if current_user.is_LTI or True: # app.recaptcha.verify(): - disable captcha (cause no login) + if current_user.is_LTI or current_user.is_admin: return run_task() else: abort(401) diff --git a/app/server.py b/app/server.py index bc6a737f..986dcd48 100644 --- a/app/server.py +++ b/app/server.py @@ -15,7 +15,6 @@ request, url_for) from flask_login import (LoginManager, current_user, login_required, login_user, logout_user) -from flask_recaptcha import ReCaptcha import servants.user as user from app.utils import format_check_for_table, check_file @@ -59,7 +58,6 @@ app = Flask(__name__, static_folder="./../src/", template_folder="./templates/") app.config.from_pyfile('settings.py') -app.recaptcha = ReCaptcha(app=app) app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER app.config['CELERY_RESULT_BACKEND'] = os.environ.get("CELERY_RESULT_BACKEND", "redis://localhost:6379") diff --git a/app/settings.py b/app/settings.py index 3792f9d2..1a54230b 100644 --- a/app/settings.py +++ b/app/settings.py @@ -30,9 +30,6 @@ # setup variables ADMIN_PASSWORD = os.environ.get('ADMIN_PASSWORD', '') -RECAPTCHA_ENABLED = True -RECAPTCHA_SITE_KEY = os.environ.get('RECAPTCHA_SITE_KEY', '') -RECAPTCHA_SECRET_KEY = os.environ.get('RECAPTCHA_SECRET_KEY', '') SECRET_KEY = os.environ.get('SECRET_KEY', '') SIGNUP_PAGE_ENABLED = os.environ.get('SIGNUP_PAGE_ENABLED', 'True') == 'True' diff --git a/app/templates/upload.html b/app/templates/upload.html index f4a140d0..7a020be8 100644 --- a/app/templates/upload.html +++ b/app/templates/upload.html @@ -51,9 +51,6 @@
{{ uploading_label }}
aria-valuenow="10" aria-valuemin="0" aria-valuemax="100" style="width: 10%"> - {% if not (current_user.is_LTI or current_user.is_admin) %} - {{ recaptcha }} - {% endif %}

Общий объем загружаемых файлов не должен превышать {{ config.MAX_CONTENT_LENGTH//1024//1024 }} Mб.

diff --git a/assets/scripts/upload.js b/assets/scripts/upload.js index 1db21586..ba5eb3f8 100644 --- a/assets/scripts/upload.js +++ b/assets/scripts/upload.js @@ -44,9 +44,6 @@ const showBdOverwhelmedMessage = () => { alert('База данных перегружена (недостаточно места для загрузки новых файлов). Свяжитесь с администратором'); } -const showRecaptchaMessage = () => { - alert('Пройдите recaptcha, чтобы продолжить!'); -} const resetFileUpload = () => { pdf_uploaded = false; @@ -138,10 +135,6 @@ async function upload() { } formData.append("file", file); formData.append("file_type", file_type); - if ($('div.g-recaptcha').length) { - let response = grecaptcha.getResponse(); - formData.append("g-recaptcha-response", response); - } const bar = $("#uploading_progress"); $("#uploading_progress_holder").css("display", "block"); @@ -198,12 +191,8 @@ async function upload() { } upload_button.click(async () => { - if ($('div.g-recaptcha').length && grecaptcha.getResponse().length === 0) { - showRecaptchaMessage(); - } else { - upload_button.prop("disabled", true); - await upload(); - } + upload_button.prop("disabled", true); + await upload(); }); function toggleTable(tableId) { diff --git a/requirements.txt b/requirements.txt index ca44fa76..d9728c0a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,6 @@ docx2python~=2.0.4 filetype==1.2.0 Flask==2.0.3 flask-login==0.5.0 -flask-recaptcha==0.4.2 flask-security==3.0.0 flower==1.2.0 fsspec==2022.2.0 From 4c4901849fd23aa5ca3ac97372c3a434f390e2bf Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Tue, 10 Feb 2026 19:17:28 +0300 Subject: [PATCH 12/21] little fix in check description / feedback --- app/main/checks/presentation_checks/image_share.py | 1 - app/main/checks/report_checks/chapters.py | 2 +- app/main/checks/report_checks/headers_at_page_top_check.py | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/main/checks/presentation_checks/image_share.py b/app/main/checks/presentation_checks/image_share.py index 60e51330..be62b088 100644 --- a/app/main/checks/presentation_checks/image_share.py +++ b/app/main/checks/presentation_checks/image_share.py @@ -23,4 +23,3 @@ def check(self): ограничение - {round(self.limit, 2)}') else: return answer(True, f'Пройдена!') - return answer(False, 'Во время обработки произошла критическая ошибка') diff --git a/app/main/checks/report_checks/chapters.py b/app/main/checks/report_checks/chapters.py index e822d7fc..8d2838a5 100644 --- a/app/main/checks/report_checks/chapters.py +++ b/app/main/checks/report_checks/chapters.py @@ -74,7 +74,7 @@ def check(self): break if not marked_style: err = f"Заголовок \"{header['text']}\": " - err += f'Стиль "{header["style"]}" не соответстует ни одному из стилей заголовков.' + err += f'Стиль "{header["style"]}" не соответствует ни одному из стилей заголовков.' result_str += (str(err) + "
") if not result_str: diff --git a/app/main/checks/report_checks/headers_at_page_top_check.py b/app/main/checks/report_checks/headers_at_page_top_check.py index d699cd1b..772119ba 100644 --- a/app/main/checks/report_checks/headers_at_page_top_check.py +++ b/app/main/checks/report_checks/headers_at_page_top_check.py @@ -2,7 +2,7 @@ class ReportHeadersAtPageTopCheck(BaseReportCriterion): - label = "Проверка расположения разделов первого уровня с новой страницы" + label = "Проверка расположения разделов второго уровня с новой страницы" description = '' id = "headers_at_page_top_check" From 70360edaa43993c7d5e73b9bf6077fa07e0e4ccc Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Tue, 10 Feb 2026 20:02:20 +0300 Subject: [PATCH 13/21] Update checks for LR compability --- app/main/check_packs/base_criterion_pack.py | 3 +- app/main/check_packs/utils.py | 3 +- app/main/checks/base_check.py | 6 +- .../banned_words_in_literature.py | 3 +- app/main/checks/report_checks/chapters.py | 65 +++++++++---------- .../headers_at_page_top_check.py | 3 +- .../checks/report_checks/image_references.py | 29 ++++----- .../report_checks/literature_references.py | 3 +- .../checks/report_checks/main_text_check.py | 65 +++++++++---------- .../report_checks/short_sections_check.py | 3 +- .../report_checks/style_check_settings.py | 32 ++++----- .../checks/report_checks/table_references.py | 19 +++--- .../reports/docx_uploader/docx_uploader.py | 39 ++++++----- 13 files changed, 129 insertions(+), 144 deletions(-) diff --git a/app/main/check_packs/base_criterion_pack.py b/app/main/check_packs/base_criterion_pack.py index b48e811c..c387ce40 100644 --- a/app/main/check_packs/base_criterion_pack.py +++ b/app/main/check_packs/base_criterion_pack.py @@ -18,7 +18,8 @@ def __init__(self, raw_criterions, file_type, min_score=1.0, name=None, **kwargs def init(self, file_info): # create criterion objects, ignore errors - validation was performed earlier - self.criterions, errors = init_criterions(self.raw_criterions, file_type=self.file_type, file_info=file_info) + file_info['file_type'] = self.file_type + self.criterions, errors = init_criterions(self.raw_criterions, file_info=file_info) def check(self): result = [] diff --git a/app/main/check_packs/utils.py b/app/main/check_packs/utils.py index 41e105d8..94de6606 100644 --- a/app/main/check_packs/utils.py +++ b/app/main/check_packs/utils.py @@ -6,11 +6,12 @@ logger = getLogger('root_logger') -def init_criterions(criterions, file_type, file_info={}): +def init_criterions(criterions, file_info): """ criterions = [[criterion_id, criterion_params], ...] # criterion_params is dict """ try: + file_type = file_info['file_type'] existing_criterions = AVAILABLE_CHECKS.get(file_type['type'], {}) errors = [] initialized_checks = [] diff --git a/app/main/checks/base_check.py b/app/main/checks/base_check.py index 5e431d10..302795f1 100644 --- a/app/main/checks/base_check.py +++ b/app/main/checks/base_check.py @@ -13,7 +13,6 @@ def answer(mod, *args): class BaseCriterion: description = None label = None - file_type = None id = None priority = False # if priority criterion is failed -> check is failed @@ -21,6 +20,7 @@ def __init__(self, file_info): self.file = file_info.get('file') self.filename = file_info.get('filename', '') self.pdf_id = file_info.get('pdf_id') + self.file_type = file_info.get('file_type') def check(self): raise NotImplementedError() @@ -36,8 +36,8 @@ def name(self): class BasePresCriterion(BaseCriterion): - file_type = 'pres' + pass class BaseReportCriterion(BaseCriterion): - file_type = {'type': 'report', 'report_type': 'VKR'} + pass \ No newline at end of file diff --git a/app/main/checks/report_checks/banned_words_in_literature.py b/app/main/checks/report_checks/banned_words_in_literature.py index 498afa56..4b3d24a6 100644 --- a/app/main/checks/report_checks/banned_words_in_literature.py +++ b/app/main/checks/report_checks/banned_words_in_literature.py @@ -27,6 +27,7 @@ def check(self): if self.file.page_counter() < 4: return answer(False, "В отчете недостаточно страниц. Нечего проверять.") detected_words_dict = {} + # TODO: проверить совместимость / дублируемость LR и VKR if self.file_type['report_type'] == 'LR': list_of_literature = self.find_literature() if len(list_of_literature) == 0: @@ -51,7 +52,7 @@ def check(self): else: detected_words_dict[child_number] = banned_word else: - return answer(False, 'Во время обработки произошла критическая ошибка') + return answer(False, 'Во время обработки произошла критическая ошибка - указан неверный тип работы в наборе критериев') if detected_words_dict: result_str = "" for i in sorted(detected_words_dict.keys()): diff --git a/app/main/checks/report_checks/chapters.py b/app/main/checks/report_checks/chapters.py index 8d2838a5..52b54526 100644 --- a/app/main/checks/report_checks/chapters.py +++ b/app/main/checks/report_checks/chapters.py @@ -13,7 +13,7 @@ class ReportChapters(BaseReportCriterion): def __init__(self, file_info): super().__init__(file_info) self.headers = [] - self.target_styles = StyleCheckSettings.VKR_CONFIG + self.target_styles = StyleCheckSettings.VKR_CONFIG if (self.file_type['report_type'] == 'VKR') else StyleCheckSettings.LR_CONFIG self.target_styles = list(map(lambda elem: { "style": self.construct_style_from_description(elem["style"]) }, self.target_styles.values())) @@ -29,7 +29,7 @@ def __init__(self, file_info): level += 1 def late_init(self): - self.headers = self.file.make_chapters(self.file_type['report_type']) + self.headers = self.file.make_chapters()#self.file_type['report_type']) @staticmethod def construct_style_from_description(style_dict): @@ -57,38 +57,35 @@ def check(self): return answer(False, "В отчете недостаточно страниц. Нечего проверять.") self.late_init() result_str = '' - if self.file_type['report_type'] == 'VKR': - if not len(self.headers): - return answer(False, "Не найдено ни одного заголовка.

Проверьте корректность использования стилей.") - for header in self.headers: - marked_style = 0 - for key in self.docx_styles.keys(): - if not marked_style: - for style_name in self.docx_styles[key]: - if header["style"].find(style_name) >= 0: - if self.style_regex[key].match(header["text"]): - marked_style = 1 - err = self.style_diff(header["styled_text"], self.target_styles[key]["style"]) - err = list(map(lambda msg: f'Стиль "{header["style"]}": ' + msg, err)) - result_str += ("
".join(err) + "
" if len(err) else "") - break + if not len(self.headers): + return answer(False, "Не найдено ни одного заголовка.

Проверьте корректность использования стилей.") + for header in self.headers: + marked_style = 0 + for key in self.docx_styles.keys(): if not marked_style: - err = f"Заголовок \"{header['text']}\": " - err += f'Стиль "{header["style"]}" не соответствует ни одному из стилей заголовков.' - result_str += (str(err) + "
") + for style_name in self.docx_styles[key]: + if header["style"].find(style_name) >= 0: + if self.style_regex[key].match(header["text"]): + marked_style = 1 + err = self.style_diff(header["styled_text"], self.target_styles[key]["style"]) + err = list(map(lambda msg: f'Стиль "{header["style"]}": ' + msg, err)) + result_str += ("
".join(err) + "
" if len(err) else "") + break + if not marked_style: + err = f"Заголовок \"{header['text']}\": " + err += f'Стиль "{header["style"]}" не соответствует ни одному из стилей заголовков.' + result_str += (str(err) + "
") - if not result_str: - return answer(True, "Форматирование заголовков соответствует требованиям.") - else: - result_string = f'Найдены ошибки в оформлении заголовков:
{result_str}
' - result_string += ''' - Попробуйте сделать следующее: -
    -
  • Убедитесь в соответствии стиля заголовка требованиям к отчету по ВКР;
  • -
  • Убедитесь, что названия разделов и нумированные разделы оформлены по ГОСТу;
  • -
  • Убедитесь, что красная строка не сделана с помощью пробелов или табуляции.
  • -
- ''' - return answer(False, result_string) + if not result_str: + return answer(True, "Форматирование заголовков соответствует требованиям.") else: - return answer(False, 'Во время обработки произошла критическая ошибка') + result_string = f'Найдены ошибки в оформлении заголовков:
{result_str}
' + result_string += ''' + Попробуйте сделать следующее: +
    +
  • Убедитесь в соответствии стиля заголовка требованиям к отчету по ВКР;
  • +
  • Убедитесь, что названия разделов и нумированные разделы оформлены по ГОСТу;
  • +
  • Убедитесь, что красная строка не сделана с помощью пробелов или табуляции.
  • +
+ ''' + return answer(False, result_string) diff --git a/app/main/checks/report_checks/headers_at_page_top_check.py b/app/main/checks/report_checks/headers_at_page_top_check.py index 772119ba..677c337e 100644 --- a/app/main/checks/report_checks/headers_at_page_top_check.py +++ b/app/main/checks/report_checks/headers_at_page_top_check.py @@ -23,6 +23,7 @@ def check(self): return answer(False, "В отчете недостаточно страниц. Нечего проверять.") result = True result_str = "" + # TODO: проверить совместимость / дублируемость LR и VKR if self.file_type["report_type"] == 'LR': for header in self.headers: found = False @@ -77,7 +78,7 @@ def check(self): f"Заголовок второго уровня \"{header['text']}\" " f"находится не в начале страницы или занимает больше двух строк.") else: - result_str = "Во время обработки произошла критическая ошибка" + result_str = "Во время обработки произошла критическая ошибка - указан неверный тип работы в наборе критериев" return answer(False, result_str) if not result_str: diff --git a/app/main/checks/report_checks/image_references.py b/app/main/checks/report_checks/image_references.py index f9f68e6b..a55d66ad 100644 --- a/app/main/checks/report_checks/image_references.py +++ b/app/main/checks/report_checks/image_references.py @@ -21,22 +21,19 @@ def check(self): if self.file.page_counter() < 4: return answer(False, "В отчете недостаточно страниц. Нечего проверять.") result_str = '' - if self.file_type['report_type'] == 'VKR': - self.late_init_vkr() - if not len(self.headers): - return answer(False, "Не найдено ни одного заголовка.

Проверьте корректность использования стилей.") - number_of_images, all_numbers = self.count_images_vkr() - count_file_image_object = self.file.pdf_file.get_image_num() - if count_file_image_object and not number_of_images: - return answer(False, f'В отчёте найдено {count_file_image_object} рисунков, но не найдено ни одной подписи рисунка.

Если в вашей работе присутствуют рисунки, убедитесь, что для их подписи был ' - f'использован стиль {self.image_style}, и формат: ' - f'"Рисунок <Номер рисунка> — <Название рисунка>".') - elif not number_of_images: - return answer(True, f'Не найдено ни одного рисунка.

Если в вашей работе присутствуют рисунки, убедитесь, что для их подписи был ' - f'использован стиль {self.image_style}, и формат: ' - f'"Рисунок <Номер рисунка> — <Название рисунка>".') - else: - return answer(False, 'Во время обработки произошла критическая ошибка') + self.late_init_vkr() + if not len(self.headers): + return answer(False, "Не найдено ни одного заголовка.

Проверьте корректность использования стилей.") + number_of_images, all_numbers = self.count_images_vkr() + count_file_image_object = self.file.pdf_file.get_image_num() + if count_file_image_object and not number_of_images: + return answer(False, f'В отчёте найдено {count_file_image_object} рисунков, но не найдено ни одной подписи рисунка.

Если в вашей работе присутствуют рисунки, убедитесь, что для их подписи был ' + f'использован стиль {self.image_style}, и формат: ' + f'"Рисунок <Номер рисунка> — <Название рисунка>".') + elif not number_of_images: + return answer(True, f'Не найдено ни одного рисунка.

Если в вашей работе присутствуют рисунки, убедитесь, что для их подписи был ' + f'использован стиль {self.image_style}, и формат: ' + f'"Рисунок <Номер рисунка> — <Название рисунка>".') references = self.search_references() if len(references.symmetric_difference(all_numbers)) == 0: diff --git a/app/main/checks/report_checks/literature_references.py b/app/main/checks/report_checks/literature_references.py index bcd89792..c0243d48 100644 --- a/app/main/checks/report_checks/literature_references.py +++ b/app/main/checks/report_checks/literature_references.py @@ -36,6 +36,7 @@ def check(self): number_of_sources = 0 start_literature_par = 0 result_str = '' + # TODO: проверить совместимость / дублируемость LR и VKR if self.file_type['report_type'] == 'LR': start_literature_par = self.find_start_paragraph() if start_literature_par: @@ -51,7 +52,7 @@ def check(self): start_literature_par = header["number"] number_of_sources = self.count_sources_vkr(header) else: - return answer(False, 'Во время обработки произошла критическая ошибка') + return answer(False, 'Во время обработки произошла критическая ошибка - указан неверный тип работы в наборе критериев') if not number_of_sources: return answer(False, f'В Списке использованных источников не найдено ни одного источника.

Проверьте корректность использования нумированного списка.') diff --git a/app/main/checks/report_checks/main_text_check.py b/app/main/checks/report_checks/main_text_check.py index bc73b149..8ff371e5 100644 --- a/app/main/checks/report_checks/main_text_check.py +++ b/app/main/checks/report_checks/main_text_check.py @@ -51,38 +51,35 @@ def check(self): return answer(False, "В отчете недостаточно страниц. Нечего проверять.") self.late_init() result_str = '' - if self.file_type['report_type'] == 'VKR': - if not len(self.headers): - return answer(False, - "Не найдено ни одного заголовка.

Проверьте корректность использования стилей.") - for header in self.headers: - if header["text"].find("ПРИЛОЖЕНИЕ") >= 0: - break - err_string = '' - for child in header["child"]: - marked_style = 0 - for i in range(len(self.main_text_styles)): - if child["style"].find(self.main_text_styles[i]) >= 0: - marked_style = 1 - err = self.style_diff(child["styled_text"], self.target_styles[i]["style"]) - err = list(map(lambda msg: f'Стиль "{child["style"]}": ' + msg, err)) - err_string += ("
".join(err) + "
" if len(err) else "") - break - if not marked_style: - err = f"Абзац \"{child['text'][:17] + '...' if len(child['text']) > 20 else child['text']}\": " - err += f'Стиль "{child["style"]}" не соответстует ни одному из стилей основного текста.' - err_string += (str(err) + "
") - if err_string: - result_str += f'    Ошибки в разделе {header["text"]}:
' - result_str += err_string - if not result_str: - return answer(True, "Форматирование текста соответствует требованиям.") - else: - result_str += f'

Перечень допустимых стилей основного текста (Названия как в документе):' \ - f'

{"
".join(x for x in self.main_text_styles_names)}' - result_str += f'

Если в вашем документе нет какого-либо из перечисленных стилей, перенесите ' \ - f'текст пояснительной записки в документ с последней версией ' \ - f'шаблона.' - return answer(False, result_str) + if not len(self.headers): + return answer(False, + "Не найдено ни одного заголовка, что не позволило определить разделы отчета.

Проверьте корректность использования стилей.") + for header in self.headers: + if header["text"].find("ПРИЛОЖЕНИЕ") >= 0: + break + err_string = '' + for child in header["child"]: + marked_style = 0 + for i in range(len(self.main_text_styles)): + if child["style"].find(self.main_text_styles[i]) >= 0: + marked_style = 1 + err = self.style_diff(child["styled_text"], self.target_styles[i]["style"]) + err = list(map(lambda msg: f'Стиль "{child["style"]}": ' + msg, err)) + err_string += ("
".join(err) + "
" if len(err) else "") + break + if not marked_style: + err = f"Абзац \"{child['text'][:17] + '...' if len(child['text']) > 20 else child['text']}\": " + err += f'Стиль "{child["style"]}" не соответстует ни одному из стилей основного текста.' + err_string += (str(err) + "
") + if err_string: + result_str += f'    Ошибки в разделе {header["text"]}:
' + result_str += err_string + if not result_str: + return answer(True, "Форматирование текста соответствует требованиям.") else: - return answer(False, 'Во время обработки произошла критическая ошибка') + result_str += f'

Перечень допустимых стилей основного текста (Названия как в документе):' \ + f'

{"
".join(x for x in self.main_text_styles_names)}' + result_str += f'

Если в вашем документе нет какого-либо из перечисленных стилей, перенесите ' \ + f'текст пояснительной записки в документ с последней версией ' \ + f'шаблона.' + return answer(False, result_str) diff --git a/app/main/checks/report_checks/short_sections_check.py b/app/main/checks/report_checks/short_sections_check.py index 53729cfb..b50c5f30 100644 --- a/app/main/checks/report_checks/short_sections_check.py +++ b/app/main/checks/report_checks/short_sections_check.py @@ -48,6 +48,7 @@ def check(self): return answer(False, "В отчете недостаточно страниц. Нечего проверять.") result = True result_str = "" + # TODO: проверить совместимость / дублируемость LR и VKR if self.file_type['report_type'] == 'LR': self.late_init() if self.cutoff_line is None: @@ -109,7 +110,7 @@ def check(self): result_str = "Все обязательные разделы достигают рекомендуемой длины." return answer(result, result_str) else: - return answer(False, 'Во время обработки произошла критическая ошибка') + return answer(False, 'Во время обработки произошла критическая ошибка - указан неверный тип работы в наборе критериев') def build_header_hierarchy(self): cutoff_index = 0 diff --git a/app/main/checks/report_checks/style_check_settings.py b/app/main/checks/report_checks/style_check_settings.py index 40a7cd21..31775d75 100644 --- a/app/main/checks/report_checks/style_check_settings.py +++ b/app/main/checks/report_checks/style_check_settings.py @@ -8,8 +8,8 @@ class StyleCheckSettings: HEADER_2_NUM_REGEX = "^[1-9][0-9]*\\.([1-9][0-9]*\\ )([\\w\\s]+)$" HEADER_NUM_REGEX = "^\\d.+$" HEADER_REGEX = "^\\D+.+$" - HEADER_1_REGEX = "^()([\\w\\s]+)$" - HEADER_2_REGEX = "^()([\\w\\s]+)\\.$" + HEADER_1_REGEX = r"^([1-9][0-9]*\.([1-9][0-9]*\.)){0,1}([\w\s]+)$" + HEADER_2_REGEX = r"^([1-9][0-9]*\.([1-9][0-9]*\.)*){0,1}([\w\s]+)$" STD_BANNED_WORDS = ('мы', 'моя', 'мои', 'моё', 'наш', 'наши', 'аттач', 'билдить', 'бинарник', 'валидный', 'дебаг', 'деплоить', 'десктопное', 'железо', 'исходники', 'картинка', 'консольное', 'конфиг', 'кусок', 'либа', 'лог', 'мануал', @@ -85,10 +85,10 @@ class StyleCheckSettings: "line_spacing": 1.0 } TABLE_CAPTION_STYLE_VKR = { - "alignment": WD_ALIGN_PARAGRAPH.JUSTIFY, + "alignment": WD_ALIGN_PARAGRAPH.LEFT, "font_name": "Times New Roman", "font_size_pt": 14.0, - "first_line_indent_cm": 1.25, + "first_line_indent_cm": 0.0, "line_spacing": 1.0 } @@ -96,25 +96,17 @@ class StyleCheckSettings: LR_CONFIG = { 'any_header': { - "style": HEADER_1_STYLE, - "docx_style": ["heading 1"], - "headers": ["Исходный код программы"], - "unify_regex": APPENDIX_UNIFY_REGEX, - "regex": APPENDIX_REGEX, + "style": HEADER_2_STYLE, + "docx_style": ["heading 3", "heading 4"], + "headers": ["Цель работы", "Выполнение работы", "Выводы"], + "unify_regex": HEADER_2_REGEX, + "regex": HEADER_2_REGEX, "banned_words": STD_BANNED_WORDS, "warned_words": STD_WARNED_WORDS, 'min_count_for_banned_words_check': 3, 'max_count_for_banned_words_check': 6, 'min_ref_for_literature_references_check': STD_MIN_LIT_REF, 'mах_ref_for_literature_references_check': STD_MAX_LIT_REF - }, - 'second_header': - { - "style": HEADER_2_STYLE, - "docx_style": ["heading 2"], - "headers": ["Цель работы", "Выполнение работы", "Выводы"], - "unify_regex": None, - "regex": HEADER_1_REGEX, } } @@ -298,12 +290,12 @@ class StyleCheckSettings: "style": LISTING_STYLE }, { - "name": "Подпись рисунка", + "name": "вкр_подпись для рисунков", "style": IMAGE_CAPTION_STYLE }, { - "name": "Подпись таблицы", - "style": TABLE_CAPTION_STYLE + "name": "вкр_подпись таблицы", + "style": TABLE_CAPTION_STYLE_VKR } ] diff --git a/app/main/checks/report_checks/table_references.py b/app/main/checks/report_checks/table_references.py index 05afb271..d0861600 100644 --- a/app/main/checks/report_checks/table_references.py +++ b/app/main/checks/report_checks/table_references.py @@ -21,17 +21,14 @@ def check(self): if self.file.page_counter() < 4: return answer(False, "В отчете недостаточно страниц. Нечего проверять.") result_str = '' - if self.file_type['report_type'] == 'VKR': - self.late_init_vkr() - if not len(self.headers): - return answer(False, "Не найдено ни одного заголовка.

Проверьте корректность использования стилей.") - number_of_tables, all_numbers = self.count_tables_vkr() - if not number_of_tables: - return answer(True, f'Не найдено ни одной таблицы.

Если в вашей работе присутствуют таблицы, убедитесь, что для их подписи был ' - f'использован стиль {self.table_style} и формат ' - f'"Таблица <Номер таблицы> -- <Название таблицы>".') - else: - return answer(False, 'Во время обработки произошла критическая ошибка') + self.late_init_vkr() + if not len(self.headers): + return answer(False, "Не найдено ни одного заголовка, что не позволило определить разделы отчета.

Проверьте корректность использования стилей.") + number_of_tables, all_numbers = self.count_tables_vkr() + if not number_of_tables: + return answer(True, f'Не найдено ни одной таблицы.

Если в вашей работе присутствуют таблицы, убедитесь, что для их подписи был ' + f'использован стиль {self.table_style} и формат ' + f'"Таблица <Номер таблицы> -- <Название таблицы>".') references = self.search_references() if len(references.symmetric_difference(all_numbers)) == 0: return answer(True, f"Пройдена!") diff --git a/app/main/reports/docx_uploader/docx_uploader.py b/app/main/reports/docx_uploader/docx_uploader.py index ac30dee4..d300d131 100644 --- a/app/main/reports/docx_uploader/docx_uploader.py +++ b/app/main/reports/docx_uploader/docx_uploader.py @@ -43,28 +43,27 @@ def __make_paragraphs(self, paragraphs): tmp_paragraphs.append(Paragraph(paragraphs[i])) return tmp_paragraphs - def make_chapters(self, work_type): + def make_chapters(self, work_type='VKR'): if not self.chapters: tmp_chapters = [] - if work_type == 'VKR': - # find headers - header_ind = -1 - par_num = 0 - head_par_ind = -1 - for par_ind in range(len(self.styled_paragraphs)): - head_par_ind += 1 - style_name = self.paragraphs[par_ind].paragraph_style_name.lower() - if style_name.find("heading") >= 0: - header_ind += 1 - par_num = 0 - tmp_chapters.append({"style": style_name, "text": self.styled_paragraphs[par_ind]["text"].strip(), - "styled_text": self.styled_paragraphs[par_ind], "number": head_par_ind, - "child": []}) - elif header_ind >= 0: - par_num += 1 - tmp_chapters[header_ind]["child"].append( - {"style": style_name, "text": self.styled_paragraphs[par_ind]["text"], - "styled_text": self.styled_paragraphs[par_ind], "number": head_par_ind}) + # find headers + header_ind = -1 + par_num = 0 + head_par_ind = -1 + for par_ind in range(len(self.styled_paragraphs)): + head_par_ind += 1 + style_name = self.paragraphs[par_ind].paragraph_style_name.lower() + if style_name.find("heading") >= 0: + header_ind += 1 + par_num = 0 + tmp_chapters.append({"style": style_name, "text": self.styled_paragraphs[par_ind]["text"].strip(), + "styled_text": self.styled_paragraphs[par_ind], "number": head_par_ind, + "child": []}) + elif header_ind >= 0: + par_num += 1 + tmp_chapters[header_ind]["child"].append( + {"style": style_name, "text": self.styled_paragraphs[par_ind]["text"], + "styled_text": self.styled_paragraphs[par_ind], "number": head_par_ind}) self.chapters = tmp_chapters return self.chapters From 7dc0aa9284a08994ba40263443bfa35a3a3baf90 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Tue, 10 Feb 2026 20:37:35 +0300 Subject: [PATCH 14/21] update raw_criterions using --- app/routes/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routes/api.py b/app/routes/api.py index fac54056..a84da7c6 100644 --- a/app/routes/api.py +++ b/app/routes/api.py @@ -60,7 +60,7 @@ def api_criteria_pack(): if file_type == DEFAULT_REPORT_TYPE_INFO['type']: file_type_info['report_type'] = report_type if report_type in REPORT_TYPES else DEFAULT_REPORT_TYPE_INFO[ 'report_type'] - inited, err = init_criterions(raw_criterions, file_type=file_type_info) + inited, err = init_criterions(raw_criterions, file_info={"file_type": file_type_info}) if len(raw_criterions) != len(inited) or err: msg = f"При инициализации набора {pack_name} возникли ошибки. JSON-конфигурация: '{raw_criterions}'. Успешно инициализированные: {inited}. Возникшие ошибки: {err}." return {'data': msg, 'time': datetime.now()}, 400 From ab70f5188f5119af763cdfc98de489d99bc4a91a Mon Sep 17 00:00:00 2001 From: baydakov-georgiy Date: Fri, 13 Feb 2026 00:03:32 +0300 Subject: [PATCH 15/21] bring out the general logic of pres and report criteria --- app/main/checks/decimal_places_check.py | 140 ++++++++++++++++++ .../checks/presentation_checks/__init__.py | 1 + .../presentation_checks/decimal_places.py | 16 ++ .../excessive_decimal_places.py | 49 ------ app/main/checks/report_checks/__init__.py | 1 + .../checks/report_checks/decimal_places.py | 19 +++ .../excessive_decimal_places_check.py | 51 ------- 7 files changed, 177 insertions(+), 100 deletions(-) create mode 100644 app/main/checks/decimal_places_check.py create mode 100644 app/main/checks/presentation_checks/decimal_places.py delete mode 100644 app/main/checks/presentation_checks/excessive_decimal_places.py create mode 100644 app/main/checks/report_checks/decimal_places.py delete mode 100644 app/main/checks/report_checks/excessive_decimal_places_check.py diff --git a/app/main/checks/decimal_places_check.py b/app/main/checks/decimal_places_check.py new file mode 100644 index 00000000..31570558 --- /dev/null +++ b/app/main/checks/decimal_places_check.py @@ -0,0 +1,140 @@ +import re +from .base_check import BaseReportCriterion +from collections import defaultdict + + +class DecimalPlacesCheck: + DECIMAL_PATTERN = r'\b\d{1,3}(?:[.,]\d{3})*(?:[.,]\d+)?\b' + + def __init__(self, file_info, max_decimal_places=2, max_violations=3): + self.file_type = file_info['file_type']['type'] + self.max_decimal_places = max_decimal_places + self.max_violations = max_violations + + def is_valid_number(self, match, text): + start_pos = match.start() + end_pos = match.end() + + char_before = text[start_pos - 1] if start_pos > 0 else ' ' + + if char_before == '.': + return False + + if end_pos < len(text): + char_after = text[end_pos] + if char_after == '.' and end_pos + 1 < len(text) and text[end_pos + 1].isdigit(): + return False + + return True + + def count_decimal_places(self, number_str): + normalized = number_str.replace(',', '.') + + if '.' in normalized: + decimal_part = normalized.split('.')[1] + return len(decimal_part) + + return 0 + + def find_violations_in_text(self, text): + violations = [] + matches = re.finditer(self.DECIMAL_PATTERN, text) + + for match in matches: + if not self.is_valid_number(match, text): + continue + + number_str = match.group() + decimal_places = self.count_decimal_places(number_str) + + if decimal_places > self.max_decimal_places: + violations.append((number_str, decimal_places, match)) + + return violations + + def find_violations_in_lines(self, lines): + violations = [] + + for line_idx, line in enumerate(lines): + line_violations = self.find_violations_in_text(line) + + for number_str, decimal_places, _ in line_violations: + violations.append((line_idx, number_str, decimal_places, line)) + + return violations + + def find_violations_in_texts(self, texts): + pages = defaultdict(list) + total_violations = 0 + + for idx, text in texts: + lines = re.split(r'\n', text) + violations = self.find_violations_in_lines(lines) + + for line_idx, number_str, decimal_places, line in violations: + total_violations += 1 + violation_msg = self.format_violation_message( + line_idx, line, number_str, decimal_places + ) + pages[idx].append(violation_msg) + + return (total_violations, pages) + + def highlight_number(self, line, number_str): + return line.replace(number_str, f'{number_str}', 1) + + def format_violation_message(self, line_idx, line, number_str, decimal_places): + highlighted_line = self.highlight_number(line, number_str) + return ( + f'Строка {line_idx + 1}: {highlighted_line} ' + f'(найдено {decimal_places} знаков после запятой, ' + f'максимум: {self.max_decimal_places})' + ) + + def format_success_message(self): + return ( + f'Проверка пройдена! Все числа имеют допустимое количество ' + f'десятичных знаков (не более {self.max_decimal_places}).' + ) + + def format_failure_message(self, total_violations, + violations_by_location, + format_page_link_fn=None): + result_str = ( + f'Найдены числа с избыточным количеством десятичных знаков!
' + f'Максимально допустимое количество знаков после запятой: {self.max_decimal_places}
' + f'Максимально допустимое количество нарушений: {self.max_violations}
' + f'Всего нарушений: {total_violations}

' + ) + + for location_num, violations in violations_by_location.items(): + if format_page_link_fn: + location_str = f'Страница {format_page_link_fn([location_num])}' + else: + location_str = f'Страница №{location_num}' + + result_str += f'{location_str}:
' + result_str += '
'.join(violations) + result_str += '

' + + return result_str + + def get_result_msg_and_score(self, total_violations, detected_pages, format_page_link_fn=None): + if total_violations > self.max_violations: + result_str = self.format_failure_message( + total_violations, + detected_pages, + format_page_link_fn + ) + result_score = 0 + elif total_violations > 0: + result_str = self.format_failure_message( + total_violations, + detected_pages, + format_page_link_fn + ) + result_score = 1.0 + else: + result_str = self.format_success_message() + result_score = 1.0 + return result_str, result_score \ No newline at end of file diff --git a/app/main/checks/presentation_checks/__init__.py b/app/main/checks/presentation_checks/__init__.py index 52bd5f73..8a0a64fb 100644 --- a/app/main/checks/presentation_checks/__init__.py +++ b/app/main/checks/presentation_checks/__init__.py @@ -17,3 +17,4 @@ from .name_of_image_check import PresImageCaptureCheck from .task_tracker import TaskTracker from .overview_in_tasks import OverviewInTasks +from .decimal_places import PresDecimalPlacesCheck \ No newline at end of file diff --git a/app/main/checks/presentation_checks/decimal_places.py b/app/main/checks/presentation_checks/decimal_places.py new file mode 100644 index 00000000..f2739d74 --- /dev/null +++ b/app/main/checks/presentation_checks/decimal_places.py @@ -0,0 +1,16 @@ +from ..base_check import BasePresCriterion, answer +from ..decimal_places_check import DecimalPlacesCheck + +class PresDecimalPlacesCheck(BasePresCriterion): + description = 'Проверка на избыточное количество десятичных знаков в числах' + label = 'Проверка на избыточное количество десятичных знаков' + id = 'decimal_places_check' + + def __init__(self, file_info, max_decimal_places=2, max_violations=3): + super().__init__(file_info) + self.checker = DecimalPlacesCheck(file_info, max_decimal_places, max_violations) + + def check(self): + total_violations, detected_pages = self.checker.find_violations_in_file(enumerate(self.file.get_text_from_slides())) + result_str, result_score = self.checker.get_result_msg_and_score(total_violations, detected_pages, self.format_page_link) + return answer(result_score, result_str) diff --git a/app/main/checks/presentation_checks/excessive_decimal_places.py b/app/main/checks/presentation_checks/excessive_decimal_places.py deleted file mode 100644 index 87bb769b..00000000 --- a/app/main/checks/presentation_checks/excessive_decimal_places.py +++ /dev/null @@ -1,49 +0,0 @@ -import re -from ..base_check import BasePresCriterion, answer -from utils.decimal_checker import DecimalPlacesChecker, DocumentType - - -class PresExcessiveDecimalPlacesCheck(BasePresCriterion): - description = 'Проверка на избыточное количество десятичных знаков в числах' - label = "Проверка на избыточное количество десятичных знаков" - id = 'pres_excessive_decimal_places_check' - - def __init__(self, file_info, max_decimal_places=2, max_number_of_violations=1): - super().__init__(file_info) - self.max_decimal_places = max_decimal_places - self.max_number_of_violations = max_number_of_violations - self.checker = DecimalPlacesChecker(max_decimal_places, DocumentType.PRESENTATION) - - def check(self): - - detected_slides = {} - total_violations = 0 - - for slide_num, slide_text in enumerate(self.file.get_text_from_slides()): - lines = re.split(r'\n', slide_text) - - slide_violations = self.checker.find_violations_in_lines(lines) - - for line_idx, number_str, decimal_places, line in slide_violations: - total_violations += 1 - - if slide_num not in detected_slides: - detected_slides[slide_num] = [] - - violation_msg = self.checker.format_violation_message( - line_idx, line, number_str, decimal_places - ) - detected_slides[slide_num].append(violation_msg) - - if total_violations > self.max_number_of_violations: - result_str = self.checker.format_failure_message( - total_violations, - detected_slides, - format_page_link_fn=self.format_page_link - ) - result_score = 0 - else: - result_str = self.checker.format_success_message() - result_score = 1 - - return answer(result_score, result_str) diff --git a/app/main/checks/report_checks/__init__.py b/app/main/checks/report_checks/__init__.py index 30f22617..7b1b974b 100644 --- a/app/main/checks/report_checks/__init__.py +++ b/app/main/checks/report_checks/__init__.py @@ -34,3 +34,4 @@ 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 diff --git a/app/main/checks/report_checks/decimal_places.py b/app/main/checks/report_checks/decimal_places.py new file mode 100644 index 00000000..8d134044 --- /dev/null +++ b/app/main/checks/report_checks/decimal_places.py @@ -0,0 +1,19 @@ +from ..decimal_places_check import DecimalPlacesCheck +from ..base_check import BaseReportCriterion, answer + +class ReportDecimalPlacesCheck(BaseReportCriterion): + label = 'Проверка на избыточное количество десятичных знаков' + description = 'Проверка на избыточное количество десятичных знаков в числах' + id = 'decimal_places_check' + + def __init__(self, file_info, max_decimal_places=2, max_violations=3): + super().__init__(file_info) + self.checker = DecimalPlacesCheck(file_info, max_decimal_places, max_violations) + + def check(self): + if self.file.page_counter() < 4: + return answer(False, "В отчете недостаточно страниц. Нечего проверять.") + + total_violations, detected_pages = self.checker.find_violations_in_texts(self.file.pdf_file.get_text_on_page().items()) + result_str, result_score = self.checker.get_result_msg_and_score(total_violations, detected_pages, self.format_page_link) + return answer(result_score, result_str) diff --git a/app/main/checks/report_checks/excessive_decimal_places_check.py b/app/main/checks/report_checks/excessive_decimal_places_check.py deleted file mode 100644 index 796f2952..00000000 --- a/app/main/checks/report_checks/excessive_decimal_places_check.py +++ /dev/null @@ -1,51 +0,0 @@ -import re -from ..base_check import BaseReportCriterion, answer -from utils.decimal_checker import DecimalPlacesChecker - - -class ReportExcessiveDecimalPlacesCheck(BaseReportCriterion): - label = "Проверка на избыточное количество десятичных знаков" - description = 'Проверка на избыточное количество десятичных знаков в числах' - id = 'excessive_decimal_places_check' - - def __init__(self, file_info, max_decimal_places=2, max_number_of_violations=2): - super().__init__(file_info) - self.max_decimal_places = max_decimal_places - self.max_number_of_violations = max_number_of_violations - self.checker = DecimalPlacesChecker(max_decimal_places) - - def check(self): - if self.file.page_counter() < 4: - return answer(False, "В отчете недостаточно страниц. Нечего проверять.") - - detected_pages = {} - total_violations = 0 - - for page_num, page_text in self.file.pdf_file.get_text_on_page().items(): - lines = re.split(r'\n', page_text) - - page_violations = self.checker.find_violations_in_lines(lines) - - for line_idx, number_str, decimal_places, line in page_violations: - total_violations += 1 - - if page_num not in detected_pages: - detected_pages[page_num] = [] - - violation_msg = self.checker.format_violation_message( - line_idx, line, number_str, decimal_places - ) - detected_pages[page_num].append(violation_msg) - - if total_violations > self.max_number_of_violations: - result_str = self.checker.format_failure_message( - total_violations, - detected_pages, - format_page_link_fn=self.format_page_link - ) - result_score = 0 - else: - result_str = self.checker.format_success_message() - result_score = 1.0 - - return answer(result_score, result_str) From 020b975f91168dbe179db6ea9869d31f9432eb25 Mon Sep 17 00:00:00 2001 From: baydakov-georgiy Date: Fri, 13 Feb 2026 15:37:05 +0300 Subject: [PATCH 16/21] fixed page num for presentation violations --- .../presentation_checks/decimal_places.py | 6 +- .../checks/report_checks/decimal_places.py | 2 +- app/utils/decimal_checker.py | 126 ------------------ .../checks => utils}/decimal_places_check.py | 1 - 4 files changed, 4 insertions(+), 131 deletions(-) delete mode 100644 app/utils/decimal_checker.py rename app/{main/checks => utils}/decimal_places_check.py (99%) diff --git a/app/main/checks/presentation_checks/decimal_places.py b/app/main/checks/presentation_checks/decimal_places.py index f2739d74..0187222c 100644 --- a/app/main/checks/presentation_checks/decimal_places.py +++ b/app/main/checks/presentation_checks/decimal_places.py @@ -1,9 +1,9 @@ +from app.utils.decimal_places_check import DecimalPlacesCheck from ..base_check import BasePresCriterion, answer -from ..decimal_places_check import DecimalPlacesCheck class PresDecimalPlacesCheck(BasePresCriterion): - description = 'Проверка на избыточное количество десятичных знаков в числах' label = 'Проверка на избыточное количество десятичных знаков' + description = 'Проверка на избыточное количество десятичных знаков в числах' id = 'decimal_places_check' def __init__(self, file_info, max_decimal_places=2, max_violations=3): @@ -11,6 +11,6 @@ def __init__(self, file_info, max_decimal_places=2, max_violations=3): self.checker = DecimalPlacesCheck(file_info, max_decimal_places, max_violations) def check(self): - total_violations, detected_pages = self.checker.find_violations_in_file(enumerate(self.file.get_text_from_slides())) + total_violations, detected_pages = self.checker.find_violations_in_texts(enumerate(self.file.get_text_from_slides(), start=1)) result_str, result_score = self.checker.get_result_msg_and_score(total_violations, detected_pages, self.format_page_link) return answer(result_score, result_str) diff --git a/app/main/checks/report_checks/decimal_places.py b/app/main/checks/report_checks/decimal_places.py index 8d134044..3cbf8a3a 100644 --- a/app/main/checks/report_checks/decimal_places.py +++ b/app/main/checks/report_checks/decimal_places.py @@ -1,4 +1,4 @@ -from ..decimal_places_check import DecimalPlacesCheck +from app.utils.decimal_places_check import DecimalPlacesCheck from ..base_check import BaseReportCriterion, answer class ReportDecimalPlacesCheck(BaseReportCriterion): diff --git a/app/utils/decimal_checker.py b/app/utils/decimal_checker.py deleted file mode 100644 index 47d9dd13..00000000 --- a/app/utils/decimal_checker.py +++ /dev/null @@ -1,126 +0,0 @@ -import re -from enum import Enum - -class DocumentType(Enum): - REPORT = 'report' - PRESENTATION = 'pres' - -class DecimalPlacesChecker: - """ - Класс для проверки чисел на избыточное количество десятичных знаков. - Игнорирует IP-адреса, версии ПО и другие составные числа. - - Проверяет: - - Обычные числа с десятичными знаками (например, -3.14159) - Не проверяет: - - IP-адреса (например, 192.168.1.1) - - Версии ПО (например, 1.2.3.4) - - Другие составные числа - """ - - DECIMAL_PATTERN = r'(? 0 else ' ' - - if char_before == '.': - return False - - if end_pos < len(text): - char_after = text[end_pos] - if char_after == '.' and end_pos + 1 < len(text) and text[end_pos + 1].isdigit(): - return False - - return True - - def count_decimal_places(self, number_str): - normalized = number_str.replace(',', '.') - - if '.' in normalized: - decimal_part = normalized.split('.')[1] - return len(decimal_part) - - return 0 - - def has_excessive_decimals(self, number_str): - return self.count_decimal_places(number_str) > self.max_decimal_places - - def find_violations_in_text(self, text): - violations = [] - matches = re.finditer(self.DECIMAL_PATTERN, text) - - for match in matches: - if not self.is_valid_number(match, text): - continue - - number_str = match.group() - decimal_places = self.count_decimal_places(number_str) - - if decimal_places > self.max_decimal_places: - violations.append((number_str, decimal_places, match)) - - return violations - - def find_violations_in_lines(self, lines): - violations = [] - - for line_idx, line in enumerate(lines): - line_violations = self.find_violations_in_text(line) - - for number_str, decimal_places, match in line_violations: - violations.append((line_idx, number_str, decimal_places, line)) - - return violations - - def highlight_number(self, line, number_str): - return line.replace(number_str, f'{number_str}', 1) - - def format_violation_message(self, line_idx, line, number_str, decimal_places): - highlighted_line = self.highlight_number(line, number_str) - return ( - f'Строка {line_idx + 1}: {highlighted_line} ' - f'(найдено {decimal_places} знаков после запятой, ' - f'максимум: {self.max_decimal_places})' - ) - - def format_success_message(self): - if self.type == DocumentType.REPORT: - document_type = "документе" - elif self.type == DocumentType.PRESENTATION: - document_type = "презентации" - return ( - f'Проверка пройдена! Все числа в {document_type} имеют допустимое количество ' - f'десятичных знаков (не более {self.max_decimal_places}).' - ) - - def format_failure_message(self, total_violations, - violations_by_location, - format_page_link_fn=None): - if self.type == DocumentType.REPORT: - location_label = "Страница" - elif self.type == DocumentType.PRESENTATION: - location_label = "Слайд" - result_str = ( - f'Найдены числа с избыточным количеством десятичных знаков!
' - f'Максимально допустимое количество знаков после запятой: {self.max_decimal_places}
' - f'Всего нарушений: {total_violations}

' - ) - - for location_num, violations in violations_by_location.items(): - if format_page_link_fn: - location_str = f'{location_label} {format_page_link_fn([location_num])}' - else: - location_str = f'{location_label} №{location_num}' - - result_str += f'{location_str}:
' - result_str += '
'.join(violations) - result_str += '

' - - return result_str diff --git a/app/main/checks/decimal_places_check.py b/app/utils/decimal_places_check.py similarity index 99% rename from app/main/checks/decimal_places_check.py rename to app/utils/decimal_places_check.py index 31570558..b4ed1650 100644 --- a/app/main/checks/decimal_places_check.py +++ b/app/utils/decimal_places_check.py @@ -1,5 +1,4 @@ import re -from .base_check import BaseReportCriterion from collections import defaultdict From 35c5865eca11b3e2439a8d0dbb55107112482dbe Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Sun, 15 Feb 2026 20:43:00 +0300 Subject: [PATCH 17/21] update .gitignore --- .gitignore | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 57966ff7..dd2a34fe 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ __pycache__/ .idea venv +.venv +.vscode *.pyc files/* @@ -14,7 +16,5 @@ node_modules src/ .env -/VERSION.json +VERSION.json -app/main/mse22/converted_files/ -/app/main/mse22/for_testing/test/.pytest_cache/ From 2de95e1f1539c734a9375e3c6618708872431b5b Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Sun, 15 Feb 2026 20:43:25 +0300 Subject: [PATCH 18/21] update docker image tag in build.yml --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c8d871b3..f985d7c5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,7 +12,7 @@ jobs: - name: Build system images (non-pulling) run: | # build base image - docker build -f Dockerfile_base -t dvivanov/dis-base:v0.3 . + docker build -f Dockerfile_base -t dvivanov/dis-base:v0.5 . - name: Build docker-compose run: | cp .env_example .env From ff99bf8b11310f8632b75b527c340e772e1cfed6 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Sun, 15 Feb 2026 20:43:36 +0300 Subject: [PATCH 19/21] rm unused workflow --- .github/workflows/collect_commits.yml | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 .github/workflows/collect_commits.yml diff --git a/.github/workflows/collect_commits.yml b/.github/workflows/collect_commits.yml deleted file mode 100644 index 4082c7bb..00000000 --- a/.github/workflows/collect_commits.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Collect Commits -on: - push: - -jobs: - collect-commits: - runs-on: [self-hosted, mse] - steps: - - name: Collect info - run: | - cd /data/ - mkdir -p '${{github.repository}}' - - echo '${{ toJson(github) }}' > "./run.json" - - python3 get_info.py From a1e9e7bf20e90c9d12de50be80eb20b15ecbe274 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Sun, 15 Feb 2026 20:49:07 +0300 Subject: [PATCH 20/21] update pandas version (requirements.txt) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d9728c0a..28962a73 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,7 @@ numpy==1.26.4 oauthlib~=3.1.0 odfpy==1.4.1 odfpy==1.4.1 -pandas~=2.0.3 +pandas==3.0.0 pdfplumber==0.6.1 PyMuPDF==1.26.6 PyPDF2~=3.0.1 From 34125f002ea159288190c9d59ad0d31a054ed88e Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Sun, 15 Feb 2026 21:08:15 +0300 Subject: [PATCH 21/21] fix ReportDecimalPlacesCheck --- app/main/checks/report_checks/decimal_places.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/main/checks/report_checks/decimal_places.py b/app/main/checks/report_checks/decimal_places.py index 3cbf8a3a..7e6b8823 100644 --- a/app/main/checks/report_checks/decimal_places.py +++ b/app/main/checks/report_checks/decimal_places.py @@ -11,9 +11,6 @@ def __init__(self, file_info, max_decimal_places=2, max_violations=3): self.checker = DecimalPlacesCheck(file_info, max_decimal_places, max_violations) def check(self): - if self.file.page_counter() < 4: - return answer(False, "В отчете недостаточно страниц. Нечего проверять.") - total_violations, detected_pages = self.checker.find_violations_in_texts(self.file.pdf_file.get_text_on_page().items()) result_str, result_score = self.checker.get_result_msg_and_score(total_violations, detected_pages, self.format_page_link) return answer(result_score, result_str)