diff --git a/pype/ftrack/actions/action_asset_delete.py b/pype/ftrack/actions/action_asset_delete.py new file mode 100644 index 0000000000..5c91acfff7 --- /dev/null +++ b/pype/ftrack/actions/action_asset_delete.py @@ -0,0 +1,131 @@ +import sys +import argparse +import logging +import getpass +import ftrack_api +from ftrack_action_handler import BaseAction + + +class AssetDelete(BaseAction): + '''Custom action.''' + + #: Action identifier. + identifier = 'asset.delete' + #: Action label. + label = 'Asset Delete' + + + def discover(self, session, entities, event): + ''' Validation ''' + + if (len(entities) != 1 or entities[0].entity_type + not in ['Shot', 'Asset Build']): + return False + + return True + + + def interface(self, session, entities, event): + + if not event['data'].get('values', {}): + entity = entities[0] + + items = [] + for asset in entity['assets']: + # get asset name for label + label = 'None' + if asset['name']: + label = asset['name'] + + items.append({ + 'label':label, + 'name':label, + 'value':False, + 'type':'boolean' + }) + + if len(items) < 1: + return { + 'success': False, + 'message': 'There are no assets to delete' + } + + return items + + def launch(self, session, entities, event): + + entity = entities[0] + # if values were set remove those items + if 'values' in event['data']: + values = event['data']['values'] + # get list of assets to delete from form + to_delete = [] + for key in values: + if values[key]: + to_delete.append(key) + # delete them by name + for asset in entity['assets']: + if asset['name'] in to_delete: + session.delete(asset) + try: + session.commit() + except: + session.rollback() + raise + + return { + 'success': True, + 'message': 'Asset deleted.' + } + + +def register(session, **kw): + '''Register action. Called when used as an event plugin.''' + + # Validate that session is an instance of ftrack_api.Session. If not, + # assume that register is being called from an old or incompatible API and + # return without doing anything. + if not isinstance(session, ftrack_api.session.Session): + return + + action_handler = AssetDelete(session) + action_handler.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_client_review_sort.py b/pype/ftrack/actions/action_client_review_sort.py index 4df73178e9..7c364e4505 100644 --- a/pype/ftrack/actions/action_client_review_sort.py +++ b/pype/ftrack/actions/action_client_review_sort.py @@ -19,22 +19,10 @@ class ClientReviewSort(BaseAction): label = 'Sort Review' - def validateSelection(self, entities): - '''Return true if the selection is valid. ''' - - if len(entities) == 0: - return False - - return True - - def discover(self, session, entities, event): - '''Return action config if triggered on a single selection.''' + ''' Validation ''' - selection = event['data']['selection'] - # this action will only handle a single version. - if (not self.validateSelection(entities) or - selection[0]['entityType'] != 'reviewsession'): + if (len(entities) == 0 or entities[0].entity_type != 'ReviewSession'): return False return True @@ -42,8 +30,7 @@ class ClientReviewSort(BaseAction): def launch(self, session, entities, event): - entity_type, entity_id = entities[0] - entity = session.get(entity_type, entity_id) + entity = entities[0] # Get all objects from Review Session and all 'sort order' possibilities obj_list = [] @@ -53,8 +40,8 @@ class ClientReviewSort(BaseAction): sort_order_list.append(obj['sort_order']) # Sort criteria - obj_list = sorted(obj_list, key=lambda k: k['asset_version']['task']['name']) obj_list = sorted(obj_list, key=lambda k: k['version']) + obj_list = sorted(obj_list, key=lambda k: k['asset_version']['task']['name']) obj_list = sorted(obj_list, key=lambda k: k['name']) # Set 'sort order' to sorted list, so they are sorted in Ftrack also diff --git a/pype/ftrack/actions/action_component_open.py b/pype/ftrack/actions/action_component_open.py new file mode 100644 index 0000000000..1c67f4f22f --- /dev/null +++ b/pype/ftrack/actions/action_component_open.py @@ -0,0 +1,119 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2015 Milan Kolar + +import sys +import argparse +import logging +import getpass +import subprocess +import os +import ftrack_api +from ftrack_action_handler import BaseAction + + +class ComponentOpen(BaseAction): + '''Custom action.''' + + # Action identifier + identifier = 'component.open' + # Action label + label = 'Open File' + # Action icon + icon = 'https://cdn4.iconfinder.com/data/icons/rcons-application/32/application_go_run-256.png', + + + def discover(self, session, entities, event): + ''' Validation ''' + + if len(entities) != 1 or entities[0].entity_type != 'Component': + return False + + return True + + + def launch(self, session, entities, event): + + entity = entities[0] + + # Return error if component is on ftrack server + if entity['component_locations'][0]['location']['name'] == 'ftrack.server': + return { + 'success': False, + 'message': "This component is stored on ftrack server!" + } + + # Get component filepath + fpath = entity['component_locations'][0]['resource_identifier'] + + if os.path.isfile(fpath): + if sys.platform == 'win': # windows + subprocess.Popen('explorer "%s"' % fpath) + elif sys.platform == 'darwin': # macOS + subprocess.Popen(['open', fpath]) + else: # linux + try: + subprocess.Popen(['xdg-open', fpath]) + except OSError: + raise OSError('unsupported xdg-open call??') + else: + return { + 'success': False, + 'message': "Didn't found file: " + fpath + } + + return { + 'success': True, + 'message': 'Component Opened' + } + + +def register(session, **kw): + '''Register action. Called when used as an event plugin.''' + + # Validate that session is an instance of ftrack_api.Session. If not, + # assume that register is being called from an old or incompatible API and + # return without doing anything. + if not isinstance(session, ftrack_api.session.Session): + return + + action_handler = ComponentOpen(session) + action_handler.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_createCustomAttributes.py b/pype/ftrack/actions/action_createCustomAttributes.py index 70085f9e69..bba05e7a77 100644 --- a/pype/ftrack/actions/action_createCustomAttributes.py +++ b/pype/ftrack/actions/action_createCustomAttributes.py @@ -21,27 +21,16 @@ class AvalonIdAttribute(BaseAction): #: Action description. description = 'Creates Avalon/Mongo ID for double check' - def validate_selection(self, session, entities): - '''Return if *entities* is a valid selection.''' - # if (len(entities) != 1): - # # If entities contains more than one item return early since - # # metadata cannot be edited for several entites at the same time. - # return False - # entity_type, entity_id = entities[0] - # if ( - # entity_type not in session.types - # ): - # # Return False if the target entity does not have a metadata - # # attribute. - # return False - pass - return True def discover(self, session, entities, event): - '''Return True if action is valid.''' + ''' Validation ''' - self.logger.info('Got selection: {0}'.format(entities)) - return self.validate_selection(session, entities) + # userId = event['source']['user']['id'] + # user = session.query('User where id is ' + userId).one() + # if user['user_security_roles'][0]['security_role']['name'] != 'Administrator': + # return False + + return True def importToAvalon(self, session, entity): diff --git a/pype/ftrack/actions/action_delete_unpublished.py b/pype/ftrack/actions/action_delete_unpublished.py new file mode 100644 index 0000000000..effa66072e --- /dev/null +++ b/pype/ftrack/actions/action_delete_unpublished.py @@ -0,0 +1,96 @@ +import sys +import argparse +import logging +import getpass +import ftrack_api +from ftrack_action_handler import BaseAction + + +class VersionsCleanup(BaseAction): + '''Custom action.''' + + # Action identifier + identifier = 'versions.cleanup' + # Action label + label = 'Versions cleanup' + + + def discover(self, session, entities, event): + ''' Validation ''' + + # Only 1 AssetVersion is allowed + if len(entities) != 1 or entities[0].entity_type != 'AssetVersion': + return False + + return True + + def launch(self, session, entities, event): + + entity = entities[0] + + # Go through all versions in asset + for version in entity['asset']['versions']: + if not version['is_published']: + session.delete(version) + try: + session.commit() + except: + session.rollback() + raise + + return { + 'success': True, + 'message': 'removed hidden versions' + } + + +def register(session, **kw): + '''Register action. Called when used as an event plugin.''' + + # Validate that session is an instance of ftrack_api.Session. If not, + # assume that register is being called from an old or incompatible API and + # return without doing anything. + if not isinstance(session, ftrack_api.session.Session): + return + + action_handler = VersionsCleanup(session) + action_handler.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_folders_create_lucidity.py b/pype/ftrack/actions/action_folders_create_lucidity.py new file mode 100644 index 0000000000..857b1fc35e --- /dev/null +++ b/pype/ftrack/actions/action_folders_create_lucidity.py @@ -0,0 +1,186 @@ +import logging +import os +import getpass +import argparse +import errno +import sys +import threading +import ftrack_api +from ftrack_action_handler import BaseAction + +PLUGIN_DIRECTORY = os.path.abspath( + os.path.join(os.path.dirname(__file__), '..')) + +if PLUGIN_DIRECTORY not in sys.path: + sys.path.append(PLUGIN_DIRECTORY) + +import ft_utils + + +def async(fn): + '''Run *fn* asynchronously.''' + def wrapper(*args, **kwargs): + thread = threading.Thread(target=fn, args=args, kwargs=kwargs) + thread.start() + return wrapper + + +class CreateFolders(BaseAction): + + #: Action identifier. + identifier = 'create.folders' + #: Action label. + label = 'Create Folders' + #: Action Icon. + icon = 'https://cdn1.iconfinder.com/data/icons/rcons-folder-action/32/folder_add-512.png' + + raise ValueError('Not working version of action') + @async + def createFoldersFromEntity(self, entity): + '''Generate folder structure from *entity*. + + Entity is assumed to be either a project, episode, sequence or shot. + + ''' + + root = entity.getProject().getRoot() + + self.logger.info(root) + + if entity.getObjectType() in ( + 'Episode', 'Sequence', 'Folder', 'Shot'): + objects = entity.getChildren(objectType='Shot', depth=None) + objects.append(entity) + else: + objects = entity.getChildren(depth=None) + + for obj in objects: + + tasks = obj.getTasks() + paths_collected = set([]) + if obj.getObjectType() in ( + 'Episode', 'Sequence', 'Shot', 'Folder'): + task_mask = 'shot.task' + else: + task_mask = 'asset.task' + + self.logger.info(task_mask) + + for task in tasks: + self.logger.info(task) + paths = ft_utils.getAllPathsYaml(task) + self.logger.info(paths) + for path in paths: + if task_mask in path[1].name: + temppath = os.path.join( + root, path[0].lower().replace(" ", '_').replace('\'', '')) + paths_collected.add(temppath) + + for path in paths_collected: + self.logger.info(path) + try: + os.makedirs(path) + except OSError as error: + if error.errno != errno.EEXIST: + raise + + + def discover(self, session, entities, event): + + if len(entities) == 0 or entities[0].entity_type not in [ + 'Episode', 'Sequence', 'Shot', 'Folder', 'Asset Build']: + return False + + return True + + + def launch(self, session, entities, event): + + userId = event['source']['user']['id'] + user = session.query('User where id is ' + userId).one() + + job = session.create('Job', { + 'user': user, + 'status': 'running', + 'data': json.dumps({ + 'description': 'Creating Folders' + }) + }) + + '''Callback method for custom action.''' + + try: + session.event_hub.publishReply( + event, + data={ + 'success': True, + 'message': 'Folder Creation Job Started!' + } + ) + + for entity in entities: + self.createFoldersFromEntity(entity) + + job.setStatus('done') + except: + job.setStatus('failed') + raise + + + return { + 'success': True, + 'message': 'Created Folders Successfully!' + } + + +def register(session, **kw): + '''Register plugin. Called when used as an plugin.''' + + # Validate that session is an instance of ftrack_api.Session. If not, + # assume that register is being called from an old or incompatible API and + # return without doing anything. + if not isinstance(session, ftrack_api.session.Session): + return + + action_handler = CreateFolders(session) + action_handler.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_killRunningJobs.py b/pype/ftrack/actions/action_killRunningJobs.py index 450788df0e..da8ad30e33 100644 --- a/pype/ftrack/actions/action_killRunningJobs.py +++ b/pype/ftrack/actions/action_killRunningJobs.py @@ -20,20 +20,14 @@ class JobKiller(BaseAction): description = 'Killing all running jobs younger than day' - def validate_selection(self, session, entities): - '''Return if *entities* is a valid selection.''' - pass + def discover(self, session, entities, event): + ''' Validation ''' return True - def discover(self, session, entities, event): - '''Return True if action is valid.''' - - self.logger.info('Got selection: {0}'.format(entities)) - return self.validate_selection(session, entities) def launch(self, session, entities, event): - """ JOB SETTING """ + """ GET JOB """ yesterday = datetime.date.today() - datetime.timedelta(days=1) @@ -48,7 +42,10 @@ class JobKiller(BaseAction): print('Changing Job ({}) status: {} -> failed'.format(job['id'], job['status'])) job['status'] = 'failed' - session.commit() + try: + session.commit() + except: + session.rollback() print('All running jobs were killed Successfully!') return { diff --git a/pype/ftrack/actions/action_open_folder.py b/pype/ftrack/actions/action_open_folder.py index b301d36ae7..6c582adf2a 100644 --- a/pype/ftrack/actions/action_open_folder.py +++ b/pype/ftrack/actions/action_open_folder.py @@ -18,21 +18,16 @@ class openFolder(BaseAction): label = 'Open Folders' #: Action Icon. icon = "https://cdn3.iconfinder.com/data/icons/stroke/53/Open-Folder-256.png" + raise ValueError('Not working version of action') + def discover(self, session, entities, event): + ''' Validation ''' - def validateSelection(self, selection): - '''Return true if the selection is valid. ''' - - if len(selection) == 0 or selection[0]['entityType'] in ['assetversion', 'Component']: + if len(entities) == 0 or entities[0].entity_type in ['assetversion', 'Component']: return False return True - def discover(self, session, entities, event): - selection = event['data']['selection'] - - # validate selection, and only return action if it is valid. - return self.validateSelection(selection) def get_paths(self, entity): '''Prepare all the paths for the entity. @@ -72,8 +67,6 @@ class openFolder(BaseAction): hits = set([]) for entity in entities: - entity_type, entity_id = entity - entity = session.get(entity_type, entity_id) # Get paths base on the entity. # This function needs to be chagned to fit your path logic diff --git a/pype/ftrack/actions/action_set_version.py b/pype/ftrack/actions/action_set_version.py new file mode 100644 index 0000000000..416f4db960 --- /dev/null +++ b/pype/ftrack/actions/action_set_version.py @@ -0,0 +1,125 @@ +import sys +import argparse +import logging +import getpass +import ftrack_api +from ftrack_action_handler import BaseAction + + +class SetVersion(BaseAction): + '''Custom action.''' + + #: Action identifier. + identifier = 'version.set' + + #: Action label. + label = 'Version Set' + + + def discover(self, session, entities, event): + ''' Validation ''' + + # Only 1 AssetVersion is allowed + if len(entities) != 1 or entities[0].entity_type != 'AssetVersion': + return False + + return True + + def interface(self, session, entities, event): + + if not event['data'].get('values', {}): + entity = entities[0] + + # Get actual version of asset + act_ver = entity['version'] + # Set form + items = [{ + 'label': 'Version number', + 'type': 'number', + 'name': 'version_number', + 'value': act_ver + }] + + return items + + def launch(self, session, entities, event): + + entity = entities[0] + + # Do something with the values or return a new form. + values = event['data'].get('values', {}) + # Default is action True + scs = True + msg = 'Version was changed to v{0}'.format(values['version_number']) + + if not values['version_number']: + scs = False, + msg = "You didn't enter any version." + elif int(values['version_number']) <= 0: + scs = False + msg = 'Negative or zero version is not valid.' + else: + entity['version'] = values['version_number'] + + try: + session.commit() + except: + session.rollback() + raise + + return { + 'success': scs, + 'message': msg + } + + +def register(session, **kw): + '''Register action. Called when used as an event plugin.''' + + # Validate that session is an instance of ftrack_api.Session. If not, + # assume that register is being called from an old or incompatible API and + # return without doing anything. + if not isinstance(session, ftrack_api.session.Session): + return + + action_handler = SetVersion(session) + action_handler.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_syncToAvalon.py b/pype/ftrack/actions/action_syncToAvalon.py index ed65396f37..a557a169ab 100644 --- a/pype/ftrack/actions/action_syncToAvalon.py +++ b/pype/ftrack/actions/action_syncToAvalon.py @@ -23,27 +23,11 @@ class SyncToAvalon(BaseAction): #: Action icon. icon = 'https://cdn1.iconfinder.com/data/icons/hawcons/32/699650-icon-92-inbox-download-512.png' - def validate_selection(self, session, entities): - '''Return if *entities* is a valid selection.''' - # if (len(entities) != 1): - # # If entities contains more than one item return early since - # # metadata cannot be edited for several entites at the same time. - # return False - # entity_type, entity_id = entities[0] - # if ( - # entity_type not in session.types - # ): - # # Return False if the target entity does not have a metadata - # # attribute. - # return False - pass - return True def discover(self, session, entities, event): - '''Return True if action is valid.''' + ''' Validation ''' - self.logger.info('Got selection: {0}'.format(entities)) - return self.validate_selection(session, entities) + return True def importToAvalon(self, session, entity): @@ -51,6 +35,7 @@ class SyncToAvalon(BaseAction): custAttrName = 'avalon_mongo_id' # TODO read from file, which data are in scope??? # get needed info of entity and all parents + for e in entity['link']: tmp = session.get(e['type'], e['id']) if e['name'].find(" ") == -1: @@ -198,17 +183,15 @@ class SyncToAvalon(BaseAction): # get all entities separately/unique for entity in entities: - entity_type, entity_id = entity - act_ent = session.get(entity_type, entity_id) - getShotAsset(act_ent) + getShotAsset(entity) for e in importable: self.importToAvalon(session, e) job['status'] = 'done' session.commit() - print('Synchronization to Avalon was successfull!') + except Exception as e: job['status'] = 'failed' print('During synchronization to Avalon went something wrong!') diff --git a/pype/ftrack/actions/action_test.py b/pype/ftrack/actions/action_test.py index aae44aad44..08d7704825 100644 --- a/pype/ftrack/actions/action_test.py +++ b/pype/ftrack/actions/action_test.py @@ -18,32 +18,22 @@ class TestAction(BaseAction): #: Action identifier. identifier = 'test.action' - #: Action label. label = 'Test action' - #: Action description. description = 'Test action' - def validate_selection(self, session, entities): - '''Return if *entities* is a valid selection.''' + + def discover(self, session, entities, event): + ''' Validation ''' return True - def discover(self, session, entities, event): - '''Return True if action is valid.''' - - self.logger.info('Got selection: {0}'.format(entities)) - return self.validate_selection(session, entities) def launch(self, session, entities, event): for entity in entities: - entity_type, entity_id = entity - entity = session.get(entity_type, entity_id) - - import ft_utils - print(ft_utils.getNewContext(entity)) + print("TEST") return True diff --git a/pype/ftrack/actions/action_thumbToChildern.py b/pype/ftrack/actions/action_thumbToChildern.py index 8a2889aae0..54974c22d6 100644 --- a/pype/ftrack/actions/action_thumbToChildern.py +++ b/pype/ftrack/actions/action_thumbToChildern.py @@ -20,23 +20,14 @@ class ThumbToChildren(BaseAction): # Action icon icon = "https://cdn3.iconfinder.com/data/icons/transfers/100/239322-download_transfer-128.png" - def validateSelection(self, selection): - '''Return true if the selection is valid. - - Legacy plugins can only be started from a single Task. ''' - - if len(selection) > 0: - if selection[0]['entityType'] in ['assetversion', 'task']: - return True - - return False def discover(self, session, entities, event): - '''Return action config if triggered on asset versions.''' - selection = event['data']['selection'] + ''' Validation ''' - # validate selection, and only return action if it is valid. - return self.validateSelection(selection) + if (len(entities) <= 0 or entities[0].entity_type in ['Project']): + return False + + return True def launch(self, session, entities, event): @@ -55,9 +46,6 @@ class ThumbToChildren(BaseAction): try: for entity in entities: - entity_type, entity_id = entity - entity = session.get(entity_type, entity_id) - thumbid = entity['thumbnail_id'] if thumbid: for child in entity['children']: diff --git a/pype/ftrack/actions/action_thumbToParent.py b/pype/ftrack/actions/action_thumbToParent.py index 08c6c24674..82954ae0e5 100644 --- a/pype/ftrack/actions/action_thumbToParent.py +++ b/pype/ftrack/actions/action_thumbToParent.py @@ -20,23 +20,13 @@ class ThumbToParent(BaseAction): icon = "https://cdn3.iconfinder.com/data/icons/transfers/100/239419-upload_transfer-512.png" - def validateSelection(self, selection): - '''Return true if the selection is valid. - - Legacy plugins can only be started from a single Task. - - ''' - if len(selection) > 0: - if selection[0]['entityType'] in ['assetversion', 'task']: - return True - - return False - def discover(self, session, entities, event): '''Return action config if triggered on asset versions.''' - selection = event['data']['selection'] - # validate selection, and only return action if it is valid. - return self.validateSelection(selection) + + if len(entities) <= 0 or entities[0].entity_type in ['Project']: + return False + + return True def launch(self, session, entities, event): @@ -55,9 +45,6 @@ class ThumbToParent(BaseAction): try: for entity in entities: - entity_type, entity_id = entity - entity = session.get(entity_type, entity_id) - if entity.entity_type.lower() == 'assetversion': try: parent = entity['task'] @@ -76,6 +63,7 @@ class ThumbToParent(BaseAction): # inform the user that the job is done job['status'] = 'done' session.commit() + except: # fail the job if something goes wrong job['status'] = 'failed' diff --git a/pype/ftrack/actions/batch_create.py b/pype/ftrack/actions/batch_create.py new file mode 100644 index 0000000000..c17596367c --- /dev/null +++ b/pype/ftrack/actions/batch_create.py @@ -0,0 +1,349 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2015 ftrack +import sys +import argparse +import logging +import collections +import threading +import getpass +import ftrack_api +from ftrack_action_handler import BaseAction + +STRUCTURE_NAMES = ['episode', 'sequence', 'shot'] + + +TASK_TYPE_ENUMERATOR_OPTIONS = [ + {'label': task_type.getName(), 'value': task_type.getId()} + for task_type in ftrack.getTaskTypes() +] + +TASK_TYPE_LOOKUP = dict( + (task_type.getId(), task_type.getName()) + for task_type in ftrack.getTaskTypes() +) + + +def async(fn): + '''Run *fn* asynchronously.''' + def wrapper(*args, **kwargs): + thread = threading.Thread(target=fn, args=args, kwargs=kwargs) + thread.start() + return wrapper + + +def get_names(base_name, padding, start, end, incremental): + '''Return names from expression.''' + names = [] + for part in range(start, end + incremental, incremental): + names.append( + base_name + str(part).zfill(padding) + ) + return names + + +def generate_structure(values): + '''Return structure from *values*.''' + structure = [] + + for structure_name in STRUCTURE_NAMES: + if (structure_name + '_expression') not in values: + continue + + object_expression = values[structure_name + '_expression'] + object_incremental = values[structure_name + '_incremental'] + + padding = object_expression.count('#') + _range, incremental = object_incremental.split(':') + start, end = _range.split('-') + + start = int(start) + end = int(end) + incremental = int(incremental) + + base_name = object_expression.replace('#', '') + + logging.info( + ( + 'Create from expression {expression} with {base_name}, ' + '{padding} and {start}-{end}:{incremental}' + ).format( + expression=object_expression, + base_name=base_name, + padding=padding, + start=start, + end=end, + incremental=incremental + ) + ) + + names = get_names( + base_name=base_name, + padding=padding, + start=start, + end=end, + incremental=incremental + ) + + structure.append({ + 'object_type': structure_name, + 'data': names + }) + + tasks = collections.defaultdict(dict) + for name, value in values.iteritems(): + if name.startswith('task_'): + _, index, key = name.split('_') + if key == 'bid' and value: + value = float(value) * 3600 + tasks[index][key] = value + + task_data = [] + structure.append({ + 'object_type': 'task', + 'data': task_data + }) + for task in tasks.values(): + task_data.append(task) + + return structure + + +@async +def create(parent, structure): + '''Create *structure* under *parent*.''' + return create_from_structure(parent, structure) + + +def create_from_structure(parent, structure): + '''Create *structure* under *parent*.''' + level = structure[0] + children = structure[1:] + object_type = level['object_type'] + + for data in level['data']: + + if object_type == 'episode': + new_object = parent.createEpisode(data) + + if object_type == 'sequence': + new_object = parent.createSequence(data) + + if object_type == 'shot': + new_object = parent.createShot(data) + + if object_type == 'task': + new_object = parent.createTask( + TASK_TYPE_LOOKUP[data['typeid']] + ) + new_object.set(data) + + logging.info( + 'Created {new_object} on parent {parent}'.format( + parent=parent, new_object=new_object + ) + ) + if children: + create_from_structure(new_object, children) + + +def get_form(number_of_tasks, structure_type, prefix, padding_count): + '''Return form from *number_of_tasks* and *structure_type*.''' + mappings = { + 'episode': ['episode', 'sequence', 'shot'], + 'sequence': ['sequence', 'shot'], + 'shot': ['shot'] + } + + items = [] + + for structure_name in mappings[structure_type]: + items.extend( + [ + { + 'value': '##{0}##'.format(structure_name.capitalize()), + 'type': 'label' + }, { + 'label': 'Expression', + 'type': 'text', + 'value': prefix + '#' * padding_count, + 'name': '{0}_expression'.format(structure_name) + }, { + 'label': 'Incremental', + 'type': 'text', + 'value': '10-20:10', + 'name': '{0}_incremental'.format(structure_name) + } + ] + ) + + for index in range(0, number_of_tasks): + items.extend( + [ + { + 'value': '##Template for Task{0}##'.format(index), + 'type': 'label' + }, + { + 'label': 'Type', + 'type': 'enumerator', + 'name': 'task_{0}_typeid'.format(index), + 'data': TASK_TYPE_ENUMERATOR_OPTIONS + }, + { + 'label': 'Bid', + 'type': 'number', + 'name': 'task_{0}_bid'.format(index) + } + ] + ) + + return {'items': items} + + +class BatchCreate(BaseAction): + '''Batch create objects in ftrack.''' + + #: Action identifier. + identifier = 'batch_create' + #: Action label. + label = 'Batch create' + + def discover(self, session, entities, event): + + if (len(entities) != 1 or entities[0].entity_type.lower() + not in ['project', 'episode', 'sequence']): + return False + + return True + + def interface(self, session, entities, event): + if 'values' not in event['data']: + data = [ + { + 'label': 'Episode, Sequence, Shot', + 'value': 'episode' + }, { + 'label': 'Sequence, Shot', + 'value': 'sequence' + }, { + 'label': 'Shot', + 'value': 'shot' + } + ] + entity = None + data_value = 'episode' + entity_name = '' + try: + entity = ftrack.Project(selection[0]['entityId']) + entity_name = entity.getFullName() + except: + pass + try: + entity = ftrack.Task(selection[0]['entityId']) + object_type = entity.getObjectType() + entity_name = entity.getName() + + if object_type == 'Episode': + del data[0] + data_value = 'sequence' + + if object_type == 'Sequence': + del data[0] + del data[0] + data_value = 'shot' + except: + pass + return [ + { + 'label': 'Select structure', + 'type': 'enumerator', + 'value': data_value, + 'name': 'structure_type', + 'data': data + }, { + 'label': 'Padding count', + 'type': 'number', + 'name': 'padding_count', + 'value': 4 + }, { + 'label': 'Number of tasks', + 'type': 'number', + 'name': 'number_of_tasks', + 'value': 2 + } + ] + + def launch(self, session, entities, event): + '''Callback method for action.''' + selection = event['data'].get('selection', []) + values = event['data'].get('values', {}) + if values: + if 'number_of_tasks' in values: + form = get_form( + int(values['number_of_tasks']), + values['structure_type'], + entity_name + '_', + int(values['padding_count']) + ) + return form + + else: + structure = generate_structure(values) + logging.info('Creating structure "{0}"'.format(str(structure))) + create(entity, structure) + return { + 'success': True, + 'message': 'Action completed successfully' + } + + +def register(session, **kw): + '''Register plugin. Called when used as an plugin.''' + + # Validate that session is an instance of ftrack_api.Session. If not, + # assume that register is being called from an old or incompatible API and + # return without doing anything. + if not isinstance(session, ftrack_api.session.Session): + return + + action_handler = BatchCreate(session) + action_handler.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/djvview_launch.py b/pype/ftrack/actions/djvview_launch.py new file mode 100644 index 0000000000..6b3e491cff --- /dev/null +++ b/pype/ftrack/actions/djvview_launch.py @@ -0,0 +1,115 @@ +import os +import logging +import json + +import ftrack +import ftrack_api +import clique +import ftrack_template + +log = logging.getLogger(__name__) + + +def modify_launch(session, event): + """Modify the application launch command with potential files to open""" + + # Collect published paths + data = {} + for item in event["data"].get("selection", []): + + versions = [] + + if entity.entity_type == "Assetversion": + version = ftrack.AssetVersion(item["entityId"]) + if version.getAsset().getType().getShort() in ["img", "mov"]: + versions.append(version) + + # Add latest version of "img" and "mov" type from tasks. + if item["entityType"] == "task": + task = ftrack.Task(item["entityId"]) + for asset in task.getAssets(assetTypes=["img", "mov"]): + versions.append(asset.getVersions()[-1]) + + for version in versions: + for component in version.getComponents(): + component_list = data.get(component.getName(), []) + component_list.append(component) + data[component.getName()] = component_list + + label = "v{0} - {1} - {2}" + label = label.format( + str(version.getVersion()).zfill(3), + version.getAsset().getType().getName(), + component.getName() + ) + + file_path = component.getFilesystemPath() + if component.isSequence(): + if component.getMembers(): + frame = int(component.getMembers()[0].getName()) + file_path = file_path % frame + + event["data"]["items"].append( + {"label": label, "value": file_path} + ) + + # Collect workspace paths + session = ftrack_api.Session() + for item in event["data"].get("selection", []): + if item["entityType"] == "task": + templates = ftrack_template.discover_templates() + task_area, template = ftrack_template.format( + {}, templates, entity=session.get("Task", item["entityId"]) + ) + + # Traverse directory and collect collections from json files. + instances = [] + for root, dirs, files in os.walk(task_area): + for f in files: + if f.endswith(".json"): + with open(os.path.join(root, f)) as json_data: + for data in json.load(json_data): + instances.append(data) + + check_values = [] + for data in instances: + if "collection" in data: + + # Check all files in the collection + collection = clique.parse(data["collection"]) + for f in list(collection): + if not os.path.exists(f): + collection.remove(f) + + if list(collection): + value = list(collection)[0] + + # Check if value already exists + if value in check_values: + continue + else: + check_values.append(value) + + # Add workspace items + event["data"]["items"].append( + { + "label": "{0} - {1}".format( + data["name"], + os.path.basename(collection.format()) + ), + "value": value + } + ) + + return event + + +def register(session, **kw): + # Validate session + if not isinstance(session, ftrack_api.session.Session): + return + + session.event_hub.subscribe( + 'topic=djvview.launch', + modify_launch(session) + ) diff --git a/pype/ftrack/actions/ftrack_action_handler.py b/pype/ftrack/actions/ftrack_action_handler.py index 80fbaabb5d..d1e2ea58df 100644 --- a/pype/ftrack/actions/ftrack_action_handler.py +++ b/pype/ftrack/actions/ftrack_action_handler.py @@ -83,6 +83,7 @@ class AppAction(object): ) if accepts: + self.logger.info('Selection is valid') return { 'items': [{ 'label': self.label, @@ -92,6 +93,8 @@ class AppAction(object): 'icon': self.icon, }] } + else: + self.logger.info('Selection is _not_ valid') def discover(self, session, entities, event): '''Return true if we can handle the selected entities. @@ -425,6 +428,7 @@ class BaseAction(object): ) if accepts: + self.logger.info(u'Discovering action with selection: {0}'.format(args[1]['data'].get('selection', []))) return { 'items': [{ 'label': self.label, @@ -462,7 +466,8 @@ class BaseAction(object): for entity in _selection: _entities.append( ( - self._get_entity_type(entity), entity.get('entityId') + session.get(self._get_entity_type(entity), entity.get('entityId')) + # self._get_entity_type(entity), entity.get('entityId') ) )