diff --git a/pype/ftrack/actions/action_sync_hier_attrs.py b/pype/ftrack/actions/action_sync_hier_attrs.py new file mode 100644 index 0000000000..432cd6b493 --- /dev/null +++ b/pype/ftrack/actions/action_sync_hier_attrs.py @@ -0,0 +1,214 @@ +import os +import sys +import argparse +import logging +import collections + +from pype.vendor import ftrack_api +from pype.ftrack import BaseAction, lib +from avalon.tools.libraryloader.io_nonsingleton import DbConnector +from bson.objectid import ObjectId + + +class SyncHierarchicalAttrs(BaseAction): + + db_con = DbConnector() + ca_mongoid = lib.get_ca_mongoid() + + #: Action identifier. + identifier = 'sync.hierarchical.attrs' + #: Action label. + label = 'Sync hierarchical attributes' + #: Action description. + description = 'Synchronize hierarchical attributes' + #: Icon + icon = '{}/ftrack/action_icons/SyncHierarchicalAttrs.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') + ) + + #: roles that are allowed to register this action + role_list = ['Administrator'] + + def discover(self, session, entities, event): + ''' Validation ''' + for entity in entities: + if ( + entity['context_type'].lower() in ('show', 'task') and + entity.entity_type.lower() != 'task' + ): + return True + return False + + def launch(self, session, entities, event): + # Collect hierarchical attrs + custom_attributes = {} + all_avalon_attr = session.query( + 'CustomAttributeGroup where name is "avalon"' + ).one() + for cust_attr in all_avalon_attr['custom_attribute_configurations']: + if 'avalon_' in cust_attr['key']: + continue + + if not cust_attr['is_hierarchical']: + continue + + if cust_attr['default']: + self.log.warning(( + 'Custom attribute "{}" has set default value.' + ' This attribute can\'t be synchronized' + ).format(cust_attr['label'])) + continue + + custom_attributes[cust_attr['key']] = cust_attr + + if not custom_attributes: + msg = 'No hierarchical attributes to sync.' + self.log.debug(msg) + return { + 'success': True, + 'message': msg + } + + entity = entities[0] + if entity.entity_type.lower() == 'project': + project_name = entity['full_name'] + else: + project_name = entity['project']['full_name'] + + self.db_con.install() + self.db_con.Session['AVALON_PROJECT'] = project_name + + for entity in entities: + for key in custom_attributes: + # check if entity has that attribute + if key not in entity['custom_attributes']: + self.log.debug( + 'Hierachical attribute "{}" not found on "{}"'.format( + key, entity.get('name', entity) + ) + ) + continue + + value = self.get_hierarchical_value(key, entity) + if value is None: + self.log.warning( + 'Hierarchical attribute "{}" not set on "{}"'.format( + key, entity.get('name', entity) + ) + ) + continue + + self.update_hierarchical_attribute(entity, key, value) + + self.db_con.uninstall() + + return True + + def get_hierarchical_value(self, key, entity): + value = entity['custom_attributes'][key] + if ( + value is not None or + entity.entity_type.lower() == 'project' + ): + return value + + return self.get_hierarchical_value(key, entity['parent']) + + def update_hierarchical_attribute(self, entity, key, value): + if ( + entity['context_type'].lower() not in ('show', 'task') or + entity.entity_type.lower() == 'task' + ): + return + # collect entity's custom attributes + custom_attributes = entity.get('custom_attributes') + if not custom_attributes: + return + + mongoid = custom_attributes.get(self.ca_mongoid) + if not mongoid: + self.log.debug('Entity "{}" is not synchronized to avalon.'.format( + entity.get('name', entity) + )) + return + + try: + mongoid = ObjectId(mongoid) + except Exception: + self.log.warning('Entity "{}" has stored invalid MongoID.'.format( + entity.get('name', entity) + )) + return + # Find entity in Mongo DB + mongo_entity = self.db_con.find_one({'_id': mongoid}) + if not mongo_entity: + self.log.warning( + 'Entity "{}" is not synchronized to avalon.'.format( + entity.get('name', entity) + ) + ) + return + + # Change value if entity has set it's own + entity_value = custom_attributes[key] + if entity_value is not None: + value = entity_value + + data = mongo_entity.get('data') or {} + + data[key] = value + self.db_con.update_many( + {'_id': mongoid}, + {'$set': {'data': data}} + ) + + for child in entity.get('children', []): + self.update_hierarchical_attribute(child, key, value) + + +def register(session, **kw): + '''Register plugin. Called when used as an plugin.''' + + if not isinstance(session, ftrack_api.session.Session): + return + + SyncHierarchicalAttrs(session).register() + + +def main(arguments=None): + '''Set up logging and register action.''' + if arguments is None: + arguments = [] + + parser = argparse.ArgumentParser() + # Allow setting of logging level from arguments. + loggingLevels = {} + for level in ( + logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING, + logging.ERROR, logging.CRITICAL + ): + loggingLevels[logging.getLevelName(level).lower()] = level + + parser.add_argument( + '-v', '--verbosity', + help='Set the logging output verbosity.', + choices=loggingLevels.keys(), + default='info' + ) + namespace = parser.parse_args(arguments) + + # Set up basic logging + logging.basicConfig(level=loggingLevels[namespace.verbosity]) + + session = ftrack_api.Session() + register(session) + + # Wait for events + logging.info( + 'Registered actions and listening for events. Use Ctrl-C to abort.' + ) + session.event_hub.wait() + + +if __name__ == '__main__': + raise SystemExit(main(sys.argv[1:])) diff --git a/pype/ftrack/events/event_sync_hier_attr.py b/pype/ftrack/events/event_sync_hier_attr.py new file mode 100644 index 0000000000..da033e3e0c --- /dev/null +++ b/pype/ftrack/events/event_sync_hier_attr.py @@ -0,0 +1,122 @@ +import os +import sys + +from avalon.tools.libraryloader.io_nonsingleton import DbConnector + +from pype.vendor import ftrack_api +from pype.ftrack import BaseEvent, lib +from bson.objectid import ObjectId + + +class SyncHierarchicalAttrs(BaseEvent): + # After sync to avalon event! + priority = 101 + db_con = DbConnector() + ca_mongoid = lib.get_ca_mongoid() + + def launch(self, session, event): + # Filter entities and changed values if it makes sence to run script + processable = [] + processable_ent = {} + for ent in event['data']['entities']: + keys = ent.get('keys') + if not keys: + continue + + entity = session.get(ent['entity_type'], ent['entityId']) + processable.append(ent) + processable_ent[ent['entityId']] = entity + + if not processable: + return True + + for entity in processable_ent.values(): + try: + base_proj = entity['link'][0] + except Exception: + continue + ft_project = session.get(base_proj['type'], base_proj['id']) + break + + # check if project is set to auto-sync + if ( + ft_project is None or + 'avalon_auto_sync' not in ft_project['custom_attributes'] or + ft_project['custom_attributes']['avalon_auto_sync'] is False + ): + return True + + custom_attributes = {} + query = 'CustomAttributeGroup where name is "avalon"' + all_avalon_attr = session.query(query).one() + for cust_attr in all_avalon_attr['custom_attribute_configurations']: + if 'avalon_' in cust_attr['key']: + continue + if not cust_attr['is_hierarchical']: + continue + custom_attributes[cust_attr['key']] = cust_attr + + if not custom_attributes: + return True + + self.db_con.install() + self.db_con.Session['AVALON_PROJECT'] = ft_project['full_name'] + + for ent in processable: + for key in ent['keys']: + if key not in custom_attributes: + continue + + entity = processable_ent[ent['entityId']] + attr_value = entity['custom_attributes'][key] + self.update_hierarchical_attribute(entity, key, attr_value) + + self.db_con.uninstall() + + return True + + def update_hierarchical_attribute(self, entity, key, value): + custom_attributes = entity.get('custom_attributes') + if not custom_attributes: + return + + mongoid = custom_attributes.get(self.ca_mongoid) + if not mongoid: + return + + try: + mongoid = ObjectId(mongoid) + except Exception: + return + + mongo_entity = self.db_con.find_one({'_id': mongoid}) + if not mongo_entity: + return + + data = mongo_entity.get('data') or {} + cur_value = data.get(key) + if cur_value: + if cur_value == value: + return + + data[key] = value + self.db_con.update_many( + {'_id': mongoid}, + {'$set': {'data': data}} + ) + + for child in entity.get('children', []): + if key not in child.get('custom_attributes', {}): + continue + child_value = child['custom_attributes'][key] + if child_value is not None: + continue + self.update_hierarchical_attribute(child, key, value) + + +def register(session, **kw): + '''Register plugin. Called when used as an plugin.''' + if not isinstance(session, ftrack_api.session.Session): + return + + SyncHierarchicalAttrs(session).register() diff --git a/pype/ftrack/events/event_sync_to_avalon.py b/pype/ftrack/events/event_sync_to_avalon.py index 9dd7355d5e..f6b2b48a1f 100644 --- a/pype/ftrack/events/event_sync_to_avalon.py +++ b/pype/ftrack/events/event_sync_to_avalon.py @@ -4,6 +4,8 @@ from pype.ftrack import BaseEvent, lib class Sync_to_Avalon(BaseEvent): + priority = 100 + ignore_entityType = [ 'assetversion', 'job', 'user', 'reviewsessionobject', 'timer', 'socialfeed', 'socialnotification', 'timelog' diff --git a/pype/ftrack/lib/avalon_sync.py b/pype/ftrack/lib/avalon_sync.py index f0112e84e1..775b8c0332 100644 --- a/pype/ftrack/lib/avalon_sync.py +++ b/pype/ftrack/lib/avalon_sync.py @@ -132,13 +132,22 @@ def import_to_avalon( entity, session, custom_attributes ) + cur_data = av_project.get('data') or {} + + enter_data = {} + for k, v in cur_data.items(): + enter_data[k] = v + for k, v in data.items(): + enter_data[k] = v + database[project_name].update_many( {'_id': ObjectId(projectId)}, {'$set': { 'name': project_name, 'config': config, - 'data': data, - }}) + 'data': data + }} + ) entity['custom_attributes'][ca_mongoid] = str(projectId) session.commit() @@ -293,6 +302,18 @@ def import_to_avalon( output['errors'] = errors return output + avalon_asset = database[project_name].find_one( + {'_id': ObjectId(mongo_id)} + ) + + cur_data = avalon_asset.get('data') or {} + + enter_data = {} + for k, v in cur_data.items(): + enter_data[k] = v + for k, v in data.items(): + enter_data[k] = v + database[project_name].update_many( {'_id': ObjectId(mongo_id)}, {'$set': { @@ -359,6 +380,10 @@ def get_data(entity, session, custom_attributes): data['entityType'] = entity_type for cust_attr in custom_attributes: + # skip hierarchical attributes + if cust_attr.get('is_hierarchical', False): + continue + key = cust_attr['key'] if cust_attr['entity_type'].lower() in ['asset']: data[key] = entity['custom_attributes'][key] diff --git a/res/ftrack/action_icons/SyncHierarchicalAttrs.svg b/res/ftrack/action_icons/SyncHierarchicalAttrs.svg new file mode 100644 index 0000000000..0c59189168 --- /dev/null +++ b/res/ftrack/action_icons/SyncHierarchicalAttrs.svg @@ -0,0 +1 @@ + \ No newline at end of file