diff --git a/pype/hosts/maya/expected_files.py b/pype/hosts/maya/expected_files.py index 52c8893e4b..d39e5fa204 100644 --- a/pype/hosts/maya/expected_files.py +++ b/pype/hosts/maya/expected_files.py @@ -32,6 +32,9 @@ Attributes: ImagePrefixes (dict): Mapping between renderers and their respective image prefix atrribute names. +Todo: + Determine `multipart` from render instance. + """ import types @@ -94,6 +97,10 @@ class ExpectedFiles: multipart = False + def __init__(self, render_instance): + """Constructor.""" + self._render_instance = render_instance + def get(self, renderer, layer): """Get expected files for given renderer and render layer. @@ -114,15 +121,20 @@ class ExpectedFiles: renderSetup.instance().switchToLayerUsingLegacyName(layer) if renderer.lower() == "arnold": - return self._get_files(ExpectedFilesArnold(layer)) + return self._get_files(ExpectedFilesArnold(layer, + self._render_instance)) elif renderer.lower() == "vray": - return self._get_files(ExpectedFilesVray(layer)) + return self._get_files(ExpectedFilesVray( + layer, self._render_instance)) elif renderer.lower() == "redshift": - return self._get_files(ExpectedFilesRedshift(layer)) + return self._get_files(ExpectedFilesRedshift( + layer, self._render_instance)) elif renderer.lower() == "mentalray": - return self._get_files(ExpectedFilesMentalray(layer)) + return self._get_files(ExpectedFilesMentalray( + layer, self._render_instance)) elif renderer.lower() == "renderman": - return self._get_files(ExpectedFilesRenderman(layer)) + return self._get_files(ExpectedFilesRenderman( + layer, self._render_instance)) else: raise UnsupportedRendererException( "unsupported {}".format(renderer) @@ -149,9 +161,10 @@ class AExpectedFiles: layer = None multipart = False - def __init__(self, layer): + def __init__(self, layer, render_instance): """Constructor.""" self.layer = layer + self.render_instance = render_instance @abstractmethod def get_aovs(self): @@ -460,9 +473,9 @@ class ExpectedFilesArnold(AExpectedFiles): "maya": "", } - def __init__(self, layer): + def __init__(self, layer, render_instance): """Constructor.""" - super(ExpectedFilesArnold, self).__init__(layer) + super(ExpectedFilesArnold, self).__init__(layer, render_instance) self.renderer = "arnold" def get_aovs(self): @@ -531,9 +544,9 @@ class ExpectedFilesArnold(AExpectedFiles): class ExpectedFilesVray(AExpectedFiles): """Expected files for V-Ray renderer.""" - def __init__(self, layer): + def __init__(self, layer, render_instance): """Constructor.""" - super(ExpectedFilesVray, self).__init__(layer) + super(ExpectedFilesVray, self).__init__(layer, render_instance) self.renderer = "vray" def get_renderer_prefix(self): @@ -614,24 +627,25 @@ class ExpectedFilesVray(AExpectedFiles): if default_ext == "exr (multichannel)" or default_ext == "exr (deep)": default_ext = "exr" + # add beauty as default enabled_aovs.append( (u"beauty", default_ext) ) - if not self.maya_is_true( - cmds.getAttr("vraySettings.relements_enableall") - ): - return enabled_aovs + # handle aovs from references + use_ref_aovs = self.render_instance.data.get( + "vrayUseReferencedAovs", False) or False - # filter all namespace prefixed AOVs - they are pulled in from - # references and are not rendered. - vr_aovs = [ - n - for n in cmds.ls( - type=["VRayRenderElement", "VRayRenderElementSet"] - ) - if len(n.split(":")) == 1 - ] + # this will have list of all aovs no matter if they are coming from + # reference or not. + vr_aovs = cmds.ls( + type=["VRayRenderElement", "VRayRenderElementSet"]) or [] + if not use_ref_aovs: + ref_aovs = cmds.ls( + type=["VRayRenderElement", "VRayRenderElementSet"], + referencedNodes=True) or [] + # get difference + vr_aovs = list(set(vr_aovs) - set(ref_aovs)) for aov in vr_aovs: enabled = self.maya_is_true(cmds.getAttr("{}.enabled".format(aov))) @@ -703,9 +717,9 @@ class ExpectedFilesRedshift(AExpectedFiles): ext_mapping = ["iff", "exr", "tif", "png", "tga", "jpg"] - def __init__(self, layer): + def __init__(self, layer, render_instance): """Construtor.""" - super(ExpectedFilesRedshift, self).__init__(layer) + super(ExpectedFilesRedshift, self).__init__(layer, render_instance) self.renderer = "redshift" def get_renderer_prefix(self): @@ -822,9 +836,9 @@ class ExpectedFilesRenderman(AExpectedFiles): This is very rudimentary and needs more love and testing. """ - def __init__(self, layer): + def __init__(self, layer, render_instance): """Constructor.""" - super(ExpectedFilesRenderman, self).__init__(layer) + super(ExpectedFilesRenderman, self).__init__(layer, render_instance) self.renderer = "renderman" def get_aovs(self): @@ -887,7 +901,7 @@ class ExpectedFilesRenderman(AExpectedFiles): class ExpectedFilesMentalray(AExpectedFiles): """Skeleton unimplemented class for Mentalray renderer.""" - def __init__(self, layer): + def __init__(self, layer, render_instance): """Constructor. Raises: diff --git a/pype/lib/__init__.py b/pype/lib/__init__.py index 9444ef5195..09cc998b7c 100644 --- a/pype/lib/__init__.py +++ b/pype/lib/__init__.py @@ -56,7 +56,11 @@ from .plugin_tools import ( filter_pyblish_plugins, source_hash, get_unique_layer_name, - get_background_layers + get_background_layers, + oiio_supported, + decompress, + get_decompress_dir, + should_decompress ) from .user_settings import ( @@ -108,6 +112,10 @@ __all__ = [ "source_hash", "get_unique_layer_name", "get_background_layers", + "oiio_supported", + "decompress", + "get_decompress_dir", + "should_decompress", "version_up", "get_version_from_path", diff --git a/pype/lib/plugin_tools.py b/pype/lib/plugin_tools.py index 13d311d96c..6d074329bc 100644 --- a/pype/lib/plugin_tools.py +++ b/pype/lib/plugin_tools.py @@ -5,6 +5,9 @@ import inspect import logging import re import json +import tempfile + +from . import execute from pype.settings import get_project_settings @@ -134,3 +137,115 @@ def get_background_layers(file_url): layer.get("filename")). replace("\\", "/")) return layers + + +def oiio_supported(): + """ + Checks if oiiotool is configured for this platform. + + Expects full path to executable. + + 'should_decompress' will throw exception if configured, + but not present or not working. + Returns: + (bool) + """ + oiio_path = os.getenv("PYPE_OIIO_PATH", "") + if not oiio_path or not os.path.exists(oiio_path): + log.debug("OIIOTool is not configured or not present at {}". + format(oiio_path)) + return False + + return True + + +def decompress(target_dir, file_url, + input_frame_start=None, input_frame_end=None, log=None): + """ + Decompresses DWAA 'file_url' .exr to 'target_dir'. + + Creates uncompressed files in 'target_dir', they need to be cleaned. + + File url could be for single file or for a sequence, in that case + %0Xd will be as a placeholder for frame number AND input_frame* will + be filled. + In that case single oiio command with '--frames' will be triggered for + all frames, this should be faster then looping and running sequentially + + Args: + target_dir (str): extended from stagingDir + file_url (str): full urls to source file (with or without %0Xd) + input_frame_start (int) (optional): first frame + input_frame_end (int) (optional): last frame + log (Logger) (optional): pype logger + """ + is_sequence = input_frame_start is not None and \ + input_frame_end is not None and \ + (int(input_frame_end) > int(input_frame_start)) + + oiio_cmd = [] + oiio_cmd.append(os.getenv("PYPE_OIIO_PATH")) + + oiio_cmd.append("--compression none") + + base_file_name = os.path.basename(file_url) + oiio_cmd.append(file_url) + + if is_sequence: + oiio_cmd.append("--frames {}-{}".format(input_frame_start, + input_frame_end)) + + oiio_cmd.append("-o") + oiio_cmd.append(os.path.join(target_dir, base_file_name)) + + subprocess_exr = " ".join(oiio_cmd) + + if not log: + log = logging.getLogger(__name__) + + log.debug("Decompressing {}".format(subprocess_exr)) + execute.execute( + subprocess_exr, shell=True, logger=log + ) + + +def get_decompress_dir(): + """ + Creates temporary folder for decompressing. + Its local, in case of farm it is 'local' to the farm machine. + + Should be much faster, needs to be cleaned up later. + """ + return os.path.normpath( + tempfile.mkdtemp(prefix="pyblish_tmp_") + ) + + +def should_decompress(file_url): + """ + Tests that 'file_url' is compressed with DWAA. + + Uses 'oiio_supported' to check that OIIO tool is available for this + platform. + + Shouldn't throw exception as oiiotool is guarded by check function. + Currently implemented this way as there is no support for Mac and Linux + In the future, it should be more strict and throws exception on + misconfiguration. + + Args: + file_url (str): path to rendered file (in sequence it would be + first file, if that compressed it is expected that whole seq + will be too) + Returns: + (bool): 'file_url' is DWAA compressed and should be decompressed + and we can decompress (oiiotool supported) + """ + if oiio_supported(): + output = execute.execute([ + os.getenv("PYPE_OIIO_PATH"), + "--info", "-v", file_url]) + return "compression: \"dwaa\"" in output or \ + "compression: \"dwab\"" in output + + return False diff --git a/pype/modules/__init__.py b/pype/modules/__init__.py index 4f76dc2df0..46ac0b918c 100644 --- a/pype/modules/__init__.py +++ b/pype/modules/__init__.py @@ -32,7 +32,7 @@ from .ftrack import ( IFtrackEventHandlerPaths ) from .clockify import ClockifyModule -from .logging import LoggingModule +from .log_viewer import LogViewModule from .muster import MusterModule from .standalonepublish_action import StandAlonePublishAction from .websocket_server import WebsocketModule @@ -70,7 +70,7 @@ __all__ = ( "ClockifyModule", "IdleManager", - "LoggingModule", + "LogViewModule", "MusterModule", "StandAlonePublishAction", diff --git a/pype/modules/ftrack/actions/action_applications.py b/pype/modules/ftrack/actions/action_applications.py index cf047a658d..5b6657793a 100644 --- a/pype/modules/ftrack/actions/action_applications.py +++ b/pype/modules/ftrack/actions/action_applications.py @@ -28,8 +28,8 @@ class AppplicationsAction(BaseAction): identifier = "pype_app.{}.".format(str(uuid4())) icon_url = os.environ.get("PYPE_STATICS_SERVER") - def __init__(self, session, plugins_presets=None): - super().__init__(session, plugins_presets) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self.application_manager = ApplicationManager() self.dbcon = AvalonMongoDB() @@ -210,6 +210,6 @@ class AppplicationsAction(BaseAction): } -def register(session, plugins_presets=None): +def register(session): """Register action. Called when used as an event plugin.""" - AppplicationsAction(session, plugins_presets).register() + AppplicationsAction(session).register() diff --git a/pype/modules/ftrack/actions/action_batch_task_creation.py b/pype/modules/ftrack/actions/action_batch_task_creation.py index ef370d55eb..477971773d 100644 --- a/pype/modules/ftrack/actions/action_batch_task_creation.py +++ b/pype/modules/ftrack/actions/action_batch_task_creation.py @@ -158,7 +158,7 @@ class BatchTasksAction(BaseAction): } -def register(session, plugins_presets=None): +def register(session): '''Register action. Called when used as an event plugin.''' - BatchTasksAction(session, plugins_presets).register() + BatchTasksAction(session).register() diff --git a/pype/modules/ftrack/actions/action_clean_hierarchical_attributes.py b/pype/modules/ftrack/actions/action_clean_hierarchical_attributes.py index e81e587f0a..f9824ec8ea 100644 --- a/pype/modules/ftrack/actions/action_clean_hierarchical_attributes.py +++ b/pype/modules/ftrack/actions/action_clean_hierarchical_attributes.py @@ -9,7 +9,6 @@ class CleanHierarchicalAttrsAction(BaseAction): label = "Pype Admin" variant = "- Clean hierarchical custom attributes" description = "Unset empty hierarchical attribute values." - role_list = ["Pypeclub", "Administrator", "Project Manager"] icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg") all_project_entities_query = ( @@ -20,12 +19,17 @@ class CleanHierarchicalAttrsAction(BaseAction): "select value, entity_id from CustomAttributeValue " "where entity_id in ({}) and configuration_id is \"{}\"" ) + settings_key = "clean_hierarchical_attr" def discover(self, session, entities, event): """Show only on project entity.""" - if len(entities) == 1 and entities[0].entity_type.lower() == "project": - return True - return False + if ( + len(entities) != 1 + or entities[0].entity_type.lower() != "project" + ): + return False + + return self.valid_roles(session, entities, event) def launch(self, session, entities, event): project = entities[0] @@ -98,7 +102,7 @@ class CleanHierarchicalAttrsAction(BaseAction): return True -def register(session, plugins_presets={}): +def register(session): '''Register plugin. Called when used as an plugin.''' - CleanHierarchicalAttrsAction(session, plugins_presets).register() + CleanHierarchicalAttrsAction(session).register() diff --git a/pype/modules/ftrack/actions/action_client_review_sort.py b/pype/modules/ftrack/actions/action_client_review_sort.py index 72387fe695..1c5c429cf2 100644 --- a/pype/modules/ftrack/actions/action_client_review_sort.py +++ b/pype/modules/ftrack/actions/action_client_review_sort.py @@ -84,7 +84,7 @@ class ClientReviewSort(BaseAction): } -def register(session, plugins_presets={}): +def register(session): '''Register action. Called when used as an event plugin.''' - ClientReviewSort(session, plugins_presets).register() + ClientReviewSort(session).register() diff --git a/pype/modules/ftrack/actions/action_component_open.py b/pype/modules/ftrack/actions/action_component_open.py index 5fe8fe831b..2928f54b15 100644 --- a/pype/modules/ftrack/actions/action_component_open.py +++ b/pype/modules/ftrack/actions/action_component_open.py @@ -60,7 +60,7 @@ class ComponentOpen(BaseAction): } -def register(session, plugins_presets={}): +def register(session): '''Register action. Called when used as an event plugin.''' - ComponentOpen(session, plugins_presets).register() + ComponentOpen(session).register() diff --git a/pype/modules/ftrack/actions/action_create_cust_attrs.py b/pype/modules/ftrack/actions/action_create_cust_attrs.py index a63c77c198..a6601775f1 100644 --- a/pype/modules/ftrack/actions/action_create_cust_attrs.py +++ b/pype/modules/ftrack/actions/action_create_cust_attrs.py @@ -131,9 +131,8 @@ class CustomAttributes(BaseAction): 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 = statics_icon("ftrack", "action_icons", "PypeAdmin.svg") + settings_key = "create_update_attributes" required_keys = ("key", "label", "type") @@ -150,7 +149,7 @@ class CustomAttributes(BaseAction): Validation - action is only for Administrators ''' - return True + return self.valid_roles(session, entities, event) def launch(self, session, entities, event): # JOB SETTINGS @@ -814,7 +813,7 @@ class CustomAttributes(BaseAction): } -def register(session, plugins_presets={}): +def register(session): '''Register plugin. Called when used as an plugin.''' - CustomAttributes(session, plugins_presets).register() + CustomAttributes(session).register() diff --git a/pype/modules/ftrack/actions/action_create_folders.py b/pype/modules/ftrack/actions/action_create_folders.py index a131a0e35b..d70232ae8f 100644 --- a/pype/modules/ftrack/actions/action_create_folders.py +++ b/pype/modules/ftrack/actions/action_create_folders.py @@ -243,6 +243,6 @@ class CreateFolders(BaseAction): return os.path.normpath(filled_template.split("{")[0]) -def register(session, plugins_presets={}): +def register(session): """Register plugin. Called when used as an plugin.""" - CreateFolders(session, plugins_presets).register() + CreateFolders(session).register() diff --git a/pype/modules/ftrack/actions/action_create_project_structure.py b/pype/modules/ftrack/actions/action_create_project_structure.py index 0815f82a69..64b4ba6727 100644 --- a/pype/modules/ftrack/actions/action_create_project_structure.py +++ b/pype/modules/ftrack/actions/action_create_project_structure.py @@ -238,5 +238,5 @@ class CreateProjectFolders(BaseAction): os.makedirs(path.format(project_root=project_root)) -def register(session, plugins_presets={}): - CreateProjectFolders(session, plugins_presets).register() +def register(session): + CreateProjectFolders(session).register() diff --git a/pype/modules/ftrack/actions/action_delete_asset.py b/pype/modules/ftrack/actions/action_delete_asset.py index 7d2dac3320..3bdbbe2470 100644 --- a/pype/modules/ftrack/actions/action_delete_asset.py +++ b/pype/modules/ftrack/actions/action_delete_asset.py @@ -18,8 +18,8 @@ class DeleteAssetSubset(BaseAction): #: Action description. description = "Removes from Avalon with all childs and asset from Ftrack" icon = statics_icon("ftrack", "action_icons", "DeleteAsset.svg") - #: roles that are allowed to register this action - role_list = ["Pypeclub", "Administrator", "Project Manager"] + + settings_key = "delete_asset_subset" #: Db connection dbcon = AvalonMongoDB() @@ -32,17 +32,21 @@ class DeleteAssetSubset(BaseAction): """ Validation """ task_ids = [] for ent_info in event["data"]["selection"]: - entType = ent_info.get("entityType", "") - if entType == "task": + if ent_info.get("entityType") == "task": task_ids.append(ent_info["entityId"]) + is_valid = False for entity in entities: - ftrack_id = entity["id"] - if ftrack_id not in task_ids: - continue - if entity.entity_type.lower() != "task": - return True - return False + if ( + entity["id"] in task_ids + and entity.entity_type.lower() != "task" + ): + is_valid = True + break + + if is_valid: + is_valid = self.valid_roles(session, entities, event) + return is_valid def _launch(self, event): try: @@ -662,7 +666,7 @@ class DeleteAssetSubset(BaseAction): } -def register(session, plugins_presets={}): +def register(session): '''Register plugin. Called when used as an plugin.''' - DeleteAssetSubset(session, plugins_presets).register() + DeleteAssetSubset(session).register() diff --git a/pype/modules/ftrack/actions/action_delete_old_versions.py b/pype/modules/ftrack/actions/action_delete_old_versions.py index b55f091fdc..e1c1e173a3 100644 --- a/pype/modules/ftrack/actions/action_delete_old_versions.py +++ b/pype/modules/ftrack/actions/action_delete_old_versions.py @@ -21,7 +21,6 @@ class DeleteOldVersions(BaseAction): "Delete files from older publishes so project can be" " archived with only lates versions." ) - role_list = ["Pypeclub", "Project Manager", "Administrator"] icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg") dbcon = AvalonMongoDB() @@ -31,13 +30,16 @@ class DeleteOldVersions(BaseAction): sequence_splitter = "__sequence_splitter__" def discover(self, session, entities, event): - ''' Validation ''' - selection = event["data"].get("selection") or [] - for entity in selection: - entity_type = (entity.get("entityType") or "").lower() - if entity_type == "assetversion": - return True - return False + """ Validation. """ + is_valid = False + for entity in entities: + if entity.entity_type.lower() == "assetversion": + is_valid = True + break + + if is_valid: + is_valid = self.valid_roles(session, entities, event) + return is_valid def interface(self, session, entities, event): # TODO Add roots existence validation @@ -577,7 +579,7 @@ class DeleteOldVersions(BaseAction): return (os.path.normpath(path), sequence_path) -def register(session, plugins_presets={}): +def register(session): '''Register plugin. Called when used as an plugin.''' - DeleteOldVersions(session, plugins_presets).register() + DeleteOldVersions(session).register() diff --git a/pype/modules/ftrack/actions/action_delivery.py b/pype/modules/ftrack/actions/action_delivery.py index 0f63f7f7ea..e9e939bb47 100644 --- a/pype/modules/ftrack/actions/action_delivery.py +++ b/pype/modules/ftrack/actions/action_delivery.py @@ -23,6 +23,7 @@ class Delivery(BaseAction): description = "Deliver data to client" role_list = ["Pypeclub", "Administrator", "Project manager"] icon = statics_icon("ftrack", "action_icons", "Delivery.svg") + settings_key = "delivery_action" def __init__(self, *args, **kwargs): self.db_con = AvalonMongoDB() @@ -30,11 +31,15 @@ class Delivery(BaseAction): super(Delivery, self).__init__(*args, **kwargs) def discover(self, session, entities, event): + is_valid = False for entity in entities: if entity.entity_type.lower() == "assetversion": - return True + is_valid = True + break - return False + if is_valid: + is_valid = self.valid_roles(session, entities, event) + return is_valid def interface(self, session, entities, event): if event["data"].get("values", {}): @@ -692,7 +697,7 @@ class Delivery(BaseAction): } -def register(session, plugins_presets={}): +def register(session): '''Register plugin. Called when used as an plugin.''' - Delivery(session, plugins_presets).register() + Delivery(session).register() diff --git a/pype/modules/ftrack/actions/action_djvview.py b/pype/modules/ftrack/actions/action_djvview.py index 6f667c0604..6036f9a35b 100644 --- a/pype/modules/ftrack/actions/action_djvview.py +++ b/pype/modules/ftrack/actions/action_djvview.py @@ -20,9 +20,8 @@ class DJVViewAction(BaseAction): "sgi", "rgba", "rgb", "bw", "tga", "tiff", "tif", "img" ] - def __init__(self, session, plugins_presets): - '''Expects a ftrack_api.Session instance''' - super().__init__(session, plugins_presets) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self.djv_path = self.find_djv_path() @@ -208,7 +207,7 @@ class DJVViewAction(BaseAction): return True -def register(session, plugins_presets={}): +def register(session): """Register hooks.""" - DJVViewAction(session, plugins_presets).register() + DJVViewAction(session).register() diff --git a/pype/modules/ftrack/actions/action_job_killer.py b/pype/modules/ftrack/actions/action_job_killer.py index ff23da2a54..1ddd1383a7 100644 --- a/pype/modules/ftrack/actions/action_job_killer.py +++ b/pype/modules/ftrack/actions/action_job_killer.py @@ -13,13 +13,12 @@ class JobKiller(BaseAction): #: Action description. description = 'Killing selected running jobs' #: roles that are allowed to register this action - role_list = ['Pypeclub', 'Administrator'] icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg") + settings_key = "job_killer" def discover(self, session, entities, event): ''' Validation ''' - - return True + return self.valid_roles(session, entities, event) def interface(self, session, entities, event): if not event['data'].get('values', {}): @@ -112,7 +111,7 @@ class JobKiller(BaseAction): } -def register(session, plugins_presets={}): +def register(session): '''Register plugin. Called when used as an plugin.''' - JobKiller(session, plugins_presets).register() + JobKiller(session).register() diff --git a/pype/modules/ftrack/actions/action_multiple_notes.py b/pype/modules/ftrack/actions/action_multiple_notes.py index c1a5cc6ce0..d88a91dd92 100644 --- a/pype/modules/ftrack/actions/action_multiple_notes.py +++ b/pype/modules/ftrack/actions/action_multiple_notes.py @@ -104,7 +104,7 @@ class MultipleNotes(BaseAction): return True -def register(session, plugins_presets={}): +def register(session): '''Register plugin. Called when used as an plugin.''' - MultipleNotes(session, plugins_presets).register() + MultipleNotes(session).register() diff --git a/pype/modules/ftrack/actions/action_prepare_project.py b/pype/modules/ftrack/actions/action_prepare_project.py index 970bb3d86b..3a955067d8 100644 --- a/pype/modules/ftrack/actions/action_prepare_project.py +++ b/pype/modules/ftrack/actions/action_prepare_project.py @@ -16,22 +16,23 @@ class PrepareProject(BaseAction): #: Action description. description = 'Set basic attributes on the project' #: roles that are allowed to register this action - role_list = ["Pypeclub", "Administrator", "Project manager"] icon = statics_icon("ftrack", "action_icons", "PrepareProject.svg") + settings_key = "prepare_project" + # Key to store info about trigerring create folder structure create_project_structure_key = "create_folder_structure" item_splitter = {'type': 'label', 'value': '---'} def discover(self, session, entities, event): ''' Validation ''' - if len(entities) != 1: + if ( + len(entities) != 1 + or entities[0].entity_type.lower() != "project" + ): return False - if entities[0].entity_type.lower() != "project": - return False - - return True + return self.valid_roles(session, entities, event) def interface(self, session, entities, event): if event['data'].get('values', {}): @@ -454,6 +455,6 @@ class PrepareProject(BaseAction): self.log.debug("*** Creating project specifig configs Finished ***") -def register(session, plugins_presets={}): +def register(session): '''Register plugin. Called when used as an plugin.''' - PrepareProject(session, plugins_presets).register() + PrepareProject(session).register() diff --git a/pype/modules/ftrack/actions/action_rv.py b/pype/modules/ftrack/actions/action_rv.py index eeb5672047..1c5ccfaed0 100644 --- a/pype/modules/ftrack/actions/action_rv.py +++ b/pype/modules/ftrack/actions/action_rv.py @@ -19,13 +19,8 @@ class RVAction(BaseAction): allowed_types = ["img", "mov", "exr", "mp4"] - def __init__(self, session, plugins_presets): - """ Constructor - - :param session: ftrack Session - :type session: :class:`ftrack_api.Session` - """ - super().__init__(session, plugins_presets) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) # QUESTION load RV application data from AppplicationManager? rv_path = None @@ -317,7 +312,7 @@ class RVAction(BaseAction): return paths -def register(session, plugins_presets={}): +def register(session): """Register hooks.""" - RVAction(session, plugins_presets).register() + RVAction(session).register() diff --git a/pype/modules/ftrack/actions/action_seed.py b/pype/modules/ftrack/actions/action_seed.py index d6288a03aa..549afc660c 100644 --- a/pype/modules/ftrack/actions/action_seed.py +++ b/pype/modules/ftrack/actions/action_seed.py @@ -15,7 +15,6 @@ class SeedDebugProject(BaseAction): #: priority priority = 100 #: roles that are allowed to register this action - role_list = ["Pypeclub"] icon = statics_icon("ftrack", "action_icons", "SeedProject.svg") # Asset names which will be created in `Assets` entity @@ -58,9 +57,12 @@ class SeedDebugProject(BaseAction): existing_projects = None new_project_item = "< New Project >" current_project_item = "< Current Project >" + settings_key = "seed_project" def discover(self, session, entities, event): ''' Validation ''' + if not self.valid_roles(session, entities, event): + return False return True def interface(self, session, entities, event): @@ -428,7 +430,7 @@ class SeedDebugProject(BaseAction): return True -def register(session, plugins_presets={}): +def register(session): '''Register plugin. Called when used as an plugin.''' - SeedDebugProject(session, plugins_presets).register() + SeedDebugProject(session).register() diff --git a/pype/modules/ftrack/actions/action_store_thumbnails_to_avalon.py b/pype/modules/ftrack/actions/action_store_thumbnails_to_avalon.py index e6f98d23cd..84f857e37a 100644 --- a/pype/modules/ftrack/actions/action_store_thumbnails_to_avalon.py +++ b/pype/modules/ftrack/actions/action_store_thumbnails_to_avalon.py @@ -21,8 +21,8 @@ class StoreThumbnailsToAvalon(BaseAction): # Action description description = 'Test action' # roles that are allowed to register this action - role_list = ["Pypeclub", "Administrator", "Project Manager"] icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg") + settings_key = "store_thubmnail_to_avalon" thumbnail_key = "AVALON_THUMBNAIL_ROOT" @@ -31,10 +31,15 @@ class StoreThumbnailsToAvalon(BaseAction): super(StoreThumbnailsToAvalon, self).__init__(*args, **kwargs) def discover(self, session, entities, event): + is_valid = False for entity in entities: if entity.entity_type.lower() == "assetversion": - return True - return False + is_valid = True + break + + if is_valid: + is_valid = self.valid_roles(session, entities, event) + return is_valid def launch(self, session, entities, event): user = session.query( @@ -457,5 +462,5 @@ class StoreThumbnailsToAvalon(BaseAction): return output -def register(session, plugins_presets={}): - StoreThumbnailsToAvalon(session, plugins_presets).register() +def register(session): + StoreThumbnailsToAvalon(session).register() diff --git a/pype/modules/ftrack/actions/action_sync_to_avalon.py b/pype/modules/ftrack/actions/action_sync_to_avalon.py index dfe1f2c464..b86b469d1c 100644 --- a/pype/modules/ftrack/actions/action_sync_to_avalon.py +++ b/pype/modules/ftrack/actions/action_sync_to_avalon.py @@ -41,20 +41,26 @@ class SyncToAvalonLocal(BaseAction): #: priority priority = 200 #: roles that are allowed to register this action - role_list = ["Pypeclub"] icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg") + settings_key = "sync_to_avalon_local" + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.entities_factory = SyncEntitiesFactory(self.log, self.session) def discover(self, session, entities, event): - ''' Validation ''' + """ Validate selection. """ + is_valid = False for ent in event["data"]["selection"]: # Ignore entities that are not tasks or projects if ent["entityType"].lower() in ["show", "task"]: - return True - return False + is_valid = True + break + + if is_valid: + is_valid = self.valid_roles(session, entities, event) + return is_valid def launch(self, session, in_entities, event): time_start = time.time() @@ -187,7 +193,7 @@ class SyncToAvalonLocal(BaseAction): pass -def register(session, plugins_presets={}): +def register(session): '''Register plugin. Called when used as an plugin.''' - SyncToAvalonLocal(session, plugins_presets).register() + SyncToAvalonLocal(session).register() diff --git a/pype/modules/ftrack/actions/action_test.py b/pype/modules/ftrack/actions/action_test.py index e4936274b3..c12906e340 100644 --- a/pype/modules/ftrack/actions/action_test.py +++ b/pype/modules/ftrack/actions/action_test.py @@ -22,5 +22,5 @@ class TestAction(BaseAction): return True -def register(session, plugins_presets={}): - TestAction(session, plugins_presets).register() +def register(session): + TestAction(session).register() diff --git a/pype/modules/ftrack/actions/action_thumbnail_to_childern.py b/pype/modules/ftrack/actions/action_thumbnail_to_childern.py index 3c6af10b43..b90dfa027c 100644 --- a/pype/modules/ftrack/actions/action_thumbnail_to_childern.py +++ b/pype/modules/ftrack/actions/action_thumbnail_to_childern.py @@ -15,11 +15,9 @@ class ThumbToChildren(BaseAction): icon = statics_icon("ftrack", "action_icons", "Thumbnail.svg") def discover(self, session, entities, event): - ''' Validation ''' - - if (len(entities) != 1 or entities[0].entity_type in ['Project']): + """Show only on project.""" + if (len(entities) != 1 or entities[0].entity_type in ["Project"]): return False - return True def launch(self, session, entities, event): @@ -59,7 +57,7 @@ class ThumbToChildren(BaseAction): } -def register(session, plugins_presets={}): +def register(session): '''Register action. Called when used as an event plugin.''' - ThumbToChildren(session, plugins_presets).register() + ThumbToChildren(session).register() diff --git a/pype/modules/ftrack/actions/action_thumbnail_to_parent.py b/pype/modules/ftrack/actions/action_thumbnail_to_parent.py index fb473f9aa5..5734ea6abc 100644 --- a/pype/modules/ftrack/actions/action_thumbnail_to_parent.py +++ b/pype/modules/ftrack/actions/action_thumbnail_to_parent.py @@ -85,7 +85,7 @@ class ThumbToParent(BaseAction): } -def register(session, plugins_presets={}): +def register(session): '''Register action. Called when used as an event plugin.''' - ThumbToParent(session, plugins_presets).register() + ThumbToParent(session).register() diff --git a/pype/modules/ftrack/actions/action_where_run_ask.py b/pype/modules/ftrack/actions/action_where_run_ask.py index 42640fb506..64957208da 100644 --- a/pype/modules/ftrack/actions/action_where_run_ask.py +++ b/pype/modules/ftrack/actions/action_where_run_ask.py @@ -27,7 +27,7 @@ class ActionAskWhereIRun(BaseAction): return True -def register(session, plugins_presets={}): +def register(session): '''Register plugin. Called when used as an plugin.''' - ActionAskWhereIRun(session, plugins_presets).register() + ActionAskWhereIRun(session).register() diff --git a/pype/modules/ftrack/actions/action_where_run_show.py b/pype/modules/ftrack/actions/action_where_run_show.py index a084547a45..f872d17d27 100644 --- a/pype/modules/ftrack/actions/action_where_run_show.py +++ b/pype/modules/ftrack/actions/action_where_run_show.py @@ -76,7 +76,7 @@ class ActionShowWhereIRun(BaseAction): return True -def register(session, plugins_presets={}): +def register(session): '''Register plugin. Called when used as an plugin.''' - ActionShowWhereIRun(session, plugins_presets).register() + ActionShowWhereIRun(session).register() diff --git a/pype/modules/ftrack/events/action_push_frame_values_to_task.py b/pype/modules/ftrack/events/action_push_frame_values_to_task.py index de61728a62..87d9d5afe9 100644 --- a/pype/modules/ftrack/events/action_push_frame_values_to_task.py +++ b/pype/modules/ftrack/events/action_push_frame_values_to_task.py @@ -430,5 +430,5 @@ class PushHierValuesToNonHier(ServerAction): session.commit() -def register(session, plugins_presets={}): - PushHierValuesToNonHier(session, plugins_presets).register() +def register(session): + PushHierValuesToNonHier(session).register() diff --git a/pype/modules/ftrack/events/action_sync_to_avalon.py b/pype/modules/ftrack/events/action_sync_to_avalon.py index 80b5939d84..486b977f04 100644 --- a/pype/modules/ftrack/events/action_sync_to_avalon.py +++ b/pype/modules/ftrack/events/action_sync_to_avalon.py @@ -182,6 +182,6 @@ class SyncToAvalonServer(ServerAction): pass -def register(session, plugins_presets={}): +def register(session): '''Register plugin. Called when used as an plugin.''' - SyncToAvalonServer(session, plugins_presets).register() + SyncToAvalonServer(session).register() diff --git a/pype/modules/ftrack/events/event_del_avalon_id_from_new.py b/pype/modules/ftrack/events/event_del_avalon_id_from_new.py index ee82c9589d..21e581e76a 100644 --- a/pype/modules/ftrack/events/event_del_avalon_id_from_new.py +++ b/pype/modules/ftrack/events/event_del_avalon_id_from_new.py @@ -47,6 +47,6 @@ class DelAvalonIdFromNew(BaseEvent): continue -def register(session, plugins_presets): +def register(session): '''Register plugin. Called when used as an plugin.''' - DelAvalonIdFromNew(session, plugins_presets).register() + DelAvalonIdFromNew(session).register() diff --git a/pype/modules/ftrack/events/event_first_version_status.py b/pype/modules/ftrack/events/event_first_version_status.py index 8754d092ab..cfca047c09 100644 --- a/pype/modules/ftrack/events/event_first_version_status.py +++ b/pype/modules/ftrack/events/event_first_version_status.py @@ -182,7 +182,7 @@ class FirstVersionStatus(BaseEvent): return filtered_ents -def register(session, plugins_presets): +def register(session): '''Register plugin. Called when used as an plugin.''' - FirstVersionStatus(session, plugins_presets).register() + FirstVersionStatus(session).register() diff --git a/pype/modules/ftrack/events/event_next_task_update.py b/pype/modules/ftrack/events/event_next_task_update.py index 14847f8656..1079681d3c 100644 --- a/pype/modules/ftrack/events/event_next_task_update.py +++ b/pype/modules/ftrack/events/event_next_task_update.py @@ -260,5 +260,5 @@ class NextTaskUpdate(BaseEvent): return types -def register(session, plugins_presets): - NextTaskUpdate(session, plugins_presets).register() +def register(session): + NextTaskUpdate(session).register() diff --git a/pype/modules/ftrack/events/event_push_frame_values_to_task.py b/pype/modules/ftrack/events/event_push_frame_values_to_task.py index 00457c8bfc..061002c13f 100644 --- a/pype/modules/ftrack/events/event_push_frame_values_to_task.py +++ b/pype/modules/ftrack/events/event_push_frame_values_to_task.py @@ -364,5 +364,5 @@ class PushFrameValuesToTaskEvent(BaseEvent): return output, hiearchical -def register(session, plugins_presets): - PushFrameValuesToTaskEvent(session, plugins_presets).register() +def register(session): + PushFrameValuesToTaskEvent(session).register() diff --git a/pype/modules/ftrack/events/event_radio_buttons.py b/pype/modules/ftrack/events/event_radio_buttons.py index b2ab4e75ec..90811e5f45 100644 --- a/pype/modules/ftrack/events/event_radio_buttons.py +++ b/pype/modules/ftrack/events/event_radio_buttons.py @@ -34,7 +34,7 @@ class RadioButtons(BaseEvent): session.commit() -def register(session, plugins_presets): +def register(session): '''Register plugin. Called when used as an plugin.''' - RadioButtons(session, plugins_presets).register() + RadioButtons(session).register() diff --git a/pype/modules/ftrack/events/event_sync_to_avalon.py b/pype/modules/ftrack/events/event_sync_to_avalon.py index 2a69a559bd..0209dfd53a 100644 --- a/pype/modules/ftrack/events/event_sync_to_avalon.py +++ b/pype/modules/ftrack/events/event_sync_to_avalon.py @@ -53,7 +53,7 @@ class SyncToAvalonEvent(BaseEvent): created_entities = [] report_splitter = {"type": "label", "value": "---"} - def __init__(self, session, plugins_presets={}): + def __init__(self, session): '''Expects a ftrack_api.Session instance''' # Debug settings # - time expiration in seconds @@ -67,7 +67,7 @@ class SyncToAvalonEvent(BaseEvent): self.dbcon = AvalonMongoDB() # Set processing session to not use global self.set_process_session(session) - super().__init__(session, plugins_presets) + super().__init__(session) def debug_logs(self): """This is debug method for printing small debugs messages. """ @@ -2513,6 +2513,6 @@ class SyncToAvalonEvent(BaseEvent): return mongo_id_configuration_id -def register(session, plugins_presets): +def register(session): '''Register plugin. Called when used as an plugin.''' - SyncToAvalonEvent(session, plugins_presets).register() + SyncToAvalonEvent(session).register() diff --git a/pype/modules/ftrack/events/event_task_to_parent_status.py b/pype/modules/ftrack/events/event_task_to_parent_status.py index 4620f84395..30c995495e 100644 --- a/pype/modules/ftrack/events/event_task_to_parent_status.py +++ b/pype/modules/ftrack/events/event_task_to_parent_status.py @@ -61,8 +61,8 @@ class TaskStatusToParent(BaseEvent): session, event, project_id ) # Load settings - project_settings = self.get_settings_for_project( - session, event, project_entity=project_entity + project_settings = self.get_project_settings_from_event( + event, project_entity ) # Prepare loaded settings and check if can be processed @@ -419,5 +419,5 @@ class TaskStatusToParent(BaseEvent): return output -def register(session, plugins_presets): - TaskStatusToParent(session, plugins_presets).register() +def register(session): + TaskStatusToParent(session).register() diff --git a/pype/modules/ftrack/events/event_task_to_version_status.py b/pype/modules/ftrack/events/event_task_to_version_status.py index b49fd01a91..fa2cb043bd 100644 --- a/pype/modules/ftrack/events/event_task_to_version_status.py +++ b/pype/modules/ftrack/events/event_task_to_version_status.py @@ -102,8 +102,8 @@ class TaskToVersionStatus(BaseEvent): project_entity = self.get_project_entity_from_event( session, event, project_id ) - project_settings = self.get_settings_for_project( - session, event, project_entity=project_entity + project_settings = self.get_project_settings_from_event( + event, project_entity ) project_name = project_entity["full_name"] @@ -372,5 +372,5 @@ class TaskToVersionStatus(BaseEvent): return last_asset_versions_by_task_id -def register(session, plugins_presets): - TaskToVersionStatus(session, plugins_presets).register() +def register(session): + TaskToVersionStatus(session).register() diff --git a/pype/modules/ftrack/events/event_test.py b/pype/modules/ftrack/events/event_test.py index 0a86bd1754..c07f8b8d16 100644 --- a/pype/modules/ftrack/events/event_test.py +++ b/pype/modules/ftrack/events/event_test.py @@ -1,7 +1,3 @@ -import os -import sys -import re -import ftrack_api from pype.modules.ftrack import BaseEvent @@ -20,7 +16,7 @@ class TestEvent(BaseEvent): return True -def register(session, plugins_presets): +def register(session): '''Register plugin. Called when used as an plugin.''' - TestEvent(session, plugins_presets).register() + TestEvent(session).register() diff --git a/pype/modules/ftrack/events/event_thumbnail_updates.py b/pype/modules/ftrack/events/event_thumbnail_updates.py index 0044c5e21c..cda43b05a9 100644 --- a/pype/modules/ftrack/events/event_thumbnail_updates.py +++ b/pype/modules/ftrack/events/event_thumbnail_updates.py @@ -22,8 +22,8 @@ class ThumbnailEvents(BaseEvent): project_entity = self.get_project_entity_from_event( session, event, project_id ) - project_settings = self.get_settings_for_project( - session, event, project_entity=project_entity + project_settings = self.get_project_settings_from_event( + event, project_entity ) project_name = project_entity["full_name"] @@ -151,5 +151,5 @@ class ThumbnailEvents(BaseEvent): return filtered_entities_info -def register(session, plugins_presets): - ThumbnailEvents(session, plugins_presets).register() +def register(session): + ThumbnailEvents(session).register() diff --git a/pype/modules/ftrack/events/event_user_assigment.py b/pype/modules/ftrack/events/event_user_assigment.py index 9b0dfe84d1..59880fabe5 100644 --- a/pype/modules/ftrack/events/event_user_assigment.py +++ b/pype/modules/ftrack/events/event_user_assigment.py @@ -250,9 +250,9 @@ class UserAssigmentEvent(BaseEvent): return True -def register(session, plugins_presets): +def register(session): """ Register plugin. Called when used as an plugin. """ - UserAssigmentEvent(session, plugins_presets).register() + UserAssigmentEvent(session).register() diff --git a/pype/modules/ftrack/events/event_version_to_task_statuses.py b/pype/modules/ftrack/events/event_version_to_task_statuses.py index d094c2a8fd..6debd4aac4 100644 --- a/pype/modules/ftrack/events/event_version_to_task_statuses.py +++ b/pype/modules/ftrack/events/event_version_to_task_statuses.py @@ -51,8 +51,8 @@ class VersionToTaskStatus(BaseEvent): project_entity = self.get_project_entity_from_event( session, event, project_id ) - project_settings = self.get_settings_for_project( - session, event, project_entity=project_entity + project_settings = self.get_project_settings_from_event( + event, project_entity ) project_name = project_entity["full_name"] @@ -241,7 +241,7 @@ class VersionToTaskStatus(BaseEvent): return output -def register(session, plugins_presets): +def register(session): '''Register plugin. Called when used as an plugin.''' - VersionToTaskStatus(session, plugins_presets).register() + VersionToTaskStatus(session).register() diff --git a/pype/modules/ftrack/ftrack_server/ftrack_server.py b/pype/modules/ftrack/ftrack_server/ftrack_server.py index 93c7cd3a67..3e0c752596 100644 --- a/pype/modules/ftrack/ftrack_server/ftrack_server.py +++ b/pype/modules/ftrack/ftrack_server/ftrack_server.py @@ -108,21 +108,10 @@ class FtrackServer: " in registered paths: \"{}\"" ).format("| ".join(paths))) - # TODO replace with settings or get rid of passing the dictionary - plugins_presets = {} - - function_counter = 0 for function_dict in register_functions_dict: register = function_dict["register"] try: - if len(inspect.signature(register).parameters) == 1: - register(self.session) - else: - register(self.session, plugins_presets=plugins_presets) - - if function_counter % 7 == 0: - time.sleep(0.1) - function_counter += 1 + register(self.session) except Exception as exc: msg = '"{}" - register was not successful ({})'.format( function_dict['name'], str(exc) diff --git a/pype/modules/ftrack/lib/ftrack_action_handler.py b/pype/modules/ftrack/lib/ftrack_action_handler.py index a550d9e7d3..11952cf3c0 100644 --- a/pype/modules/ftrack/lib/ftrack_action_handler.py +++ b/pype/modules/ftrack/lib/ftrack_action_handler.py @@ -29,7 +29,9 @@ class BaseAction(BaseHandler): icon = None type = 'Action' - def __init__(self, session, plugins_presets={}): + settings_frack_subkey = "user_handlers" + + def __init__(self, session): '''Expects a ftrack_api.Session instance''' if self.label is None: raise ValueError('Action missing label.') @@ -37,7 +39,7 @@ class BaseAction(BaseHandler): if self.identifier is None: raise ValueError('Action missing identifier.') - super().__init__(session, plugins_presets) + super().__init__(session) def register(self): ''' @@ -67,6 +69,9 @@ class BaseAction(BaseHandler): def _discover(self, event): entities = self._translate_event(event) + if not entities: + return + accepts = self.discover(self.session, entities, event) if not accepts: return @@ -146,21 +151,18 @@ class BaseAction(BaseHandler): def _launch(self, event): entities = self._translate_event(event) + if not entities: + return preactions_launched = self._handle_preactions(self.session, event) if preactions_launched is False: return - interface = self._interface( - self.session, entities, event - ) - + interface = self._interface(self.session, entities, event) if interface: return interface - response = self.launch( - self.session, entities, event - ) + response = self.launch(self.session, entities, event) return self._handle_result(response) @@ -196,50 +198,29 @@ class BaseAction(BaseHandler): return result + @staticmethod + def roles_check(settings_roles, user_roles, default=True): + """Compare roles from setting and user's roles. -class ServerAction(BaseAction): - """Action class meant to be used on event server. + Args: + settings_roles(list): List of role names from settings. + user_roles(list): User's lowered role names. + default(bool): If `settings_roles` is empty list. - Unlike the `BaseAction` roles are not checked on register but on discover. - For the same reason register is modified to not filter topics by username. - """ + Returns: + bool: `True` if user has at least one role from settings or + default if `settings_roles` is empty. + """ + if not settings_roles: + return default - def __init__(self, *args, **kwargs): - if not self.role_list: - self.role_list = set() - else: - self.role_list = set( - role_name.lower() - for role_name in self.role_list - ) - super(ServerAction, self).__init__(*args, **kwargs) - - def _register_role_check(self): - # Skip register role check. - return - - def _discover(self, event): - """Check user discover availability.""" - if not self._check_user_discover(event): - return - return super(ServerAction, self)._discover(event) - - def _check_user_discover(self, event): - """Should be action discovered by user trying to show actions.""" - if not self.role_list: - return True - - user_entity = self._get_user_entity(event) - if not user_entity: - return False - - for role in user_entity["user_security_roles"]: - lowered_role = role["security_role"]["name"].lower() - if lowered_role in self.role_list: + for role_name in settings_roles: + if role_name.lower() in user_roles: return True return False - def _get_user_entity(self, event): + @classmethod + def get_user_entity_from_event(cls, session, event): """Query user entity from event.""" not_set = object() @@ -251,17 +232,88 @@ class ServerAction(BaseAction): user_id = user_info.get("id") username = user_info.get("username") if user_id: - user_entity = self.session.query( + user_entity = session.query( "User where id is {}".format(user_id) ).first() if not user_entity and username: - user_entity = self.session.query( + user_entity = session.query( "User where username is {}".format(username) ).first() event["data"]["user_entity"] = user_entity return user_entity + @classmethod + def get_user_roles_from_event(cls, session, event): + """Query user entity from event.""" + not_set = object() + + user_roles = event["data"].get("user_roles", not_set) + if user_roles is not_set: + user_roles = [] + user_entity = cls.get_user_entity_from_event(session, event) + for role in user_entity["user_security_roles"]: + user_roles.append(role["security_role"]["name"].lower()) + event["data"]["user_roles"] = user_roles + return user_roles + + def get_project_entity_from_event(self, session, event, entities): + """Load or query and fill project entity from/to event data. + + Project data are stored by ftrack id because in most cases it is + easier to access project id than project name. + + Args: + session (ftrack_api.Session): Current session. + event (ftrack_api.Event): Processed event by session. + entities (list): Ftrack entities of selection. + """ + + # Try to get project entity from event + project_entity = event["data"].get("project_entity") + if not project_entity: + project_entity = self.get_project_from_entity( + entities[0], session + ) + event["data"]["project_entity"] = project_entity + return project_entity + + def get_ftrack_settings(self, session, event, entities): + project_entity = self.get_project_entity_from_event( + session, event, entities + ) + project_settings = self.get_project_settings_from_event( + event, project_entity + ) + return project_settings["ftrack"] + + def valid_roles(self, session, entities, event): + """Validate user roles by settings. + + Method requires to have set `settings_key` attribute. + """ + ftrack_settings = self.get_ftrack_settings(session, event, entities) + settings = ( + ftrack_settings[self.settings_frack_subkey][self.settings_key] + ) + if not settings.get("enabled", True): + return False + + user_role_list = self.get_user_roles_from_event(session, event) + if not self.roles_check(settings.get("role_list"), user_role_list): + return False + return True + + +class ServerAction(BaseAction): + """Action class meant to be used on event server. + + Unlike the `BaseAction` roles are not checked on register but on discover. + For the same reason register is modified to not filter topics by username. + """ + + settings_frack_subkey = "events" + def register(self): """Register subcription to Ftrack event hub.""" self.session.event_hub.subscribe( diff --git a/pype/modules/ftrack/lib/ftrack_base_handler.py b/pype/modules/ftrack/lib/ftrack_base_handler.py index 72b6272b76..2a8f400101 100644 --- a/pype/modules/ftrack/lib/ftrack_base_handler.py +++ b/pype/modules/ftrack/lib/ftrack_base_handler.py @@ -37,14 +37,13 @@ class BaseHandler(object): type = 'No-type' ignore_me = False preactions = [] - role_list = [] @staticmethod def join_query_keys(keys): """Helper to join keys to query.""" return ",".join(["\"{}\"".format(key) for key in keys]) - def __init__(self, session, plugins_presets=None): + def __init__(self, session): '''Expects a ftrack_api.Session instance''' self.log = Logger().get_logger(self.__class__.__name__) if not( @@ -65,31 +64,19 @@ class BaseHandler(object): # Using decorator self.register = self.register_decorator(self.register) self.launch = self.launch_log(self.launch) - if plugins_presets is None: - plugins_presets = {} - self.plugins_presets = plugins_presets # Decorator def register_decorator(self, func): @functools.wraps(func) def wrapper_register(*args, **kwargs): - - presets_data = self.plugins_presets.get(self.__class__.__name__) - if presets_data: - for key, value in presets_data.items(): - if not hasattr(self, key): - continue - setattr(self, key, value) - if self.ignore_me: return - label = self.__class__.__name__ - if hasattr(self, 'label'): - if self.variant is None: - label = self.label - else: - label = '{} {}'.format(self.label, self.variant) + label = getattr(self, "label", self.__class__.__name__) + variant = getattr(self, "variant", None) + if variant: + label = "{} {}".format(label, variant) + try: self._preregister() @@ -126,12 +113,10 @@ class BaseHandler(object): def launch_log(self, func): @functools.wraps(func) def wrapper_launch(*args, **kwargs): - label = self.__class__.__name__ - if hasattr(self, 'label'): - label = self.label - if hasattr(self, 'variant'): - if self.variant is not None: - label = '{} {}'.format(self.label, self.variant) + label = getattr(self, "label", self.__class__.__name__) + variant = getattr(self, "variant", None) + if variant: + label = "{} {}".format(label, variant) self.log.info(('{} "{}": Launched').format(self.type, label)) try: @@ -156,28 +141,7 @@ class BaseHandler(object): def reset_session(self): self.session.reset() - def _register_role_check(self): - if not self.role_list or not isinstance(self.role_list, (list, tuple)): - return - - user_entity = self.session.query( - "User where username is \"{}\"".format(self.session.api_user) - ).one() - available = False - lowercase_rolelist = [ - role_name.lower() - for role_name in self.role_list - ] - for role in user_entity["user_security_roles"]: - if role["security_role"]["name"].lower() in lowercase_rolelist: - available = True - break - if available is False: - raise MissingPermision - def _preregister(self): - self._register_role_check() - # Custom validations result = self.preregister() if result is None: @@ -564,7 +528,7 @@ class BaseHandler(object): "Publishing event: {}" ).format(str(event.__dict__))) - def get_project_from_entity(self, entity): + def get_project_from_entity(self, entity, session=None): low_entity_type = entity.entity_type.lower() if low_entity_type == "project": return entity @@ -585,61 +549,30 @@ class BaseHandler(object): return parent["project"] project_data = entity["link"][0] - return self.session.query( + + if session is None: + session = self.session + return session.query( "Project where id is {}".format(project_data["id"]) ).one() - def get_project_entity_from_event(self, session, event, project_id): - """Load or query and fill project entity from/to event data. - - Project data are stored by ftrack id because in most cases it is - easier to access project id than project name. - - Args: - session (ftrack_api.Session): Current session. - event (ftrack_api.Event): Processed event by session. - project_id (str): Ftrack project id. - """ - if not project_id: - raise ValueError( - "Entered `project_id` is not valid. {} ({})".format( - str(project_id), str(type(project_id)) - ) - ) - # Try to get project entity from event - project_entities = event["data"].get("project_entities") - if not project_entities: - project_entities = {} - event["data"]["project_entities"] = project_entities - - project_entity = project_entities.get(project_id) - if not project_entity: - # Get project entity from task and store to event - project_entity = session.get("Project", project_id) - event["data"]["project_entities"][project_id] = project_entity - return project_entity - - def get_settings_for_project( - self, session, event, project_id=None, project_entity=None - ): + def get_project_settings_from_event(self, event, project_entity): """Load or fill pype's project settings from event data. Project data are stored by ftrack id because in most cases it is easier to access project id than project name. Args: - session (ftrack_api.Session): Current session. event (ftrack_api.Event): Processed event by session. - project_id (str): Ftrack project id. Must be entered if - project_entity is not. - project_entity (ftrack_api.Entity): Project entity. Must be entered - if project_id is not. + project_entity (ftrack_api.Entity): Project entity. """ if not project_entity: - project_entity = self.get_project_entity_from_event( - session, event, project_id - ) + raise AssertionError(( + "Invalid arguments entered. Project entity or project id" + "must be entered." + )) + project_id = project_entity["id"] project_name = project_entity["full_name"] project_settings_by_id = event["data"].get("project_settings") diff --git a/pype/modules/ftrack/lib/ftrack_event_handler.py b/pype/modules/ftrack/lib/ftrack_event_handler.py index 770b942844..73cebc4d34 100644 --- a/pype/modules/ftrack/lib/ftrack_event_handler.py +++ b/pype/modules/ftrack/lib/ftrack_event_handler.py @@ -15,10 +15,6 @@ class BaseEvent(BaseHandler): type = 'Event' - def __init__(self, session, plugins_presets={}): - '''Expects a ftrack_api.Session instance''' - super().__init__(session, plugins_presets) - # Decorator def launch_log(self, func): @functools.wraps(func) @@ -50,3 +46,33 @@ class BaseEvent(BaseHandler): session, ignore=['socialfeed', 'socialnotification'] ) + + def get_project_entity_from_event(self, session, event, project_id): + """Load or query and fill project entity from/to event data. + + Project data are stored by ftrack id because in most cases it is + easier to access project id than project name. + + Args: + session (ftrack_api.Session): Current session. + event (ftrack_api.Event): Processed event by session. + project_id (str): Ftrack project id. + """ + if not project_id: + raise ValueError( + "Entered `project_id` is not valid. {} ({})".format( + str(project_id), str(type(project_id)) + ) + ) + # Try to get project entity from event + project_entities = event["data"].get("project_entities") + if not project_entities: + project_entities = {} + event["data"]["project_entities"] = project_entities + + project_entity = project_entities.get(project_id) + if not project_entity: + # Get project entity from task and store to event + project_entity = session.get("Project", project_id) + event["data"]["project_entities"][project_id] = project_entity + return project_entity diff --git a/pype/modules/log_viewer/__init__.py b/pype/modules/log_viewer/__init__.py new file mode 100644 index 0000000000..672f47c015 --- /dev/null +++ b/pype/modules/log_viewer/__init__.py @@ -0,0 +1,6 @@ +from .log_view_module import LogViewModule + + +__all__ = ( + "LogViewModule", +) diff --git a/pype/modules/logging/logging_module.py b/pype/modules/log_viewer/log_view_module.py similarity index 96% rename from pype/modules/logging/logging_module.py rename to pype/modules/log_viewer/log_view_module.py index 06101b51a5..1252eaf888 100644 --- a/pype/modules/logging/logging_module.py +++ b/pype/modules/log_viewer/log_view_module.py @@ -2,7 +2,7 @@ from pype.api import Logger from .. import PypeModule, ITrayModule -class LoggingModule(PypeModule, ITrayModule): +class LogViewModule(PypeModule, ITrayModule): name = "log_viewer" def initialize(self, modules_settings): diff --git a/pype/modules/logging/tray/__init__.py b/pype/modules/log_viewer/tray/__init__.py similarity index 100% rename from pype/modules/logging/tray/__init__.py rename to pype/modules/log_viewer/tray/__init__.py diff --git a/pype/modules/logging/tray/app.py b/pype/modules/log_viewer/tray/app.py similarity index 100% rename from pype/modules/logging/tray/app.py rename to pype/modules/log_viewer/tray/app.py diff --git a/pype/modules/logging/tray/models.py b/pype/modules/log_viewer/tray/models.py similarity index 100% rename from pype/modules/logging/tray/models.py rename to pype/modules/log_viewer/tray/models.py diff --git a/pype/modules/logging/tray/widgets.py b/pype/modules/log_viewer/tray/widgets.py similarity index 100% rename from pype/modules/logging/tray/widgets.py rename to pype/modules/log_viewer/tray/widgets.py diff --git a/pype/modules/logging/__init__.py b/pype/modules/logging/__init__.py deleted file mode 100644 index c87d8b7f43..0000000000 --- a/pype/modules/logging/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .logging_module import LoggingModule - - -__all__ = ( - "LoggingModule", -) diff --git a/pype/plugins/global/publish/extract_burnin.py b/pype/plugins/global/publish/extract_burnin.py index 501162b6a6..d29af63483 100644 --- a/pype/plugins/global/publish/extract_burnin.py +++ b/pype/plugins/global/publish/extract_burnin.py @@ -6,6 +6,9 @@ import tempfile import pype.api import pyblish +from pype.lib import should_decompress, \ + get_decompress_dir, decompress +import shutil class ExtractBurnin(pype.api.Extractor): @@ -28,7 +31,8 @@ class ExtractBurnin(pype.api.Extractor): "premiere", "standalonepublisher", "harmony", - "fusion" + "fusion", + "aftereffects" ] optional = True @@ -204,6 +208,26 @@ class ExtractBurnin(pype.api.Extractor): # Prepare paths and files for process. self.input_output_paths(new_repre, temp_data, filename_suffix) + decompressed_dir = '' + full_input_path = temp_data["full_input_path"] + do_decompress = should_decompress(full_input_path) + if do_decompress: + decompressed_dir = get_decompress_dir() + + decompress( + decompressed_dir, + full_input_path, + temp_data["frame_start"], + temp_data["frame_end"], + self.log + ) + + # input path changed, 'decompressed' added + input_file = os.path.basename(full_input_path) + temp_data["full_input_path"] = os.path.join( + decompressed_dir, + input_file) + # Data for burnin script script_data = { "input": temp_data["full_input_path"], @@ -263,6 +287,9 @@ class ExtractBurnin(pype.api.Extractor): os.remove(filepath) self.log.debug("Removed: \"{}\"".format(filepath)) + if do_decompress and os.path.exists(decompressed_dir): + shutil.rmtree(decompressed_dir) + def prepare_basic_data(self, instance): """Pick data from instance for processing and for burnin strings. diff --git a/pype/plugins/global/publish/extract_jpeg.py b/pype/plugins/global/publish/extract_jpeg.py index 551e57796a..af90d4366d 100644 --- a/pype/plugins/global/publish/extract_jpeg.py +++ b/pype/plugins/global/publish/extract_jpeg.py @@ -3,6 +3,9 @@ import os import pyblish.api import pype.api import pype.lib +from pype.lib import should_decompress, \ + get_decompress_dir, decompress +import shutil class ExtractJpegEXR(pyblish.api.InstancePlugin): @@ -22,7 +25,8 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): if 'crypto' in instance.data['subset']: return - # ffmpeg doesn't support multipart exrs + do_decompress = False + # ffmpeg doesn't support multipart exrs, use oiiotool if available if instance.data.get("multipartExr") is True: return @@ -36,10 +40,6 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): # filter out mov and img sequences representations_new = representations[:] - if instance.data.get("multipartExr"): - # ffmpeg doesn't support multipart exrs - return - for repre in representations: tags = repre.get("tags", []) self.log.debug(repre) @@ -60,6 +60,19 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): full_input_path = os.path.join(stagingdir, input_file) self.log.info("input {}".format(full_input_path)) + decompressed_dir = '' + do_decompress = should_decompress(full_input_path) + if do_decompress: + decompressed_dir = get_decompress_dir() + + decompress( + decompressed_dir, + full_input_path) + # input path changed, 'decompressed' added + full_input_path = os.path.join( + decompressed_dir, + input_file) + filename = os.path.splitext(input_file)[0] if not filename.endswith('.'): filename += "." @@ -93,7 +106,14 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): # run subprocess self.log.debug("{}".format(subprocess_jpeg)) - pype.api.subprocess(subprocess_jpeg, shell=True) + try: # temporary until oiiotool is supported cross platform + pype.api.subprocess(subprocess_jpeg, shell=True) + except RuntimeError as exp: + if "Compression" in str(exp): + self.log.debug("Unsupported compression on input files. " + + "Skipping!!!") + return + raise if "representations" not in instance.data: instance.data["representations"] = [] @@ -111,4 +131,7 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): self.log.debug("Adding: {}".format(representation)) representations_new.append(representation) + if do_decompress and os.path.exists(decompressed_dir): + shutil.rmtree(decompressed_dir) + instance.data["representations"] = representations_new diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py index aa8d8accb5..37fe83bf10 100644 --- a/pype/plugins/global/publish/extract_review.py +++ b/pype/plugins/global/publish/extract_review.py @@ -6,6 +6,8 @@ import pyblish.api import clique import pype.api import pype.lib +from pype.lib import should_decompress, \ + get_decompress_dir, decompress class ExtractReview(pyblish.api.InstancePlugin): @@ -14,7 +16,7 @@ class ExtractReview(pyblish.api.InstancePlugin): Compulsory attribute of representation is tags list with "review", otherwise the representation is ignored. - All new represetnations are created and encoded by ffmpeg following + All new representations are created and encoded by ffmpeg following presets found in `pype-config/presets/plugins/global/ publish.json:ExtractReview:outputs`. """ @@ -188,9 +190,17 @@ class ExtractReview(pyblish.api.InstancePlugin): temp_data = self.prepare_temp_data(instance, repre, output_def) - ffmpeg_args = self._ffmpeg_arguments( - output_def, instance, new_repre, temp_data - ) + try: # temporary until oiiotool is supported cross platform + ffmpeg_args = self._ffmpeg_arguments( + output_def, instance, new_repre, temp_data + ) + except ZeroDivisionError: + if 'exr' in temp_data["origin_repre"]["ext"]: + self.log.debug("Unsupported compression on input " + + "files. Skipping!!!") + return + raise + subprcs_cmd = " ".join(ffmpeg_args) # run subprocess @@ -318,9 +328,9 @@ class ExtractReview(pyblish.api.InstancePlugin): Args: output_def (dict): Currently processed output definition. instance (Instance): Currently processed instance. - new_repre (dict): Reprensetation representing output of this + new_repre (dict): Representation representing output of this process. - temp_data (dict): Base data for successfull process. + temp_data (dict): Base data for successful process. """ # Get FFmpeg arguments from profile presets @@ -331,9 +341,35 @@ class ExtractReview(pyblish.api.InstancePlugin): ffmpeg_video_filters = out_def_ffmpeg_args.get("video_filters") or [] ffmpeg_audio_filters = out_def_ffmpeg_args.get("audio_filters") or [] + if isinstance(new_repre['files'], list): + input_files_urls = [os.path.join(new_repre["stagingDir"], f) for f + in new_repre['files']] + test_path = input_files_urls[0] + else: + test_path = os.path.join( + new_repre["stagingDir"], new_repre['files']) + do_decompress = should_decompress(test_path) + + if do_decompress: + # change stagingDir, decompress first + # calculate all paths with modified directory, used on too many + # places + # will be purged by cleanup.py automatically + orig_staging_dir = new_repre["stagingDir"] + new_repre["stagingDir"] = get_decompress_dir() + # Prepare input and output filepaths self.input_output_paths(new_repre, output_def, temp_data) + if do_decompress: + input_file = temp_data["full_input_path"].\ + replace(new_repre["stagingDir"], orig_staging_dir) + + decompress(new_repre["stagingDir"], input_file, + temp_data["frame_start"], + temp_data["frame_end"], + self.log) + # Set output frames len to 1 when ouput is single image if ( temp_data["output_ext_is_image"] @@ -930,7 +966,7 @@ class ExtractReview(pyblish.api.InstancePlugin): return regexes def validate_value_by_regexes(self, value, in_list): - """Validates in any regexe from list match entered value. + """Validates in any regex from list match entered value. Args: in_list (list): List with regexes. @@ -955,9 +991,9 @@ class ExtractReview(pyblish.api.InstancePlugin): def profile_exclusion(self, matching_profiles): """Find out most matching profile byt host, task and family match. - Profiles are selectivelly filtered. Each profile should have + Profiles are selectively filtered. Each profile should have "__value__" key with list of booleans. Each boolean represents - existence of filter for specific key (host, taks, family). + existence of filter for specific key (host, tasks, family). Profiles are looped in sequence. In each sequence are split into true_list and false_list. For next sequence loop are used profiles in true_list if there are any profiles else false_list is used. @@ -1036,7 +1072,7 @@ class ExtractReview(pyblish.api.InstancePlugin): highest_profile_points = -1 # Each profile get 1 point for each matching filter. Profile with most - # points is returnd. For cases when more than one profile will match + # points is returned. For cases when more than one profile will match # are also stored ordered lists of matching values. for profile in self.profiles: profile_points = 0 @@ -1648,7 +1684,7 @@ class ExtractReview(pyblish.api.InstancePlugin): def add_video_filter_args(self, args, inserting_arg): """ - Fixing video filter argumets to be one long string + Fixing video filter arguments to be one long string Args: args (list): list of string arguments diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index 26e5fff699..133b4fc6ef 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -15,7 +15,7 @@ from avalon import io from avalon.vendor import filelink import pype.api from datetime import datetime -from pype.modules import ModulesManager +# from pype.modules import ModulesManager # this is needed until speedcopy for linux is fixed if sys.platform == "win32": @@ -933,15 +933,15 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): remote_site = None sync_server_presets = None - manager = ModulesManager() - sync_server = manager.modules_by_name["sync_server"] - try: - if sync_server.enabled: - local_site, remote_site = sync_server.get_sites_for_project() - except ValueError: - log.debug(("There are not set presets for SyncServer." - " No credentials provided, no synching possible"). - format(str(sync_server_presets))) + # manager = ModulesManager() + # sync_server = manager.modules_by_name["sync_server"] + # try: + # if sync_server.enabled: + # local_site, remote_site = sync_server.get_sites_for_project() + # except ValueError: + # log.debug(("There are not set presets for SyncServer." + # " No credentials provided, no synching possible"). + # format(str(sync_server_presets))) rec = { "_id": io.ObjectId(), diff --git a/pype/plugins/maya/create/create_render.py b/pype/plugins/maya/create/create_render.py index bdd237a54e..b718079b43 100644 --- a/pype/plugins/maya/create/create_render.py +++ b/pype/plugins/maya/create/create_render.py @@ -193,6 +193,7 @@ class CreateRender(avalon.maya.Creator): self.data["tilesX"] = 2 self.data["tilesY"] = 2 self.data["convertToScanline"] = False + self.data["vrayUseReferencedAovs"] = False # Disable for now as this feature is not working yet # self.data["assScene"] = False diff --git a/pype/plugins/maya/publish/collect_render.py b/pype/plugins/maya/publish/collect_render.py index 3dde3b1592..0853473120 100644 --- a/pype/plugins/maya/publish/collect_render.py +++ b/pype/plugins/maya/publish/collect_render.py @@ -149,7 +149,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): # return all expected files for all cameras and aovs in given # frame range - ef = ExpectedFiles() + ef = ExpectedFiles(render_instance) exp_files = ef.get(renderer, layer_name) self.log.info("multipart: {}".format(ef.multipart)) assert exp_files, "no file names were generated, this is bug" @@ -248,7 +248,8 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "tilesX": render_instance.data.get("tilesX") or 2, "tilesY": render_instance.data.get("tilesY") or 2, "priority": render_instance.data.get("priority"), - "convertToScanline": render_instance.data.get("convertToScanline") or False # noqa: E501 + "convertToScanline": render_instance.data.get("convertToScanline") or False, # noqa: E501 + "vrayUseReferencedAovs": render_instance.data.get("vrayUseReferencedAovs") or False # noqa: E501 } if self.sync_workfile_version: diff --git a/pype/plugins/maya/publish/validate_vray_referenced_aovs.py b/pype/plugins/maya/publish/validate_vray_referenced_aovs.py new file mode 100644 index 0000000000..120677021d --- /dev/null +++ b/pype/plugins/maya/publish/validate_vray_referenced_aovs.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +"""Validate if there are AOVs pulled from references.""" +import pyblish.api +import types +from maya import cmds + +import pype.hosts.maya.action + + +class ValidateVrayReferencedAOVs(pyblish.api.InstancePlugin): + """Validate whether the V-Ray Render Elements (AOVs) include references. + + This will check if there are AOVs pulled from references. If + `Vray Use Referenced Aovs` is checked on render instance, u must add those + manually to Render Elements as Pype will expect them to be rendered. + + """ + + order = pyblish.api.ValidatorOrder + label = 'VRay Referenced AOVs' + hosts = ['maya'] + families = ['renderlayer'] + actions = [pype.api.RepairContextAction] + + def process(self, instance): + """Plugin main entry point.""" + if instance.data.get("renderer") != "vray": + # If not V-Ray ignore.. + return + + ref_aovs = cmds.ls( + type=["VRayRenderElement", "VRayRenderElementSet"], + referencedNodes=True) + ref_aovs_enabled = ValidateVrayReferencedAOVs.maya_is_true( + cmds.getAttr("vraySettings.relements_usereferenced")) + + if not instance.data.get("vrayUseReferencedAovs"): + if ref_aovs_enabled and ref_aovs: + self.log.warning(( + "Referenced AOVs are enabled in Vray " + "Render Settings and are detected in scene, but " + "Pype render instance option for referenced AOVs is " + "disabled. Those AOVs will be rendered but not published " + "by Pype." + )) + self.log.warning(", ".join(ref_aovs)) + else: + if not ref_aovs: + self.log.warning(( + "Use of referenced AOVs enabled but there are none " + "in the scene." + )) + if not ref_aovs_enabled: + self.log.error(( + "'Use referenced' not enabled in Vray Render Settings." + )) + raise AssertionError("Invalid render settings") + + @classmethod + def repair(cls, context): + """Repair action.""" + vray_settings = cmds.ls(type="VRaySettingsNode") + if not vray_settings: + node = cmds.createNode("VRaySettingsNode") + else: + node = vray_settings[0] + + cmds.setAttr("{}.relements_usereferenced".format(node), True) + + @staticmethod + def maya_is_true(attr_val): + """Whether a Maya attr evaluates to True. + + When querying an attribute value from an ambiguous object the + Maya API will return a list of values, which need to be properly + handled to evaluate properly. + + Args: + attr_val (mixed): Maya attribute to be evaluated as bool. + + Returns: + bool: cast Maya attribute to Pythons boolean value. + + """ + if isinstance(attr_val, types.BooleanType): + return attr_val + elif isinstance(attr_val, (types.ListType, types.GeneratorType)): + return any(attr_val) + else: + return bool(attr_val) diff --git a/pype/settings/defaults/project_settings/ftrack.json b/pype/settings/defaults/project_settings/ftrack.json index 941c091f5f..3801ff5ffc 100644 --- a/pype/settings/defaults/project_settings/ftrack.json +++ b/pype/settings/defaults/project_settings/ftrack.json @@ -97,13 +97,11 @@ "ignored_statuses": [ "In Progress", "Omitted", - "On hold" + "On hold", + "Approved" ], "status_change": { - "In Progress": [], - "Ready": [ - "Not Ready" - ] + "In Progress": [] } }, "create_update_attributes": { @@ -170,7 +168,8 @@ "sync_to_avalon_local": { "enabled": true, "role_list": [ - "Pypeclub" + "Pypeclub", + "Administrator" ] }, "seed_project": {