diff --git a/config.json b/config.json index 2eff887b..d9adf32b 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/app_context.py b/src/app_context.py index 7dacdc58..fa15d5d3 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/config_manager.py b/src/config_manager.py index eaa31808..d50ab183 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 4786992d..f75de4ec 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}" @@ -166,7 +161,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/db/db_client.py b/src/db/db_client.py index 819516f3..e911bc26 100644 --- a/src/db/db_client.py +++ b/src/db/db_client.py @@ -8,8 +8,8 @@ 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, Chat, Curator, @@ -67,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: @@ -201,17 +183,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/src/db/db_objects.py b/src/db/db_objects.py index e3a7cbf5..dcf51c27 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/drive/drive_client.py b/src/drive/drive_client.py index d17a43d2..93ad24a2 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/focalboard/focalboard_client.py b/src/focalboard/focalboard_client.py deleted file mode 100644 index 4b4e622a..00000000 --- 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/jobs/__init__.py b/src/jobs/__init__.py index e701b5bb..61326929 100644 --- a/src/jobs/__init__.py +++ b/src/jobs/__init__.py @@ -10,13 +10,10 @@ 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 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/board_my_cards_razvitie_job.py b/src/jobs/board_my_cards_razvitie_job.py index 9b0c798c..1ff34cae 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__) @@ -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 diff --git a/src/jobs/config_updater_job.py b/src/jobs/config_updater_job.py index 78080d67..62401eb7 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/jobs/db_fetch_all_team_members_job.py b/src/jobs/db_fetch_all_team_members_job.py index 8fe37db6..4a3f20f1 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 fcc00385..00000000 --- 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/fill_posts_list_focalboard_job.py b/src/jobs/fill_posts_list_focalboard_job.py deleted file mode 100644 index 72e75f21..00000000 --- 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 d89ea2bc..00000000 --- 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/jobs/hr_acquisition_job.py b/src/jobs/hr_acquisition_job.py index eef5b406..73c3305a 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 1328f215..0353f3ac 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/jobs/trello_get_articles_rubric_job.py b/src/jobs/trello_get_articles_rubric_job.py index f672641a..890990ec 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 diff --git a/src/jobs/utils.py b/src/jobs/utils.py index fab4c89a..be43c65a 100644 --- a/src/jobs/utils.py +++ b/src/jobs/utils.py @@ -1,17 +1,14 @@ 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 +from ..planka.board_objects import TrelloMember logger = logging.getLogger(__name__) @@ -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 Authors sheet, 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 is no known authors. - 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 diff --git a/src/planka/board_objects.py b/src/planka/board_objects.py new file mode 100644 index 00000000..988d189e --- /dev/null +++ b/src/planka/board_objects.py @@ -0,0 +1,121 @@ +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" + + def to_dict(self): + return {"id": self.id, "name": self.name, "url": self.url} + + +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" + + def to_dict(self): + return {"id": self.id, "name": self.name, "board_id": 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" + + +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 9f956601..ac949fb0 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_client.py b/src/sheets/sheets_client.py index a2acbd7f..f910881b 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/sheets/sheets_objects.py b/src/sheets/sheets_objects.py index fed31af7..97c52902 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/handler_registry.py b/src/tg/handler_registry.py index a4dc3e56..7ce6dfbb 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, @@ -108,14 +90,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, @@ -265,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, @@ -323,12 +290,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/src/tg/handlers/__init__.py b/src/tg/handlers/__init__.py index 3854c526..b816d6c0 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, @@ -23,12 +22,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/access_config_handler.py b/src/tg/handlers/access_config_handler.py index 03d87202..fdd54913 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("."): diff --git a/src/tg/handlers/flow_handlers.py b/src/tg/handlers/flow_handlers.py index 780f190c..d4bb7e80 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,19 +301,14 @@ 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) - # finished with last action for /trello_client_get_lists return None @@ -345,59 +338,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 +360,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 a44efc30..9fcf534a 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" @@ -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] diff --git a/src/tg/handlers/user_message_handler.py b/src/tg/handlers/user_message_handler.py index d4dd0060..f144b108 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, diff --git a/src/trello/trello_client.py b/src/trello/trello_client.py deleted file mode 100644 index e1ab917d..00000000 --- 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 diff --git a/src/trello/trello_objects.py b/src/trello/trello_objects.py deleted file mode 100644 index 9dc0c079..00000000 --- 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" diff --git a/src/utils/card_checks.py b/src/utils/card_checks.py deleted file mode 100644 index 478346e1..00000000 --- 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 dfbd7825..00000000 --- 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", -} diff --git a/src/utils/telegram.py b/src/utils/telegram.py new file mode 100644 index 00000000..2efd2593 --- /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/static/config_override.json b/tests/unit/static/config_override.json index 021aacac..3dc2bd54 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_db_client.py b/tests/unit/test_db_client.py index b8ef58bb..bec3eacd 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" + ) diff --git a/tests/unit/test_hr_acquisition_jobs.py b/tests/unit/test_hr_acquisition_jobs.py new file mode 100644 index 00000000..105891eb --- /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 diff --git a/tests/unit/test_sheets_client.py b/tests/unit/test_sheets_client.py index c0451463..2fa1df7a 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 dfd82b72..63d04d32 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'] # ),