From 2689eda8b6718f2576cbece7022fa1ad76f8adb8 Mon Sep 17 00:00:00 2001 From: Alex Kulikov Date: Thu, 21 May 2026 23:59:30 +0100 Subject: [PATCH 01/15] refactor: remove Trello and Focalboard get_tasks_report variants Only get_tasks_report_planka remains; the Trello/Focalboard handlers, their flow branches, and the URL-based board-entry flow are deleted. Co-Authored-By: Claude Sonnet 4.6 --- src/tg/handler_registry.py | 8 -- src/tg/handlers/__init__.py | 7 +- src/tg/handlers/flow_handlers.py | 55 +------------ src/tg/handlers/get_tasks_report_handler.py | 91 ++++----------------- 4 files changed, 20 insertions(+), 141 deletions(-) diff --git a/src/tg/handler_registry.py b/src/tg/handler_registry.py index a4dc3e5..4d61492 100644 --- a/src/tg/handler_registry.py +++ b/src/tg/handler_registry.py @@ -108,14 +108,6 @@ class HandlerConfig: job_type="manager_reply", description="создать папки для иллюстраторов", ), - HandlerConfig( - command="get_tasks_report_focalboard", - category=CommandCategories.MOST_USED, - access_level="manager", - handler_func=handlers.get_tasks_report_focalboard, - direct_only=True, - description="получить список задач из Focalboard", - ), HandlerConfig( command="get_tasks_report_planka", category=CommandCategories.MOST_USED, diff --git a/src/tg/handlers/__init__.py b/src/tg/handlers/__init__.py index 3854c52..e9ba0d2 100644 --- a/src/tg/handlers/__init__.py +++ b/src/tg/handlers/__init__.py @@ -23,12 +23,7 @@ # Admin (developer) handlers from .get_rubrics_handler import get_rubrics -from .get_tasks_report_handler import ( - get_tasks_report, - get_tasks_report_advanced, - get_tasks_report_focalboard, - get_tasks_report_planka, -) +from .get_tasks_report_handler import get_tasks_report_planka from .help_handler import help from .list_chats_handler import list_chats from .list_job_handler import list_jobs diff --git a/src/tg/handlers/flow_handlers.py b/src/tg/handlers/flow_handlers.py index 780f190..5163790 100644 --- a/src/tg/handlers/flow_handlers.py +++ b/src/tg/handlers/flow_handlers.py @@ -16,11 +16,9 @@ ) from ...db.db_client import DBClient from ...db.db_objects import Reminder -from ...focalboard.focalboard_client import FocalboardClient from ...planka.planka_client import PlankaClient from ...strings import load from ...tg.handlers import get_tasks_report_handler -from ...trello.trello_client import TrelloClient from .utils import get_sender_id, reply, get_sender_username, get_chat_id, get_chat_name logger = logging.getLogger(__name__) @@ -303,15 +301,11 @@ def _handle_task_report_helper(command_data, add_labels, update): board_id = command_data[consts.GetTasksReportData.BOARD_ID] list_id = command_data[consts.GetTasksReportData.LIST_ID] introduction = command_data[consts.GetTasksReportData.INTRO_TEXT] - use_focalboard = command_data[consts.GetTasksReportData.USE_FOCALBOARD] - use_planka = command_data.get(consts.GetTasksReportData.USE_PLANKA, False) messages = get_tasks_report_handler.generate_report_messages( board_id, list_id, introduction, add_labels, - use_focalboard=use_focalboard, - use_planka=use_planka, ) for message in messages: reply(message, update) @@ -345,59 +339,16 @@ def handle(self) -> Optional[PlainTextUserAction]: return None -class GetTasksReportEnterBoardUrlHandler(BaseUserMessageHandler): - def handle(self) -> Optional[PlainTextUserAction]: - trello_client = TrelloClient() - try: - board = trello_client.get_board_by_url(self.user_input) - trello_lists = trello_client.get_lists(board.id) - except Exception: - reply(load("get_tasks_report_handler__board_not_found"), self.update) - return PlainTextUserAction.GET_TASKS_REPORT__ENTER_BOARD_URL - - self.command_data[consts.GetTasksReportData.BOARD_ID] = board.id - self.command_data[consts.GetTasksReportData.LISTS] = [ - lst.to_dict() for lst in trello_lists - ] - - trello_lists_formatted = "\n".join( - [f"{i + 1}) {lst.name}" for i, lst in enumerate(trello_lists)] - ) - reply( - load( - "get_tasks_report_handler__choose_trello_list", - lists=trello_lists_formatted, - ), - self.update, - ) - return PlainTextUserAction.GET_TASKS_REPORT__ENTER_LIST_NUMBER - - class GetTasksReportEnterBoardNumberHandler(BaseUserMessageHandler): def handle(self) -> Optional[PlainTextUserAction]: - trello_client = TrelloClient() - focalboard_client = FocalboardClient() planka_client = PlankaClient() try: board_list = self.tg_context.chat_data[consts.GetTasksReportData.LISTS] - use_focalboard = self.tg_context.chat_data.get( - consts.GetTasksReportData.USE_FOCALBOARD, False - ) - use_planka = self.tg_context.chat_data.get( - consts.GetTasksReportData.USE_PLANKA, False - ) list_idx = int(self.user_input) - 1 assert 0 <= list_idx < len(board_list) board_id = board_list[list_idx]["id"] - if use_planka: - trello_lists = planka_client.get_lists(board_id, sorted=True) - trello_lists = trello_lists[::-1] - elif use_focalboard: - trello_lists = focalboard_client.get_lists(board_id, sorted=True) - trello_lists = trello_lists[::-1] - else: - trello_lists = trello_client.get_lists(board_id) - trello_lists = trello_lists[::-1] + trello_lists = planka_client.get_lists(board_id, sorted=True) + trello_lists = trello_lists[::-1] except Exception as e: logger.warning("Failed to parse board number", exc_info=e) reply( @@ -410,8 +361,6 @@ def handle(self) -> Optional[PlainTextUserAction]: return PlainTextUserAction.GET_TASKS_REPORT__ENTER_BOARD_NUMBER self.command_data[consts.GetTasksReportData.BOARD_ID] = board_id - self.command_data[consts.GetTasksReportData.USE_FOCALBOARD] = use_focalboard - self.command_data[consts.GetTasksReportData.USE_PLANKA] = use_planka self.command_data[consts.GetTasksReportData.LISTS] = [ lst.to_dict() for lst in trello_lists ] diff --git a/src/tg/handlers/get_tasks_report_handler.py b/src/tg/handlers/get_tasks_report_handler.py index a44efc3..86c0473 100644 --- a/src/tg/handlers/get_tasks_report_handler.py +++ b/src/tg/handlers/get_tasks_report_handler.py @@ -18,36 +18,11 @@ logger = logging.getLogger(__name__) -@manager_only -def get_tasks_report(update: telegram.Update, tg_context: telegram.ext.CallbackContext): - _get_task_report_base(update, tg_context, advanced=False) - - return - - -@manager_only -def get_tasks_report_focalboard( - update: telegram.Update, tg_context: telegram.ext.CallbackContext -): - _get_task_report_base(update, tg_context, advanced=False, use_focalboard=True) - - return - - @manager_only def get_tasks_report_planka( update: telegram.Update, tg_context: telegram.ext.CallbackContext ): - _get_task_report_base(update, tg_context, advanced=False, use_planka=True) - - return - - -@manager_only -def get_tasks_report_advanced( - update: telegram.Update, tg_context: telegram.ext.CallbackContext -): - _get_task_report_base(update, tg_context, advanced=True) + _get_task_report_base(update, tg_context, advanced=False) return @@ -56,27 +31,14 @@ def _get_task_report_base( update: telegram.Update, tg_context: telegram.ext.CallbackContext, advanced: bool, - use_focalboard: bool = False, - use_planka: bool = False, ): app_context = AppContext() - if use_planka: - telegram_username = update.effective_user.username - - boards_list = app_context.planka_client.get_boards_for_telegram_user( - telegram_username, - app_context.db_client, - ) - elif use_focalboard: - telegram_username = update.effective_user.username - - boards_list = app_context.focalboard_client.get_boards_for_user( - telegram_username=telegram_username, - db_client=app_context.db_client, - ) - else: - boards_list = app_context.trello_client.get_boards_for_user() + telegram_username = update.effective_user.username + boards_list = app_context.planka_client.get_boards_for_telegram_user( + telegram_username, + app_context.db_client, + ) boards_list = sorted(boards_list, key=lambda board: board.name) boards_list_formatted = "\n".join( [f"{i + 1}) {brd.name}" for i, brd in enumerate(boards_list)] @@ -86,8 +48,7 @@ def _get_task_report_base( tg_context.chat_data[consts.GetTasksReportData.LISTS] = [ lst.to_dict() for lst in boards_list ] - tg_context.chat_data[consts.GetTasksReportData.USE_FOCALBOARD] = use_focalboard - tg_context.chat_data[consts.GetTasksReportData.USE_PLANKA] = use_planka + tg_context.chat_data[consts.GetTasksReportData.USE_PLANKA] = True tg_context.chat_data[TASK_NAME] = { consts.NEXT_ACTION: consts.PlainTextUserAction.GET_TASKS_REPORT__ENTER_BOARD_NUMBER.value } @@ -105,29 +66,16 @@ def generate_report_messages( list_id: str, introduction: str, add_labels: bool, - use_focalboard: bool, - use_planka: bool = False, ) -> List[str]: app_context = AppContext() - paragraphs = [] # list of paragraph strings - - if use_planka: - trello_list = app_context.planka_client.get_list(board_id, list_id) - elif use_focalboard: - trello_list = app_context.focalboard_client.get_list(board_id, list_id) - else: - trello_list = app_context.trello_client.get_list(list_id) + paragraphs = [] + + trello_list = app_context.planka_client.get_list(board_id, list_id) paragraphs.append(load("common__bold_wrapper", arg=trello_list.name)) - if use_planka: - list_cards = app_context.planka_client.get_cards(list_id, board_id) - elif use_focalboard: - list_cards = app_context.focalboard_client.get_cards([list_id], board_id) - else: - list_cards = app_context.trello_client.get_cards([list_id], board_id) - print(list_cards) + list_cards = app_context.planka_client.get_cards(list_id, board_id) paragraphs += _create_paragraphs_from_cards( - list_cards, introduction, add_labels, app_context + list_cards, introduction, add_labels, app_context.db_client ) return paragraphs_to_messages(paragraphs) @@ -136,7 +84,7 @@ def _create_paragraphs_from_cards( cards: Iterable[TrelloCard], introduction: str, need_label: bool, - app_context: AppContext, + db_client, ): paragraphs = [] if introduction: @@ -145,20 +93,17 @@ def _create_paragraphs_from_cards( members = _get_members(cards) for member in members: lines = [] - member_name = _make_member_text(member, app_context.db_client) + member_name = _make_member_text(member, db_client) lines.append(member_name) member_cards = _get_member_cards(member, cards) - cards_text = _make_cards_text(member_cards, need_label, app_context) + cards_text = _make_cards_text(member_cards, need_label) lines += cards_text paragraphs.append("\n".join(lines)) - # cards without members at the end cards_without_members = _get_cards_without_members(cards) if cards_without_members: lines = [load("get_tasks_report_handler__misc")] - cards_without_members_text = _make_cards_text( - cards_without_members, need_label, app_context - ) + cards_without_members_text = _make_cards_text(cards_without_members, need_label) lines += cards_without_members_text paragraphs.append("\n".join(lines)) return paragraphs @@ -220,9 +165,7 @@ def _make_deadline_text(card: TrelloCard) -> str: ) -def _make_cards_text( - cards: Iterable[TrelloCard], need_label: bool, app_context: AppContext -) -> List[str]: +def _make_cards_text(cards: Iterable[TrelloCard], need_label: bool) -> List[str]: # generates the text of the cards, cards come already sorted by date return [_format_card(card, need_label) for card in cards] From ab9e973b6680378065e1eb6e7154684ec4cadf18 Mon Sep 17 00:00:00 2001 From: Alex Kulikov Date: Sat, 23 May 2026 12:19:47 +0100 Subject: [PATCH 02/15] fix: normalize numeric telegram usernames --- src/jobs/hr_acquisition_job.py | 11 ++++------- src/jobs/hr_acquisition_pt_job.py | 13 ++++--------- src/utils/telegram.py | 4 ++++ tests/unit/test_hr_acquisition_jobs.py | 15 +++++++++++++++ 4 files changed, 27 insertions(+), 16 deletions(-) create mode 100644 src/utils/telegram.py create mode 100644 tests/unit/test_hr_acquisition_jobs.py diff --git a/src/jobs/hr_acquisition_job.py b/src/jobs/hr_acquisition_job.py index eef5b40..73c3305 100644 --- a/src/jobs/hr_acquisition_job.py +++ b/src/jobs/hr_acquisition_job.py @@ -7,6 +7,7 @@ from ..sheets.sheets_objects import HRPersonProcessed, HRPersonRaw from ..strings import load from ..tg.sender import pretty_send +from ..utils.telegram import normalize_telegram_username from .base_job import BaseJob logger = logging.getLogger(__name__) @@ -47,10 +48,6 @@ def _process_new_people( return new_items - @staticmethod - def _normalize_telegram(username: str) -> str: - return username.strip().lstrip("@").lower() - @staticmethod def _process_raw_forms( forms_raw: Table, forms_processed: Table @@ -59,7 +56,7 @@ def _process_raw_forms( existing_people = [person for person in people if person.status] new_people = [person for person in people if not person.status] existing_telegrams = { - HRAcquisitionJob._normalize_telegram(p.telegram) + normalize_telegram_username(p.telegram) for p in existing_people if p.telegram } @@ -71,9 +68,9 @@ def _process_raw_forms( person.status = load("sheets__hr__raw__status_rejection") continue if person.telegram: - normalized = HRAcquisitionJob._normalize_telegram(person.telegram) + normalized = normalize_telegram_username(person.telegram) new_telegrams = { - HRAcquisitionJob._normalize_telegram(p.telegram) + normalize_telegram_username(p.telegram) for p in new_items if p.telegram } diff --git a/src/jobs/hr_acquisition_pt_job.py b/src/jobs/hr_acquisition_pt_job.py index 1328f21..0353f3a 100644 --- a/src/jobs/hr_acquisition_pt_job.py +++ b/src/jobs/hr_acquisition_pt_job.py @@ -7,6 +7,7 @@ from ..sheets.sheets_objects import HRPersonPTProcessed, HRPersonPTRaw from ..strings import load from ..tg.sender import pretty_send +from ..utils.telegram import normalize_telegram_username from .base_job import BaseJob logger = logging.getLogger(__name__) @@ -47,10 +48,6 @@ def _process_new_people( return new_items - @staticmethod - def _normalize_telegram(username: str) -> str: - return username.strip().lstrip("@").lower() - @staticmethod def _process_raw_forms( forms_raw: Table, forms_processed: Table @@ -59,7 +56,7 @@ def _process_raw_forms( existing_people = [person for person in people if person.status] new_people = [person for person in people if not person.status] existing_telegrams = { - HRAcquisitionPTJob._normalize_telegram(p.telegram) + normalize_telegram_username(p.telegram) for p in existing_people if p.telegram } @@ -70,11 +67,9 @@ def _process_raw_forms( if not person.telegram: person.status = load("sheets__hr__pt__raw__status_rejection") continue - normalized = HRAcquisitionPTJob._normalize_telegram(person.telegram) + normalized = normalize_telegram_username(person.telegram) new_telegrams = { - HRAcquisitionPTJob._normalize_telegram(p.telegram) - for p in new_items - if p.telegram + normalize_telegram_username(p.telegram) for p in new_items if p.telegram } if normalized in existing_telegrams or normalized in new_telegrams: person.status = load("sheets__hr__pt__raw__status_double") diff --git a/src/utils/telegram.py b/src/utils/telegram.py new file mode 100644 index 0000000..2efd259 --- /dev/null +++ b/src/utils/telegram.py @@ -0,0 +1,4 @@ +def normalize_telegram_username(username: object) -> str: + if username is None: + return "" + return str(username).strip().lstrip("@").lower() diff --git a/tests/unit/test_hr_acquisition_jobs.py b/tests/unit/test_hr_acquisition_jobs.py new file mode 100644 index 0000000..105891e --- /dev/null +++ b/tests/unit/test_hr_acquisition_jobs.py @@ -0,0 +1,15 @@ +import pytest + +from src.utils.telegram import normalize_telegram_username + + +@pytest.mark.parametrize( + "raw_telegram, expected", + ( + (" @ExampleUser ", "exampleuser"), + (123456789, "123456789"), + (None, ""), + ), +) +def test_normalize_telegram_username_handles_sheet_values(raw_telegram, expected): + assert normalize_telegram_username(raw_telegram) == expected From e1892352b697b442d84ba55bccf65572c3447d34 Mon Sep 17 00:00:00 2001 From: Alex Kulikov Date: Sat, 23 May 2026 12:59:59 +0100 Subject: [PATCH 03/15] refactor: resolve telegram usernames from team members --- src/db/db_client.py | 23 ++++++++++-------- tests/unit/test_db_client.py | 45 ++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/src/db/db_client.py b/src/db/db_client.py index 819516f..095aeac 100644 --- a/src/db/db_client.py +++ b/src/db/db_client.py @@ -8,6 +8,7 @@ from .. import consts from ..sheets.sheets_client import GoogleSheetsClient from ..utils.singleton import Singleton +from ..utils.telegram import normalize_telegram_username from .db_objects import ( Author, Base, @@ -201,17 +202,21 @@ def find_focalboard_username_by_telegram_username(self, telegram_username: str): return member.focalboard def find_author_telegram_by_trello(self, trello_id: str): - # TODO: make batch queries - session = self.Session() - author = ( - session.query(Author) - .filter((Author.trello == trello_id) | (Author.focalboard == trello_id)) - .first() + normalized = normalize_telegram_username(trello_id) + members = self.Session().query(TeamMember).all() + member = next( + ( + m + for m in members + if normalize_telegram_username(m.trello) == normalized + or normalize_telegram_username(m.focalboard) == normalized + ), + None, ) - if author is None: - logger.warning(f"Telegram id not found for author {trello_id}") + if member is None: + logger.warning(f"Telegram id not found for team member {trello_id}") return None - return author.telegram + return member.telegram def get_curator_by_telegram(self, telegram: str) -> Curator: session = self.Session() diff --git a/tests/unit/test_db_client.py b/tests/unit/test_db_client.py index b8ef58b..bec3eac 100644 --- a/tests/unit/test_db_client.py +++ b/tests/unit/test_db_client.py @@ -1,6 +1,51 @@ +import pytest + +from src.db.db_client import DBClient +from src.db.db_objects import TeamMember + + +@pytest.fixture(autouse=True) +def reset_db_client_singleton(): + DBClient.drop_instance() + yield + DBClient.drop_instance() + + def test_init(mock_db_client): pass def test_fetch_all(mock_db_client, mock_sheets_client): mock_db_client.fetch_all(mock_sheets_client) + + +def test_find_author_telegram_by_trello_reads_team_members(mock_db_client): + session = mock_db_client.Session() + session.add_all( + [ + TeamMember( + id="1", + name="Trello Person", + telegram="@TrelloTelegram", + trello="@TrelloUser", + focalboard="@DifferentFocalboardUser", + ), + TeamMember( + id="2", + name="Focalboard Person", + telegram="@FocalboardTelegram", + trello="@DifferentTrelloUser", + focalboard="@FocalboardUser", + ), + ] + ) + session.commit() + + assert ( + mock_db_client.find_author_telegram_by_trello(" trellouser ") + == "@TrelloTelegram" + ) + assert ( + mock_db_client.find_author_telegram_by_trello("@focalboarduser") + == "@FocalboardTelegram" + ) From 281d4c750ea3482524750d1d0d75b02ca96e60dd Mon Sep 17 00:00:00 2001 From: Alex Kulikov Date: Sat, 23 May 2026 14:10:42 +0100 Subject: [PATCH 04/15] refactor: remove author table sync --- config.json | 1 - src/db/db_client.py | 19 --------- src/db/db_objects.py | 50 +---------------------- src/jobs/__init__.py | 1 - src/jobs/db_fetch_all_team_members_job.py | 6 --- src/jobs/db_fetch_authors_sheet_job.py | 20 --------- src/jobs/utils.py | 4 +- src/sheets/sheets_client.py | 4 -- src/tg/handler_registry.py | 6 --- tests/unit/static/config_override.json | 1 - tests/unit/test_sheets_client.py | 7 ---- tests/unit/test_telegram_jobs.py | 4 -- 12 files changed, 3 insertions(+), 120 deletions(-) delete mode 100644 src/jobs/db_fetch_authors_sheet_job.py diff --git a/config.json b/config.json index 2eff887..d9adf32 100644 --- a/config.json +++ b/config.json @@ -25,7 +25,6 @@ }, "sheets": { "api_key_path": "do_not_set_here_please_go_to_config_override", - "authors_sheet_key": "do_not_set_here_please_go_to_config_override", "curators_sheet_key": "do_not_set_here_please_go_to_config_override", "hr_sheet_key": "do_not_set_here_please_go_to_config_override", "hr_pt_sheet_key": "do_not_set_here_please_go_to_config_override", diff --git a/src/db/db_client.py b/src/db/db_client.py index 095aeac..e911bc2 100644 --- a/src/db/db_client.py +++ b/src/db/db_client.py @@ -10,7 +10,6 @@ from ..utils.singleton import Singleton from ..utils.telegram import normalize_telegram_username from .db_objects import ( - Author, Base, Chat, Curator, @@ -68,28 +67,10 @@ def _ensure_team_telegram_id_column(self): ) def fetch_all(self, sheets_client: GoogleSheetsClient): - self.fetch_authors_sheet(sheets_client) self.fetch_curators_sheet(sheets_client) self.fetch_team_sheet(sheets_client) self.fetch_rubrics_sheet(sheets_client) - def fetch_authors_sheet(self, sheets_client: GoogleSheetsClient): - session = self.Session() - try: - # clean this table - session.query(Author).delete() - # re-download it - authors = sheets_client.fetch_authors() - for item in authors: - author = Author.from_sheetfu_item(item) - session.add(author) - session.commit() - except Exception as e: - logger.warning("Failed to update authors table from sheet", exc_info=e) - session.rollback() - return 0 - return len(authors) - def fetch_curators_sheet(self, sheets_client: GoogleSheetsClient): session = self.Session() try: diff --git a/src/db/db_objects.py b/src/db/db_objects.py index e3a7cbf..dcf51c2 100644 --- a/src/db/db_objects.py +++ b/src/db/db_objects.py @@ -43,58 +43,10 @@ def process_result_value(self, value, dialect): return value -class Author(Base): - __tablename__ = "authors" - - name = Column(String, primary_key=True) - curator = Column(String) - status = Column(String) - telegram = Column(String) - trello = Column(String) - focalboard = Column(String) - - def __repr__(self): - return f"Author {self.name} tg={self.telegram} trello={self.trello} f={self.focalboard}" - - @classmethod - def from_dict(cls, data): - author = cls() - author.name = _get_str_data_item(data, "name") - author.curator = _get_str_data_item(data, "curator") - author.status = _get_str_data_item(data, "status") - author.telegram = _get_str_data_item(data, "telegram") - author.trello = _get_str_data_item(data, "trello") - author.focalboard = _get_str_data_item(data, "focalboard") - return author - - def to_dict(self): - return { - "name": self.name, - "curator": self.curator, - "status": self.status, - "telegram": self.telegram, - "trello": self.trello, - "focalboard": self.focalboard, - } - - @classmethod - def from_sheetfu_item(cls, item): - author = cls() - author.name = item.get_field_value(load("sheets__what_is_your_name")) - author.curator = item.get_field_value(load("sheets__curator_as_author")) - author.status = item.get_field_value(load("sheets__status")) - author.telegram = item.get_field_value(load("sheets__telegram")) - author.trello = item.get_field_value(load("sheets__trello")) - author.focalboard = item.get_field_value(load("sheets__focalboard")) - return author - - class Curator(Base): __tablename__ = "curators" - role = Column( - String, ForeignKey("authors.curator"), primary_key=True - ) # e.g. "Куратор NLP 1" + role = Column(String, primary_key=True) # e.g. "Куратор NLP 1" name = Column(String, primary_key=True) telegram = Column(String) team = Column(String) # e.g. "Авторы" diff --git a/src/jobs/__init__.py b/src/jobs/__init__.py index e701b5b..01357f6 100644 --- a/src/jobs/__init__.py +++ b/src/jobs/__init__.py @@ -10,7 +10,6 @@ from .config_updater_job import ConfigUpdaterJob from .create_folders_for_illustrators_job import CreateFoldersForIllustratorsJob from .db_fetch_all_team_members_job import DBFetchAllTeamMembersJob -from .db_fetch_authors_sheet_job import DBFetchAuthorsSheetJob from .db_fetch_curators_sheet_job import DBFetchCuratorsSheetJob from .db_fetch_strings_sheet_job import DBFetchStringsSheetJob from .db_fetch_team_sheet_job import DBFetchTeamSheetJob diff --git a/src/jobs/db_fetch_all_team_members_job.py b/src/jobs/db_fetch_all_team_members_job.py index 8fe37db..4a3f20f 100644 --- a/src/jobs/db_fetch_all_team_members_job.py +++ b/src/jobs/db_fetch_all_team_members_job.py @@ -13,12 +13,6 @@ class DBFetchAllTeamMembersJob(BaseJob): def _execute( app_context: AppContext, send: Callable[[str], None], called_from_handler=False ): - num_authors = app_context.db_client.fetch_authors_sheet( - app_context.sheets_client - ) - logger.info(f"Fetched {num_authors} authors") - send(load("db_fetch_authors_sheet_job__success", num_authors=num_authors)) - num_curators = app_context.db_client.fetch_curators_sheet( app_context.sheets_client ) diff --git a/src/jobs/db_fetch_authors_sheet_job.py b/src/jobs/db_fetch_authors_sheet_job.py deleted file mode 100644 index fcc0038..0000000 --- a/src/jobs/db_fetch_authors_sheet_job.py +++ /dev/null @@ -1,20 +0,0 @@ -import logging -from typing import Callable - -from ..app_context import AppContext -from ..strings import load -from .base_job import BaseJob - -logger = logging.getLogger(__name__) - - -class DBFetchAuthorsSheetJob(BaseJob): - @staticmethod - def _execute( - app_context: AppContext, send: Callable[[str], None], called_from_handler=False - ): - num_authors = app_context.db_client.fetch_authors_sheet( - app_context.sheets_client - ) - logger.info(f"Fetched {num_authors} authors") - send(load("db_fetch_authors_sheet_job__success", num_authors=num_authors)) diff --git a/src/jobs/utils.py b/src/jobs/utils.py index fab4c89..3aebd98 100644 --- a/src/jobs/utils.py +++ b/src/jobs/utils.py @@ -50,7 +50,7 @@ def retrieve_curator_names_by_author( """ Tries to find a curator for trello member. Returns nothing if user is curator. Returns: "John Smith (@jsmith_tg)" where possible, otherwise "John Smith". - If trello member or curator could not be found in Authors sheet, returns None + If trello member or curator could not be found in the team data, returns None """ trello_id = "@" + trello_member.username try: @@ -69,7 +69,7 @@ def retrieve_curator_names_by_author( def retrieve_curator_names_by_categories(labels: List[str], db_client: DBClient): """ - To be used when there is no known authors. + To be used when there are no known card members. Category is a trello label (e.g. NLP) """ curators = set() diff --git a/src/sheets/sheets_client.py b/src/sheets/sheets_client.py index a2acbd7..f910881 100644 --- a/src/sheets/sheets_client.py +++ b/src/sheets/sheets_client.py @@ -29,7 +29,6 @@ def update_config(self, new_sheets_config: dict): def _update_from_config(self): """Update attributes according to current self._sheets_config""" - self.authors_sheet_key = self._sheets_config["authors_sheet_key"] self.curators_sheet_key = self._sheets_config["curators_sheet_key"] self.hr_sheet_key = self._sheets_config["hr_sheet_key"] self.hr_pt_sheet_key = self._sheets_config["hr_pt_sheet_key"] @@ -46,9 +45,6 @@ def _update_from_config(self): def _authorize(self): self.client = SpreadsheetApp(self._sheets_config["api_key_path"]) - def fetch_authors(self) -> Table: - return self._fetch_table(self.authors_sheet_key, "Кураторы и контакты") - def fetch_curators(self) -> Table: return self._fetch_table(self.curators_sheet_key) diff --git a/src/tg/handler_registry.py b/src/tg/handler_registry.py index 4d61492..98fb2aa 100644 --- a/src/tg/handler_registry.py +++ b/src/tg/handler_registry.py @@ -315,12 +315,6 @@ class HandlerConfig: job_name="sample_job", job_type="admin_reply", ), - HandlerConfig( - command="db_fetch_authors_sheet", - access_level="hidden", - job_name="db_fetch_authors_sheet_job", - job_type="admin_reply", - ), HandlerConfig( command="db_fetch_curators_sheet", access_level="hidden", diff --git a/tests/unit/static/config_override.json b/tests/unit/static/config_override.json index 021aaca..3dc2bd5 100644 --- a/tests/unit/static/config_override.json +++ b/tests/unit/static/config_override.json @@ -10,7 +10,6 @@ }, "sheets": { "api_key_path": "stub", - "authors_sheet_key": "authors_sheet_key", "curators_sheet_key": "curators_sheet_key", "hr_sheet_key": "hr_sheet_key", "hr_pt_sheet_key": "hr_pt_sheet_key", diff --git a/tests/unit/test_sheets_client.py b/tests/unit/test_sheets_client.py index c045146..2fa1df7 100644 --- a/tests/unit/test_sheets_client.py +++ b/tests/unit/test_sheets_client.py @@ -24,7 +24,6 @@ def _make_sheets_client(monkeypatch, sheets_config): def _base_sheets_config(team_identity_sheet_key=None): config = { "api_key_path": "stub", - "authors_sheet_key": "authors_sheet_key", "curators_sheet_key": "curators_sheet_key", "hr_sheet_key": "hr_sheet_key", "hr_pt_sheet_key": "hr_pt_sheet_key", @@ -115,12 +114,6 @@ def _fetch_table(self, sheet_key, sheet_name=None): assert calls == [("team_identity_sheet_key", "telegram")] -@pytest.mark.skip(reason="TODO") -def test_fetch_authors(mock_sheets_client): - authors = [author.to_dict() for author in mock_sheets_client.fetch_authors()] - json_loader.assert_equal(authors, "authors.json") - - @pytest.mark.skip(reason="TODO") def test_fetch_curators(mock_sheets_client): curators = [curator.to_dict() for curator in mock_sheets_client.fetch_curators()] diff --git a/tests/unit/test_telegram_jobs.py b/tests/unit/test_telegram_jobs.py index dfd82b7..63d04d3 100644 --- a/tests/unit/test_telegram_jobs.py +++ b/tests/unit/test_telegram_jobs.py @@ -35,10 +35,6 @@ # # jobs.fill_posts_list_job.FillPostsListJob, # # ), # ( - # jobs.db_fetch_authors_sheet_job.DBFetchAuthorsSheetJob, - # ['Fetched 2'] - # ), - # ( # jobs.db_fetch_curators_sheet_job.DBFetchCuratorsSheetJob, # ['Fetched 1'] # ), From fd38ebcb218ad755146df523e911df867661a044 Mon Sep 17 00:00:00 2001 From: Alex Kulikov Date: Mon, 25 May 2026 18:45:15 +0100 Subject: [PATCH 05/15] refactor: remove fill_posts_list Trello and Focalboard jobs Both jobs and their handler registrations have been dead for 2+ years. Will be re-added as a new feature when ported to Planka. Co-Authored-By: Claude Sonnet 4.6 --- src/jobs/__init__.py | 2 - src/jobs/fill_posts_list_focalboard_job.py | 136 --------------------- src/jobs/fill_posts_list_job.py | 129 ------------------- src/tg/handler_registry.py | 18 --- 4 files changed, 285 deletions(-) delete mode 100644 src/jobs/fill_posts_list_focalboard_job.py delete mode 100644 src/jobs/fill_posts_list_job.py diff --git a/src/jobs/__init__.py b/src/jobs/__init__.py index 01357f6..6132692 100644 --- a/src/jobs/__init__.py +++ b/src/jobs/__init__.py @@ -14,8 +14,6 @@ from .db_fetch_strings_sheet_job import DBFetchStringsSheetJob from .db_fetch_team_sheet_job import DBFetchTeamSheetJob from .fb_analytics_report_job import FBAnalyticsReportJob -from .fill_posts_list_focalboard_job import FillPostsListFocalboardJob -from .fill_posts_list_job import FillPostsListJob from .hr_acquisition_job import HRAcquisitionJob from .hr_acquisition_pt_job import HRAcquisitionPTJob from .hr_status_job import HRStatusJob diff --git a/src/jobs/fill_posts_list_focalboard_job.py b/src/jobs/fill_posts_list_focalboard_job.py deleted file mode 100644 index 72e75f2..0000000 --- a/src/jobs/fill_posts_list_focalboard_job.py +++ /dev/null @@ -1,136 +0,0 @@ -import datetime -import logging -from typing import Callable, List - -from ..app_context import AppContext -from ..consts import BoardCardColor, TrelloCardColor, BoardListAlias -from ..focalboard.focalboard_client import FocalboardClient -from ..sheets.sheets_objects import RegistryPost -from ..strings import load -from ..tg.sender import pretty_send -from .base_job import BaseJob -from .utils import check_trello_card, format_errors - -logger = logging.getLogger(__name__) - - -class FillPostsListFocalboardJob(BaseJob): - @staticmethod - def _execute( - app_context: AppContext, - send: Callable[[str], None], - called_from_handler=False, - ): - errors = {} - registry_posts = [] - all_rubrics = app_context.db_client.get_rubrics() - - registry_posts += FillPostsListFocalboardJob._retrieve_cards_for_registry( - focalboard_client=app_context.focalboard_client, - list_aliases=( - BoardListAlias.PUBLISH_BACKLOG_9, - BoardListAlias.PUBLISH_IN_PROGRESS_10, - ), - all_rubrics=all_rubrics, - errors=errors, - ) - - if len(errors) == 0: - posts_added = app_context.sheets_client.update_posts_registry( - registry_posts - ) - if len(posts_added) == 0: - paragraphs = [load("fill_posts_list_job__unchanged")] - else: - paragraphs = [load("fill_posts_list_job__success")] + [ - "\n".join( - f"{index + 1}) {post_name}" - for index, post_name in enumerate(posts_added) - ) - ] - else: - paragraphs = format_errors(errors) - - pretty_send(paragraphs, send) - - @staticmethod - def _retrieve_cards_for_registry( - focalboard_client: FocalboardClient, - list_aliases: List[str], - errors: dict, - all_rubrics: List, - show_due=True, - need_illustrators=True, - strict_archive_rules=True, - ) -> List[str]: - """ - Returns a list of paragraphs that should always go in a single message. - """ - list_ids = focalboard_client.get_list_id_from_aliases(list_aliases) - cards = focalboard_client.get_cards(list_ids) - if show_due: - cards.sort(key=lambda card: card.due or datetime.datetime.min) - parse_failure_counter = 0 - - registry_posts = [] - - for card in cards: - if not card: - parse_failure_counter += 1 - continue - - card_fields = focalboard_client.get_custom_fields(card.id) - - label_names = [label.name for label in card_fields._data] - card.labels = card_fields._data - is_main_post = load("common_trello_label__main_post") in label_names - is_archive_post = load("common_trello_label__archive") in label_names - - card.due = focalboard_client.get_card_due( - card.id, focalboard_client.board_id - ) - card_is_ok = check_trello_card( - card, - errors, - is_bad_title=( - card_fields.title is None - and card.lst.id - != focalboard_client.lists_config[BoardListAlias.PENDING_EDITOR_5] - ), - is_bad_google_doc=card_fields.google_doc is None, - is_bad_authors=len(card_fields.authors) == 0, - is_bad_editors=len(card_fields.editors) == 0, - is_bad_cover=card_fields.cover is None and not is_archive_post, - is_bad_illustrators=( - len(card_fields.illustrators) == 0 - and need_illustrators - and not is_archive_post - ), - is_bad_due_date=card.due is None and show_due, - is_bad_label_names=len( - [ - label - for label in card.labels - if label.color - not in [TrelloCardColor.BLACK, BoardCardColor.BLACK] - ] - ) - == 0, - ) - - if not card_is_ok: - continue - - registry_posts.append( - RegistryPost( - card, - card_fields, - is_main_post, - is_archive_post, - all_rubrics, - ) - ) - - if parse_failure_counter > 0: - logger.error(f"Unparsed cards encountered: {parse_failure_counter}") - return registry_posts diff --git a/src/jobs/fill_posts_list_job.py b/src/jobs/fill_posts_list_job.py deleted file mode 100644 index d89ea2b..0000000 --- a/src/jobs/fill_posts_list_job.py +++ /dev/null @@ -1,129 +0,0 @@ -import datetime -import logging -from typing import Callable, List - -from ..app_context import AppContext -from ..consts import BoardCardColor, TrelloCardColor, BoardListAlias -from ..sheets.sheets_objects import RegistryPost -from ..strings import load -from ..tg.sender import pretty_send -from ..trello.trello_client import TrelloClient -from .base_job import BaseJob -from .utils import check_trello_card, format_errors - -logger = logging.getLogger(__name__) - - -class FillPostsListJob(BaseJob): - @staticmethod - def _execute( - app_context: AppContext, send: Callable[[str], None], called_from_handler=False - ): - errors = {} - registry_posts = [] - all_rubrics = app_context.db_client.get_rubrics() - - registry_posts += FillPostsListJob._retrieve_cards_for_registry( - trello_client=app_context.trello_client, - list_aliases=[BoardListAlias.PUBLISH_DONE_11], - all_rubrics=all_rubrics, - errors=errors, - show_due=True, - strict_archive_rules=True, - ) - - if len(errors) == 0: - posts_added = app_context.sheets_client.update_posts_registry( - registry_posts - ) - if len(posts_added) == 0: - paragraphs = [load("fill_posts_list_job__unchanged")] - else: - paragraphs = [load("fill_posts_list_job__success")] + [ - "\n".join( - f"{index + 1}) {post_name}" - for index, post_name in enumerate(posts_added) - ) - ] - else: - paragraphs = format_errors(errors) - - pretty_send(paragraphs, send) - - @staticmethod - def _retrieve_cards_for_registry( - trello_client: TrelloClient, - list_aliases: List[str], - errors: dict, - all_rubrics: List, - show_due=True, - need_illustrators=True, - strict_archive_rules=True, - ) -> List[str]: - """ - Returns a list of paragraphs that should always go in a single message. - """ - list_ids = trello_client.get_list_id_from_aliases(list_aliases) - cards = trello_client.get_cards(list_ids) - if show_due: - cards.sort(key=lambda card: card.due or datetime.datetime.min) - parse_failure_counter = 0 - - registry_posts = [] - - for card in cards: - label_names = [label.name for label in card.labels] - is_main_post = load("common_trello_label__main_post") in label_names - is_archive_post = load("common_trello_label__archive") in label_names - - if not card: - parse_failure_counter += 1 - continue - - card_fields = trello_client.get_custom_fields(card.id) - - card_is_ok = check_trello_card( - card, - errors, - is_bad_title=( - card_fields.title is None - and card.lst.id - != trello_client.lists_config[BoardListAlias.PENDING_EDITOR_5] - ), - is_bad_google_doc=card_fields.google_doc is None, - is_bad_authors=len(card_fields.authors) == 0, - is_bad_editors=len(card_fields.editors) == 0, - is_bad_cover=card_fields.cover is None and not is_archive_post, - is_bad_illustrators=( - len(card_fields.illustrators) == 0 - and need_illustrators - and not is_archive_post - ), - is_bad_due_date=card.due is None and show_due, - is_bad_label_names=len( - [ - label - for label in card.labels - if label.color - not in [TrelloCardColor.BLACK, BoardCardColor.BLACK] - ] - ) - == 0, - ) - - if not card_is_ok: - continue - - registry_posts.append( - RegistryPost( - card, - card_fields, - is_main_post, - is_archive_post, - all_rubrics, - ) - ) - - if parse_failure_counter > 0: - logger.error(f"Unparsed cards encountered: {parse_failure_counter}") - return registry_posts diff --git a/src/tg/handler_registry.py b/src/tg/handler_registry.py index 98fb2aa..7285983 100644 --- a/src/tg/handler_registry.py +++ b/src/tg/handler_registry.py @@ -50,24 +50,6 @@ class HandlerConfig: direct_only=True, description="получить мои карточки из доски Развитие", ), - HandlerConfig( - command="fill_posts_list", - category=CommandCategories.DEBUG, - access_level="manager", - job_name="fill_posts_list_job", - job_type="manager_reply", - direct_only=True, - description="заполнить реестр постов (пока не работает)", - ), - HandlerConfig( - command="fill_posts_list_focalboard", - category=CommandCategories.DEBUG, - access_level="manager", - job_name="fill_posts_list_focalboard_job", - job_type="manager_reply", - direct_only=True, - description="заполнить реестр постов из Focalboard (пока не работает)", - ), HandlerConfig( command="hr_acquisition", category=CommandCategories.HR, From 2b8a049d24b5e8dca485d549f0c70f9810751bcd Mon Sep 17 00:00:00 2001 From: Alex Kulikov Date: Mon, 25 May 2026 18:47:47 +0100 Subject: [PATCH 06/15] refactor: remove dead curator-by-trello lookup functions from jobs/utils get_cards_by_curator, get_curators_by_card, retrieve_curator_names_by_author, retrieve_curator_names_by_categories, and _make_curator_string had no callers and referenced three db_client methods that no longer exist. Co-Authored-By: Claude Sonnet 4.6 --- src/jobs/utils.py | 86 ----------------------------------------------- 1 file changed, 86 deletions(-) diff --git a/src/jobs/utils.py b/src/jobs/utils.py index 3aebd98..634c9b3 100644 --- a/src/jobs/utils.py +++ b/src/jobs/utils.py @@ -1,14 +1,11 @@ import datetime import inspect import logging -from collections import defaultdict from typing import List from .. import jobs -from ..app_context import AppContext from ..consts import BoardCardColor, TrelloCardColor, TrelloCardFieldErrorAlias from ..db.db_client import DBClient -from ..db.db_objects import Curator from ..drive.drive_client import GoogleDriveClient from ..strings import load from ..trello.trello_objects import TrelloMember @@ -44,59 +41,6 @@ def retrieve_usernames( return [retrieve_username(member, db_client) for member in trello_members] -def retrieve_curator_names_by_author( - trello_member: TrelloMember, db_client: DBClient -) -> List[str]: - """ - Tries to find a curator for trello member. Returns nothing if user is curator. - Returns: "John Smith (@jsmith_tg)" where possible, otherwise "John Smith". - If trello member or curator could not be found in the team data, returns None - """ - trello_id = "@" + trello_member.username - try: - curator = db_client.get_curator_by_trello_id(trello_id) - if curator: - curators = [curator] - else: - curators = db_client.find_curators_by_author_trello(trello_id) - except Exception as e: - logger.error("Could not retrieve curators by author", exc_info=e) - return - if not curators: - return [] - return [_make_curator_string(curator) for curator in curators] - - -def retrieve_curator_names_by_categories(labels: List[str], db_client: DBClient): - """ - To be used when there are no known card members. - Category is a trello label (e.g. NLP) - """ - curators = set() - try: - for label in labels: - curators = curators.union( - set(db_client.find_curators_by_trello_label(label.name)) - ) - except Exception as e: - logger.error("Could not retrieve curators by category", exc_info=e) - return - if not curators: - return [] - return [_make_curator_string(curator) for curator in curators] - - -def _make_curator_string(curator: Curator): - """ - Returns: (pretty_curator_string, tg_login_or_None) - """ - if curator.name: - if curator.telegram: - return f"{curator.name} ({curator.telegram})", curator.telegram - return curator.name, None - return curator.telegram, curator.telegram - - def get_job_runnable(job_id: str): """ Finds a job class inside a module and returns its execute method. @@ -271,33 +215,3 @@ def check_trello_card( errors[card] = this_card_bad_fields return False return True - - -def get_cards_by_curator(app_context: AppContext, focalboard=False): - if focalboard: - cards = app_context.focalboard_client.get_cards() - else: - cards = app_context.trello_client.get_cards() - curator_cards = defaultdict(list) - for card in cards: - curators = get_curators_by_card(card, app_context.db_client) - if not curators: - # TODO: get main curator from spreadsheet - curators = [("Илья Булгаков (@bulgak0v)", "@bulgak0v")] - for curator in curators: - curator_cards[curator].append(card) - - return curator_cards - - -def get_curators_by_card(card, db_client): - curators = set() - for member in card.members: - curator_names = retrieve_curator_names_by_author(member, db_client) - curators.update(curator_names) - if curators: - return curators - - # e.g. if no members in a card, should tag curators based on label - curators_by_label = retrieve_curator_names_by_categories(card.labels, db_client) - return curators_by_label From be6eed5a1a8dee225a44d74f6a49cfaa5272581e Mon Sep 17 00:00:00 2001 From: Alex Kulikov Date: Mon, 25 May 2026 18:51:21 +0100 Subject: [PATCH 07/15] fix: remove stale GET_TASKS_REPORT__ENTER_BOARD_URL handler mapping GetTasksReportEnterBoardUrlHandler was removed in 2689eda when the get_tasks_report flow was ported to Planka (board number picker replaced the old Trello URL entry step), but the mapping and enum value were left behind, breaking the module import chain. Co-Authored-By: Claude Sonnet 4.6 --- src/consts.py | 1 - src/tg/handlers/user_message_handler.py | 1 - 2 files changed, 2 deletions(-) diff --git a/src/consts.py b/src/consts.py index 4786992..40337ed 100644 --- a/src/consts.py +++ b/src/consts.py @@ -166,7 +166,6 @@ class PlainTextUserAction(Enum): """ # /get_tasks_report items - GET_TASKS_REPORT__ENTER_BOARD_URL = "get_tasks_report__board_url" GET_TASKS_REPORT__ENTER_BOARD_NUMBER = "get_tasks_report__board_number" GET_TASKS_REPORT__ENTER_LIST_NUMBER = "get_tasks_report__list_number" GET_TASKS_REPORT__ENTER_INTRO = "get_tasks_report__introduction" diff --git a/src/tg/handlers/user_message_handler.py b/src/tg/handlers/user_message_handler.py index d4dd006..f144b10 100644 --- a/src/tg/handlers/user_message_handler.py +++ b/src/tg/handlers/user_message_handler.py @@ -24,7 +24,6 @@ def handle_callback_query( ACTION_HANDLERS = { PlainTextUserAction.GET_RUBRICS__CHOOSE_RUBRIC: flow_handlers.GetRubricsChooseRubricHandler, - PlainTextUserAction.GET_TASKS_REPORT__ENTER_BOARD_URL: flow_handlers.GetTasksReportEnterBoardUrlHandler, PlainTextUserAction.GET_TASKS_REPORT__ENTER_BOARD_NUMBER: flow_handlers.GetTasksReportEnterBoardNumberHandler, PlainTextUserAction.GET_TASKS_REPORT__ENTER_LIST_NUMBER: flow_handlers.GetTasksReportEnterListNumberHandler, PlainTextUserAction.GET_TASKS_REPORT__ENTER_INTRO: flow_handlers.GetTasksReportEnterIntroHandler, From 6c1ccefdb3c53bd28f19d5c6cea49bd0b7b2a7eb Mon Sep 17 00:00:00 2001 From: Alex Kulikov Date: Mon, 25 May 2026 19:04:29 +0100 Subject: [PATCH 08/15] refactor: port get_articles_rubric job to Planka, remove Trello/Focalboard Replaces the deprecated/conditional Trello+Focalboard branching with a direct Planka client call. TrelloClient and FocalboardClient are no longer referenced by this job. Co-Authored-By: Claude Sonnet 4.6 --- src/jobs/trello_get_articles_rubric_job.py | 40 ++++++++-------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/src/jobs/trello_get_articles_rubric_job.py b/src/jobs/trello_get_articles_rubric_job.py index f672641..890990e 100644 --- a/src/jobs/trello_get_articles_rubric_job.py +++ b/src/jobs/trello_get_articles_rubric_job.py @@ -2,10 +2,9 @@ from ..app_context import AppContext from ..consts import BoardListAlias +from ..planka.planka_client import PlankaClient from ..strings import load from ..tg.sender import pretty_send -from ..trello.trello_client import TrelloClient -from ..trello.trello_objects import TrelloCard from . import utils from .base_job import BaseJob @@ -44,8 +43,7 @@ def _execute( BoardListAlias.PUBLISH_DONE_11, ]: paragraphs += TrelloGetArticlesRubricJob._get_rubric_paragraphs( - app_context=app_context, - trello_client=app_context.trello_client, + planka_client=app_context.planka_client, rubric_title=load(alias.value), rubric_alias=alias, rubric_name=rubric_name, @@ -54,11 +52,8 @@ def _execute( pretty_send(paragraphs, send) @staticmethod - def _format_card(card: TrelloCard, app_context: AppContext) -> str: - if not app_context.trello_client.deprecated: - card_fields = app_context.trello_client.get_custom_fields(card.id) - else: - card_fields = app_context.focalboard_client.get_custom_fields(card.id) + def _format_card(card, planka_client: PlankaClient) -> str: + card_fields = planka_client.get_custom_fields(card.id) return load( "rubric_report_job__card", date=card.due.strftime("%d.%m").lower() if card.due else "", @@ -70,24 +65,18 @@ def _format_card(card: TrelloCard, app_context: AppContext) -> str: ) def _get_rubric_paragraphs( - app_context: AppContext, - trello_client: TrelloClient, + planka_client: PlankaClient, rubric_title: str, rubric_alias: str, rubric_name: str, ) -> List[str]: - if not trello_client.deprecated: - list_ids = trello_client.get_list_id_from_aliases([rubric_alias]) - cards = trello_client.get_cards(list_ids) - else: - list_ids = app_context.focalboard_client.get_list_id_from_aliases( - [rubric_alias] - ) - cards = app_context.focalboard_client.get_cards(list_ids) - cards_filtered = [] - for card in cards: - if rubric_name in [label.name for label in card.labels]: - cards_filtered.append(card) + list_ids = planka_client.get_list_id_from_aliases([rubric_alias]) + cards = planka_client.get_cards(list_ids) + cards_filtered = [ + card + for card in cards + if rubric_name in [label.name for label in card.labels] + ] paragraphs = [ load( @@ -97,6 +86,7 @@ def _get_rubric_paragraphs( ) ] for card in cards_filtered: - formatted_card = TrelloGetArticlesRubricJob._format_card(card, app_context) - paragraphs.append(formatted_card) + paragraphs.append( + TrelloGetArticlesRubricJob._format_card(card, planka_client) + ) return paragraphs From 1db7f00147f9a4f1987d2bf7c802f54ebd7c0352 Mon Sep 17 00:00:00 2001 From: Alex Kulikov Date: Mon, 25 May 2026 19:09:37 +0100 Subject: [PATCH 09/15] refactor: delete unused card_checks and card_checks_focalboard modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No importers found — both files were dead code. Co-Authored-By: Claude Sonnet 4.6 --- src/utils/card_checks.py | 135 ---------------------------- src/utils/card_checks_focalboard.py | 131 --------------------------- 2 files changed, 266 deletions(-) delete mode 100644 src/utils/card_checks.py delete mode 100644 src/utils/card_checks_focalboard.py diff --git a/src/utils/card_checks.py b/src/utils/card_checks.py deleted file mode 100644 index 478346e..0000000 --- a/src/utils/card_checks.py +++ /dev/null @@ -1,135 +0,0 @@ -import datetime -from typing import Tuple - -from ..app_context import AppContext -from ..consts import BoardListAlias -from ..strings import load -from ..trello.trello_objects import TrelloCard - - -def make_card_failure_reasons(card: TrelloCard, app_context: AppContext): - """ - Returns card description with failure reasons, if any. - If card does not match any of FILTER_TO_FAILURE_REASON, returns None. - """ - failure_reasons = [] - for filter_func, reason_alias in FILTER_TO_FAILURE_REASON.items(): - is_failed, kwargs = filter_func(card, app_context) - if is_failed: - reason = load(reason_alias, **kwargs) - if reason and len(failure_reasons) > 0: - reason = reason[0].lower() + reason[1:] - failure_reasons.append(reason) - return failure_reasons - - -def is_deadline_missed(card: TrelloCard, app_context: AppContext) -> Tuple[bool, dict]: - list_ids = app_context.trello_client.get_list_id_from_aliases( - [BoardListAlias.DRAFT_N_PROGRESS_3] - ) - is_missed = ( - card.lst.id in list_ids - and card.due is not None - and card.due.date() < datetime.datetime.now().date() - ) - return is_missed, {"date": card.due.strftime("%d.%m")} if is_missed else {} - - -def is_due_date_missing(card: TrelloCard, app_context: AppContext) -> Tuple[bool, dict]: - if card.due: - return False, {} - list_ids = app_context.trello_client.get_list_id_from_aliases( - [BoardListAlias.DRAFT_N_PROGRESS_3] - ) - return card.lst.id in list_ids, {} - - -def is_author_missing(card: TrelloCard, app_context: AppContext) -> Tuple[bool, dict]: - if card.members: - return False, {} - - list_aliases = ( - BoardListAlias.DRAFT_N_PROGRESS_3, - BoardListAlias.DRAFT_COMPLETED_4, - BoardListAlias.PENDING_EDITOR_5, - BoardListAlias.PENDING_SEO_EDITOR_6, - BoardListAlias.APPROVED_EDITOR_7, - BoardListAlias.PENDING_CHIEF_EDITOR_8, - BoardListAlias.PUBLISH_BACKLOG_9, - BoardListAlias.PUBLISH_IN_PROGRESS_10, - BoardListAlias.PUBLISH_DONE_11, - ) - list_ids = app_context.trello_client.get_list_id_from_aliases(list_aliases) - return card.lst.id in list_ids, {} - - -def is_tag_missing(card: TrelloCard, app_context: AppContext) -> Tuple[bool, dict]: - if card.labels: - return False, {} - - list_aliases = ( - BoardListAlias.DRAFT_N_PROGRESS_3, - BoardListAlias.DRAFT_COMPLETED_4, - BoardListAlias.PENDING_EDITOR_5, - BoardListAlias.PENDING_SEO_EDITOR_6, - BoardListAlias.APPROVED_EDITOR_7, - BoardListAlias.PENDING_CHIEF_EDITOR_8, - BoardListAlias.PUBLISH_BACKLOG_9, - BoardListAlias.PUBLISH_IN_PROGRESS_10, - BoardListAlias.PUBLISH_DONE_11, - ) - list_ids = app_context.trello_client.get_list_id_from_aliases(list_aliases) - return card.lst.id in list_ids, {} - - -def is_doc_missing(card: TrelloCard, app_context: AppContext) -> Tuple[bool, dict]: - list_aliases = ( - BoardListAlias.DRAFT_COMPLETED_4, - BoardListAlias.PENDING_EDITOR_5, - BoardListAlias.PENDING_SEO_EDITOR_6, - BoardListAlias.APPROVED_EDITOR_7, - BoardListAlias.PENDING_CHIEF_EDITOR_8, - BoardListAlias.PUBLISH_BACKLOG_9, - BoardListAlias.PUBLISH_IN_PROGRESS_10, - BoardListAlias.PUBLISH_DONE_11, - ) - list_ids = app_context.trello_client.get_list_id_from_aliases(list_aliases) - if card.lst.id not in list_ids: - return False, {} - - doc_url = app_context.trello_client.get_custom_fields(card.id).google_doc - return not doc_url, {} - - -def has_no_doc_access(card: TrelloCard, app_context: AppContext) -> Tuple[bool, dict]: - list_aliases = ( - BoardListAlias.DRAFT_COMPLETED_4, - BoardListAlias.PENDING_EDITOR_5, - BoardListAlias.PENDING_SEO_EDITOR_6, - BoardListAlias.APPROVED_EDITOR_7, - BoardListAlias.PENDING_CHIEF_EDITOR_8, - BoardListAlias.PUBLISH_BACKLOG_9, - BoardListAlias.PUBLISH_IN_PROGRESS_10, - BoardListAlias.PUBLISH_DONE_11, - ) - list_ids = app_context.trello_client.get_list_id_from_aliases(list_aliases) - if card.lst.id not in list_ids: - return False, {} - - doc_url = app_context.trello_client.get_custom_fields(card.id).google_doc - if not doc_url: - # should be handled by is_doc_missing - return False, {} - - is_open_for_edit = app_context.drive_client.is_open_for_edit(doc_url) - return not is_open_for_edit, {} - - -FILTER_TO_FAILURE_REASON = { - is_author_missing: "trello_board_state_job__title_author_missing", - is_due_date_missing: "trello_board_state_job__title_due_date_missing", - is_deadline_missed: "trello_board_state_job__title_due_date_expired", - is_tag_missing: "trello_board_state_job__title_tag_missing", - is_doc_missing: "trello_board_state_job__title_no_doc", - has_no_doc_access: "trello_board_state_job__title_no_doc_access", -} diff --git a/src/utils/card_checks_focalboard.py b/src/utils/card_checks_focalboard.py deleted file mode 100644 index dfbd782..0000000 --- a/src/utils/card_checks_focalboard.py +++ /dev/null @@ -1,131 +0,0 @@ -import datetime -from typing import Tuple - -from ..app_context import AppContext -from ..consts import BoardListAlias -from ..strings import load -from ..trello.trello_objects import TrelloCard - - -def make_card_failure_reasons(card: TrelloCard, app_context: AppContext): - """ - Returns card description with failure reasons, if any. - If card does not match any of FILTER_TO_FAILURE_REASON, returns None. - """ - failure_reasons = [] - for filter_func, reason_alias in FILTER_TO_FAILURE_REASON.items(): - is_failed, kwargs = filter_func(card, app_context) - if is_failed: - reason = load(reason_alias, **kwargs) - if reason and len(failure_reasons) > 0: - reason = reason[0].lower() + reason[1:] - failure_reasons.append(reason) - return failure_reasons - - -def is_deadline_missed(card: TrelloCard, app_context: AppContext) -> Tuple[bool, dict]: - list_ids = app_context.focalboard_client.get_list_id_from_aliases( - [BoardListAlias.DRAFT_N_PROGRESS_3] - ) - is_missed = ( - card.lst.id in list_ids - and card.due is not None - and card.due.date() < datetime.datetime.now().date() - ) - return is_missed, {"date": card.due.strftime("%d.%m")} if is_missed else {} - - -def is_due_date_missing(card: TrelloCard, app_context: AppContext) -> Tuple[bool, dict]: - if card.due: - return False, {} - list_ids = app_context.focalboard_client.get_list_id_from_aliases( - [BoardListAlias.DRAFT_N_PROGRESS_3] - ) - return card.lst.id in list_ids, {} - - -def is_author_missing(card: TrelloCard, app_context: AppContext) -> Tuple[bool, dict]: - if card.members: - return False, {} - - list_aliases = ( - BoardListAlias.DRAFT_N_PROGRESS_3, - BoardListAlias.DRAFT_COMPLETED_4, - BoardListAlias.PENDING_EDITOR_5, - BoardListAlias.PENDING_SEO_EDITOR_6, - BoardListAlias.APPROVED_EDITOR_7, - BoardListAlias.PENDING_CHIEF_EDITOR_8, - BoardListAlias.PUBLISH_BACKLOG_9, - BoardListAlias.PUBLISH_IN_PROGRESS_10, - ) - list_ids = app_context.focalboard_client.get_list_id_from_aliases(list_aliases) - return card.lst.id in list_ids, {} - - -def is_tag_missing(card: TrelloCard, app_context: AppContext) -> Tuple[bool, dict]: - if card.labels: - return False, {} - - list_aliases = ( - BoardListAlias.DRAFT_N_PROGRESS_3, - BoardListAlias.DRAFT_COMPLETED_4, - BoardListAlias.PENDING_EDITOR_5, - BoardListAlias.PENDING_SEO_EDITOR_6, - BoardListAlias.APPROVED_EDITOR_7, - BoardListAlias.PENDING_CHIEF_EDITOR_8, - BoardListAlias.PUBLISH_BACKLOG_9, - BoardListAlias.PUBLISH_IN_PROGRESS_10, - ) - list_ids = app_context.focalboard_client.get_list_id_from_aliases(list_aliases) - return card.lst.id in list_ids, {} - - -def is_doc_missing(card: TrelloCard, app_context: AppContext) -> Tuple[bool, dict]: - list_aliases = ( - BoardListAlias.DRAFT_COMPLETED_4, - BoardListAlias.PENDING_EDITOR_5, - BoardListAlias.PENDING_SEO_EDITOR_6, - BoardListAlias.APPROVED_EDITOR_7, - BoardListAlias.PENDING_CHIEF_EDITOR_8, - BoardListAlias.PUBLISH_BACKLOG_9, - BoardListAlias.PUBLISH_IN_PROGRESS_10, - ) - list_ids = app_context.focalboard_client.get_list_id_from_aliases(list_aliases) - if card.lst.id not in list_ids: - return False, {} - - doc_url = app_context.focalboard_client.get_custom_fields(card.id).google_doc - return not doc_url, {} - - -def has_no_doc_access(card: TrelloCard, app_context: AppContext) -> Tuple[bool, dict]: - list_aliases = ( - BoardListAlias.DRAFT_COMPLETED_4, - BoardListAlias.PENDING_EDITOR_5, - BoardListAlias.PENDING_SEO_EDITOR_6, - BoardListAlias.APPROVED_EDITOR_7, - BoardListAlias.PENDING_CHIEF_EDITOR_8, - BoardListAlias.PUBLISH_BACKLOG_9, - BoardListAlias.PUBLISH_IN_PROGRESS_10, - ) - list_ids = app_context.focalboard_client.get_list_id_from_aliases(list_aliases) - if card.lst.id not in list_ids: - return False, {} - - doc_url = app_context.focalboard_client.get_custom_fields(card.id).google_doc - if not doc_url: - # should be handled by is_doc_missing - return False, {} - - is_open_for_edit = app_context.drive_client.is_open_for_edit(doc_url) - return not is_open_for_edit, {} - - -FILTER_TO_FAILURE_REASON = { - is_author_missing: "trello_board_state_job__title_author_missing", - is_due_date_missing: "trello_board_state_job__title_due_date_missing", - is_deadline_missed: "trello_board_state_job__title_due_date_expired", - is_tag_missing: "trello_board_state_job__title_tag_missing", - is_doc_missing: "trello_board_state_job__title_no_doc", - has_no_doc_access: "trello_board_state_job__title_no_doc_access", -} From 66f2a0cbf282904edff5b8b1125cbf11aba3919a Mon Sep 17 00:00:00 2001 From: Alex Kulikov Date: Mon, 25 May 2026 19:44:13 +0100 Subject: [PATCH 10/15] refactor: remove TrelloClient and FocalboardClient from app_context and config updater Neither client is used by any active job or handler. Removes initialization in AppContext and config hot-reload in ConfigUpdaterJob. Co-Authored-By: Claude Sonnet 4.6 --- src/app_context.py | 8 -------- src/jobs/config_updater_job.py | 8 -------- src/tg/handlers/flow_handlers.py | 1 - 3 files changed, 17 deletions(-) diff --git a/src/app_context.py b/src/app_context.py index 7dacdc5..fa15d5d 100644 --- a/src/app_context.py +++ b/src/app_context.py @@ -6,14 +6,12 @@ from .db.db_client import DBClient from .drive.drive_client import GoogleDriveClient from .facebook.facebook_client import FacebookClient -from .focalboard.focalboard_client import FocalboardClient from .instagram.instagram_client import InstagramClient from .n8n.n8n_client import N8nClient from .planka.planka_client import PlankaClient from .sheets.sheets_client import GoogleSheetsClient from .strings import StringsDBClient from .tg.tg_client import TgClient -from .trello.trello_client import TrelloClient from .utils.singleton import Singleton logger = logging.getLogger(__name__) @@ -47,12 +45,6 @@ def __init__( self.strings_db_client.fetch_strings_sheet(self.sheets_client) self.db_client.fetch_all(self.sheets_client) - self.trello_client = TrelloClient( - trello_config=config_manager.get_trello_config() - ) - self.focalboard_client = FocalboardClient( - focalboard_config=config_manager.get_focalboard_config() - ) self.planka_client = PlankaClient( planka_config=config_manager.get_planka_config() ) diff --git a/src/jobs/config_updater_job.py b/src/jobs/config_updater_job.py index 78080d6..62401eb 100644 --- a/src/jobs/config_updater_job.py +++ b/src/jobs/config_updater_job.py @@ -54,14 +54,6 @@ def _execute( app_context.tg_client.update_config(tg_config) # update admins and managers app_context.set_access_rights(tg_config) - # update config['trello'] - app_context.trello_client.update_config( - job_scheduler.config_manager.get_trello_config() - ) - # update config['focalboard'] - app_context.focalboard_client.update_config( - job_scheduler.config_manager.get_focalboard_config() - ) # update config['sheets'] app_context.sheets_client.update_config( job_scheduler.config_manager.get_sheets_config() diff --git a/src/tg/handlers/flow_handlers.py b/src/tg/handlers/flow_handlers.py index 5163790..d4bb7e8 100644 --- a/src/tg/handlers/flow_handlers.py +++ b/src/tg/handlers/flow_handlers.py @@ -309,7 +309,6 @@ def _handle_task_report_helper(command_data, add_labels, update): ) for message in messages: reply(message, update) - # finished with last action for /trello_client_get_lists return None From 785084a1ede40600103d749b7c83727b1d44737f Mon Sep 17 00:00:00 2001 From: Alex Kulikov Date: Mon, 25 May 2026 20:06:46 +0100 Subject: [PATCH 11/15] refactor: remove change_board command and Trello/Focalboard config plumbing change_board was writing to trello.board_id which no longer does anything. Also removes get_trello_config, get_focalboard_config from ConfigManager and the associated TRELLO_CONFIG, FOCALBOARD_CONFIG, TRELLO_BOARD_ID constants. Co-Authored-By: Claude Sonnet 4.6 --- src/config_manager.py | 6 ------ src/consts.py | 5 ----- src/tg/handler_registry.py | 7 ------- src/tg/handlers/__init__.py | 1 - src/tg/handlers/access_config_handler.py | 18 ------------------ 5 files changed, 37 deletions(-) diff --git a/src/config_manager.py b/src/config_manager.py index eaa3180..d50ab18 100644 --- a/src/config_manager.py +++ b/src/config_manager.py @@ -57,12 +57,6 @@ def get_latest_jobs_config(self): logger.debug(f"Got config, last updated: {self._latest_jobs_config_ts}") return self._latest_jobs_config - def get_trello_config(self): - return self.get_latest_config().get(consts.TRELLO_CONFIG, {}) - - def get_focalboard_config(self): - return self.get_latest_config().get(consts.FOCALBOARD_CONFIG, {}) - def get_planka_config(self): return self.get_latest_config().get(consts.PLANKA_CONFIG, {}) diff --git a/src/consts.py b/src/consts.py index 40337ed..f75de4e 100644 --- a/src/consts.py +++ b/src/consts.py @@ -39,8 +39,6 @@ class AppSource(Enum): # Upper level config keys TELEGRAM_CONFIG = "telegram" -TRELLO_CONFIG = "trello" -FOCALBOARD_CONFIG = "focalboard" PLANKA_CONFIG = "planka" SHEETS_CONFIG = "sheets" DRIVE_CONFIG = "drive" @@ -59,9 +57,6 @@ class AppSource(Enum): # Telegram keys TELEGRAM_MANAGER_IDS = "manager_chat_ids" -# Trello keys -TRELLO_BOARD_ID = "board_id" - # Vk consts VK_POST_LINK = "https://vk.com/{group_alias}?w=wall-{group_id}_{post_id}" diff --git a/src/tg/handler_registry.py b/src/tg/handler_registry.py index 7285983..7ce6dfb 100644 --- a/src/tg/handler_registry.py +++ b/src/tg/handler_registry.py @@ -239,13 +239,6 @@ class HandlerConfig: handler_func=handlers.add_manager, description="добавить менеджера в список", ), - HandlerConfig( - command="change_board", - category=CommandCategories.CONFIG, - access_level="admin", - handler_func=handlers.change_board, - description="изменить Trello board_id", - ), HandlerConfig( command="send_reminders", category=CommandCategories.BROADCAST, diff --git a/src/tg/handlers/__init__.py b/src/tg/handlers/__init__.py index e9ba0d2..b816d6c 100644 --- a/src/tg/handlers/__init__.py +++ b/src/tg/handlers/__init__.py @@ -5,7 +5,6 @@ from .access_config_handler import ( add_manager, - change_board, get_config, get_config_jobs, reload_config_jobs, diff --git a/src/tg/handlers/access_config_handler.py b/src/tg/handlers/access_config_handler.py index 03d8720..fdd5491 100644 --- a/src/tg/handlers/access_config_handler.py +++ b/src/tg/handlers/access_config_handler.py @@ -147,24 +147,6 @@ def add_manager(update, tg_context): return -@admin_only -def change_board(update, tg_context): - try: - tokens = update.message.text.strip().split(maxsplit=2) - assert len(tokens) == 2 - board_id = json.loads(tokens[1]) - _set_config( - update, - f"{consts.TRELLO_CONFIG}.{consts.TRELLO_BOARD_ID}", - board_id, - ConfigManager(), - ) - except Exception as e: - reply(load("access_config_handler__change_board_usage_example"), update) - logger.warning("Failed to change boards", exc_info=e) - return - - def _set_config(update, config_path: str, new_value, config_manager: ConfigManager): current_config = config_manager.get_latest_config() for config_item in config_path.split("."): From f6e8fc472b6053b6ac5048f104eb41cc72315507 Mon Sep 17 00:00:00 2001 From: Alex Kulikov Date: Mon, 25 May 2026 23:22:21 +0100 Subject: [PATCH 12/15] refactor: delete TrelloClient, FocalboardClient and the focalboard module No remaining callers after previous cleanup commits. The trello/ directory now only contains trello_objects.py (the shared board data model), to be renamed separately. Co-Authored-By: Claude Sonnet 4.6 --- src/focalboard/focalboard_client.py | 508 ---------------------------- src/trello/trello_client.py | 316 ----------------- 2 files changed, 824 deletions(-) delete mode 100644 src/focalboard/focalboard_client.py delete mode 100644 src/trello/trello_client.py diff --git a/src/focalboard/focalboard_client.py b/src/focalboard/focalboard_client.py deleted file mode 100644 index 4b4e622..0000000 --- a/src/focalboard/focalboard_client.py +++ /dev/null @@ -1,508 +0,0 @@ -import json -import logging -from datetime import datetime -from typing import List, Optional -from urllib.parse import urljoin -from cachetools import cached, TTLCache - -import requests - -from ..consts import TrelloCustomFieldTypeAlias, TrelloCustomFieldTypes, BoardListAlias -from ..db import db_client -from ..strings import load -from ..trello import trello_objects as objects -from ..utils.singleton import Singleton - -logger = logging.getLogger(__name__) - - -class FocalboardClient(Singleton): - def __init__(self, focalboard_config=None): - if self.was_initialized(): - return - - self._focalboard_config = focalboard_config - self._update_from_config() - logger.info("FocalboardClient successfully initialized") - - def get_boards_for_user(self, telegram_username: str = None, db_client=None): - focal_username = None - if telegram_username and db_client: - raw_focal = db_client.find_focalboard_username_by_telegram_username( - f"@{telegram_username}" - ) - if raw_focal: - focal_username = raw_focal.strip().lstrip("@").lower() - logger.debug( - f"Normalized focalboard username from DB = {focal_username!r}" - ) - else: - logger.warning( - f"No focalboard username found for telegram={telegram_username!r}. " - "Accessible boards can't be determined. Managers should fill the mapping table." - ) - return [] - else: - logger.debug( - "No telegram_username provided, returning all boards (unfiltered)." - ) - - try: - _, data = self._make_request("api/v2/teams/0/boards") - all_boards = [ - objects.TrelloBoard.from_focalboard_dict(board) for board in data - ] - except Exception as e: - logger.error("Error fetching boards from Focalboard", exc_info=e) - return [] - - if not focal_username: - return all_boards - - accessible = [] - for board in all_boards: - try: - members = self.get_members(board.id) - member_usernames = [ - m.username.strip().lstrip("@").lower() - for m in members - if m.username - ] - if focal_username in member_usernames: - accessible.append(board) - except Exception as e: - logger.error(f"Error fetching members for board {board.id}", exc_info=e) - - logger.info( - f"Telegram user @{telegram_username} has access to {len(accessible)} boards: " - f"{[b.name for b in accessible]}" - ) - return accessible - - def get_lists(self, board_id=None, sorted=False): - if board_id is None: - # default board - board_id = self.board_id - # TODO make it more efficient - # essentially all list information is already passed via boards handler - _, data = self._make_request("api/v2/teams/0/boards") - list_data = [ - prop - for prop in [board for board in data if board["id"] == board_id][0][ - "cardProperties" - ] - if prop["name"] == "Колонка" - ][0] - lists_data = list_data["options"] - lists = [ - objects.TrelloList.from_focalboard_dict(trello_list, board_id) - for trello_list in lists_data - ] - if sorted: - # we need to get sorting order from the view, which is currently not efficient - try: - _, data = self._make_request( - f"api/v2/boards/{board_id}/blocks?all=true" - ) - view = [card_dict for card_dict in data if card_dict["type"] == "view"][ - 0 - ] - order = view["fields"]["visibleOptionIds"] - sorted_lists = [] - for list_id in order: - this_list = [lst for lst in lists if lst.id == list_id] - # this is needed to fix a strange bug - # sometimes Focalboard returns ID in visibleOptionIds but not in a full list - if this_list: - sorted_lists.append(this_list[0]) - lists = sorted_lists - except Exception as e: - logger.error("can't sort focalboard lists", exc_info=e) - logger.debug(f"get_lists: {lists}") - return lists - - def get_list(self, board_id, list_id): - _, data = self._make_request("api/v2/teams/0/boards") - lists_data = [ - prop - for prop in [board for board in data if board["id"] == board_id][0][ - "cardProperties" - ] - if prop["name"] == "Колонка" - ][0]["options"] - lst = [ - objects.TrelloList.from_focalboard_dict(trello_list, board_id) - for trello_list in lists_data - if trello_list["id"] == list_id - ][0] - logger.debug(f"get_list: {lst}") - return lst - - def _get_labels(self, board_id=None): - if board_id is None: - # default board - board_id = self.board_id - _, data = self._make_request("api/v2/teams/0/boards") - label_data = [ - prop - for prop in [board for board in data if board["id"] == board_id][0][ - "cardProperties" - ] - if prop["name"] == "Рубрика" - ][0] - labels_data = label_data["options"] - labels = [ - objects.TrelloCardLabel.from_focalboard_dict(label) for label in labels_data - ] - return labels - - def _get_list_property(self, board_id): - _, data = self._make_request("api/v2/teams/0/boards") - return [ - prop - for prop in [board for board in data if board["id"] == board_id][0][ - "cardProperties" - ] - if prop["name"] == "Колонка" - ][0]["id"] - - def _get_label_property(self, board_id=None): - if board_id is None: - # default board - board_id = self.board_id - _, data = self._make_request("api/v2/teams/0/boards") - return [ - prop - for prop in [board for board in data if board["id"] == board_id][0][ - "cardProperties" - ] - if prop["name"] == "Рубрика" - ][0]["id"] - - def _get_due_property(self, board_id=None): - if board_id is None: - # default board - board_id = self.board_id - _, data = self._make_request("api/v2/teams/0/boards") - return [ - prop - for prop in [board for board in data if board["id"] == board_id][0][ - "cardProperties" - ] - if prop["name"] == "Дедлайн" - ][0]["id"] - - def get_card_due(self, card_id: str, board_id: str) -> Optional[datetime]: - _, data = self._make_request(f"api/v2/cards/{card_id}") - due_id = self._get_due_property(board_id) - - raw = data["properties"].get(due_id) - if not raw: - return None - - try: - payload = json.loads(raw) - except json.JSONDecodeError: - logger.error(f"Cannot parse due field for card {card_id}: {raw}") - return None - - ts_ms = payload.get("to") - if not ts_ms: - ts_ms = payload.get("from") - if not ts_ms: - return None - - return datetime.fromtimestamp(ts_ms / 1000) - - def _get_member_property(self, board_id): - _, data = self._make_request("api/v2/teams/0/boards") - return [ - prop - for prop in [board for board in data if board["id"] == board_id][0][ - "cardProperties" - ] - if prop["name"] == "Ответственный" - ][0]["id"] - - def get_list_id_from_aliases(self, list_aliases): - list_ids = [ - self.lists_config[alias] - for alias in list_aliases - if alias in self.lists_config - ] - if len(list_ids) != len(list_aliases): - logger.error( - f"list_ids not found for aliases: " - f"{[alias for alias in list_aliases if alias not in self.lists_config]}" - ) - return list_ids - - def _fill_alias_id_map(self, items, item_enum): - result = {} - for alias in item_enum: - suitable_items = [ - item for item in items if item.name.startswith(load(alias.value)) - ] - if len(suitable_items) > 1: - raise ValueError( - f"Enum {item_enum.__name__} name {alias.value} is ambiguous!" - ) - if len(suitable_items) > 0: - result[alias] = suitable_items[0].id - return result - - def _fill_id_type_map(self, items, item_enum): - result = {} - for item in items: - result[item.id] = TrelloCustomFieldTypes(item.type) - return result - - def get_board_custom_field_types(self): - board_id = self.board_id - _, data = self._make_request(f"api/v2/boards/{board_id}") - custom_field_types = [ - objects.TrelloCustomFieldType.from_focalboard_dict(custom_field_type) - for custom_field_type in data["cardProperties"] - ] - logger.debug(f"get_board_custom_field_types: {custom_field_types}") - return custom_field_types - - def get_custom_fields(self, card_id: str) -> objects.CardCustomFields: - card_fields = objects.CardCustomFields(card_id) - board_labels = self._get_labels() - card_fields_dict = {} - card_labels_ids = [] - card_labels = [] - _, data = self._make_request(f"api/v2/cards/{card_id}") - fields = data["properties"] - for alias, type_id in self.custom_fields_config.items(): - if type_id in fields: - changed_alias = alias.name.split(".")[-1].lower() - card_fields_dict[changed_alias] = fields[type_id] - - board_label_id = self._get_label_property() - - for type_id, value in fields.items(): - if type_id == board_label_id: - card_labels_ids = value - - if isinstance(card_labels_ids, str): - card_labels_ids = [card_labels_ids] - - for card_label_id in card_labels_ids: - for board_label in board_labels: - if card_label_id == board_label.id: - card_labels.append(board_label) - - card_fields.authors = [ - author.strip() for author in card_fields_dict.get("author", "").split(",") - ] - card_fields.editors = [ - editor.strip() for editor in card_fields_dict.get("editor", "").split(",") - ] - card_fields.illustrators = [ - illustrator.strip() - for illustrator in card_fields_dict.get("illustrator", "").split(",") - ] - card_fields.cover = ( - card_fields_dict["cover"] if "cover" in card_fields_dict else None - ) - card_fields.google_doc = ( - card_fields_dict["google_doc"] if "google_doc" in card_fields_dict else None - ) - card_fields.title = ( - card_fields_dict["title"] if "title" in card_fields_dict else None - ) - - card_fields._data = card_labels - return card_fields - - def set_card_custom_field(self, card: objects.TrelloCard, field_alias, value): - board_id = self.board_id - field_id = self.custom_fields_config[field_alias] - data = {"updatedFields": {"properties": card._fields_properties}} - data["updatedFields"]["properties"][field_id] = value - code = self._make_patch_request( - f"api/v2/boards/{board_id}/blocks/{card.id}", payload=data - ) - logger.debug(f"set_card_custom_field: {code}") - - def get_members(self, board_id) -> List[objects.TrelloMember]: - _, data = self._make_request(f"api/v2/boards/{board_id}/members") - members = [] - for member in data: - _, data = self._get_member(member["userId"]) - members.append(objects.TrelloMember.from_focalboard_dict(data)) - logger.debug(f"get_members: {members}") - return members - - @cached(cache=TTLCache(maxsize=1000, ttl=60 * 60 * 24 * 30)) # cache for 30d - def _get_member(self, user_id): - return self._make_request(f"api/v2/users/{user_id}") - - def get_cards(self, list_ids=None, board_id=None): - if board_id is None: - board_id = self.board_id - _, data = self._make_request(f"api/v2/boards/{board_id}/blocks?all=true") - cards = [] - # TODO: move this to app state - members = self.get_members(board_id) - lists = self.get_lists(board_id=board_id) - list_prop = self._get_list_property(board_id) - labels = self._get_labels() - label_prop = self._get_label_property(board_id) - due_prop = self._get_due_property(board_id) - member_prop = self._get_member_property(board_id) - view_id = [card_dict for card_dict in data if card_dict["type"] == "view"][0][ - "id" - ] - if list_ids: - data = [ - card_dict - for card_dict in data - if card_dict["type"] == "card" - and card_dict["fields"]["properties"].get(list_prop, "") in list_ids - ] - else: - data = [card_dict for card_dict in data if card_dict["type"] == "card"] - for card_dict in data: - card = objects.TrelloCard.from_focalboard_dict(card_dict) - card.url = urljoin(self.url, f"{board_id}/{view_id}/{card.id}") - card.labels = [] - raw_labels = card._fields_properties.get(label_prop, []) - if isinstance(raw_labels, str): - raw_labels = [raw_labels] - for label_id in raw_labels: - for label in labels: - if label.id == label_id: - card.labels.append(label) - try: - due = card._fields_properties.get(due_prop, "{}") - due_ts = json.loads(due).get("to") - if due_ts: - card.due = datetime.fromtimestamp(due_ts // 1000) - except Exception as e: - print(due) - print(e) - # TODO: move this to app state - for trello_list in lists: - if trello_list.id == card._fields_properties.get(list_prop, ""): - card.lst = trello_list - break - else: - logger.error( - f"List name not found for {card}: {card._fields_properties.get(list_prop, '')}" - ) - # TODO: move this to app state - raw_members = card._fields_properties.get(member_prop, []) - if isinstance(raw_members, str): - raw_members = [raw_members] - if len(raw_members) > 0: - for member_id in raw_members: - # check if member is in board members - matched_member = next( - (m for m in members if m.id == member_id), None - ) - if matched_member: - card.members.append(matched_member) - else: - # fallback: User left board but is still assigned to card. Fetch them explicitly. - try: - _, m_data = self._get_member(member_id) - m_obj = objects.TrelloMember.from_focalboard_dict(m_data) - card.members.append(m_obj) - except Exception as e: - logger.error( - f"Failed to fetch user {member_id} for {card}: {e}" - ) - - if len(card.members) == 0: - logger.error(f"Member username not found for {card}") - cards.append(card) - logger.debug(f"get_cards: {cards}") - return cards - - def update_config(self, new_focalboard_config): - """To be called after config automatic update""" - self._focalboard_config = new_focalboard_config - self._update_from_config() - - def _update_from_config(self): - """Update attributes according to current self._focalboard_config""" - self.token = self._focalboard_config["token"] - self.url = self._focalboard_config["url"] - self.board_id = self._focalboard_config["board_id"] - self.headers = { - "Accept": "application/json", - "Content-Type": "application/json", - "Authorization": f"Bearer {self.token}", - "X-Requested-With": "XMLHttpRequest", - } - try: - lists = self.get_lists() - self.lists_config = self._fill_alias_id_map(lists, BoardListAlias) - custom_field_types = self.get_board_custom_field_types() - self.custom_fields_type_config = self._fill_id_type_map( - custom_field_types, TrelloCustomFieldTypes - ) - self.custom_fields_config = self._fill_alias_id_map( - custom_field_types, TrelloCustomFieldTypeAlias - ) - except Exception as e: - # TODO remove this when main board is migrated - logger.error( - "something went wrong when setting up focalboard client", exc_info=e - ) - pass - - def _make_request(self, uri, payload={}): - response = requests.get( - urljoin(self.url, uri), params=payload, headers=self.headers - ) - logger.debug(f"{response.url}") - return response.status_code, response.json() - - def _make_patch_request(self, uri, payload={}): - response = requests.patch( - urljoin(self.url, uri), json=payload, headers=self.headers - ) - logger.debug(f"{response.url}") - return response.status_code, response.json() - - def get_boards_for_telegram_user( - self, - telegram_username: str, - db_client: db_client.DBClient, - ) -> List[objects.TrelloBoard]: - raw_focal = db_client.find_focalboard_username_by_telegram_username( - f"@{telegram_username}" - ) - if raw_focal: - focal_username = raw_focal.strip().lstrip("@").lower() - - logger.debug(f"Normalized focalboard username from DB = {focal_username!r}") - - else: - logger.warning( - f"No focalboard username found for telegram={telegram_username!r}. " - f"Accessible boards can't be determined. Managers should check and fill the table." - ) - - return [] - - # 2) Fetch all boards - all_boards = self.get_boards_for_user() - - # 3) Filter by membership - accessible = [] - for board in all_boards: - try: - members = self.get_members(board.id) - if focal_username in [ - m.username.strip().lstrip("@").lower() for m in members - ]: - accessible.append(board) - except Exception as e: - logger.error(f"Error fetching members for board {board.id}", exc_info=e) - - return accessible diff --git a/src/trello/trello_client.py b/src/trello/trello_client.py deleted file mode 100644 index e1ab917..0000000 --- a/src/trello/trello_client.py +++ /dev/null @@ -1,316 +0,0 @@ -import json -import logging -from typing import List -from urllib.parse import quote, urljoin - -import requests - -from ..consts import TrelloCustomFieldTypeAlias, TrelloCustomFieldTypes, BoardListAlias -from ..strings import load -from ..utils.singleton import Singleton -from . import trello_objects as objects - -logger = logging.getLogger(__name__) - -BASE_URL = "https://api.trello.com/1/" - - -class TrelloClient(Singleton): - def __init__(self, trello_config=None): - if self.was_initialized(): - return - - self._trello_config = trello_config - self._update_from_config() - logger.info("TrelloClient successfully initialized") - - def get_board(self, board_id=None): - if not board_id: - board_id = self.board_id - _, data = self._make_request(f"boards/{board_id}") - return objects.TrelloBoard.from_dict(data) - - def get_board_by_url(self, board_url): - # Safari may copy unquoted url with cyrillic symbols - board_url = quote(board_url, safe=":/%") - _, data = self._make_request("members/me/boards") - for board in data: - if board.get("url") == board_url: - return objects.TrelloBoard.from_dict(board) - raise ValueError(f"Board {board_url} not found!") - - def get_board_labels(self, board_id=None): - if not board_id: - board_id = self.board_id - _, data = self._make_request(f"boards/{board_id}/labels") - labels = [objects.TrelloBoardLabel.from_dict(label) for label in data] - logger.debug(f"get_board_labels: {labels}") - return labels - - def get_boards_for_user(self, user_id=None): - _, data = self._make_request("members/me/boards") - boards = [objects.TrelloBoard.from_dict(label) for label in data] - logger.debug(f"get_boards_for_user: {boards}") - return boards - - def get_lists(self, board_id=None): - if not board_id: - board_id = self.board_id - _, data = self._make_request(f"boards/{board_id}/lists") - lists = [objects.TrelloList.from_dict(trello_list) for trello_list in data] - logger.debug(f"get_lists: {lists}") - return lists - - def get_list(self, list_id): - _, data = self._make_request(f"lists/{list_id}") - lst = objects.TrelloList.from_dict(data) - logger.debug(f"get_list: {list}") - return lst - - def get_cards(self, list_ids=None, board_id=None): - if not board_id: - board_id = self.board_id - if list_ids is not None and len(list_ids) == 1: - _, data = self._make_request(f"lists/{list_ids[0]}/cards") - else: - _, data = self._make_request(f"boards/{board_id}/cards") - if list_ids: - data = [ - card_dict for card_dict in data if card_dict["idList"] in list_ids - ] - cards = [] - # TODO: move this to app state - members = self.get_members(board_id) - lists = self.get_lists(board_id) - for card_dict in data: - card = objects.TrelloCard.from_dict(card_dict) - # TODO: move this to app state - for trello_list in lists: - if trello_list.id == card_dict["idList"]: - card.lst = trello_list - break - else: - logger.error(f"List name not found for {card}") - # TODO: move this to app state - if len(card_dict["idMembers"]) > 0: - for member in members: - if member.id in card_dict["idMembers"]: - card.members.append(member) - if len(card.members) == 0: - logger.error(f"Member username not found for {card}") - cards.append(card) - logger.debug(f"get_cards: {cards}") - return cards - - def get_board_custom_field_types(self, board_id=None): - if not board_id: - board_id = self.board_id - _, data = self._make_request(f"boards/{board_id}/customFields") - custom_field_types = [ - objects.TrelloCustomFieldType.from_dict(custom_field_type) - for custom_field_type in data - ] - logger.debug(f"get_board_custom_field_types: {custom_field_types}") - return custom_field_types - - def get_card_custom_fields(self, card_id: str) -> List[objects.TrelloCustomField]: - _, data = self._make_request(f"cards/{card_id}/customFieldItems") - custom_fields = [ - objects.TrelloCustomField.from_dict( - custom_field, self.custom_fields_type_config - ) - for custom_field in data - ] - logger.debug(f"get_card_custom_fields: {custom_fields}") - return custom_fields - - def get_card_custom_fields_dict(self, card_id): - custom_fields = self.get_card_custom_fields(card_id) - custom_fields_dict = {} - for alias, type_id in self.custom_fields_config.items(): - suitable_fields = [fld for fld in custom_fields if fld.type_id == type_id] - if len(suitable_fields) > 0: - custom_fields_dict[alias] = suitable_fields[0] - return custom_fields_dict - - def get_custom_fields(self, card_id: str) -> objects.CardCustomFields: - # TODO: think about better naming - card_fields_dict = self.get_card_custom_fields_dict(card_id) - card_fields = objects.CardCustomFields(card_id) - card_fields._data = card_fields_dict - card_fields.authors = ( - [ - author.strip() - for author in card_fields_dict[ - TrelloCustomFieldTypeAlias.AUTHOR - ].value.split(",") - ] - if TrelloCustomFieldTypeAlias.AUTHOR in card_fields_dict - else [] - ) - card_fields.editors = ( - [ - editor.strip() - for editor in card_fields_dict[ - TrelloCustomFieldTypeAlias.EDITOR - ].value.split(",") - ] - if TrelloCustomFieldTypeAlias.EDITOR in card_fields_dict - else [] - ) - card_fields.illustrators = ( - [ - illustrator.strip() - for illustrator in card_fields_dict[ - TrelloCustomFieldTypeAlias.ILLUSTRATOR - ].value.split(",") - ] - if TrelloCustomFieldTypeAlias.ILLUSTRATOR in card_fields_dict - else [] - ) - card_fields.cover = ( - card_fields_dict[TrelloCustomFieldTypeAlias.COVER].value - if TrelloCustomFieldTypeAlias.COVER in card_fields_dict - else None - ) - card_fields.google_doc = ( - card_fields_dict[TrelloCustomFieldTypeAlias.GOOGLE_DOC].value - if TrelloCustomFieldTypeAlias.GOOGLE_DOC in card_fields_dict - else None - ) - card_fields.title = ( - card_fields_dict[TrelloCustomFieldTypeAlias.TITLE].value - if TrelloCustomFieldTypeAlias.TITLE in card_fields_dict - else None - ) - return card_fields - - def set_card_custom_field(self, card_id, field_alias, value): - data = {"value": {"text": value}} - field_id = self.custom_fields_config[field_alias] - code = self._make_put_request( - f"cards/{card_id}/customField/{field_id}/item", data=data - ) - logger.debug(f"set_card_custom_field: {code}") - - def get_action_create_card(self, card_id): - _, data = self._make_request( - f"cards/{card_id}/actions", payload={"filter": "createCard"} - ) - card_actions = [ - objects.TrelloActionCreateCard.from_dict(action) - for action in data - if action["type"] == "createCard" - ] - logger.debug(f"get_action_create_card: {card_actions}") - return card_actions - - def get_action_create_cards(self, card_ids): - card_actions = {} - for card_id in card_ids: - card_actions[card_id] = self.get_action_create_card(card_id) - return card_actions - - def get_action_update_card(self, card_id): - _, data = self._make_request( - f"cards/{card_id}/actions", payload={"filter": "updateCard"} - ) - card_actions = [ - objects.TrelloActionUpdateCard.from_dict(action) - for action in data - if action["type"] == "updateCard" - ] - logger.debug(f"get_action_update_card: {card_actions}") - return card_actions - - def get_action_update_cards(self, card_ids): - card_actions = {} - for card_id in card_ids: - card_actions[card_id] = self.get_action_update_card(card_id) - return card_actions - - def get_members(self, board_id=None) -> List[objects.TrelloMember]: - if not board_id: - board_id = self.board_id - _, data = self._make_request(f"boards/{board_id}/members") - members = [objects.TrelloMember.from_dict(member) for member in data] - logger.debug(f"get_members: {members}") - return members - - def update_config(self, new_trello_config): - """To be called after config automatic update""" - self._trello_config = new_trello_config - self._update_from_config() - - def _update_from_config(self): - """Update attributes according to current self._trello_config""" - self.api_key = self._trello_config["api_key"] - self.token = self._trello_config["token"] - self.board_id = self._trello_config["board_id"] - self.deprecated = self._trello_config["deprecated"] - self.default_payload = { - "key": self.api_key, - "token": self.token, - } - # TODO(alexeyqu): move to DB - lists = self.get_lists() - self.lists_config = self._fill_alias_id_map(lists, BoardListAlias) - custom_field_types = self.get_board_custom_field_types() - self.custom_fields_type_config = self._fill_id_type_map( - custom_field_types, TrelloCustomFieldTypes - ) - self.custom_fields_config = self._fill_alias_id_map( - custom_field_types, TrelloCustomFieldTypeAlias - ) - - def get_list_id_from_aliases(self, list_aliases): - list_ids = [ - self.lists_config[alias] - for alias in list_aliases - if alias in self.lists_config - ] - if len(list_ids) != len(list_aliases): - logger.error( - f"list_ids not found for aliases: " - f"{[alias for alias in list_aliases if alias not in self.lists_config]}" - ) - return list_ids - - def _fill_alias_id_map(self, items, item_enum): - result = {} - for alias in item_enum: - suitable_items = [ - item for item in items if item.name.startswith(load(alias.value)) - ] - if len(suitable_items) > 1: - raise ValueError( - f"Enum {item_enum.__name__} name {alias.value} is ambiguous!" - ) - if len(suitable_items) > 0: - result[alias] = suitable_items[0].id - return result - - def _fill_id_type_map(self, items, item_enum): - result = {} - for item in items: - result[item.id] = TrelloCustomFieldTypes(item.type) - return result - - def _make_request(self, uri, payload={}): - payload.update(self.default_payload) - response = requests.get( - urljoin(BASE_URL, uri), - params=payload, - ) - logger.debug(f"{response.url}") - return response.status_code, response.json() - - def _make_put_request(self, uri, data={}): - response = requests.put( - urljoin(BASE_URL, uri), - params=self.default_payload, - data=json.dumps(data), - headers={"Content-Type": "application/json"}, - ) - logger.debug(f"{response.url}") - return response.status_code From 50ba419321ca8e122e1bd70da371647de3b3938f Mon Sep 17 00:00:00 2001 From: Alex Kulikov Date: Mon, 25 May 2026 23:26:37 +0100 Subject: [PATCH 13/15] refactor: move board objects to planka/board_objects.py, remove dead classes Deleted: TrelloBoardLabel, TrelloCustomFieldType, TrelloCustomField, TrelloActionCreateCard, TrelloActionUpdateCard, all from_dict / from_focalboard_dict methods, and the focalboard-only _fields_properties attribute. The trello/ directory is now gone entirely. Co-Authored-By: Claude Sonnet 4.6 --- src/drive/drive_client.py | 2 +- src/jobs/board_my_cards_razvitie_job.py | 2 +- src/jobs/utils.py | 2 +- src/planka/board_objects.py | 115 ++++ src/planka/planka_client.py | 4 +- src/sheets/sheets_objects.py | 2 +- src/tg/handlers/get_tasks_report_handler.py | 2 +- src/trello/trello_objects.py | 574 -------------------- 8 files changed, 122 insertions(+), 581 deletions(-) create mode 100644 src/planka/board_objects.py delete mode 100644 src/trello/trello_objects.py diff --git a/src/drive/drive_client.py b/src/drive/drive_client.py index d17a43d..93ad24a 100644 --- a/src/drive/drive_client.py +++ b/src/drive/drive_client.py @@ -11,7 +11,7 @@ from googleapiclient.http import MediaIoBaseDownload from oauth2client.service_account import ServiceAccountCredentials -from ..trello.trello_objects import TrelloCard +from ..planka.board_objects import TrelloCard from ..utils.singleton import Singleton logger = logging.getLogger(__name__) diff --git a/src/jobs/board_my_cards_razvitie_job.py b/src/jobs/board_my_cards_razvitie_job.py index 9b0c798..2d552d0 100644 --- a/src/jobs/board_my_cards_razvitie_job.py +++ b/src/jobs/board_my_cards_razvitie_job.py @@ -4,7 +4,7 @@ from ..app_context import AppContext from ..strings import load from ..tg.handlers.get_tasks_report_handler import _make_cards_text -from ..trello.trello_objects import TrelloCard +from ..planka.board_objects import TrelloCard from .base_job import BaseJob logger = logging.getLogger(__name__) diff --git a/src/jobs/utils.py b/src/jobs/utils.py index 634c9b3..be43c65 100644 --- a/src/jobs/utils.py +++ b/src/jobs/utils.py @@ -8,7 +8,7 @@ from ..db.db_client import DBClient from ..drive.drive_client import GoogleDriveClient from ..strings import load -from ..trello.trello_objects import TrelloMember +from ..planka.board_objects import TrelloMember logger = logging.getLogger(__name__) diff --git a/src/planka/board_objects.py b/src/planka/board_objects.py new file mode 100644 index 0000000..123ea53 --- /dev/null +++ b/src/planka/board_objects.py @@ -0,0 +1,115 @@ +TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ" + + +class TrelloBoard: + def __init__(self): + self.id = None + self.name = None + self.url = None + self._ok = True + + def __bool__(self): + return self._ok + + def __str__(self): + return self.name + + def __repr__(self): + return f"Board" + + +class TrelloList: + def __init__(self): + self.id = None + self.name = None + self.board_id = None + self._ok = True + + def __bool__(self): + return self._ok + + def __str__(self): + return self.name + + def __repr__(self): + return f"List" + + +class TrelloCardLabel: + def __init__(self): + self.id = None + self.name = None + self.color = None + self._ok = True + + def __bool__(self): + return self._ok + + def __str__(self): + return self.name + + def __repr__(self): + return f"CardLabel" + + +class TrelloCard: + def __init__(self): + self.id = None + self.name = None + self.labels = [] + self.url = None + self.due = None + self.lst = None + self.members = [] + self._ok = True + + def __bool__(self): + return self._ok + + def __str__(self): + return self.url + + def __repr__(self): + return f"Card" + + def __eq__(self, other): + return str(self) == str(other) + + def __hash__(self): + return hash(self.id) + + +class TrelloMember: + def __init__(self): + self.id = None + self.username = None + self.full_name = None + + def __str__(self): + return self.username + + def __repr__(self): + return f"Member" + + def __eq__(self, other): + return isinstance(other, TrelloMember) and self.username == other.username + + def __lt__(self, other): + return isinstance(other, TrelloMember) and self.username < other.username + + def __hash__(self): + return hash(self.username) + + +class CardCustomFields: + def __init__(self, card_id): + self.card_id = card_id + self.authors = None + self.editors = None + self.illustrators = None + self.cover = None + self.title = None + self.google_doc = None + + def __repr__(self): + return f"CardCustomFields" diff --git a/src/planka/planka_client.py b/src/planka/planka_client.py index 9f95660..ac949fb 100644 --- a/src/planka/planka_client.py +++ b/src/planka/planka_client.py @@ -11,8 +11,8 @@ from ..consts import TrelloCardColor, TrelloCustomFieldTypeAlias from ..db import db_client from ..strings import load -from ..trello import trello_objects as objects -from ..trello.trello_objects import TIME_FORMAT +from . import board_objects as objects +from .board_objects import TIME_FORMAT from ..utils.singleton import Singleton logger = logging.getLogger(__name__) diff --git a/src/sheets/sheets_objects.py b/src/sheets/sheets_objects.py index fed31af..97c5290 100644 --- a/src/sheets/sheets_objects.py +++ b/src/sheets/sheets_objects.py @@ -6,7 +6,7 @@ from ..consts import BoardCardColor, TrelloCardColor from ..strings import load -from ..trello.trello_objects import CardCustomFields, TrelloCard +from ..planka.board_objects import CardCustomFields, TrelloCard from .utils import convert_excel_datetime_to_string logger = logging.getLogger(__name__) diff --git a/src/tg/handlers/get_tasks_report_handler.py b/src/tg/handlers/get_tasks_report_handler.py index 86c0473..9fcf534 100644 --- a/src/tg/handlers/get_tasks_report_handler.py +++ b/src/tg/handlers/get_tasks_report_handler.py @@ -10,7 +10,7 @@ from ...jobs.utils import retrieve_username from ...strings import load from ...tg.sender import paragraphs_to_messages -from ...trello.trello_objects import TrelloCard +from ...planka.board_objects import TrelloCard from .utils import manager_only, reply TASK_NAME = "get_tasks_report" diff --git a/src/trello/trello_objects.py b/src/trello/trello_objects.py deleted file mode 100644 index 9dc0c07..0000000 --- a/src/trello/trello_objects.py +++ /dev/null @@ -1,574 +0,0 @@ -import html -import logging -from datetime import datetime - -from ..consts import BoardCardColor, TrelloCardColor, TrelloCustomFieldTypes - -logger = logging.getLogger(__name__) - -TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ" - - -class TrelloBoard: - def __init__(self): - self.id = None - self.name = None - self.url = None - - self._ok = True - - def __bool__(self): - return self._ok - - def __str__(self): - return self.name - - def __repr__(self): - return f"Board" - - @classmethod - def from_dict(cls, data): - board = cls() - try: - board.id = data["id"] - board.name = html.escape(data["name"]) - board.url = data["shortUrl"] - except Exception as e: - board._ok = False - logger.error(f"Bad board json {data}", exc_info=e) - return board - - @classmethod - def from_focalboard_dict(cls, data): - board = cls() - try: - board.id = data["id"] - board.name = html.escape(data["title"]) - # board.url = data["shortUrl"] - except Exception as e: - board._ok = False - logger.error(f"Bad board json {data}", exc_info=e) - return board - - def to_dict(self): - return { - "id": self.id, - "name": self.name, - "url": self.url, - } - - -class TrelloBoardLabel: - def __init__(self): - self.id = None - self.name = None - self.color = None - - self._ok = True - - def __bool__(self): - return self._ok - - def __str__(self): - return self.name - - def __repr__(self): - return f"BoardLabel" - - @classmethod - def from_dict(cls, data): - label = cls() - try: - label.id = data["id"] - label.name = html.escape(data["name"]) - label.color = data["color"] - except Exception as e: - label._ok = False - logger.error(f"Bad board label json {data}", exc_info=e) - return label - - @classmethod - def from_focalboard_dict(cls, data): - label = cls() - try: - label.id = data["id"] - label.name = html.escape(data["value"]) - label.color = data["color"] - except Exception as e: - label._ok = False - logger.error(f"Bad board label json {data}", exc_info=e) - return label - - def to_dict(self): - return { - "id": self.id, - "name": self.name, - "color": self.color, - } - - -class TrelloList: - def __init__(self): - self.id = None - self.name = None - self.board_id = None - - self._ok = True - - def __bool__(self): - return self._ok - - def __str__(self): - return self.name - - def __repr__(self): - return f"List" - - @classmethod - def from_dict(cls, data): - trello_list = cls() - try: - trello_list.id = data["id"] - trello_list.name = html.escape(data["name"]) - trello_list.board_id = data["idBoard"] - except Exception as e: - trello_list._ok = False - logger.error(f"Bad list json {data}", exc_info=e) - return trello_list - - @classmethod - def from_focalboard_dict(cls, data, board_id): - trello_list = cls() - try: - trello_list.id = data["id"] - trello_list.name = html.escape(data["value"]) - trello_list.board_id = board_id - except Exception as e: - trello_list._ok = False - logger.error(f"Bad list json {data}", exc_info=e) - return trello_list - - def to_dict(self): - return { - "id": self.id, - "name": self.name, - "idBoard": self.board_id, - } - - -class TrelloCardLabel: - def __init__(self): - self.id = None - self.name = None - self.color = None - - self._ok = True - - def __bool__(self): - return self._ok - - def __str__(self): - return self.name - - def __repr__(self): - return f"CardLabel" - - @classmethod - def from_dict(cls, data): - label = cls() - try: - label.id = data["id"] - label.name = html.escape(data["name"]) - label.color = None - try: - label.color = TrelloCardColor(data["color"]) - except Exception: - label.color = TrelloCardColor(TrelloCardColor.UNKNOWN) - except Exception as e: - label._ok = False - logger.error(f"Bad card label json {data}", exc_info=e) - return label - - @classmethod - def from_focalboard_dict(cls, data): - label = cls() - try: - label.id = data["id"] - label.name = html.escape(data["value"]) - label.color = None - try: - label.color = BoardCardColor(data["color"]) - except Exception: - label.color = BoardCardColor(BoardCardColor.UNKNOWN) - except Exception as e: - label._ok = False - logger.error(f"Bad card label json {data}", exc_info=e) - return label - - def to_dict(self): - return { - "id": self.id, - "name": self.name, - "color": self.color.value, - } - - -class TrelloCard: - def __init__(self): - self.id = None - self.name = None - self.labels = [] - self.url = None - self.due = None - # TODO: move this to app state - self.lst = None - self.members = [] - # focalboard fields - self._fields_properties = None - - self._ok = True - - def __bool__(self): - return self._ok - - def __str__(self): - return self.url - - def __repr__(self): - return f"Card" - - def __eq__(self, other): - return str(self) == str(other) - - def __hash__(self): - return hash(self.id) - - @classmethod - def from_dict(cls, data): - card = cls() - try: - card.id = data["id"] - card.name = html.escape(data["name"]) - card.labels = [TrelloCardLabel.from_dict(label) for label in data["labels"]] - card.url = data["shortUrl"] - card.due = ( - datetime.strptime(data["due"], TIME_FORMAT) if data["due"] else None - ) - except Exception as e: - card._ok = False - logger.error(f"Bad card json {data}", exc_info=e) - return card - - @classmethod - def from_focalboard_dict(cls, data): - card = cls() - try: - card.id = data["id"] - card.name = html.escape(data["title"]) - card._fields_properties = data["fields"]["properties"] - except Exception as e: - card._ok = False - logger.error(f"Bad card json {data}", exc_info=e) - return card - - def to_dict(self): - return { - "id": self.id, - "name": self.name, - "labels": [label.to_dict() for label in self.labels], - "url": self.url, - "due": datetime.strftime(self.due, TIME_FORMAT) if self.due else None, - "list": self.lst.to_dict() if self.lst is not None else {}, - "members": [member.to_dict() for member in self.members], - } - - -class TrelloCustomFieldType: - def __init__(self): - self.id = None - self.name = None - self.type = None - self.options = None - - self._ok = True - - def __bool__(self): - return self._ok - - def __str__(self): - return self.name - - def __repr__(self): - return f"CustomFieldType" - - @classmethod - def from_dict(cls, data): - field_type = cls() - try: - field_type.id = data["id"] - field_type.name = html.escape(data["name"]) - field_type.type = TrelloCustomFieldTypes(data["type"]) - if field_type.type == TrelloCustomFieldTypes.LIST: - field_type.options = { - option["id"]: option["value"]["text"] for option in data["options"] - } - except Exception as e: - field_type._ok = False - logger.error(f"Bad field type json {data}", exc_info=e) - return field_type - - @classmethod - def from_focalboard_dict(cls, data): - field_type = cls() - try: - field_type.id = data["id"] - field_type.name = html.escape(data["name"]) - field_type.type = TrelloCustomFieldTypes(data["type"]) - if field_type.type == TrelloCustomFieldTypes.LIST: - field_type.options = { - option["id"]: option["value"] for option in data["options"] - } - except Exception as e: - field_type._ok = False - logger.error(f"Bad field type json {data}", exc_info=e) - return field_type - - def to_dict(self): - dct = { - "id": self.id, - "name": self.name, - "type": self.type.value, - } - if self.type == TrelloCustomFieldTypes.LIST: - dct["options"] = [ - {"id": idValue, "value": {"text": value}} - for idValue, value in self.options.items() - ] - return dct - - -class TrelloCustomField: - def __init__(self): - self.id = None - self.value = None - self.type_id = None - - self._ok = True - self._custom_fields_type_config = None - - def __bool__(self): - return self._ok - - def __str__(self): - return self.value - - def __repr__(self): - return f"CustomField" - - @classmethod - def from_dict(cls, data, custom_fields_type_config): - custom_field = cls() - custom_field._custom_fields_type_config = custom_fields_type_config - try: - custom_field.id = data["id"] - custom_field.type_id = data["idCustomField"] - # TODO probably support other custom field value types - if ( - custom_fields_type_config[custom_field.type_id] - == TrelloCustomFieldTypes.TEXT - ): - custom_field.value = html.escape(data["value"]["text"]) - except Exception as e: - custom_field._ok = False - logger.error(f"Bad custom field json {data}", exc_info=e) - return custom_field - - def to_dict(self): - dct = { - "id": self.id, - "idCustomField": self.type_id, - } - # TODO probably support other custom field value types - if self._custom_fields_type_config[self.type_id] == TrelloCustomFieldTypes.TEXT: - dct["value"] = {"text": self.value} - return dct - - -class TrelloActionCreateCard: - def __init__(self): - self.id = None - self.date = None - self.card_url = None - self.list_id = None - self.list_name = None - - self._ok = True - - def __bool__(self): - return self._ok - - def __str__(self): - return f'Card "{self.card_url}" created in {self.list_name}' - - def __repr__(self): - return ( - f"ActionCreateCard" - ) - - @classmethod - def from_dict(cls, data): - action = cls() - try: - assert data["type"] == "createCard" - action.id = data["id"] - action.date = datetime.strptime(data["date"], TIME_FORMAT) - action.card_url = ( - "https://trello.com/c/" + data["data"]["card"]["shortLink"] - ) - if "list" in data["data"]: - action.list_id = data["data"]["list"].get("id") - action.list_name = data["data"]["list"].get("name") - except Exception as e: - action._ok = False - logger.error(f"Bad createCard action json {data}", exc_info=e) - return action - - def to_dict(self): - return { - "id": self.id, - "date": datetime.strftime(self.date, TIME_FORMAT), - "type": "createCard", - "data": { - "card": {"shortLink": self.card_url.split("/")[-1]}, - "list": { - "id": self.list_id, - "name": self.list_name, - }, - }, - } - - -class TrelloActionUpdateCard: - def __init__(self): - self.id = None - self.date = None - # probably will update fields if need be - self.card_url = None - self.list_before_id = None - self.list_before_name = None - self.list_after_id = None - self.list_after_name = None - - self._ok = True - - def __bool__(self): - return self._ok - - def __str__(self): - return f'Card "{self.card_url}" moved {self.list_before_name} -> {self.list_after_name}' - - def __repr__(self): - return ( - f"ActionUpdateCard" - ) - - @classmethod - def from_dict(cls, data): - action = cls() - try: - assert data["type"] == "updateCard" - action.id = data["id"] - action.date = datetime.strptime(data["date"], TIME_FORMAT) - action.card_url = ( - "https://trello.com/c/" + data["data"]["card"]["shortLink"] - ) - if "listBefore" in data["data"]: - action.list_before_id = data["data"]["listBefore"]["id"] - action.list_before_name = data["data"]["listBefore"]["name"] - if "listAfter" in data["data"]: - action.list_after_id = data["data"]["listAfter"]["id"] - action.list_after_name = data["data"]["listAfter"]["name"] - except Exception as e: - action._ok = False - logger.error(f"Bad updateCard action json {data}", exc_info=e) - return action - - def to_dict(self): - return { - "id": self.id, - "date": datetime.strftime(self.date, TIME_FORMAT), - "type": "updateCard", - "data": { - "card": {"shortLink": self.card_url.split("/")[-1]}, - "listBefore": { - "id": self.list_before_id, - "name": self.list_before_name, - }, - "listAfter": { - "id": self.list_after_id, - "name": self.list_after_name, - }, - }, - } - - -class TrelloMember: - def __init__(self): - self.id = None - self.username = None - self.full_name = None - - def __str__(self): - return self.username - - def __repr__(self): - return f"Member" - - def __eq__(self, other): - return isinstance(other, TrelloMember) and self.username == other.username - - def __lt__(self, other): - return isinstance(other, TrelloMember) and self.username < other.username - - def __hash__(self): - return hash(self.username) - - @classmethod - def from_dict(cls, data): - member = cls() - member.id = data["id"] - member.username = data["username"] - member.full_name = data["fullName"] - return member - - @classmethod - def from_focalboard_dict(cls, data): - member = cls() - member.id = data["id"] - member.username = data["username"] - member.full_name = data["username"] - return member - - def to_dict(self): - return { - "id": self.id, - "username": self.username, - "fullName": self.full_name, - } - - -class CardCustomFields: - def __init__(self, card_id): - self.card_id = card_id - self.authors = None - self.editors = None - self.illustrators = None - self.cover = None - self.title = None - self.google_doc = None - self._data = None - - def __repr__(self): - return f"CardCustomFields" From 8c743e32c26eb50b8c947accd024dd455c86bee7 Mon Sep 17 00:00:00 2001 From: Alex Kulikov Date: Mon, 25 May 2026 23:34:03 +0100 Subject: [PATCH 14/15] fix: add to_dict() to TrelloBoard and TrelloList Required by get_tasks_report flow to serialize board/list objects into chat_data for multi-step handler state. Co-Authored-By: Claude Sonnet 4.6 --- src/planka/board_objects.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/planka/board_objects.py b/src/planka/board_objects.py index 123ea53..988d189 100644 --- a/src/planka/board_objects.py +++ b/src/planka/board_objects.py @@ -17,6 +17,9 @@ def __str__(self): def __repr__(self): return f"Board" + def to_dict(self): + return {"id": self.id, "name": self.name, "url": self.url} + class TrelloList: def __init__(self): @@ -34,6 +37,9 @@ def __str__(self): def __repr__(self): return f"List" + def to_dict(self): + return {"id": self.id, "name": self.name, "board_id": self.board_id} + class TrelloCardLabel: def __init__(self): From c5ff4cea20d81776e122220244fc972dd7289121 Mon Sep 17 00:00:00 2001 From: Alex Kulikov Date: Mon, 25 May 2026 23:40:15 +0100 Subject: [PATCH 15/15] fix: drop stale app_context arg from _make_cards_text call _make_cards_text no longer takes app_context since the Trello/Focalboard removal; BoardMyCardsRazvitieJob was still passing it. Co-Authored-By: Claude Sonnet 4.6 --- src/jobs/board_my_cards_razvitie_job.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/jobs/board_my_cards_razvitie_job.py b/src/jobs/board_my_cards_razvitie_job.py index 2d552d0..1ff34ca 100644 --- a/src/jobs/board_my_cards_razvitie_job.py +++ b/src/jobs/board_my_cards_razvitie_job.py @@ -72,7 +72,7 @@ def _execute( ] if my_cards: list_report = BoardMyCardsRazvitieJob._create_paragraphs_from_cards( - my_cards, f"📜 {board_list.name}", True, app_context + my_cards, f"📜 {board_list.name}", True ) paragraphs += list_report paragraphs.append("") # hotfix for separating lists @@ -86,13 +86,12 @@ def _create_paragraphs_from_cards( cards: Iterable[TrelloCard], introduction: str, need_label: bool, - app_context: AppContext, ): paragraphs = [] if introduction: paragraphs.append(introduction) - paragraphs += _make_cards_text(cards, need_label, app_context) + paragraphs += _make_cards_text(cards, need_label) return paragraphs @staticmethod