From 34156e280bf7ebbd565e9754ae9f71fdf463d79f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 9 Jun 2022 18:17:38 +0200 Subject: [PATCH 1/4] create new action to create daily review sessions --- .../action_create_review_session.py | 286 ++++++++++++++++++ 1 file changed, 286 insertions(+) create mode 100644 openpype/modules/ftrack/event_handlers_server/action_create_review_session.py diff --git a/openpype/modules/ftrack/event_handlers_server/action_create_review_session.py b/openpype/modules/ftrack/event_handlers_server/action_create_review_session.py new file mode 100644 index 0000000000..3b7cb224f0 --- /dev/null +++ b/openpype/modules/ftrack/event_handlers_server/action_create_review_session.py @@ -0,0 +1,286 @@ +import threading +import datetime +import copy +import collections + +import ftrack_api + +from openpype.lib import get_datetime_data +from openpype.api import get_project_settings +from openpype_modules.ftrack.lib import ServerAction + + +class CreateDailyReviewSessionServerAction(ServerAction): + """Create daily review session object per project. + + Action creates review sessions based on settings. Settings define if is + action enabled and what is a template for review session name. Logic works + in a way that if review session with the name already exists then skip + process. If review session for current day does not exist but yesterdays + review exists and is empty then yesterdays is renamed otherwise creates + new review session. + + Also contains cycle creation of dailies which is triggered each morning. + This option must be enabled in project settings. Cycle creation is also + checked on registration of action. + """ + + identifier = "create.daily.review.session" + #: Action label. + label = "OpenPype Admin" + variant = "- Create Daily Review Session (Server)" + #: Action description. + description = "Manually create daily review session" + role_list = {"Pypeclub", "Administrator", "Project Manager"} + + settings_key = "create_daily_review_session" + default_template = "{yy}{mm}{dd}" + + def __init__(self, *args, **kwargs): + super(CreateDailyReviewSessionServerAction, self).__init__(*args, **kwargs) + + self._cycle_timer = None + self._last_cyle_time = None + self._day_delta = datetime.timedelta(days=1) + + def discover(self, session, entities, event): + """Show action only on AssetVersions.""" + + valid_selection = False + for ent in event["data"]["selection"]: + # Ignore entities that are not tasks or projects + if ent["entityType"].lower() in ( + "show", "task", "reviewsession", "assetversion" + ): + valid_selection = True + break + else: + self.log.info(ent["entityType"]) + + if not valid_selection: + return False + return self.valid_roles(session, entities, event) + + def launch(self, session, entities, event): + project_entity = self.get_project_from_entity(entities[0], session) + project_name = project_entity["full_name"] + project_settings = self.get_project_settings_from_event( + event, project_name + ) + action_settings = self._extract_action_settings(project_settings) + project_name_by_id = { + project_entity["id"]: project_name + } + settings_by_project_id = { + project_entity["id"]: action_settings + } + self._process_review_session( + session, settings_by_project_id, project_name_by_id + ) + return True + + def register(self, *args, **kwargs): + """Override register to be able trigger """ + # Register server action as would be normally + super(CreateDailyReviewSessionServerAction, self).register(*args, **kwargs) + + # Create threading timer which will trigger creation of report + # at the 00:00:01 of next day + # - callback will trigger another timer which will have 1 day offset + now = datetime.datetime.now() + # Create object of today morning + today_morning = datetime.datetime( + now.year, now.month, now.day, 0, 0, 1 + ) + # Add a day delta (to calculate next day date) + next_day_morning = today_morning + self._day_delta + # Calculate first delta in seconds for first threading timer + first_delta = (next_day_morning - now).total_seconds() + # Store cycle time which will be used to create next timer + self._last_cyle_time = next_day_morning + # Create timer thread + self._cycle_timer = threading.Timer(first_delta, self._timer_callback) + self._cycle_timer.start() + + self._check_review_session() + + def _timer_callback(self): + if ( + self._cycle_timer is not None + and self._last_cyle_time is not None + ): + now = datetime.datetime.now() + while self._last_cyle_time < now: + self._last_cyle_time = self._last_cyle_time + self._day_delta + + delay = (self._last_cyle_time - now).total_seconds() + + self._cycle_timer = threading.Timer(delay, self._timer_callback) + self._cycle_timer.start() + self._check_review_session() + + def _check_review_session(self): + session = ftrack_api.Session( + server_url=self.session.server_url, + api_key=self.session.api_key, + api_user=self.session.api_user, + auto_connect_event_hub=False + ) + project_entities = session.query( + "select id, full_name from Project" + ).all() + project_names_by_id = { + project_entity["id"]: project_entity["full_name"] + for project_entity in project_entities + } + + action_settings_by_project_id = self._get_action_settings( + project_names_by_id + ) + enabled_action_settings_by_project_id = {} + for item in action_settings_by_project_id.items(): + project_id, action_settings = item + if action_settings.get("cycle_enabled"): + enabled_action_settings_by_project_id[project_id] = ( + action_settings + ) + + if not enabled_action_settings_by_project_id: + self.log.info(( + "There are no projects that have enabled" + " cycle review sesison creation" + )) + + else: + self._process_review_session( + session, + enabled_action_settings_by_project_id, + project_names_by_id + ) + + session.close() + + def _process_review_session( + self, session, settings_by_project_id, project_names_by_id + ): + review_sessions = session.query(( + "select id, name, project_id" + " from ReviewSession where project_id in ({})" + ).format(self.join_query_keys(settings_by_project_id))).all() + + review_sessions_by_project_id = collections.defaultdict(list) + for review_session in review_sessions: + project_id = review_session["project_id"] + review_sessions_by_project_id[project_id].append(review_session) + + # Prepare fill data for today's review sesison and yesterdays + now = datetime.datetime.now() + today_obj = datetime.datetime( + now.year, now.month, now.day, 0, 0, 0 + ) + yesterday_obj = today_obj - self._day_delta + + today_fill_data = get_datetime_data(today_obj) + yesterday_fill_data = get_datetime_data(yesterday_obj) + + # Loop through projects and try to create daily reviews + for project_id, action_settings in settings_by_project_id.items(): + review_session_template = ( + action_settings["review_session_template"] + ).strip() or self.default_template + + today_project_fill_data = copy.deepcopy(today_fill_data) + yesterday_project_fill_data = copy.deepcopy(yesterday_fill_data) + project_name = project_names_by_id[project_id] + today_project_fill_data["project_name"] = project_name + yesterday_project_fill_data["project_name"] = project_name + + today_session_name = self._fill_review_template( + review_session_template, today_project_fill_data + ) + yesterday_session_name = self._fill_review_template( + review_session_template, yesterday_project_fill_data + ) + # Skip if today's session name could not be filled + if not today_session_name: + continue + + # Find matchin review session + project_review_sessions = review_sessions_by_project_id[project_id] + todays_session = None + yesterdays_session = None + for review_session in project_review_sessions: + session_name = review_session["name"] + if session_name == today_session_name: + todays_session = review_session + break + elif session_name == yesterday_session_name: + yesterdays_session = review_session + + # Skip if today's session already exist + if todays_session is not None: + self.log.debug(( + "Todays ReviewSession \"{}\"" + " in project \"{}\" already exists" + ).format(today_session_name, project_name)) + continue + + # Check if there is yesterday's session and is empty + # - in that case just rename it + if ( + yesterdays_session is not None + and len(yesterdays_session["review_session_objects"]) == 0 + ): + self.log.debug(( + "Renaming yesterdays empty review session \"{}\" to \"{}\"" + " in project \"{}\"" + ).format( + yesterday_session_name, today_session_name, project_name + )) + yesterdays_session["name"] = today_session_name + session.commit() + continue + + # Create new review session with new name + self.log.debug(( + "Creating new review session \"{}\" in project \"{}\"" + ).format(today_session_name, project_name)) + session.create("ReviewSession", { + "project_id": project_id, + "name": today_session_name + }) + session.commit() + + def _get_action_settings(self, project_names_by_id): + settings_by_project_id = {} + for project_id, project_name in project_names_by_id.items(): + project_settings = get_project_settings(project_name) + action_settings = self._extract_action_settings(project_settings) + settings_by_project_id[project_id] = action_settings + return settings_by_project_id + + def _extract_action_settings(self, project_settings): + return ( + project_settings + .get("ftrack", {}) + .get(self.settings_frack_subkey, {}) + .get(self.settings_key) + ) or {} + + def _fill_review_template(self, template, data): + output = None + try: + output = template.format(**data) + except Exception: + self.log.warning( + ( + "Failed to fill review session template {} with data {}" + ).format(template, data), + exc_info=True + ) + return output + + +def register(session): + '''Register plugin. Called when used as an plugin.''' + CreateDailyReviewSessionServerAction(session).register() From 08d1f97f1112e7efc0190f146139c366d8f84de8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 9 Jun 2022 18:17:54 +0200 Subject: [PATCH 2/4] added settings for new action --- .../defaults/project_settings/ftrack.json | 9 +++++ .../schema_project_ftrack.json | 38 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index 9d59deea3d..831c34835e 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -116,6 +116,15 @@ "Administrator", "Project manager" ] + }, + "create_daily_review_session": { + "enabled": true, + "role_list": [ + "Administrator", + "Project Manager" + ], + "cycle_enabled": false, + "review_session_template": "{yy}{mm}{dd}" } }, "user_handlers": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json index 16cab49d5d..f8f9d5093d 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -388,6 +388,44 @@ "object_type": "text" } ] + }, + { + "key": "create_daily_review_session", + "label": "Create daily review session", + "type": "dict", + "is_group": true, + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled" + }, + { + "type": "list", + "key": "role_list", + "label": "Roles", + "object_type": "text", + "use_label_wrap": true + }, + { + "type": "boolean", + "key": "cycle_enabled", + "label": "Create daily review session" + }, + { + "type": "separator" + }, + { + "type": "text", + "key": "review_session_template", + "label": "ReviewSession template", + "placeholder": "Default: {yy}{mm}{dd}" + }, + { + "type": "label", + "label": "Possible formatting keys in template:
- \"project_name\" - <Name of project>
- \"d\" - <Day of month number> in shortest possible way.
- \"dd\" - <Day of month number> with 2 digits.
- \"ddd\" - <Week day name> shortened week day. e.g.: `Mon`, ...
- \"dddd\" - <Week day name> full name of week day. e.g.: `Monday`, ...
- \"m\" - <Month number> in shortest possible way. e.g.: `1` if January
- \"mm\" - <Month number> with 2 digits.
- \"mmm\" - <Month name> shortened month name. e.g.: `Jan`, ...
- \"mmmm\" -<Month name> full month name. e.g.: `January`, ...
- \"yy\" - <Year number> shortened year. e.g.: `19`, `20`, ...
- \"yyyy\" - <Year number> full year. e.g.: `2019`, `2020`, ..." + } + ] } ] }, From 5999693fdf748025aa79cf9cf232d77406c4c9a6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 9 Jun 2022 18:27:00 +0200 Subject: [PATCH 3/4] fix too long lines --- .../event_handlers_server/action_create_review_session.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_create_review_session.py b/openpype/modules/ftrack/event_handlers_server/action_create_review_session.py index 3b7cb224f0..8a1d898193 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_create_review_session.py +++ b/openpype/modules/ftrack/event_handlers_server/action_create_review_session.py @@ -37,7 +37,9 @@ class CreateDailyReviewSessionServerAction(ServerAction): default_template = "{yy}{mm}{dd}" def __init__(self, *args, **kwargs): - super(CreateDailyReviewSessionServerAction, self).__init__(*args, **kwargs) + super(CreateDailyReviewSessionServerAction, self).__init__( + *args, **kwargs + ) self._cycle_timer = None self._last_cyle_time = None @@ -82,7 +84,9 @@ class CreateDailyReviewSessionServerAction(ServerAction): def register(self, *args, **kwargs): """Override register to be able trigger """ # Register server action as would be normally - super(CreateDailyReviewSessionServerAction, self).register(*args, **kwargs) + super(CreateDailyReviewSessionServerAction, self).register( + *args, **kwargs + ) # Create threading timer which will trigger creation of report # at the 00:00:01 of next day From b227ba58568a3521ead74da5f2135b6e7645b132 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 10 Jun 2022 14:54:48 +0200 Subject: [PATCH 4/4] removed debug log --- .../event_handlers_server/action_create_review_session.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_create_review_session.py b/openpype/modules/ftrack/event_handlers_server/action_create_review_session.py index 8a1d898193..8a8e86e7b9 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_create_review_session.py +++ b/openpype/modules/ftrack/event_handlers_server/action_create_review_session.py @@ -56,8 +56,6 @@ class CreateDailyReviewSessionServerAction(ServerAction): ): valid_selection = True break - else: - self.log.info(ent["entityType"]) if not valid_selection: return False