From bac2ffbafacd687b81f74c49aad5216bc2a0e534 Mon Sep 17 00:00:00 2001 From: Alex Kulikov Date: Sat, 4 Jul 2026 16:53:59 +0100 Subject: [PATCH 1/3] fix: replace deprecated FB reach metrics, add paid views stat Meta deprecated page_impressions_unique and page_impressions_organic_unique (all API versions), which silently zeroed out reach in the weekly FB report. Total reach now uses page_total_media_view_unique (true unique-reach replacement); organic reach uses page_media_view broken down by is_from_ads since Meta removed the organic unique-reach metric with no replacement. Also adds get_weekly_paid_views for the same is_from_ads breakdown. Co-Authored-By: Claude Sonnet 5 --- src/analytics/api_facebook_analytics.py | 10 +++++ src/analytics/base_analytics.py | 6 +++ src/facebook/facebook_client.py | 60 +++++++++++++++++++++---- src/jobs/fb_analytics_report_job.py | 3 ++ 4 files changed, 70 insertions(+), 9 deletions(-) diff --git a/src/analytics/api_facebook_analytics.py b/src/analytics/api_facebook_analytics.py index 3e03dce1..0cb20ea9 100644 --- a/src/analytics/api_facebook_analytics.py +++ b/src/analytics/api_facebook_analytics.py @@ -32,6 +32,16 @@ def get_weekly_organic_reach_of_new_posts(self, end_week: datetime): return 0 return result[0][1] + def get_weekly_paid_views(self, end_week: datetime): + result = self._fb_client.get_paid_views( + ApiFacebookAnalytics._get_end_week_day_start(end_week), + ApiFacebookAnalytics._get_end_week_day_end(end_week), + period=ReportPeriod.WEEK, + ) + if not result: + return 0 + return result[0][1] + def get_weekly_new_follower_count(self, end_week: datetime): result = self._fb_client.get_new_follower_count( ApiFacebookAnalytics._get_end_week_day_start(end_week), diff --git a/src/analytics/base_analytics.py b/src/analytics/base_analytics.py index 60491fd7..fe0bce1f 100644 --- a/src/analytics/base_analytics.py +++ b/src/analytics/base_analytics.py @@ -20,6 +20,12 @@ def get_weekly_organic_reach_of_new_posts(self, end_week: datetime): """ raise NotImplementedError("") + def get_weekly_paid_views(self, end_week: datetime): + """ + Get weekly statistics on paid (ads) views of new posts + """ + raise NotImplementedError("") + def get_weekly_new_follower_count(self, end_week: datetime): """ Get the number of new followers for the week diff --git a/src/facebook/facebook_client.py b/src/facebook/facebook_client.py index cf430381..81ce1827 100644 --- a/src/facebook/facebook_client.py +++ b/src/facebook/facebook_client.py @@ -70,12 +70,12 @@ def get_total_reach( ) -> List[Tuple[datetime, int]]: """ Get statistics on the total reach of new posts. - NOTE: 'page_posts_impressions_unique' is deprecated in v19.0. - Using 'page_impressions_unique' (Total Page Reach) as replacement. + NOTE: 'page_impressions_unique' was deprecated by Meta (June 2026). + Using 'page_total_media_view_unique' as the replacement unique-reach metric. """ batches = self._get_all_batches( connection_name="insights", - metric="page_impressions_unique", + metric="page_total_media_view_unique", period=period.value, since=since, until=until, @@ -89,19 +89,53 @@ def get_organic_reach( self, since: datetime, until: datetime, period: ReportPeriod ) -> List[Tuple[datetime, int]]: """ - Get statistics on the organic reach of new posts. + Get statistics on organic (non-paid) views of page content. + NOTE: 'page_impressions_organic_unique' was deprecated by Meta (June 2026) + with no unique-reach replacement. Using 'page_media_view' broken down by + 'is_from_ads', filtered to the organic (is_from_ads == "0") rows. This is + a view count, not a unique-people count, so it isn't directly comparable + to historical organic-reach values. """ batches = self._get_all_batches( connection_name="insights", - metric="page_impressions_organic_unique", + metric="page_media_view", period=period.value, + breakdown="is_from_ads", since=since, until=until, ) - organic_reach = self._get_values_from_batches( - batches=batches, since=since, until=until + organic_views = self._get_values_from_batches( + batches=batches, + since=since, + until=until, + breakdown_filter={"is_from_ads": "0"}, ) - return organic_reach + return organic_views + + def get_paid_views( + self, since: datetime, until: datetime, period: ReportPeriod + ) -> List[Tuple[datetime, int]]: + """ + Get statistics on paid (ads) views of page content. + Uses 'page_media_view' broken down by 'is_from_ads', filtered to the + paid (is_from_ads == "1") rows. See get_organic_reach for context on + why this is a view count rather than a unique-people count. + """ + batches = self._get_all_batches( + connection_name="insights", + metric="page_media_view", + period=period.value, + breakdown="is_from_ads", + since=since, + until=until, + ) + paid_views = self._get_values_from_batches( + batches=batches, + since=since, + until=until, + breakdown_filter={"is_from_ads": "1"}, + ) + return paid_views def get_new_follower_count( self, since: datetime, until: datetime, period: ReportPeriod @@ -214,11 +248,19 @@ def _iterate_over_pages( @staticmethod def _get_values_from_batches( - batches: List[dict], since: datetime, until: datetime + batches: List[dict], + since: datetime, + until: datetime, + breakdown_filter: dict = None, ) -> List[Tuple[datetime, int]]: value_by_date = [] for batch in batches: for value_info in batch["values"]: + if breakdown_filter and any( + value_info.get(key) != value + for key, value in breakdown_filter.items() + ): + continue end_time = dateparser.isoparse(value_info["end_time"]) if end_time < since or end_time > until: continue diff --git a/src/jobs/fb_analytics_report_job.py b/src/jobs/fb_analytics_report_job.py index 9e778c43..2beb0d0d 100644 --- a/src/jobs/fb_analytics_report_job.py +++ b/src/jobs/fb_analytics_report_job.py @@ -49,6 +49,9 @@ def _execute( organic_reach=app_context.facebook_analytics.get_weekly_organic_reach_of_new_posts( end_week_day ), + paid_views=app_context.facebook_analytics.get_weekly_paid_views( + end_week_day + ), ) ] pretty_send(paragraphs, send) From 74b98e39c634843df668cef66fafb9c47c2803f1 Mon Sep 17 00:00:00 2001 From: Alex Kulikov Date: Sat, 4 Jul 2026 17:09:53 +0100 Subject: [PATCH 2/3] fix: dedupe Telegram album messages in weekly analytics report Telegram's broadcast stats API reports each photo in a gallery post as a separate message with the same view count, which inflated new_posts_count and recent_message_views by the album size (e.g. a 9-photo post counted as 9 new posts and 9x its views). Co-Authored-By: Claude Sonnet 5 --- src/jobs/tg_analytics_report_job.py | 43 +++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/src/jobs/tg_analytics_report_job.py b/src/jobs/tg_analytics_report_job.py index 122a0296..9c5004d8 100644 --- a/src/jobs/tg_analytics_report_job.py +++ b/src/jobs/tg_analytics_report_job.py @@ -1,7 +1,9 @@ import json import logging from datetime import datetime, timezone -from typing import Callable, Tuple +from typing import Callable, List, Tuple + +from telethon.tl.types import MessageInteractionCounters from src.app_context import AppContext from src.jobs.base_job import BaseJob @@ -26,8 +28,20 @@ def _execute( entity = app_context.tg_client.api_client.loop.run_until_complete( app_context.tg_client.api_client.get_entity(app_context.tg_client.channel) ) + messages = app_context.tg_client.api_client.loop.run_until_complete( + app_context.tg_client.api_client.get_messages( + app_context.tg_client.channel, + ids=[ + message_stats.msg_id + for message_stats in stats.recent_message_interactions + ], + ) + ) app_context.tg_client.api_client.disconnect() - new_posts_count = len(stats.recent_message_interactions) + post_interactions = TgAnalyticsReportJob._deduplicate_albums( + stats.recent_message_interactions, messages + ) + new_posts_count = len(post_interactions) followers_stats = TgAnalyticsReportJob._get_followers_stats(stats) message = load( "tg_analytics_report_job__text", @@ -47,10 +61,7 @@ def _execute( 2, ), recent_message_views=sum( - [ - message_stats.views - for message_stats in stats.recent_message_interactions - ] + [message_stats.views for message_stats in post_interactions] ), views_per_post=int(stats.views_per_post.current), views_per_post_delta=TgAnalyticsReportJob._format_delta( @@ -63,6 +74,26 @@ def _execute( ) pretty_send([message], send) + @staticmethod + def _deduplicate_albums( + message_interactions: List[MessageInteractionCounters], messages: List + ) -> List[MessageInteractionCounters]: + """Collapse album (grouped_id) messages into one entry: Telegram's stats API + reports each photo of a gallery post as a separate message with the same + view count, which otherwise inflates post count and total views N-fold.""" + grouped_id_by_msg_id = { + msg.id: msg.grouped_id for msg in messages if msg is not None + } + best_by_group = {} + for message_stats in message_interactions: + group_key = ( + grouped_id_by_msg_id.get(message_stats.msg_id) or message_stats.msg_id + ) + current_best = best_by_group.get(group_key) + if current_best is None or message_stats.views > current_best.views: + best_by_group[group_key] = message_stats + return list(best_by_group.values()) + @staticmethod def _get_followers_stats(stats) -> Tuple[int, int]: """ From f2b9608d0db08eef9289800f91bd84da0a305c59 Mon Sep 17 00:00:00 2001 From: Alex Kulikov Date: Sat, 4 Jul 2026 17:25:26 +0100 Subject: [PATCH 3/3] fix: repair unit test suite broken by Trello->Planka migration conftest.py and several test files still imported the deleted src.trello.trello_client/trello_objects modules and exercised APIs (use_focalboard/use_planka kwargs, TrelloCard.to_dict(), idBoard key) that no longer exist after the Planka migration, so the whole suite failed to even collect. Removes the dead mock_trello fixture and test_trello_client.py (which tested the deleted TrelloClient), updates stale imports to src.planka.board_objects, and aligns test expectations with current handler signatures and object field names. Co-Authored-By: Claude Sonnet 5 --- tests/unit/conftest.py | 35 +--- tests/unit/static/trello/board.json | 99 ---------- .../static/trello/board_custom_fields.json | 109 ----------- tests/unit/static/trello/card_actions.json | 130 ------------- .../static/trello/card_custom_fields.json | 29 --- tests/unit/static/trello/cards.json | 179 ------------------ tests/unit/static/trello/expected/board.json | 5 - .../trello/expected/board_custom_fields.json | 50 ----- .../static/trello/expected/card_actions.json | 56 ------ .../trello/expected/card_custom_fields.json | 17 -- tests/unit/static/trello/expected/cards.json | 40 ---- tests/unit/static/trello/expected/lists.json | 52 ----- .../unit/static/trello/expected/members.json | 7 - tests/unit/static/trello/lists.json | 92 --------- tests/unit/static/trello/members.json | 7 - .../unit/test_board_my_cards_razvitie_job.py | 4 +- .../test_get_tasks_report_planka_handler.py | 7 +- tests/unit/test_planka_client.py | 47 ++--- tests/unit/test_telegram_jobs.py | 2 - tests/unit/test_trello_client.py | 51 ----- 20 files changed, 23 insertions(+), 995 deletions(-) delete mode 100644 tests/unit/static/trello/board.json delete mode 100644 tests/unit/static/trello/board_custom_fields.json delete mode 100644 tests/unit/static/trello/card_actions.json delete mode 100644 tests/unit/static/trello/card_custom_fields.json delete mode 100644 tests/unit/static/trello/cards.json delete mode 100644 tests/unit/static/trello/expected/board.json delete mode 100644 tests/unit/static/trello/expected/board_custom_fields.json delete mode 100644 tests/unit/static/trello/expected/card_actions.json delete mode 100644 tests/unit/static/trello/expected/card_custom_fields.json delete mode 100644 tests/unit/static/trello/expected/cards.json delete mode 100644 tests/unit/static/trello/expected/lists.json delete mode 100644 tests/unit/static/trello/expected/members.json delete mode 100644 tests/unit/static/trello/lists.json delete mode 100644 tests/unit/static/trello/members.json delete mode 100644 tests/unit/test_trello_client.py diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 3335ab93..191eea1e 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,10 +1,9 @@ import logging import os -from typing import Dict, List +from typing import List from unittest.mock import MagicMock import pytest -from utils.json_loader import JsonLoader from src.config_manager import ConfigManager from src.consts import USAGE_LOG_LEVEL @@ -13,7 +12,6 @@ from src.sheets.sheets_client import GoogleSheetsClient from src.strings import StringsDBClient from src.tg.sender import TelegramSender -from src.trello.trello_client import TrelloClient # logger.usage is added to the Logger class in bot.py at runtime; tests never # import bot, so we register it here to avoid AttributeError in job logging. @@ -29,7 +27,6 @@ def _usage(self, message, *args, **kws): ROOT_TEST_DIR = os.path.abspath(os.path.dirname(__file__)) STATIC_TEST_DIR = os.path.join(ROOT_TEST_DIR, "static") SHEETS_TEST_DIR = os.path.join(STATIC_TEST_DIR, "sheets") -TRELLO_TEST_DIR = os.path.join(STATIC_TEST_DIR, "trello") CONFIG_PATH = os.path.join(STATIC_TEST_DIR, "config.json") CONFIG_OVERRIDE_PATH = os.path.join(STATIC_TEST_DIR, "config_override.json") @@ -48,36 +45,6 @@ def mock_config_jobs_manager(monkeypatch): return config_manager -@pytest.fixture -def mock_trello(monkeypatch, mock_config_manager): - def _make_request(_, uri: str, payload={}) -> (int, Dict): - load_json = JsonLoader(TRELLO_TEST_DIR).load_json - - if uri.startswith("boards"): - if uri.endswith("lists"): - return 200, load_json("lists.json") - elif uri.endswith("cards"): - return 200, load_json("cards.json") - elif uri.endswith("members"): - return 200, load_json("members.json") - elif uri.endswith("customFields"): - return 200, load_json("board_custom_fields.json") - else: - return 200, load_json("board.json") - elif uri.startswith("cards"): - if uri.endswith("customFieldItems"): - return 200, load_json("card_custom_fields.json") - if uri.endswith("actions"): - return 200, load_json("card_actions.json") - elif uri.startswith("lists"): - if uri.endswith("cards"): - return 200, load_json("cards.json") - - monkeypatch.setattr(TrelloClient, "_make_request", _make_request) - - return TrelloClient(trello_config=mock_config_manager.get_trello_config()) - - @pytest.fixture def mock_sheets_client(monkeypatch, mock_config_manager): def _authorize(self): diff --git a/tests/unit/static/trello/board.json b/tests/unit/static/trello/board.json deleted file mode 100644 index b40b9a97..00000000 --- a/tests/unit/static/trello/board.json +++ /dev/null @@ -1,99 +0,0 @@ -{ - "id": "board_1", - "name": "Редакция (тест)", - "desc": "", - "descData": null, - "closed": false, - "idOrganization": "5e91d358ceb023428db2f6b0", - "idEnterprise": null, - "pinned": false, - "url": "https://trello.com/b/Kz1xBlTf/%D1%80%D0%B5%D0%B4%D0%B0%D0%BA%D1%86%D0%B8%D1%8F-%D1%82%D0%B5%D1%81%D1%82", - "shortUrl": "https://trello.com/b/test_board_url", - "labelNames": { - "green": "Общество", - "yellow": "NLP", - "orange": "Искусство", - "red": "Новости", - "purple": "Как это работает", - "blue": "", - "sky": "Визуализация", - "lime": "", - "pink": "Футурология", - "black": "ТГ. Нативно" - }, - "prefs": { - "permissionLevel": "org", - "hideVotes": false, - "voting": "disabled", - "comments": "members", - "invitations": "members", - "selfJoin": true, - "cardCovers": true, - "isTemplate": false, - "cardAging": "regular", - "calendarFeedEnabled": false, - "background": "5b71d0c5750dcf73298ad15d", - "backgroundImage": "https://trello-backgrounds.s3.amazonaws.com/SharedBackground/2560x1709/e4197a3e1a98ac04e83de59faad6e366/photo-1533883355737-25ab4d1fbefb", - "backgroundImageScaled": [ - { - "width": 140, - "height": 93, - "url": "https://trello-backgrounds.s3.amazonaws.com/SharedBackground/140x93/f28eaae9673796438691f5208db0baa3/photo-1533883355737-25ab4d1fbefb.jpg" - }, - { - "width": 256, - "height": 171, - "url": "https://trello-backgrounds.s3.amazonaws.com/SharedBackground/256x171/f1da61d7a4410faa0b2c042aec25da5a/photo-1533883355737-25ab4d1fbefb.jpg" - }, - { - "width": 480, - "height": 320, - "url": "https://trello-backgrounds.s3.amazonaws.com/SharedBackground/480x320/e9f7afa5d3e57b9d01c91240ea286d8e/photo-1533883355737-25ab4d1fbefb.jpg" - }, - { - "width": 960, - "height": 641, - "url": "https://trello-backgrounds.s3.amazonaws.com/SharedBackground/960x641/c17ff11074e36ae7ee8dfa6d86c11c89/photo-1533883355737-25ab4d1fbefb.jpg" - }, - { - "width": 1024, - "height": 684, - "url": "https://trello-backgrounds.s3.amazonaws.com/SharedBackground/1024x684/59ce0fb72a846fc237a3ae8684e5384e/photo-1533883355737-25ab4d1fbefb.jpg" - }, - { - "width": 2048, - "height": 1367, - "url": "https://trello-backgrounds.s3.amazonaws.com/SharedBackground/2048x1367/f54cd5e58124eb6e113b235960665455/photo-1533883355737-25ab4d1fbefb.jpg" - }, - { - "width": 1280, - "height": 855, - "url": "https://trello-backgrounds.s3.amazonaws.com/SharedBackground/1280x855/683c844d5f8ca8f060212f17bce852c2/photo-1533883355737-25ab4d1fbefb.jpg" - }, - { - "width": 1920, - "height": 1282, - "url": "https://trello-backgrounds.s3.amazonaws.com/SharedBackground/1920x1282/76457f800ae6e754eb19759d7bbdf6af/photo-1533883355737-25ab4d1fbefb.jpg" - }, - { - "width": 2397, - "height": 1600, - "url": "https://trello-backgrounds.s3.amazonaws.com/SharedBackground/2397x1600/1aced80c0827d90c70d8db982f45052d/photo-1533883355737-25ab4d1fbefb.jpg" - }, - { - "width": 2560, - "height": 1709, - "url": "https://trello-backgrounds.s3.amazonaws.com/SharedBackground/2560x1709/e4197a3e1a98ac04e83de59faad6e366/photo-1533883355737-25ab4d1fbefb" - } - ], - "backgroundTile": false, - "backgroundBrightness": "light", - "backgroundBottomColor": "#615b5f", - "backgroundTopColor": "#bfabae", - "canBePublic": true, - "canBeEnterprise": true, - "canBeOrg": true, - "canBePrivate": true, - "canInvite": true - } -} diff --git a/tests/unit/static/trello/board_custom_fields.json b/tests/unit/static/trello/board_custom_fields.json deleted file mode 100644 index 9a6c6d94..00000000 --- a/tests/unit/static/trello/board_custom_fields.json +++ /dev/null @@ -1,109 +0,0 @@ -[ - { - "id": "type_0", - "idModel": "5e91d48a9e9d947b95fb0df7", - "modelType": "board", - "fieldGroup": "093946898f0a74bee9f218e55e31db722938563e140b0635704307912135a6bc", - "display": { - "cardFront": true - }, - "name": "Автор", - "pos": 16384, - "type": "text" - }, - { - "id": "type_1", - "idModel": "5e91d48a9e9d947b95fb0df7", - "modelType": "board", - "fieldGroup": "a91bc8b0b9b1ff076de078de356b1462beba9f4225c89986ef405c4371402b35", - "display": { - "cardFront": false - }, - "name": "Google Doc", - "pos": 32768, - "type": "text" - }, - { - "id": "type_2", - "idModel": "5e91d48a9e9d947b95fb0df7", - "modelType": "board", - "fieldGroup": "588488e8f09eb6dbaecbd89cf5fe83fc6f3299ca8a2323522410d96f80e7c00e", - "display": { - "cardFront": true - }, - "name": "Редактор", - "pos": 49152, - "type": "text" - }, - { - "id": "type_3", - "idModel": "5e91d48a9e9d947b95fb0df7", - "modelType": "board", - "fieldGroup": "9b23e842082fcaf352f26809eb8270a3f065d8d982a8af171428cac112e46b24", - "display": { - "cardFront": false - }, - "name": "Название поста", - "pos": 65536, - "type": "text" - }, - { - "id": "type_4", - "idModel": "5e91d48a9e9d947b95fb0df7", - "modelType": "board", - "fieldGroup": "92cc10f007fa367cd04b35929c26db94cc5d292247fdf04b575d0117c4282481", - "display": { - "cardFront": true - }, - "name": "Иллюстратор", - "pos": 81920, - "type": "text" - }, - { - "id": "type_5", - "idModel": "5e91d48a9e9d947b95fb0df7", - "modelType": "board", - "fieldGroup": "92cc10f007fa367cd04b35929c26db94cc5d292247fdf04b575d0117c4282481", - "display": { - "cardFront": true - }, - "name": "Обложка", - "pos": 81920, - "type": "text" - }, - { - "id": "type_6", - "idModel": "5e91d48a9e9d947b95fb0df7", - "modelType": "board", - "fieldGroup": "28c157040ba1375386ba5971564bfb17325043de973feae6128950036e5de20b", - "display": { - "cardFront": false - }, - "name": "Сайт", - "pos": 196608, - "type": "checkbox" - }, - { - "id": "type_7", - "idModel": "5e91d48a9e9d947b95fb0df7", - "modelType": "board", - "fieldGroup": "488f1b95d2449518badfd840b7a066d9b62debf1ccb4ec0805c68565320cde5a", - "display": { - "cardFront": false - }, - "name": "Селектор", - "pos": 212992, - "options": [ - { - "id": "5ede91f8c9525241e8c6785a", - "idCustomField": "5ede91f8c9525241e8c67856", - "value": { - "text": "нет" - }, - "color": "none", - "pos": 69632 - } - ], - "type": "list" - } -] diff --git a/tests/unit/static/trello/card_actions.json b/tests/unit/static/trello/card_actions.json deleted file mode 100644 index 46814a5c..00000000 --- a/tests/unit/static/trello/card_actions.json +++ /dev/null @@ -1,130 +0,0 @@ -[ - { - "id": "5ebc8811bc56ff5809c1f833", - "idMemberCreator": "56cd570f0c81722559a4abd7", - "data": { - "old": { - "idList": "5e91d48a9e9d947b95fb0e00" - }, - "card": { - "idList": "list_1", - "id": "1", - "name": "Картографирование эпидемий", - "idShort": 166, - "shortLink": "9qBC9AWi" - }, - "board": { - "id": "5e91d48a9e9d947b95fb0df7", - "name": "Редакция (тест)", - "shortLink": "Kz1xBlTf" - }, - "listBefore": { - "id": "list_8", - "name": "Готово для верстки (Выпускающему)" - }, - "listAfter": { - "id": "list_4", - "name": "На редактуре на след.неделю (не забудь тег рубрики!)" - } - }, - "type": "updateCard", - "date": "2020-05-13T23:51:45.420Z", - "limits": {}, - "memberCreator": { - "id": "56cd570f0c81722559a4abd7", - "username": "alexeyqu", - "activityBlocked": false, - "avatarHash": "b8c99e483cd8b3ba037505feceac4483", - "avatarUrl": "https://trello-members.s3.amazonaws.com/56cd570f0c81722559a4abd7/b8c99e483cd8b3ba037505feceac4483", - "fullName": "Alexey Kulikov", - "idMemberReferrer": null, - "initials": "AK", - "nonPublic": {}, - "nonPublicAvailable": false - } - }, - { - "id": "5eb8727f1218f340bc659093", - "idMemberCreator": "550c620696262ccd4a506b3f", - "data": { - "old": { - "pos": 3994744307712 - }, - "card": { - "pos": 3994744389632, - "id": "1", - "name": "Картографирование эпидемий", - "idShort": 166, - "shortLink": "9qBC9AWi" - }, - "board": { - "id": "5e91d48a9e9d947b95fb0df7", - "name": "Редакция (тест)", - "shortLink": "Kz1xBlTf" - }, - "list": { - "id": "list_8", - "name": "Готово для верстки (Выпускающему)" - } - }, - "type": "updateCard", - "date": "2020-05-10T21:30:39.132Z", - "limits": {}, - "memberCreator": { - "id": "550c620696262ccd4a506b3f", - "username": "irinoise", - "activityBlocked": false, - "avatarHash": "f8da091665b4097042ec7eb1fe5de251", - "avatarUrl": "https://trello-members.s3.amazonaws.com/550c620696262ccd4a506b3f/f8da091665b4097042ec7eb1fe5de251", - "fullName": "Irina Shakhova", - "idMemberReferrer": null, - "initials": "IS", - "nonPublic": {}, - "nonPublicAvailable": false - } - }, - { - "id": "5eb8727f1218f340bc659091", - "idMemberCreator": "550c620696262ccd4a506b3f", - "data": { - "old": { - "idList": "5e91d48a9e9d947b95fb0dfc" - }, - "card": { - "idList": "5e91d48a9e9d947b95fb0e00", - "id": "1", - "name": "Картографирование эпидемий", - "idShort": 166, - "shortLink": "9qBC9AWi" - }, - "board": { - "id": "5e91d48a9e9d947b95fb0df7", - "name": "Редакция (тест)", - "shortLink": "Kz1xBlTf" - }, - "listBefore": { - "id": "list_4", - "name": "На редактуре на след.неделю (не забудь тег рубрики!)" - }, - "listAfter": { - "id": "list_8", - "name": "Готово для верстки (Выпускающему)" - } - }, - "type": "updateCard", - "date": "2020-05-10T21:30:39.108Z", - "limits": {}, - "memberCreator": { - "id": "550c620696262ccd4a506b3f", - "username": "irinoise", - "activityBlocked": false, - "avatarHash": "f8da091665b4097042ec7eb1fe5de251", - "avatarUrl": "https://trello-members.s3.amazonaws.com/550c620696262ccd4a506b3f/f8da091665b4097042ec7eb1fe5de251", - "fullName": "Irina Shakhova", - "idMemberReferrer": null, - "initials": "IS", - "nonPublic": {}, - "nonPublicAvailable": false - } - } -] diff --git a/tests/unit/static/trello/card_custom_fields.json b/tests/unit/static/trello/card_custom_fields.json deleted file mode 100644 index fcf67840..00000000 --- a/tests/unit/static/trello/card_custom_fields.json +++ /dev/null @@ -1,29 +0,0 @@ -[ - { - "id": "field_0", - "value": { - "text": "Илья Булгаков" - }, - "idCustomField": "type_0", - "idModel": "5e91d48a9e9d947b95fb0f75", - "modelType": "card" - }, - { - "id": "field_1", - "value": { - "text": "https://docs.google.com/document/d/DOC1" - }, - "idCustomField": "type_1", - "idModel": "5e91d48a9e9d947b95fb0f75", - "modelType": "card" - }, - { - "id": "field_2", - "value": { - "text": "Полина Матавина" - }, - "idCustomField": "type_2", - "idModel": "5e91d48a9e9d947b95fb0f75", - "modelType": "card" - } -] diff --git a/tests/unit/static/trello/cards.json b/tests/unit/static/trello/cards.json deleted file mode 100644 index e69f7f64..00000000 --- a/tests/unit/static/trello/cards.json +++ /dev/null @@ -1,179 +0,0 @@ -[ - { - "id": "card_1", - "checkItemStates": null, - "closed": false, - "dateLastActivity": "2020-04-11T14:30:36.649Z", - "desc": "Даня сам напишет.\nhttps://terraoblita.com/ru\nСм. https://terraoblita.com/ru/about", - "descData": null, - "dueReminder": 1440, - "idBoard": "board_1", - "idList": "list_1", - "idMembersVoted": [], - "idShort": 56, - "idAttachmentCover": null, - "idLabels": [ - "5e91d48a9e9d947b95fb0fd7" - ], - "manualCoverAttachment": false, - "name": "Open Memory Map", - "pos": 4565421785088, - "shortLink": "PyXDoStd", - "isTemplate": false, - "badges": { - "attachmentsByType": { - "trello": { - "board": 0, - "card": 0 - } - }, - "location": false, - "votes": 0, - "viewingMemberVoted": false, - "subscribed": false, - "fogbugz": "", - "checkItems": 0, - "checkItemsChecked": 0, - "checkItemsEarliestDue": null, - "comments": 0, - "attachments": 0, - "description": true, - "due": "2020-06-18T09:00:00.000Z", - "dueComplete": false - }, - "dueComplete": false, - "due": "2020-06-18T09:00:00.000Z", - "idChecklists": [], - "idMembers": [], - "labels": [ - { - "id": "5e91d48a9e9d947b95fb0fd7", - "idBoard": "5e91d48a9e9d947b95fb0df7", - "name": "История", - "color": "dark-dark-dark-light-red-lime" - } - ], - "shortUrl": "https://trello.com/c/card_1", - "subscribed": false, - "url": "https://trello.com/c/PyXDoStd/56-terra-oblita-open-memory-map-%D0%BA%D0%B0%D1%80%D1%82%D0%B0-%D0%BF%D0%B0%D0%BC%D1%8F%D1%82%D0%B8-%D0%B7%D0%B0%D0%B1%D1%8B%D1%82%D1%8B%D1%85-%D0%B6%D0%B5%D1%80%D1%82%D0%B2-%D0%BD%D0%B0%D1%86%D0%B8%D0%B7%D0%BC%D0%B0", - "cover": { - "idAttachment": null, - "color": null, - "idUploadedBackground": null, - "size": "normal", - "brightness": "light" - } - }, - { - "id": "card_2", - "checkItemStates": null, - "closed": false, - "dateLastActivity": "2020-04-11T14:30:36.194Z", - "desc": "", - "descData": null, - "dueReminder": null, - "idBoard": "board_1", - "idList": "list_2", - "idMembersVoted": [], - "idShort": 96, - "idAttachmentCover": null, - "idLabels": [], - "manualCoverAttachment": false, - "name": "тестовая карточка", - "pos": 4565421850624, - "shortLink": "UnqBPZ8h", - "isTemplate": false, - "badges": { - "attachmentsByType": { - "trello": { - "board": 0, - "card": 0 - } - }, - "location": false, - "votes": 0, - "viewingMemberVoted": false, - "subscribed": false, - "fogbugz": "", - "checkItems": 0, - "checkItemsChecked": 0, - "checkItemsEarliestDue": null, - "comments": 0, - "attachments": 0, - "description": false, - "due": null, - "dueComplete": false - }, - "dueComplete": false, - "due": null, - "idChecklists": [], - "idMembers": ["member_1"], - "labels": [], - "shortUrl": "https://trello.com/c/card_2", - "subscribed": false, - "url": "https://trello.com/c/UnqBPZ8h/96-%D1%82%D0%B5%D1%81%D1%82%D0%BE%D0%B2%D0%B0%D1%8F-%D0%BA%D0%B0%D1%80%D1%82%D0%BE%D1%87%D0%BA%D0%B0", - "cover": { - "idAttachment": null, - "color": null, - "idUploadedBackground": null, - "size": "normal", - "brightness": "light" - } - }, - { - "id": "card_3", - "checkItemStates": null, - "closed": false, - "dateLastActivity": "2020-04-11T14:30:36.194Z", - "desc": "", - "descData": null, - "dueReminder": null, - "idBoard": "board_1", - "idList": "list_3", - "idMembersVoted": [], - "idShort": 96, - "idAttachmentCover": null, - "idLabels": [], - "manualCoverAttachment": false, - "name": "тестовая карточка 1", - "pos": 4565421850624, - "shortLink": "UnqBPZ8h", - "isTemplate": false, - "badges": { - "attachmentsByType": { - "trello": { - "board": 0, - "card": 0 - } - }, - "location": false, - "votes": 0, - "viewingMemberVoted": false, - "subscribed": false, - "fogbugz": "", - "checkItems": 0, - "checkItemsChecked": 0, - "checkItemsEarliestDue": null, - "comments": 0, - "attachments": 0, - "description": false, - "due": null, - "dueComplete": false - }, - "dueComplete": false, - "due": null, - "idChecklists": [], - "idMembers": [], - "labels": [], - "shortUrl": "https://trello.com/c/card_3", - "subscribed": false, - "url": "https://trello.com/c/UnqBPZ8h/96-%D1%82%D0%B5%D1%81%D1%82%D0%BE%D0%B2%D0%B0%D1%8F-%D0%BA%D0%B0%D1%80%D1%82%D0%BE%D1%87%D0%BA%D0%B0", - "cover": { - "idAttachment": null, - "color": null, - "idUploadedBackground": null, - "size": "normal", - "brightness": "light" - } - } -] diff --git a/tests/unit/static/trello/expected/board.json b/tests/unit/static/trello/expected/board.json deleted file mode 100644 index 2999baa0..00000000 --- a/tests/unit/static/trello/expected/board.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "board_1", - "name": "Редакция (тест)", - "url": "https://trello.com/b/test_board_url" -} diff --git a/tests/unit/static/trello/expected/board_custom_fields.json b/tests/unit/static/trello/expected/board_custom_fields.json deleted file mode 100644 index 20df503e..00000000 --- a/tests/unit/static/trello/expected/board_custom_fields.json +++ /dev/null @@ -1,50 +0,0 @@ -[ - { - "id": "type_0", - "name": "Автор", - "type": "text" - }, - { - "id": "type_1", - "name": "Google Doc", - "type": "text" - }, - { - "id": "type_2", - "name": "Редактор", - "type": "text" - }, - { - "id": "type_3", - "name": "Название поста", - "type": "text" - }, - { - "id": "type_4", - "name": "Иллюстратор", - "type": "text" - }, - { - "id": "type_5", - "name": "Обложка", - "type": "text" - }, - { - "id": "type_6", - "name": "Сайт", - "type": "checkbox" - }, - { - "id": "type_7", - "name": "Селектор", - "options": [ - { - "id": "5ede91f8c9525241e8c6785a", - "value": { - "text": "нет" - } - } - ], - "type": "list" - } -] diff --git a/tests/unit/static/trello/expected/card_actions.json b/tests/unit/static/trello/expected/card_actions.json deleted file mode 100644 index 69816b36..00000000 --- a/tests/unit/static/trello/expected/card_actions.json +++ /dev/null @@ -1,56 +0,0 @@ -[ - { - "id": "5ebc8811bc56ff5809c1f833", - "type": "updateCard", - "data": { - "card": { - "shortLink": "9qBC9AWi" - }, - "listBefore": { - "id": "list_8", - "name": "Готово для верстки (Выпускающему)" - }, - "listAfter": { - "id": "list_4", - "name": "На редактуре на след.неделю (не забудь тег рубрики!)" - } - }, - "date": "2020-05-13T23:51:45.420000Z" - }, - { - "id": "5eb8727f1218f340bc659093", - "type": "updateCard", - "data": { - "card": { - "shortLink": "9qBC9AWi" - }, - "listBefore": { - "id": null, - "name": null - }, - "listAfter": { - "id": null, - "name": null - } - }, - "date": "2020-05-10T21:30:39.132000Z" - }, - { - "id": "5eb8727f1218f340bc659091", - "type": "updateCard", - "data": { - "card": { - "shortLink": "9qBC9AWi" - }, - "listBefore": { - "id": "list_4", - "name": "На редактуре на след.неделю (не забудь тег рубрики!)" - }, - "listAfter": { - "id": "list_8", - "name": "Готово для верстки (Выпускающему)" - } - }, - "date": "2020-05-10T21:30:39.108000Z" - } -] diff --git a/tests/unit/static/trello/expected/card_custom_fields.json b/tests/unit/static/trello/expected/card_custom_fields.json deleted file mode 100644 index d305f2e0..00000000 --- a/tests/unit/static/trello/expected/card_custom_fields.json +++ /dev/null @@ -1,17 +0,0 @@ -[ - { - "id": "field_0", - "value": {"text": "Илья Булгаков"}, - "idCustomField": "type_0" - }, - { - "id": "field_1", - "value": {"text": "https://docs.google.com/document/d/DOC1"}, - "idCustomField": "type_1" - }, - { - "id": "field_2", - "value": {"text": "Полина Матавина"}, - "idCustomField": "type_2" - } -] diff --git a/tests/unit/static/trello/expected/cards.json b/tests/unit/static/trello/expected/cards.json deleted file mode 100644 index b37fd0b0..00000000 --- a/tests/unit/static/trello/expected/cards.json +++ /dev/null @@ -1,40 +0,0 @@ -[ - { - "id": "card_1", - "name": "Open Memory Map", - "due": "2020-06-18T09:00:00.000000Z", - "members": [], - "labels": [ - { - "id": "5e91d48a9e9d947b95fb0fd7", - "name": "История", - "color": "unknown" - } - ], - "url": "https://trello.com/c/card_1", - "list": { - "id": "list_1", - "name": "Готовая тема - автор, бери!", - "idBoard": "board_id" - } - }, - { - "id": "card_2", - "due": null, - "name": "тестовая карточка", - "members": [ - { - "id": "member_1", - "username": "paulinmatavina", - "fullName": "Paulin Matavina" - } - ], - "list": { - "id": "list_2", - "name": "Уже пишу (не забудь указать дату и автора!)", - "idBoard": "board_id" - }, - "url": "https://trello.com/c/card_2", - "labels": [] - } - ] diff --git a/tests/unit/static/trello/expected/lists.json b/tests/unit/static/trello/expected/lists.json deleted file mode 100644 index 571bfb76..00000000 --- a/tests/unit/static/trello/expected/lists.json +++ /dev/null @@ -1,52 +0,0 @@ -[ - { - "id": "list_0", - "name": "Идея для статьи", - "idBoard": "board_id" - }, - { - "id": "list_1", - "name": "Готовая тема - автор, бери!", - "idBoard": "board_id" - }, - { - "id": "list_2", - "name": "Уже пишу (не забудь указать дату и автора!)", - "idBoard": "board_id" - }, - { - "id": "list_3", - "name": "Редактору", - "idBoard": "board_id" - }, - { - "id": "list_4", - "name": "На редактуре на след.неделю (не забудь тег рубрики!)", - "idBoard": "board_id" - }, - { - "id": "list_5", - "name": "Отредактировано впрок", - "idBoard": "board_id" - }, - { - "id": "list_6", - "name": "Финальная проверка и отбор (Главреду)", - "idBoard": "board_id" - }, - { - "id": "list_7", - "name": "Отобрано для публикации на неделю (Корректору)", - "idBoard": "board_id" - }, - { - "id": "list_8", - "name": "Готово для верстки (Выпускающему)", - "idBoard": "board_id" - }, - { - "id": "list_9", - "name": "Долгий Ящик", - "idBoard": "board_id" - } -] diff --git a/tests/unit/static/trello/expected/members.json b/tests/unit/static/trello/expected/members.json deleted file mode 100644 index 5a76ece2..00000000 --- a/tests/unit/static/trello/expected/members.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - "id": "member_1", - "username": "paulinmatavina", - "fullName": "Paulin Matavina" - } -] diff --git a/tests/unit/static/trello/lists.json b/tests/unit/static/trello/lists.json deleted file mode 100644 index d875670a..00000000 --- a/tests/unit/static/trello/lists.json +++ /dev/null @@ -1,92 +0,0 @@ -[ - { - "id": "list_0", - "name": "Идея для статьи", - "closed": false, - "pos": 794623, - "softLimit": null, - "idBoard": "board_id", - "subscribed": false - }, - { - "id": "list_1", - "name": "Готовая тема - автор, бери!", - "closed": false, - "pos": 811007, - "softLimit": null, - "idBoard": "board_id", - "subscribed": false - }, - { - "id": "list_2", - "name": "Уже пишу (не забудь указать дату и автора!)", - "closed": false, - "pos": 818175, - "softLimit": null, - "idBoard": "board_id", - "subscribed": false - }, - { - "id": "list_3", - "name": "Редактору", - "closed": false, - "pos": 841215, - "softLimit": null, - "idBoard": "board_id", - "subscribed": false - }, - { - "id": "list_4", - "name": "На редактуре на след.неделю (не забудь тег рубрики!)", - "closed": false, - "pos": 864255, - "softLimit": null, - "idBoard": "board_id", - "subscribed": false - }, - { - "id": "list_5", - "name": "Отредактировано впрок", - "closed": false, - "pos": 867327, - "softLimit": null, - "idBoard": "board_id", - "subscribed": false - }, - { - "id": "list_6", - "name": "Финальная проверка и отбор (Главреду)", - "closed": false, - "pos": 870399, - "softLimit": null, - "idBoard": "board_id", - "subscribed": false - }, - { - "id": "list_7", - "name": "Отобрано для публикации на неделю (Корректору)", - "closed": false, - "pos": 870911, - "softLimit": null, - "idBoard": "board_id", - "subscribed": false - }, - { - "id": "list_8", - "name": "Готово для верстки (Выпускающему)", - "closed": false, - "pos": 871423, - "softLimit": null, - "idBoard": "board_id", - "subscribed": false - }, - { - "id": "list_9", - "name": "Долгий Ящик", - "closed": false, - "pos": 1155071, - "softLimit": null, - "idBoard": "board_id", - "subscribed": false - } -] diff --git a/tests/unit/static/trello/members.json b/tests/unit/static/trello/members.json deleted file mode 100644 index 5a76ece2..00000000 --- a/tests/unit/static/trello/members.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - "id": "member_1", - "username": "paulinmatavina", - "fullName": "Paulin Matavina" - } -] diff --git a/tests/unit/test_board_my_cards_razvitie_job.py b/tests/unit/test_board_my_cards_razvitie_job.py index 39eb099f..3d045123 100644 --- a/tests/unit/test_board_my_cards_razvitie_job.py +++ b/tests/unit/test_board_my_cards_razvitie_job.py @@ -4,7 +4,7 @@ import src.jobs.board_my_cards_razvitie_job as job_module from src.jobs.board_my_cards_razvitie_job import BoardMyCardsRazvitieJob -from src.trello.trello_objects import TrelloBoard, TrelloCard, TrelloList, TrelloMember +from src.planka.board_objects import TrelloBoard, TrelloCard, TrelloList, TrelloMember class FakeDBClient: @@ -57,7 +57,7 @@ def __call__(self, message): @pytest.fixture(autouse=True) def patch_text_helpers(monkeypatch): - def fake_make_cards_text(cards, need_label, app_context): + def fake_make_cards_text(cards, need_label): return [f"card:{card.name}" for card in cards] def fake_load(string_id, **kwargs): diff --git a/tests/unit/test_get_tasks_report_planka_handler.py b/tests/unit/test_get_tasks_report_planka_handler.py index 768d6f1c..00767dc1 100644 --- a/tests/unit/test_get_tasks_report_planka_handler.py +++ b/tests/unit/test_get_tasks_report_planka_handler.py @@ -1,7 +1,7 @@ from types import SimpleNamespace import src.tg.handlers.get_tasks_report_handler as handler -from src.trello.trello_objects import TrelloBoard, TrelloCard, TrelloList, TrelloMember +from src.planka.board_objects import TrelloBoard, TrelloCard, TrelloList, TrelloMember class FakePlankaClient: @@ -47,11 +47,10 @@ def test_get_task_report_base_sets_planka_backend(monkeypatch): update = SimpleNamespace(effective_user=SimpleNamespace(username="tg_user")) tg_context = SimpleNamespace(chat_data={}) - handler._get_task_report_base(update, tg_context, advanced=False, use_planka=True) + handler._get_task_report_base(update, tg_context, advanced=False) assert planka_client.board_calls == [("tg_user", app_context.db_client)] assert tg_context.chat_data["use_planka"] is True - assert tg_context.chat_data["use_focalboard"] is False assert tg_context.chat_data["lists"] == [ {"id": "board_1", "name": "Развитие", "url": None} ] @@ -79,8 +78,6 @@ def test_generate_report_messages_uses_planka_client(monkeypatch): "list_1", "Intro", add_labels=False, - use_focalboard=False, - use_planka=True, ) assert planka_client.list_calls == [("board_1", "list_1")] diff --git a/tests/unit/test_planka_client.py b/tests/unit/test_planka_client.py index 1b5cb626..592a5146 100644 --- a/tests/unit/test_planka_client.py +++ b/tests/unit/test_planka_client.py @@ -1,9 +1,11 @@ import os +from datetime import datetime import pytest from conftest import STATIC_TEST_DIR from utils.json_loader import JsonLoader +from src.consts import TrelloCardColor from src.planka.planka_client import PlankaClient PLANKA_TEST_DIR = os.path.join(STATIC_TEST_DIR, "planka") @@ -100,8 +102,8 @@ def test_sorted_lists_include_active_lists_only(mock_planka): lists = mock_planka.get_lists(sorted=True) assert [item.to_dict() for item in lists] == [ - {"id": "list_first", "name": "First", "idBoard": "board_razvitie"}, - {"id": "list_second", "name": "Second", "idBoard": "board_razvitie"}, + {"id": "list_first", "name": "First", "board_id": "board_razvitie"}, + {"id": "list_second", "name": "Second", "board_id": "board_razvitie"}, ] @@ -111,7 +113,7 @@ def test_get_list(mock_planka): assert trello_list.to_dict() == { "id": "list_first", "name": "First", - "idBoard": "board_razvitie", + "board_id": "board_razvitie", } @@ -123,33 +125,20 @@ def test_get_list_raises_for_unknown_list(mock_planka): def test_cards_accept_single_list_id_string(mock_planka): cards = mock_planka.get_cards("list_first") - assert [card.to_dict() for card in cards] == [ - { - "id": "card_planka", - "name": "Implement Planka", - "labels": [ - { - "id": "label_feature", - "name": "Feature", - "color": "unknown", - } - ], - "url": "https://planka.example.com/cards/card_planka", - "due": "2026-05-09T10:15:00.000000Z", - "list": { - "id": "list_first", - "name": "First", - "idBoard": "board_razvitie", - }, - "members": [ - { - "id": "user_manager", - "username": "manager", - "fullName": "Manager User", - } - ], - } + assert len(cards) == 1 + card = cards[0] + assert card.id == "card_planka" + assert card.name == "Implement Planka" + assert card.url == "https://planka.example.com/cards/card_planka" + assert card.due == datetime(2026, 5, 9, 10, 15, 0) + assert card.lst.id == "list_first" + + assert [(label.id, label.name, label.color) for label in card.labels] == [ + ("label_feature", "Feature", TrelloCardColor.UNKNOWN) ] + assert [ + (member.id, member.username, member.full_name) for member in card.members + ] == [("user_manager", "manager", "Manager User")] def test_cards_accept_list_id_iterable(mock_planka): diff --git a/tests/unit/test_telegram_jobs.py b/tests/unit/test_telegram_jobs.py index 63d04d32..6fa4c8b8 100644 --- a/tests/unit/test_telegram_jobs.py +++ b/tests/unit/test_telegram_jobs.py @@ -44,7 +44,6 @@ def test_job( monkeypatch, mock_strings_db_client, - mock_trello, mock_sheets_client, mock_config_manager, mock_sender, @@ -66,7 +65,6 @@ def send_to_chat_id(message_text: str, chat_id: int, **kwargs): @pytest.mark.parametrize("job, output_parts", ((jobs.sample_job.SampleJob, ["Error"]),)) def test_job_failed( monkeypatch, - mock_trello, mock_sheets_client, mock_config_manager, mock_sender, diff --git a/tests/unit/test_trello_client.py b/tests/unit/test_trello_client.py deleted file mode 100644 index f7b6d9a9..00000000 --- a/tests/unit/test_trello_client.py +++ /dev/null @@ -1,51 +0,0 @@ -import os - -from conftest import TRELLO_TEST_DIR -from utils.json_loader import JsonLoader - -json_loader = JsonLoader(os.path.join(TRELLO_TEST_DIR, "expected")) - - -def test_init(mock_trello): - pass - - -def test_board(mock_trello): - board = mock_trello.get_board() - json_loader.assert_equal(board.to_dict(), "board.json") - - -def test_lists(mock_trello): - lists = mock_trello.get_lists() - json_loader.assert_equal([lst.to_dict() for lst in lists], "lists.json") - - -def test_cards(mock_trello): - cards = mock_trello.get_cards(["list_1", "list_2", "list_4"]) - json_loader.assert_equal([card.to_dict() for card in cards], "cards.json") - - -def test_board_custom_fields(mock_trello): - custom_field_types = mock_trello.get_board_custom_field_types() - json_loader.assert_equal( - [typ.to_dict() for typ in custom_field_types], "board_custom_fields.json" - ) - - -def test_card_custom_fields(mock_trello): - custom_fields = mock_trello.get_card_custom_fields(1) - json_loader.assert_equal( - [fld.to_dict() for fld in custom_fields], "card_custom_fields.json" - ) - - -def test_card_actions(mock_trello): - custom_fields = mock_trello.get_action_update_card(1) - json_loader.assert_equal( - [fld.to_dict() for fld in custom_fields], "card_actions.json" - ) - - -def test_members(mock_trello): - members = mock_trello.get_members() - json_loader.assert_equal([member.to_dict() for member in members], "members.json")