From ee4a90baf892baf3359bb6c15cd529e5f550f3c7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 7 Jan 2019 17:05:42 +0100 Subject: [PATCH 1/2] Added show_interface to event handler. Sync to avalon modified, need add checking statuses of tasks --- pype/ftrack/events/event_sync_to_avalon.py | 137 +++++++++++++++------ pype/ftrack/events/ftrack_event_handler.py | 42 ++++--- 2 files changed, 126 insertions(+), 53 deletions(-) diff --git a/pype/ftrack/events/event_sync_to_avalon.py b/pype/ftrack/events/event_sync_to_avalon.py index 77b036880c..d1d6519a53 100644 --- a/pype/ftrack/events/event_sync_to_avalon.py +++ b/pype/ftrack/events/event_sync_to_avalon.py @@ -9,10 +9,10 @@ from avalon.vendor import toml from bson.objectid import ObjectId from pype.ftrack import ftrack_utils + class Sync_to_Avalon(BaseEvent): def launch(self, session, entities, event): - self.ca_mongoid = 'avalon_mongo_id' # If mongo_id textfield has changed: RETURN! # - infinite loop @@ -21,7 +21,7 @@ class Sync_to_Avalon(BaseEvent): if self.ca_mongoid in ent['keys']: return self.proj = None - + self.errors = [] # get project for entity in entities: try: @@ -32,10 +32,12 @@ class Sync_to_Avalon(BaseEvent): break # check if project is set to auto-sync - if (self.proj is None or + if ( + self.proj is None or 'avalon_auto_sync' not in self.proj['custom_attributes'] or - self.proj['custom_attributes']['avalon_auto_sync'] is False): - return + self.proj['custom_attributes']['avalon_auto_sync'] is False + ): + return # check if project have Custom Attribute 'avalon_mongo_id' if self.ca_mongoid not in self.proj['custom_attributes']: @@ -51,13 +53,18 @@ class Sync_to_Avalon(BaseEvent): # get avalon project if possible io.install() try: - self.avalon_project = io.find_one({"_id": ObjectId(self.projectId)}) + self.avalon_project = io.find_one({ + "_id": ObjectId(self.projectId) + }) except: self.avalon_project = None importEntities = [] if self.avalon_project is None: - self.avalon_project = io.find_one({"type": "project", "name": self.proj["full_name"]}) + self.avalon_project = io.find_one({ + "type": "project", + "name": self.proj["full_name"] + }) if self.avalon_project is None: importEntities.append(self.proj) else: @@ -69,9 +76,10 @@ class Sync_to_Avalon(BaseEvent): if entity.entity_type.lower() in ['task']: entity = entity['parent'] - try: - mongo_id = entity['custom_attributes'][self.ca_mongoid] - except: + if ( + 'custom_attributes' not in entity or + self.ca_mongoid not in entity['custom_attributes'] + ): message = "Custom attribute '{}' for '{}' is not created or don't have set permissions for API".format(self.ca_mongoid, entity.entity_type) self.log.warning(message) self.show_message(event, message, False) @@ -93,13 +101,25 @@ class Sync_to_Avalon(BaseEvent): except ValueError as ve: message = str(ve) - self.show_message(event, message, False) + items = [{ + 'label': 'Error', + 'type': 'textarea', + 'name': 'error', + 'value': message + }] + self.show_interface(event, items) self.log.warning(message) except Exception as e: message = str(e) ftrack_message = "SyncToAvalon event ended with unexpected error please check log file for more information." - self.show_message(event, ftrack_message, False) + items = [{ + 'label': 'Error', + 'type': 'textarea', + 'name': 'error', + 'value': ftrack_message + }] + self.show_interface(event, items) self.log.error(message) io.uninstall() @@ -122,21 +142,26 @@ class Sync_to_Avalon(BaseEvent): if self.avalon_project is None: inventory.save(name, config, template) - self.avalon_project = io.find_one({'type': 'project', 'name': name}) + self.avalon_project = io.find_one({'type': type, 'name': name}) elif self.avalon_project['name'] != name: - raise ValueError('You can\'t change name {} to {}, avalon DB won\'t work properly!'.format(self.avalon_project['name'], name)) + entity['name'] = self.avalon_project['name'] + session.commit() + + msg = 'You can\'t change name {} to {}, avalon wouldn\'t work properly!\nName was changed back!'.format(self.avalon_project['name'], name) + self.errors.append(msg) + return self.projectId = self.avalon_project['_id'] - data = ftrack_utils.get_data(self, entity, session,self.custom_attributes) + data = ftrack_utils.get_data(self, entity, session, self.custom_attributes) io.update_many( {"_id": ObjectId(self.projectId)}, - {'$set':{ - 'name':name, - 'config':config, - 'data':data, + {'$set': { + 'name': name, + 'config': config, + 'data': data, }}) entity['custom_attributes'][self.ca_mongoid] = str(self.projectId) @@ -146,7 +171,7 @@ class Sync_to_Avalon(BaseEvent): if self.avalon_project is None: self.importToAvalon(session, self.proj) - data = ftrack_utils.get_data(self, entity, session,self.custom_attributes) + data = ftrack_utils.get_data(self, entity, session, self.custom_attributes) # return if entity is silo if len(data['parents']) == 0: @@ -171,30 +196,64 @@ class Sync_to_Avalon(BaseEvent): if avalon_asset is None: mongo_id = inventory.create_asset(name, silo, data, ObjectId(self.projectId)) # Raise error if it seems to be different ent. with same name - elif (avalon_asset['data']['parents'] != data['parents'] or - avalon_asset['silo'] != silo): - raise ValueError('In Avalon DB already exists entity with name "{0}"'.format(name)) - elif avalon_asset['name'] != entity['name']: - raise ValueError('You can\'t change name {} to {}, avalon DB won\'t work properly - please set name back'.format(avalon_asset['name'], name)) - elif avalon_asset['silo'] != silo or avalon_asset['data']['parents'] != data['parents']: - old_path = "/".join(avalon_asset['data']['parents']) - new_path = "/".join(data['parents']) - raise ValueError('You can\'t move with entities. Entity "{}" was moved from "{}" to "{}" , avalon DB won\'t work properly'.format(avalon_asset['name'], old_path, new_path)) + elif ( + avalon_asset['data']['parents'] != data['parents'] or + avalon_asset['silo'] != silo + ): + msg = 'In Avalon DB already exists entity with name "{0}"'.format(name) + self.errors.append(msg) + return + else: + if avalon_asset['name'] != entity['name']: + self.assetNamer(session, entity, avalon_asset) + if avalon_asset['silo'] != silo or avalon_asset['data']['parents'] != data['parents']: + old_path = "/".join(avalon_asset['data']['parents']) + new_path = "/".join(data['parents']) + msg = 'You can\'t move with entities. Entity "{}" was moved from "{}" to "{}" , avalon DB won\'t work properly'.format(avalon_asset['name'], old_path, new_path) + self.errors.append(msg) + + if len(self.errors) > 0: + return io.update_many( {"_id": ObjectId(mongo_id)}, - {'$set':{ - 'name':name, - 'silo':silo, - 'data':data, + {'$set': { + 'name': name, + 'silo': silo, + 'data': data, 'parent': ObjectId(self.projectId)}}) entity['custom_attributes'][self.ca_mongoid] = str(mongo_id) + def checkChilds(self, entity): + if entity['children']: + childs = entity['children'] + for child in childs: + if child.entity_type.lower() == 'task': + pass + else: + self.checkChilds() + + def assetNamer(self, session, entity, asset): + ability = True + if entity['children']: + childs = entity['children'] + for child in childs: + if child.entity_type.lower() == 'task': + pass + + if ability is True: + return + msg = 'You can\'t change name {} to {}, avalon wouldn\'t work properly!\nPlease create new entity.\nName was changed back!'.format(avalon_asset['name'], name) + entity['name'] = asset['name'] + session.commit() + self.errors.append(msg) + def setAvalonAttributes(self): self.custom_attributes = [] - all_avalon_attr = self.session.query('CustomAttributeGroup where name is "avalon"').one() + query = 'CustomAttributeGroup where name is "avalon"' + all_avalon_attr = self.session.query(query).one() for cust_attr in all_avalon_attr['custom_attribute_configurations']: if 'avalon_' not in cust_attr['key']: self.custom_attributes.append(cust_attr) @@ -210,10 +269,13 @@ class Sync_to_Avalon(BaseEvent): self.session, *args ) return - + def _translate_event(self, session, event): - exceptions = ['assetversion', 'job', 'user', 'reviewsessionobject', 'timer', 'socialfeed', 'timelog'] - _selection = event['data'].get('entities',[]) + exceptions = [ + 'assetversion', 'job', 'user', 'reviewsessionobject', 'timer', + 'socialfeed', 'timelog' + ] + _selection = event['data'].get('entities', []) _entities = list() for entity in _selection: @@ -227,6 +289,7 @@ class Sync_to_Avalon(BaseEvent): return [_entities, event] + def register(session, **kw): '''Register plugin. Called when used as an plugin.''' diff --git a/pype/ftrack/events/ftrack_event_handler.py b/pype/ftrack/events/ftrack_event_handler.py index e6d942af06..0cb53b74a9 100644 --- a/pype/ftrack/events/ftrack_event_handler.py +++ b/pype/ftrack/events/ftrack_event_handler.py @@ -1,19 +1,7 @@ # :coding: utf-8 # :copyright: Copyright (c) 2017 ftrack -import os -import logging -import getpass -# import platform import ftrack_api -import toml -from avalon import io, lib, pipeline -from avalon import session as sess -import acre - -from app.api import ( - Templates, - Logger -) +from app.api import Logger class BaseEvent(object): @@ -47,7 +35,7 @@ class BaseEvent(object): def _translate_event(self, session, event): '''Return *event* translated structure to be used with the API.''' - _selection = event['data'].get('entities',[]) + _selection = event['data'].get('entities', []) _entities = list() for entity in _selection: @@ -119,7 +107,7 @@ class BaseEvent(object): ''' raise NotImplementedError() - def show_message(self, event, input_message, result = False): + def show_message(self, event, input_message, result=False): """ Shows message to user who triggered event - event - just source of user id @@ -137,6 +125,8 @@ class BaseEvent(object): return user_id = event['source']['user']['id'] + target = 'applicationId=ftrack.client.web and user.id="{0}"'.format(user_id) + self.session.event_hub.publish( ftrack_api.event.base.Event( topic='ftrack.action.trigger-user-interface', @@ -145,7 +135,27 @@ class BaseEvent(object): success=result, message=message ), - target='applicationId=ftrack.client.web and user.id="{0}"'.format(user_id) + target=target + ), + on_error='ignore' + ) + + def show_interface(self, event, items): + """ + Shows interface to user who triggered event + - 'items' must be list containing Ftrack interface items + """ + user_id = event['source']['user']['id'] + target = 'applicationId=ftrack.client.web and user.id="{0}"'.format(user_id) + + self.session.event_hub.publish( + ftrack_api.event.base.Event( + topic='ftrack.action.trigger-user-interface', + data=dict( + type='widget', + items=items + ), + target=target ), on_error='ignore' ) From 44e1d87b52332321c1b0903e68cf14ddb0c054da Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 8 Jan 2019 13:25:16 +0100 Subject: [PATCH 2/2] sync to avalon shows interface to user when error has happened so he can't continue to do another errors --- pype/ftrack/events/event_sync_to_avalon.py | 117 +++++++++++++++------ pype/ftrack/ftrack_utils.py | 17 +++ 2 files changed, 100 insertions(+), 34 deletions(-) diff --git a/pype/ftrack/events/event_sync_to_avalon.py b/pype/ftrack/events/event_sync_to_avalon.py index d1d6519a53..0f2cb9d29f 100644 --- a/pype/ftrack/events/event_sync_to_avalon.py +++ b/pype/ftrack/events/event_sync_to_avalon.py @@ -10,6 +10,11 @@ from bson.objectid import ObjectId from pype.ftrack import ftrack_utils +class ExpectedError(Exception): + def __init__(self, *args, **kwargs): + super().__init__(self, *args, **kwargs) + + class Sync_to_Avalon(BaseEvent): def launch(self, session, entities, event): @@ -96,19 +101,21 @@ class Sync_to_Avalon(BaseEvent): io.install() try: for entity in importEntities: - self.importToAvalon(session, entity) + self.importToAvalon(session, event, entity) session.commit() - except ValueError as ve: - message = str(ve) - items = [{ - 'label': 'Error', - 'type': 'textarea', - 'name': 'error', - 'value': message - }] + except ExpectedError as ee: + items = [] + for error in self.errors: + info = { + 'label': 'Error', + 'type': 'textarea', + 'name': 'error', + 'value': error + } + items.append(info) + self.log.warning(error) self.show_interface(event, items) - self.log.warning(message) except Exception as e: message = str(e) @@ -126,7 +133,7 @@ class Sync_to_Avalon(BaseEvent): return - def importToAvalon(self, session, entity): + def importToAvalon(self, session, event, entity): if self.ca_mongoid not in entity['custom_attributes']: raise ValueError("Custom attribute '{}' for '{}' is not created or don't have set permissions for API".format(self.ca_mongoid, entity['name'])) @@ -169,12 +176,14 @@ class Sync_to_Avalon(BaseEvent): return if self.avalon_project is None: - self.importToAvalon(session, self.proj) + self.importToAvalon(session, event, self.proj) data = ftrack_utils.get_data(self, entity, session, self.custom_attributes) - # return if entity is silo + # only check name if entity is silo if len(data['parents']) == 0: + if self.checkSilo(entity, event, session) is False: + raise ExpectedError return else: silo = data['parents'][0] @@ -205,16 +214,20 @@ class Sync_to_Avalon(BaseEvent): return else: if avalon_asset['name'] != entity['name']: - self.assetNamer(session, entity, avalon_asset) + if self.checkChilds(entity) is False: + msg = 'You can\'t change name {} to {}, avalon wouldn\'t work properly!\n\nName was changed back!\n\nCreate new entity if you want to change name.'.format(avalon_asset['name'], entity['name']) + entity['name'] = avalon_asset['name'] + session.commit() + self.errors.append(msg) if avalon_asset['silo'] != silo or avalon_asset['data']['parents'] != data['parents']: old_path = "/".join(avalon_asset['data']['parents']) new_path = "/".join(data['parents']) - msg = 'You can\'t move with entities. Entity "{}" was moved from "{}" to "{}" , avalon DB won\'t work properly'.format(avalon_asset['name'], old_path, new_path) + msg = 'You can\'t move with entities.\nEntity "{}" was moved from "{}" to "{}"\n\nAvalon won\'t work properly, please move them back!'.format(avalon_asset['name'], old_path, new_path) self.errors.append(msg) if len(self.errors) > 0: - return + raise ExpectedError io.update_many( {"_id": ObjectId(mongo_id)}, @@ -227,28 +240,64 @@ class Sync_to_Avalon(BaseEvent): entity['custom_attributes'][self.ca_mongoid] = str(mongo_id) def checkChilds(self, entity): - if entity['children']: - childs = entity['children'] - for child in childs: - if child.entity_type.lower() == 'task': - pass + if (entity.entity_type.lower() != 'task' and 'children' not in entity): + return True + childs = entity['children'] + for child in childs: + if child.entity_type.lower() == 'task': + config = ftrack_utils.get_config_data() + if 'sync_to_avalon' in config: + config = config['sync_to_avalon'] + if 'statuses_name_change' in config: + available_statuses = config['statuses_name_change'] else: - self.checkChilds() + available_statuses = [] + ent_status = child['status']['name'].lower() + if ent_status not in available_statuses: + return False + # If not task go deeper + elif self.checkChilds(child) is False: + return False + # If everything is allright + return True - def assetNamer(self, session, entity, asset): - ability = True - if entity['children']: - childs = entity['children'] - for child in childs: - if child.entity_type.lower() == 'task': - pass + def checkSilo(self, entity, event, session): + changes = event['data']['entities'][0]['changes'] + if 'name' not in changes: + return True + new_name = changes['name']['new'] + old_name = changes['name']['old'] - if ability is True: - return - msg = 'You can\'t change name {} to {}, avalon wouldn\'t work properly!\nPlease create new entity.\nName was changed back!'.format(avalon_asset['name'], name) - entity['name'] = asset['name'] - session.commit() + if 'children' not in entity or len(entity['children']) < 1: + return True + + if self.checkChilds(entity) is True: + self.updateSilo(old_name, new_name) + return True + + new_found = 0 + old_found = 0 + for asset in io.find({'silo': new_name}): + new_found += 1 + for asset in io.find({'silo': old_name}): + old_found += 1 + + if new_found > 0 or old_found == 0: + return True + + # If any condition is possible, show error to user and change name back + msg = 'You can\'t change name {} to {}, avalon wouldn\'t work properly!\n\nName was changed back!\n\nCreate new entity if you want to change name.'.format(old_name, new_name) self.errors.append(msg) + entity['name'] = old_name + session.commit() + + return False + + def updateSilo(self, old, new): + io.update_many( + {'silo': old}, + {'$set': {'silo': new}} + ) def setAvalonAttributes(self): self.custom_attributes = [] diff --git a/pype/ftrack/ftrack_utils.py b/pype/ftrack/ftrack_utils.py index caaeb6c707..2177b3f8c3 100644 --- a/pype/ftrack/ftrack_utils.py +++ b/pype/ftrack/ftrack_utils.py @@ -1,6 +1,7 @@ import os import sys import re +import json from pprint import * import ftrack_api @@ -13,6 +14,22 @@ from app.api import Logger log = Logger.getLogger(__name__) + +def get_config_data(): + templates = os.environ['PYPE_STUDIO_TEMPLATES'] + path_items = [templates, 'presets', 'ftrack', 'ftrack_config.json'] + filepath = os.path.sep.join(path_items) + data = dict() + try: + with open(filepath) as data_file: + data = json.load(data_file) + + except Exception as e: + msg = 'Loading "Ftrack Config file" Failed. Please check log for more information. Times are set to default.' + log.warning("{} - {}".format(msg, str(e))) + + return data + def get_data(parent, entity, session, custom_attributes): entity_type = entity.entity_type