diff --git a/pype/clockify/__init__.py b/pype/clockify/__init__.py index 5f61acd751..063f88db73 100644 --- a/pype/clockify/__init__.py +++ b/pype/clockify/__init__.py @@ -7,3 +7,6 @@ __all__ = [ 'ClockifySettings', 'ClockifyModule' ] + +def tray_init(tray_widget, main_widget): + return ClockifyModule(main_widget, tray_widget) diff --git a/pype/clockify/clockify.py b/pype/clockify/clockify.py index 0b84bf3953..1476c3d488 100644 --- a/pype/clockify/clockify.py +++ b/pype/clockify/clockify.py @@ -1,3 +1,4 @@ +import os import threading from pypeapp import style from Qt import QtWidgets @@ -20,9 +21,10 @@ class ClockifyModule: self.bool_workspace_set = False self.bool_timer_run = False - def start_up(self): self.clockapi.set_master(self) self.bool_api_key_set = self.clockapi.set_api() + + def tray_start(self): if self.bool_api_key_set is False: self.show_settings() return @@ -41,7 +43,7 @@ class ClockifyModule: os.path.dirname(__file__), 'ftrack_actions' ]) - current = os.environ('FTRACK_ACTIONS_PATH', '') + current = os.environ.get('FTRACK_ACTIONS_PATH', '') if current: current += os.pathsep os.environ['FTRACK_ACTIONS_PATH'] = current + actions_path @@ -57,6 +59,24 @@ class ClockifyModule: current += os.pathsep os.environ['AVALON_ACTIONS'] = current + actions_path + if 'TimersManager' in modules: + self.timer_manager = modules['TimersManager'] + self.timer_manager.add_module(self) + + def start_timer_manager(self, data): + self.start_timer(data) + + def stop_timer_manager(self): + self.stop_timer() + + def timer_started(self, data): + if hasattr(self, 'timer_manager'): + self.timer_manager.start_timers(data) + + def timer_stopped(self): + if hasattr(self, 'timer_manager'): + self.timer_manager.stop_timers() + def start_timer_check(self): self.bool_thread_check_running = True if self.thread_timer_check is None: @@ -75,21 +95,81 @@ class ClockifyModule: def check_running(self): import time while self.bool_thread_check_running is True: + bool_timer_run = False if self.clockapi.get_in_progress() is not None: - self.bool_timer_run = True - else: - self.bool_timer_run = False - self.set_menu_visibility() + bool_timer_run = True + + if self.bool_timer_run != bool_timer_run: + if self.bool_timer_run is True: + self.timer_stopped() + else: + actual_timer = self.clockapi.get_in_progress() + if not actual_timer: + continue + + actual_project_id = actual_timer["projectId"] + project = self.clockapi.get_project_by_id( + actual_project_id + ) + project_name = project["name"] + + actual_timer_hierarchy = actual_timer["description"] + hierarchy_items = actual_timer_hierarchy.split("/") + task_name = hierarchy_items[-1] + hierarchy = hierarchy_items[:-1] + + data = { + "task_name": task_name, + "hierarchy": hierarchy, + "project_name": project_name + } + + self.timer_started(data) + + self.bool_timer_run = bool_timer_run + self.set_menu_visibility() time.sleep(5) def stop_timer(self): self.clockapi.finish_time_entry() + if self.bool_timer_run: + self.timer_stopped() self.bool_timer_run = False + def start_timer(self, input_data): + actual_timer = self.clockapi.get_in_progress() + actual_timer_hierarchy = None + actual_project_id = None + if actual_timer is not None: + actual_timer_hierarchy = actual_timer.get("description") + actual_project_id = actual_timer.get("projectId") + + desc_items = [val for val in input_data.get("hierarchy", [])] + desc_items.append(input_data["task_name"]) + description = "/".join(desc_items) + + project_id = self.clockapi.get_project_id(input_data["project_name"]) + + if ( + actual_timer is not None and + description == actual_timer_hierarchy and + project_id == actual_project_id + ): + return + + tag_ids = [] + task_tag_id = self.clockapi.get_tag_id(input_data["task_name"]) + if task_tag_id is not None: + tag_ids.append(task_tag_id) + + self.clockapi.start_time_entry( + description, project_id, tag_ids=tag_ids + ) + # Definition of Tray menu - def tray_menu(self, parent): + def tray_menu(self, parent_menu): # Menu for Tray App - self.menu = QtWidgets.QMenu('Clockify', parent) + self.menu = QtWidgets.QMenu('Clockify', parent_menu) self.menu.setProperty('submenu', 'on') self.menu.setStyleSheet(style.load_stylesheet()) @@ -109,7 +189,7 @@ class ClockifyModule: self.set_menu_visibility() - return self.menu + parent_menu.addMenu(self.menu) def show_settings(self): self.widget_settings.input_api_key.setText(self.clockapi.get_api_key()) diff --git a/pype/clockify/clockify_api.py b/pype/clockify/clockify_api.py index f5ebac0cef..ed932eedce 100644 --- a/pype/clockify/clockify_api.py +++ b/pype/clockify/clockify_api.py @@ -1,4 +1,5 @@ import os +import re import requests import json import datetime @@ -22,6 +23,7 @@ class ClockifyAPI(metaclass=Singleton): app_dir = os.path.normpath(appdirs.user_data_dir('pype-app', 'pype')) file_name = 'clockify.json' fpath = os.path.join(app_dir, file_name) + admin_permission_names = ['WORKSPACE_OWN', 'WORKSPACE_ADMIN'] master_parent = None workspace_id = None @@ -55,31 +57,41 @@ class ClockifyAPI(metaclass=Singleton): return False return True - def validate_workspace_perm(self): - test_project = '__test__' - action_url = 'workspaces/{}/projects/'.format(self.workspace_id) - body = { - "name": test_project, "clientId": "", "isPublic": "false", - "estimate": {"type": "AUTO"}, - "color": "#f44336", "billable": "true" - } - response = requests.post( - self.endpoint + action_url, - headers=self.headers, json=body + def validate_workspace_perm(self, workspace_id=None): + user_id = self.get_user_id() + if user_id is None: + return False + if workspace_id is None: + workspace_id = self.workspace_id + action_url = "/workspaces/{}/users/{}/permissions".format( + workspace_id, user_id ) - if response.status_code == 201: - self.delete_project(self.get_project_id(test_project)) - return True - else: - projects = self.get_projects() - if test_project in projects: - try: - self.delete_project(self.get_project_id(test_project)) - return True - except json.decoder.JSONDecodeError: - return False + response = requests.get( + self.endpoint + action_url, + headers=self.headers + ) + user_permissions = response.json() + for perm in user_permissions: + if perm['name'] in self.admin_permission_names: + return True return False + def get_user_id(self): + action_url = 'v1/user/' + response = requests.get( + self.endpoint + action_url, + headers=self.headers + ) + # this regex is neccessary: UNICODE strings are crashing + # during json serialization + id_regex ='\"{1}id\"{1}\:{1}\"{1}\w+\"{1}' + result = re.findall(id_regex, str(response.content)) + if len(result) != 1: + # replace with log and better message? + print('User ID was not found (this is a BUG!!!)') + return None + return json.loads('{'+result[0]+'}')['id'] + def set_workspace(self, name=None): if name is None: name = os.environ.get('CLOCKIFY_WORKSPACE', None) @@ -147,6 +159,19 @@ class ClockifyAPI(metaclass=Singleton): project["name"]: project["id"] for project in response.json() } + def get_project_by_id(self, project_id, workspace_id=None): + if workspace_id is None: + workspace_id = self.workspace_id + action_url = 'workspaces/{}/projects/{}/'.format( + workspace_id, project_id + ) + response = requests.get( + self.endpoint + action_url, + headers=self.headers + ) + + return response.json() + def get_tags(self, workspace_id=None): if workspace_id is None: workspace_id = self.workspace_id @@ -279,6 +304,9 @@ class ClockifyAPI(metaclass=Singleton): if workspace_id is None: workspace_id = self.workspace_id current = self.get_in_progress(workspace_id) + if current is None: + return + current_id = current["id"] action_url = 'workspaces/{}/timeEntries/{}'.format( workspace_id, current_id diff --git a/pype/clockify/ftrack_actions/action_clockify_start.py b/pype/clockify/ftrack_actions/action_clockify_start.py index e09d0b76e6..5b54476297 100644 --- a/pype/clockify/ftrack_actions/action_clockify_start.py +++ b/pype/clockify/ftrack_actions/action_clockify_start.py @@ -14,7 +14,7 @@ class StartClockify(BaseAction): #: Action identifier. identifier = 'clockify.start.timer' #: Action label. - label = 'Start timer' + label = 'Clockify - Start timer' #: Action description. description = 'Starts timer on clockify' #: roles that are allowed to register this action @@ -67,42 +67,3 @@ def register(session, **kw): return StartClockify(session).register() - - -def main(arguments=None): - '''Set up logging and register action.''' - if arguments is None: - arguments = [] - - parser = argparse.ArgumentParser() - # Allow setting of logging level from arguments. - loggingLevels = {} - for level in ( - logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING, - logging.ERROR, logging.CRITICAL - ): - loggingLevels[logging.getLevelName(level).lower()] = level - - parser.add_argument( - '-v', '--verbosity', - help='Set the logging output verbosity.', - choices=loggingLevels.keys(), - default='info' - ) - namespace = parser.parse_args(arguments) - - # Set up basic logging - logging.basicConfig(level=loggingLevels[namespace.verbosity]) - - session = ftrack_api.Session() - register(session) - - # Wait for events - logging.info( - 'Registered actions and listening for events. Use Ctrl-C to abort.' - ) - session.event_hub.wait() - - -if __name__ == '__main__': - raise SystemExit(main(sys.argv[1:])) diff --git a/pype/ftrack/actions/action_attributes_remapper.py b/pype/ftrack/actions/action_attributes_remapper.py index a098225125..db33fd1365 100644 --- a/pype/ftrack/actions/action_attributes_remapper.py +++ b/pype/ftrack/actions/action_attributes_remapper.py @@ -11,13 +11,14 @@ class AttributesRemapper(BaseAction): #: Action identifier. identifier = 'attributes.remapper' #: Action label. - label = 'Attributes Remapper' + label = "Pype Doctor" + variant = '- Attributes Remapper' #: Action description. description = 'Remaps attributes in avalon DB' #: roles that are allowed to register this action role_list = ["Pypeclub", "Administrator"] - icon = '{}/ftrack/action_icons/AttributesRemapper.svg'.format( + icon = '{}/ftrack/action_icons/PypeDoctor.svg'.format( os.environ.get('PYPE_STATICS_SERVER', '') ) diff --git a/pype/ftrack/actions/action_create_cust_attrs.py b/pype/ftrack/actions/action_create_cust_attrs.py index 6a68eb783e..47a6bb5d5f 100644 --- a/pype/ftrack/actions/action_create_cust_attrs.py +++ b/pype/ftrack/actions/action_create_cust_attrs.py @@ -110,12 +110,13 @@ class CustomAttributes(BaseAction): #: Action identifier. identifier = 'create.update.attributes' #: Action label. - label = 'Create/Update Avalon Attributes' + label = "Pype Admin" + variant = '- Create/Update Avalon Attributes' #: Action description. description = 'Creates Avalon/Mongo ID for double check' #: roles that are allowed to register this action role_list = ['Pypeclub', 'Administrator'] - icon = '{}/ftrack/action_icons/CustomAttributes.svg'.format( + icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format( os.environ.get('PYPE_STATICS_SERVER', '') ) diff --git a/pype/ftrack/actions/action_create_folders.py b/pype/ftrack/actions/action_create_folders.py index dc969ada02..b9e10f7c30 100644 --- a/pype/ftrack/actions/action_create_folders.py +++ b/pype/ftrack/actions/action_create_folders.py @@ -30,11 +30,13 @@ class CreateFolders(BaseAction): def discover(self, session, entities, event): ''' Validation ''' - not_allowed = ['assetversion'] if len(entities) != 1: return False + + not_allowed = ['assetversion', 'project'] if entities[0].entity_type.lower() in not_allowed: return False + return True def interface(self, session, entities, event): diff --git a/pype/ftrack/actions/action_create_project_folders.py b/pype/ftrack/actions/action_create_project_structure.py similarity index 97% rename from pype/ftrack/actions/action_create_project_folders.py rename to pype/ftrack/actions/action_create_project_structure.py index ef48df7d67..74d458b5f8 100644 --- a/pype/ftrack/actions/action_create_project_folders.py +++ b/pype/ftrack/actions/action_create_project_structure.py @@ -13,9 +13,9 @@ class CreateProjectFolders(BaseAction): '''Edit meta data action.''' #: Action identifier. - identifier = 'create.project.folders' + identifier = 'create.project.structure' #: Action label. - label = 'Create Project Folders' + label = 'Create Project Structure' #: Action description. description = 'Creates folder structure' #: roles that are allowed to register this action @@ -31,6 +31,11 @@ class CreateProjectFolders(BaseAction): def discover(self, session, entities, event): ''' Validation ''' + if len(entities) != 1: + return False + + if entities[0].entity_type.lower() != "project": + return False return True diff --git a/pype/ftrack/actions/action_cust_attr_doctor.py b/pype/ftrack/actions/action_cust_attr_doctor.py index a6ccb237a6..1b8f250e5b 100644 --- a/pype/ftrack/actions/action_cust_attr_doctor.py +++ b/pype/ftrack/actions/action_cust_attr_doctor.py @@ -12,14 +12,15 @@ class CustomAttributeDoctor(BaseAction): #: Action identifier. identifier = 'custom.attributes.doctor' #: Action label. - label = 'Custom Attributes Doctor' + label = "Pype Doctor" + variant = '- Custom Attributes Doctor' #: Action description. description = ( 'Fix hierarchical custom attributes mainly handles, fstart' ' and fend' ) - icon = '{}/ftrack/action_icons/TestAction.svg'.format( + icon = '{}/ftrack/action_icons/PypeDoctor.svg'.format( os.environ.get('PYPE_STATICS_SERVER', '') ) hierarchical_ca = ['handle_start', 'handle_end', 'fstart', 'fend'] diff --git a/pype/ftrack/actions/action_delete_asset_byname.py b/pype/ftrack/actions/action_delete_asset_byname.py index 1c05d7d88c..4f2a0e515c 100644 --- a/pype/ftrack/actions/action_delete_asset_byname.py +++ b/pype/ftrack/actions/action_delete_asset_byname.py @@ -13,12 +13,13 @@ class AssetsRemover(BaseAction): #: Action identifier. identifier = 'remove.assets' #: Action label. - label = 'Delete Assets by Name' + label = "Pype Admin" + variant = '- Delete Assets by Name' #: Action description. description = 'Removes assets from Ftrack and Avalon db with all childs' #: roles that are allowed to register this action role_list = ['Pypeclub', 'Administrator'] - icon = '{}/ftrack/action_icons/AssetsRemover.svg'.format( + icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format( os.environ.get('PYPE_STATICS_SERVER', '') ) #: Db diff --git a/pype/ftrack/actions/action_job_killer.py b/pype/ftrack/actions/action_job_killer.py index 8ac81c4ac2..8584b26aa4 100644 --- a/pype/ftrack/actions/action_job_killer.py +++ b/pype/ftrack/actions/action_job_killer.py @@ -14,12 +14,13 @@ class JobKiller(BaseAction): #: Action identifier. identifier = 'job.killer' #: Action label. - label = 'Job Killer' + label = "Pype Admin" + variant = '- Job Killer' #: Action description. description = 'Killing selected running jobs' #: roles that are allowed to register this action role_list = ['Pypeclub', 'Administrator'] - icon = '{}/ftrack/action_icons/JobKiller.svg'.format( + icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format( os.environ.get('PYPE_STATICS_SERVER', '') ) diff --git a/pype/ftrack/actions/action_prepare_project.py b/pype/ftrack/actions/action_prepare_project.py new file mode 100644 index 0000000000..60a7435907 --- /dev/null +++ b/pype/ftrack/actions/action_prepare_project.py @@ -0,0 +1,240 @@ +import os +import json + +from pype.vendor import ftrack_api +from pype.ftrack import BaseAction +from pypeapp import config +from pype.ftrack.lib import get_avalon_attr + + +class PrepareProject(BaseAction): + '''Edit meta data action.''' + + #: Action identifier. + identifier = 'prepare.project' + #: Action label. + label = 'Prepare Project' + #: Action description. + description = 'Set basic attributes on the project' + #: roles that are allowed to register this action + role_list = ["Pypeclub", "Administrator", "Project manager"] + icon = '{}/ftrack/action_icons/PrepareProject.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') + ) + + def discover(self, session, entities, event): + ''' Validation ''' + if len(entities) != 1: + return False + + if entities[0].entity_type.lower() != "project": + return False + + return True + + def interface(self, session, entities, event): + if event['data'].get('values', {}): + return + + # Inform user that this may take a while + self.show_message(event, "Preparing data... Please wait", True) + + self.log.debug("Loading custom attributes") + cust_attrs, hier_cust_attrs = get_avalon_attr(session, True) + project_defaults = config.get_presets().get("ftrack", {}).get( + "project_defaults", {} + ) + + self.log.debug("Preparing data which will be shown") + attributes_to_set = {} + for attr in hier_cust_attrs: + key = attr["key"] + attributes_to_set[key] = { + "label": attr["label"], + "object": attr, + "default": project_defaults.get(key) + } + + for attr in cust_attrs: + if attr["entity_type"].lower() != "show": + continue + key = attr["key"] + attributes_to_set[key] = { + "label": attr["label"], + "object": attr, + "default": project_defaults.get(key) + } + + # Sort by label + attributes_to_set = dict(sorted( + attributes_to_set.items(), + key=lambda x: x[1]["label"] + )) + self.log.debug("Preparing interface for keys: \"{}\"".format( + str([key for key in attributes_to_set]) + )) + + title = "Set Attribute values" + items = [] + multiselect_enumerators = [] + + # This item will be last (before enumerators) + # - sets value of auto synchronization + auto_sync_name = "avalon_auto_sync" + auto_sync_item = { + "name": auto_sync_name, + "type": "boolean", + "value": project_defaults.get(auto_sync_name, False), + "label": "AutoSync to Avalon" + } + + item_splitter = {'type': 'label', 'value': '---'} + + for key, in_data in attributes_to_set.items(): + attr = in_data["object"] + + # initial item definition + item = { + "name": key, + "label": in_data["label"] + } + + # cust attr type - may have different visualization + type_name = attr["type"]["name"].lower() + easy_types = ["text", "boolean", "date", "number"] + + easy_type = False + if type_name in easy_types: + easy_type = True + + elif type_name == "enumerator": + + attr_config = json.loads(attr["config"]) + attr_config_data = json.loads(attr_config["data"]) + + if attr_config["multiSelect"] is True: + multiselect_enumerators.append(item_splitter) + + multiselect_enumerators.append({ + "type": "label", + "value": in_data["label"] + }) + + default = in_data["default"] + names = [] + for option in sorted( + attr_config_data, key=lambda x: x["menu"] + ): + name = option["value"] + new_name = "__{}__{}".format(key, name) + names.append(new_name) + item = { + "name": new_name, + "type": "boolean", + "label": "- {}".format(option["menu"]) + } + if default: + if ( + isinstance(default, list) or + isinstance(default, tuple) + ): + if name in default: + item["value"] = True + else: + if name == default: + item["value"] = True + + multiselect_enumerators.append(item) + + multiselect_enumerators.append({ + "type": "hidden", + "name": "__hidden__{}".format(key), + "value": json.dumps(names) + }) + else: + easy_type = True + item["data"] = attr_config_data + + else: + self.log.warning(( + "Custom attribute \"{}\" has type \"{}\"." + " I don't know how to handle" + ).format(key, type_name)) + items.append({ + "type": "label", + "value": ( + "!!! Can't handle Custom attritubte type \"{}\"" + " (key: \"{}\")" + ).format(type_name, key) + }) + + if easy_type: + item["type"] = type_name + + # default value in interface + default = in_data["default"] + if default is not None: + item["value"] = default + + items.append(item) + + # Add autosync attribute + items.append(auto_sync_item) + + # Add enumerator items at the end + for item in multiselect_enumerators: + items.append(item) + + return { + 'items': items, + 'title': title + } + + def launch(self, session, entities, event): + if not event['data'].get('values', {}): + return + + in_data = event['data']['values'] + # Find hidden items for multiselect enumerators + keys_to_process = [] + for key in in_data: + if key.startswith("__hidden__"): + keys_to_process.append(key) + + self.log.debug("Preparing data for Multiselect Enumerators") + enumerators = {} + for key in keys_to_process: + new_key = key.replace("__hidden__", "") + enumerator_items = in_data.pop(key) + enumerators[new_key] = json.loads(enumerator_items) + + # find values set for multiselect enumerator + for key, enumerator_items in enumerators.items(): + in_data[key] = [] + + name = "__{}__".format(key) + + for item in enumerator_items: + value = in_data.pop(item) + if value is True: + new_key = item.replace(name, "") + in_data[key].append(new_key) + + self.log.debug("Setting Custom Attribute values:") + entity = entities[0] + for key, value in in_data.items(): + entity["custom_attributes"][key] = value + self.log.debug("- Key \"{}\" set to \"{}\"".format(key, value)) + + session.commit() + + return True + + +def register(session, plugins_presets={}): + '''Register plugin. Called when used as an plugin.''' + + if not isinstance(session, ftrack_api.session.Session): + return + + PrepareProject(session, plugins_presets).register() diff --git a/pype/ftrack/actions/action_sync_hier_attrs_local.py b/pype/ftrack/actions/action_sync_hier_attrs_local.py index 31d95f9a6f..01434470f3 100644 --- a/pype/ftrack/actions/action_sync_hier_attrs_local.py +++ b/pype/ftrack/actions/action_sync_hier_attrs_local.py @@ -19,11 +19,12 @@ class SyncHierarchicalAttrs(BaseAction): #: Action identifier. identifier = 'sync.hierarchical.attrs.local' #: Action label. - label = 'Sync HierAttrs - Local' + label = "Pype Admin" + variant = '- Sync Hier Attrs (Local)' #: Action description. description = 'Synchronize hierarchical attributes' #: Icon - icon = '{}/ftrack/action_icons/SyncHierarchicalAttrsLocal.svg'.format( + icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format( os.environ.get('PYPE_STATICS_SERVER', '') ) diff --git a/pype/ftrack/actions/action_sync_to_avalon_local.py b/pype/ftrack/actions/action_sync_to_avalon_local.py index 6e603b33b3..ad39b0ca12 100644 --- a/pype/ftrack/actions/action_sync_to_avalon_local.py +++ b/pype/ftrack/actions/action_sync_to_avalon_local.py @@ -47,11 +47,12 @@ class SyncToAvalon(BaseAction): #: Action identifier. identifier = 'sync.to.avalon.local' #: Action label. - label = 'SyncToAvalon - Local' + label = "Pype Admin" + variant = '- Sync To Avalon (Local)' #: Action description. description = 'Send data from Ftrack to Avalon' #: Action icon. - icon = '{}/ftrack/action_icons/SyncToAvalon-local.svg'.format( + icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format( os.environ.get('PYPE_STATICS_SERVER', '') ) #: roles that are allowed to register this action @@ -59,7 +60,7 @@ class SyncToAvalon(BaseAction): #: Action priority priority = 200 - def __init__(self, session): + def __init__(self, session, plugins_presets): super(SyncToAvalon, self).__init__(session) # reload utils on initialize (in case of server restart) diff --git a/pype/ftrack/actions/action_thumbToChildern.py b/pype/ftrack/actions/action_thumbnail_to_childern.py similarity index 95% rename from pype/ftrack/actions/action_thumbToChildern.py rename to pype/ftrack/actions/action_thumbnail_to_childern.py index 99ca713662..101b678512 100644 --- a/pype/ftrack/actions/action_thumbToChildern.py +++ b/pype/ftrack/actions/action_thumbnail_to_childern.py @@ -14,9 +14,11 @@ class ThumbToChildren(BaseAction): # Action identifier identifier = 'thumb.to.children' # Action label - label = 'Thumbnail to Children' + label = 'Thumbnail' + # Action variant + variant = " to Children" # Action icon - icon = '{}/ftrack/action_icons/thumbToChildren.svg'.format( + icon = '{}/ftrack/action_icons/Thumbnail.svg'.format( os.environ.get('PYPE_STATICS_SERVER', '') ) diff --git a/pype/ftrack/actions/action_thumbToParent.py b/pype/ftrack/actions/action_thumbnail_to_parent.py similarity index 96% rename from pype/ftrack/actions/action_thumbToParent.py rename to pype/ftrack/actions/action_thumbnail_to_parent.py index 9de9cd8cc4..c382d9303c 100644 --- a/pype/ftrack/actions/action_thumbToParent.py +++ b/pype/ftrack/actions/action_thumbnail_to_parent.py @@ -13,9 +13,11 @@ class ThumbToParent(BaseAction): # Action identifier identifier = 'thumb.to.parent' # Action label - label = 'Thumbnail to Parent' + label = 'Thumbnail' + # Action variant + variant = " to Parent" # Action icon - icon = '{}/ftrack/action_icons/thumbToParent.svg'.format( + icon = '{}/ftrack/action_icons/Thumbnail.svg'.format( os.environ.get('PYPE_STATICS_SERVER', '') ) diff --git a/pype/ftrack/events/action_sync_hier_attrs.py b/pype/ftrack/events/action_sync_hier_attrs.py index cd7446f5e4..22ad7bf5aa 100644 --- a/pype/ftrack/events/action_sync_hier_attrs.py +++ b/pype/ftrack/events/action_sync_hier_attrs.py @@ -20,11 +20,12 @@ class SyncHierarchicalAttrs(BaseAction): #: Action identifier. identifier = 'sync.hierarchical.attrs' #: Action label. - label = 'Sync HierAttrs' + label = "Pype Admin" + variant = '- Sync Hier Attrs (server)' #: Action description. description = 'Synchronize hierarchical attributes' #: Icon - icon = '{}/ftrack/action_icons/SyncHierarchicalAttrs.svg'.format( + icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format( os.environ.get( 'PYPE_STATICS_SERVER', 'http://localhost:{}'.format( diff --git a/pype/ftrack/events/action_sync_to_avalon.py b/pype/ftrack/events/action_sync_to_avalon.py index fc03180bdc..5628554c85 100644 --- a/pype/ftrack/events/action_sync_to_avalon.py +++ b/pype/ftrack/events/action_sync_to_avalon.py @@ -48,11 +48,12 @@ class Sync_To_Avalon(BaseAction): #: Action identifier. identifier = 'sync.to.avalon' #: Action label. - label = 'SyncToAvalon' + label = "Pype Admin" + variant = "- Sync To Avalon (Server)" #: Action description. description = 'Send data from Ftrack to Avalon' #: Action icon. - icon = '{}/ftrack/action_icons/SyncToAvalon.svg'.format( + icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format( os.environ.get( 'PYPE_STATICS_SERVER', 'http://localhost:{}'.format( diff --git a/pype/ftrack/ftrack_server/ftrack_server.py b/pype/ftrack/ftrack_server/ftrack_server.py index 69637ac3ee..2a58c12d09 100644 --- a/pype/ftrack/ftrack_server/ftrack_server.py +++ b/pype/ftrack/ftrack_server/ftrack_server.py @@ -29,8 +29,8 @@ PYTHONPATH # Path to ftrack_api and paths to all modules used in actions """ -class FtrackServer(): - def __init__(self, type='action'): +class FtrackServer: + def __init__(self, server_type='action'): """ - 'type' is by default set to 'action' - Runs Action server - enter 'event' for Event server @@ -45,21 +45,12 @@ class FtrackServer(): ftrack_log = logging.getLogger("ftrack_api") ftrack_log.setLevel(logging.WARNING) - self.type = type - self.actionsAvailable = True - self.eventsAvailable = True - # Separate all paths - if "FTRACK_ACTIONS_PATH" in os.environ: - all_action_paths = os.environ["FTRACK_ACTIONS_PATH"] - self.actionsPaths = all_action_paths.split(os.pathsep) - else: - self.actionsAvailable = False + env_key = "FTRACK_ACTIONS_PATH" + if server_type.lower() == 'event': + env_key = "FTRACK_EVENTS_PATH" - if "FTRACK_EVENTS_PATH" in os.environ: - all_event_paths = os.environ["FTRACK_EVENTS_PATH"] - self.eventsPaths = all_event_paths.split(os.pathsep) - else: - self.eventsAvailable = False + self.server_type = server_type + self.env_key = env_key def stop_session(self): if self.session.event_hub.connected is True: @@ -94,9 +85,7 @@ class FtrackServer(): # separate files by register function if 'register' not in mod_functions: - msg = ( - '"{0}" - Missing register method' - ).format(file, self.type) + msg = ('"{}" - Missing register method').format(file) log.warning(msg) continue @@ -115,7 +104,7 @@ class FtrackServer(): # Load presets for setting plugins key = "user" - if self.type.lower() == "event": + if self.server_type.lower() == "event": key = "server" plugins_presets = config.get_presets().get( "ftrack", {} @@ -142,24 +131,15 @@ class FtrackServer(): def run_server(self): self.session = ftrack_api.Session(auto_connect_event_hub=True,) - if self.type.lower() == 'event': - if self.eventsAvailable is False: - msg = ( - 'FTRACK_EVENTS_PATH is not set' - ', event server won\'t launch' - ) - log.error(msg) - return - self.set_files(self.eventsPaths) - else: - if self.actionsAvailable is False: - msg = ( - 'FTRACK_ACTIONS_PATH is not set' - ', action server won\'t launch' - ) - log.error(msg) - return - self.set_files(self.actionsPaths) + paths_str = os.environ.get(self.env_key) + if paths_str is None: + log.error(( + "Env var \"{}\" is not set, \"{}\" server won\'t launch" + ).format(self.env_key, self.server_type)) + return + + paths = paths_str.split(os.pathsep) + self.set_files(paths) log.info(60*"*") log.info('Registration of actions/events has finished!') diff --git a/pype/ftrack/lib/avalon_sync.py b/pype/ftrack/lib/avalon_sync.py index 962dc1165c..169bc4b051 100644 --- a/pype/ftrack/lib/avalon_sync.py +++ b/pype/ftrack/lib/avalon_sync.py @@ -326,13 +326,26 @@ def import_to_avalon( return output -def get_avalon_attr(session): +def get_avalon_attr(session, split_hierarchical=False): custom_attributes = [] + hier_custom_attributes = [] query = 'CustomAttributeGroup where name is "avalon"' all_avalon_attr = session.query(query).one() for cust_attr in all_avalon_attr['custom_attribute_configurations']: - if 'avalon_' not in cust_attr['key']: - custom_attributes.append(cust_attr) + if 'avalon_' in cust_attr['key']: + continue + + if split_hierarchical: + if cust_attr["is_hierarchical"]: + hier_custom_attributes.append(cust_attr) + continue + + custom_attributes.append(cust_attr) + + if split_hierarchical: + # return tuple + return custom_attributes, hier_custom_attributes + return custom_attributes diff --git a/pype/lib.py b/pype/lib.py index 18adfa355d..c926601a14 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -467,10 +467,18 @@ def filter_pyblish_plugins(plugins): host = api.current_host() + presets = config.get_presets().get('plugins', {}).get(host, {}).get( + "publish", {} + ) + # iterate over plugins for plugin in plugins[:]: + # skip if there are no presets to process + if not presets: + continue + try: - config_data = config.get_presets()['plugins'][host]["publish"][plugin.__name__] # noqa: E501 + config_data = presets[plugin.__name__] # noqa: E501 except KeyError: continue diff --git a/pype/plugins/nukestudio/publish/collect_tag_framestart.py b/pype/plugins/nukestudio/publish/collect_tag_framestart.py index c2778ea680..c73a2dd1ee 100644 --- a/pype/plugins/nukestudio/publish/collect_tag_framestart.py +++ b/pype/plugins/nukestudio/publish/collect_tag_framestart.py @@ -1,5 +1,5 @@ from pyblish import api - +import os class CollectClipTagFrameStart(api.InstancePlugin): """Collect FrameStart from Tags of selected track items.""" @@ -19,8 +19,20 @@ class CollectClipTagFrameStart(api.InstancePlugin): # gets only task family tags and collect labels if "frameStart" in t_family: + t_value = t_metadata.get("tag.value", "") + + # backward compatibility t_number = t_metadata.get("tag.number", "") - start_frame = int(t_number) + + try: + start_frame = int(t_number) or int(t_value) + except ValueError: + if "source" in t_value: + source_first = instance.data["sourceFirst"] + source_in = instance.data["sourceIn"] + handle_start = instance.data["handleStart"] + start_frame = (source_first + source_in) - handle_start + instance.data["startingFrame"] = start_frame self.log.info("Start frame on `{0}` set to `{1}`".format( instance, start_frame diff --git a/res/ftrack/action_icons/AttributesRemapper.svg b/res/ftrack/action_icons/AttributesRemapper.svg deleted file mode 100644 index 94bf8c4f14..0000000000 --- a/res/ftrack/action_icons/AttributesRemapper.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/res/ftrack/action_icons/CustomAttributes.svg b/res/ftrack/action_icons/CustomAttributes.svg deleted file mode 100644 index ee1af3378e..0000000000 --- a/res/ftrack/action_icons/CustomAttributes.svg +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/res/ftrack/action_icons/JobKiller.svg b/res/ftrack/action_icons/JobKiller.svg deleted file mode 100644 index 595c780a9b..0000000000 --- a/res/ftrack/action_icons/JobKiller.svg +++ /dev/null @@ -1,374 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/res/ftrack/action_icons/PrepareProject.svg b/res/ftrack/action_icons/PrepareProject.svg new file mode 100644 index 0000000000..bd6b460ce3 --- /dev/null +++ b/res/ftrack/action_icons/PrepareProject.svg @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/ftrack/action_icons/PypeAdmin.svg b/res/ftrack/action_icons/PypeAdmin.svg new file mode 100644 index 0000000000..c95a29dacb --- /dev/null +++ b/res/ftrack/action_icons/PypeAdmin.svg @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + + + + + diff --git a/res/ftrack/action_icons/PypeDoctor.svg b/res/ftrack/action_icons/PypeDoctor.svg new file mode 100644 index 0000000000..e921d99ee5 --- /dev/null +++ b/res/ftrack/action_icons/PypeDoctor.svg @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + diff --git a/res/ftrack/action_icons/SyncHierarchicalAttrsLocal.svg b/res/ftrack/action_icons/SyncHierarchicalAttrsLocal.svg deleted file mode 100644 index f58448ac06..0000000000 --- a/res/ftrack/action_icons/SyncHierarchicalAttrsLocal.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/res/ftrack/action_icons/SyncToAvalon-local.svg b/res/ftrack/action_icons/SyncToAvalon-local.svg deleted file mode 100644 index bf4708e8a5..0000000000 --- a/res/ftrack/action_icons/SyncToAvalon-local.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/res/ftrack/action_icons/Thumbnail.svg b/res/ftrack/action_icons/Thumbnail.svg new file mode 100644 index 0000000000..a8780b9a04 --- /dev/null +++ b/res/ftrack/action_icons/Thumbnail.svg @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/ftrack/action_icons/thumbToChildren.svg b/res/ftrack/action_icons/thumbToChildren.svg deleted file mode 100644 index 709b9549f3..0000000000 --- a/res/ftrack/action_icons/thumbToChildren.svg +++ /dev/null @@ -1,113 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/res/ftrack/action_icons/thumbToParent.svg b/res/ftrack/action_icons/thumbToParent.svg deleted file mode 100644 index 3efa426332..0000000000 --- a/res/ftrack/action_icons/thumbToParent.svg +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/setup/nukestudio/hiero_plugin_path/Icons/retiming.png b/setup/nukestudio/hiero_plugin_path/Icons/retiming.png new file mode 100644 index 0000000000..4487ac0422 Binary files /dev/null and b/setup/nukestudio/hiero_plugin_path/Icons/retiming.png differ diff --git a/setup/nukestudio/hiero_plugin_path/Icons/retiming.psd b/setup/nukestudio/hiero_plugin_path/Icons/retiming.psd new file mode 100644 index 0000000000..bac6fc6b58 Binary files /dev/null and b/setup/nukestudio/hiero_plugin_path/Icons/retiming.psd differ