- added show_message to BaseAction

- added priorities to register
- test action show only to Pypeclub user
- custom attributes can be created from json file (instructions inside)
This commit is contained in:
Jakub Trllo 2018-12-13 11:33:35 +01:00
parent 46a1f54b82
commit a4ec8b54f5
5 changed files with 627 additions and 254 deletions

View file

@ -1,246 +0,0 @@
# :coding: utf-8
# :copyright: Copyright (c) 2017 ftrack
import sys
import argparse
import logging
import os
import json
import ftrack_api
from ftrack_action_handler import BaseAction
from avalon import io, inventory, lib
from avalon.vendor import toml
class AvalonIdAttribute(BaseAction):
'''Edit meta data action.'''
#: Action identifier.
identifier = 'avalon.id.attribute'
#: Action label.
label = 'Create Avalon Attribute'
#: Action description.
description = 'Creates Avalon/Mongo ID for double check'
def discover(self, session, entities, event):
'''
Validation
- action is only for Administrators
'''
success = False
userId = event['source']['user']['id']
user = session.query('User where id is ' + userId).one()
for role in user['user_security_roles']:
if role['security_role']['name'] == 'Administrator':
success = True
return success
def launch(self, session, entities, event):
# JOB SETTINGS
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': 'Custom Attribute creation.'
})
})
session.commit()
try:
# Checkbox for event sync
cbxSyncName = 'avalon_auto_sync'
cbxSyncLabel = 'Avalon auto-sync'
cbxSyncExist = False
# Attribute Name and Label
custAttrName = 'avalon_mongo_id'
custAttrLabel = 'Avalon/Mongo Id'
attrs_update = set()
# Types that don't need object_type_id
base = {'show'}
# Don't create custom attribute on these entity types:
exceptions = ['task', 'milestone']
exceptions.extend(base)
# Get all possible object types
all_obj_types = session.query('ObjectType').all()
count_types = len(all_obj_types)
# Filter object types by exceptions
for index in range(count_types):
i = count_types - 1 - index
name = all_obj_types[i]['name'].lower()
if " " in name:
name = name.replace(" ","")
if name in exceptions:
all_obj_types.pop(i)
# Get IDs of filtered object types
all_obj_types_id = set()
for obj in all_obj_types:
all_obj_types_id.add(obj['id'])
# Get all custom attributes
current_cust_attr = session.query('CustomAttributeConfiguration').all()
# Filter already existing AvalonMongoID attr.
for attr in current_cust_attr:
if attr['key'] == cbxSyncName:
cbxSyncExist = True
cbxAttribute = attr
if attr['key'] == custAttrName:
if attr['entity_type'] in base:
base.remove(attr['entity_type'])
attrs_update.add(attr)
if attr['object_type_id'] in all_obj_types_id:
all_obj_types_id.remove(attr['object_type_id'])
attrs_update.add(attr)
# Set session back to begin("session.query" raises error on commit)
session.rollback()
# Set security roles for attribute
role_api = session.query('SecurityRole where name is "API"').one()
role_admin = session.query('SecurityRole where name is "Administrator"').one()
roles = [role_api,role_admin]
# Set Text type of Attribute
custom_attribute_type = session.query(
'CustomAttributeType where name is "text"'
).one()
# Get/Set 'avalon' group
groups = session.query('CustomAttributeGroup where name is "avalon"').all()
if len(groups) > 1:
msg = "There are more Custom attribute groups with name 'avalon'"
self.log.warning(msg)
return { 'success': False, 'message':msg }
elif len(groups) < 1:
group = session.create('CustomAttributeGroup', {
'name': 'avalon',
})
session.commit()
else:
group = groups[0]
# Checkbox for auto-sync event / Create or Update(roles + group)
if cbxSyncExist is False:
cbxType = session.query('CustomAttributeType where name is "boolean"').first()
session.create('CustomAttributeConfiguration', {
'entity_type': 'show',
'type': cbxType,
'label': cbxSyncLabel,
'key': cbxSyncName,
'default': False,
'write_security_roles': roles,
'read_security_roles': roles,
'group':group,
})
else:
cbxAttribute['write_security_roles'] = roles
cbxAttribute['read_security_roles'] = roles
cbxAttribute['group'] = group
for entity_type in base:
# Create a custom attribute configuration.
session.create('CustomAttributeConfiguration', {
'entity_type': entity_type,
'type': custom_attribute_type,
'label': custAttrLabel,
'key': custAttrName,
'default': '',
'write_security_roles': roles,
'read_security_roles': roles,
'group':group,
'config': json.dumps({'markdown': False})
})
for type in all_obj_types_id:
# Create a custom attribute configuration.
session.create('CustomAttributeConfiguration', {
'entity_type': 'task',
'object_type_id': type,
'type': custom_attribute_type,
'label': custAttrLabel,
'key': custAttrName,
'default': '',
'write_security_roles': roles,
'read_security_roles': roles,
'group':group,
'config': json.dumps({'markdown': False})
})
for attr in attrs_update:
attr['write_security_roles'] = roles
attr['read_security_roles'] = roles
attr['group'] = group
job['status'] = 'done'
session.commit()
except Exception as e:
session.rollback()
job['status'] = 'failed'
session.commit()
self.log.error("Creating custom attributes failed ({})".format(e))
return True
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 = AvalonIdAttribute(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:]))

View file

@ -0,0 +1,576 @@
# :coding: utf-8
# :copyright: Copyright (c) 2017 ftrack
import os
import sys
import argparse
import json
import ftrack_api
from ftrack_action_handler import BaseAction
"""
This action creates/updates custom attributes.
- first part take care about avalon_mongo_id attribute
- second part is based on json file in templates:
~/PYPE-TEMPLATES/presets/ftrack/ftrack_custom_attributes.json
- you can add Custom attributes based on these conditions
*** Required ***************************************************************
label (string)
- label that will show in ftrack
key (string)
- must contain only chars [a-z0-9_]
type (string)
- type of custom attribute
- possibilities: text, boolean, date, enumerator, dynamic enumerator, number
*** Required with conditions ***********************************************
entity_type (string)
- if 'is_hierarchical' is set to False
- type of entity
- possibilities: task, show, assetversion, user, list, asset
config (dictionary)
- for each entity type different requirements and possibilities:
- enumerator: multiSelect = True/False(default: False)
data = {key_1:value_1,key_2:value_2,..,key_n:value_n}
- 'data' is Required value with enumerator
- 'key' must contain only chars [a-z0-9_]
- number: isdecimal = True/False(default: False)
- text: markdown = True/False(default: False)
object_type (string)
- IF ENTITY_TYPE is set to 'task'
- default possibilities: Folder, Shot, Sequence, Task, Library,
Milestone, Episode, Asset Build,...
*** Optional ***************************************************************
write_security_roles/read_security_roles (array of strings)
- default: ["ALL"]
- strings should be role names (e.g.: ["API", "Administrator"])
- if set to ["ALL"] - all roles will be availabled
- if first is 'except' - roles will be set to all except roles in array
- Warning: Be carefull with except - roles can be different by company
- example:
write_security_roles = ["except", "User"]
read_security_roles = ["ALL"]
- User is unable to write but can read
group (string)
- default: None
- name of group
default
- default: None
- sets default value for custom attribute:
- text -> string
- number -> integer
- enumerator -> array with string of key/s
- boolean -> bool true/false
- date -> string in format: 'YYYY.MM.DD' or 'YYYY.MM.DD HH:mm:ss'
- example: "2018.12.24" / "2018.1.1 6:0:0"
- dynamic enumerator -> DON'T HAVE DEFAULT VALUE!!!
is_hierarchical (bool)
- default: False
- will set hierachical attribute
- False by default
EXAMPLE:
{
"avalon_auto_sync": {
"label": "Avalon auto-sync",
"key": "avalon_auto_sync",
"type": "boolean",
"entity_type": "show",
"group": "avalon",
"default": false,
"write_security_role": ["API","Administrator"],
"read_security_role": ["API","Administrator"]
}
}
"""
class CustAttrException(Exception):
pass
class CustomAttributes(BaseAction):
'''Edit meta data action.'''
#: Action identifier.
identifier = 'create.update.attributes'
#: Action label.
label = 'Create/Update Avalon Attributes'
#: Action description.
description = 'Creates Avalon/Mongo ID for double check'
def __init__(self, session):
super().__init__(session)
templates = os.environ['PYPE_STUDIO_TEMPLATES']
path_items = [templates,'presets','ftrack', 'ftrack_custom_attributes.json']
self.filepath = os.path.sep.join(path_items)
# self.all_current_attributes = session.query('CustomAttributeConfiguration').all()
self.types = {}
self.object_type_ids = {}
self.groups = {}
self.security_roles = {}
self.required_keys = ['key', 'label', 'type']
self.type_posibilities = ['text', 'boolean', 'date', 'enumerator',
'dynamic enumerator', 'number']
def discover(self, session, entities, event):
'''
Validation
- action is only for Administrators
'''
success = False
userId = event['source']['user']['id']
user = session.query('User where id is ' + userId).one()
for role in user['user_security_roles']:
if role['security_role']['name'] == 'Administrator':
success = True
return success
def launch(self, session, entities, event):
# JOB SETTINGS
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': 'Custom Attribute creation.'
})
})
session.commit()
try:
self.avalon_mongo_id_attributes(session)
self.custom_attributes_from_file(session, event)
job['status'] = 'done'
session.commit()
except Exception as e:
session.rollback()
job['status'] = 'failed'
session.commit()
self.log.error("Creating custom attributes failed ({})".format(e))
return True
def avalon_mongo_id_attributes(self, session):
# Attribute Name and Label
cust_attr_name = 'avalon_mongo_id'
cust_attr_label = 'Avalon/Mongo Id'
# Types that don't need object_type_id
base = {'show'}
# Don't create custom attribute on these entity types:
exceptions = ['task', 'milestone']
exceptions.extend(base)
# Get all possible object types
all_obj_types = session.query('ObjectType').all()
# Filter object types by exceptions
filtered_types_id = set()
for obj_type in all_obj_types:
name = obj_type['name']
if " " in name:
name = name.replace(" ","")
if obj_type['name'] not in self.object_type_ids:
self.object_type_ids[name] = obj_type['id']
if name.lower() not in exceptions:
filtered_types_id.add(obj_type['id'])
# Set security roles for attribute
role_list = ["API","Administrator"]
roles = self.get_security_role(role_list)
# Set Text type of Attribute
custom_attribute_type = self.get_type('text')
# Set group to 'avalon'
group = self.get_group('avalon')
data = {}
data['key'] = cust_attr_name
data['label'] = cust_attr_label
data['type'] = custom_attribute_type
data['default'] = ''
data['write_security_roles'] = roles
data['read_security_roles'] = roles
data['group'] = group
data['config'] = json.dumps({'markdown': False})
for entity_type in base:
data['entity_type'] = entity_type
self.process_attribute(data)
data['entity_type'] = 'task'
for object_type_id in filtered_types_id:
data['object_type_id'] = str(object_type_id)
self.process_attribute(data)
def custom_attributes_from_file(self, session, event):
try:
with open(self.filepath) as data_file:
json_dict = json.load(data_file)
except Exception as e:
msg = 'Loading "Custom attribute file" Failed. Please check log for more information'
self.log.warning("{} - {}".format(msg,str(e)))
self.show_message(event, msg)
return
for cust_attr_name in json_dict:
try:
data = {}
cust_attr = json_dict[cust_attr_name]
# Get key, label, type
data.update(self.get_required(cust_attr))
# Get hierachical/ entity_type/ object_id
data.update(self.get_entity_type(cust_attr))
# Get group, default, security roles
data.update(self.get_optional(cust_attr))
# Process data
self.process_attribute(data)
except CustAttrException as cae:
msg = 'Custom attribute error "{}" - {}'.format(cust_attr_name, str(cae))
self.log.warning(msg)
self.show_message(event, msg)
return True
def process_attribute(self, data):
existing_atr = self.session.query('CustomAttributeConfiguration').all()
matching = []
for attr in existing_atr:
if (attr['key'] != data['key'] or
attr['type']['name'] != data['type']['name']):
continue
if 'is_hierarchical' in data:
if data['is_hierarchical'] == attr['is_hierarchical']:
matching.append(attr)
elif 'object_type_id' in data:
if (attr['entity_type'] == data['entity_type'] and
attr['object_type_id'] == data['object_type_id']):
matching.append(attr)
else:
if attr['entity_type'] == data['entity_type']:
matching.append(attr)
if len(matching) == 0:
self.session.create('CustomAttributeConfiguration', data)
self.session.commit()
elif len(matching) == 1:
attr_update = matching[0]
for key in data:
if key not in ['is_hierarchical','entity_type', 'object_type_id']:
attr_update[key] = data[key]
self.session.commit()
else:
raise CustAttrException("Is duplicated")
def get_required(self, attr):
output = {}
for key in self.required_keys:
if key not in attr:
raise CustAttrException("Key {} is required - please set".format(key))
if attr['type'].lower() not in self.type_posibilities:
raise CustAttrException("Type {} is not valid".format(attr['type']))
type_name = attr['type'].lower()
output['key'] = attr['key']
output['label'] = attr['label']
output['type'] = self.get_type(type_name)
config = None
if type_name == 'number':
config = self.get_number_config(attr)
elif type_name == 'text':
config = self.get_text_config(attr)
elif type_name == 'enumerator':
config = self.get_enumerator_config(attr)
if config is not None:
output['config'] = config
return output
def get_number_config(self, attr):
if 'config' in attr and 'isdecimal' in attr['config']:
isdecimal = attr['config']['isdecimal']
else:
isdecimal = False
config = json.dumps({'isdecimal':isdecimal})
return config
def get_text_config(self, attr):
if 'config' in attr and 'markdown' in attr['config']:
markdown = attr['config']['markdown']
else:
markdown = False
config = json.dumps({'markdown':markdown})
return config
def get_enumerator_config(self,attr):
if 'config' not in attr:
raise CustAttrException("Missing config with data")
if 'data' not in attr['config']:
raise CustAttrException("Missing data in config")
data = []
for item in attr['config']['data']:
item_data = {}
for key in item:
# TODO key check by regex
item_data['menu'] = item[key]
item_data['value'] = key
data.append(item_data)
if 'multiSelect' in attr['config']:
multiSelect = attr['config']['multiSelect']
else:
multiSelect = False
config = json.dumps({
'multiSelect':multiSelect,
'data': json.dumps(data)
})
return config
def get_group(self, attr):
if isinstance(attr, str):
group_name = attr
else:
group_name = attr['group'].lower()
if group_name in self.groups:
return self.groups[group_name]
query = 'CustomAttributeGroup where name is "{}"'.format(group_name)
groups = self.session.query(query).all()
if len(groups) == 1:
group = groups[0]
self.groups[group_name] = group
return group
elif len(groups) < 1:
group = self.session.create('CustomAttributeGroup', {
'name': group_name,
})
self.session.commit()
return group
else:
raise CustAttrException("Found more than one group '{}'".format(group_name))
def get_role_ALL(self):
role_name = 'ALL'
if role_name in self.security_roles:
all_roles = self.security_roles[role_name]
else:
all_roles = self.session.query('SecurityRole').all()
self.security_roles[role_name] = all_roles
for role in all_roles:
if role['name'] not in self.security_roles:
self.security_roles[role['name']] = role
return all_roles
def get_security_role(self, security_roles):
roles = []
if len(security_roles) == 0 or security_roles[0] == 'ALL':
roles = self.get_role_ALL()
elif security_roles[0] == 'except':
excepts = security_roles[1:]
all = self.get_role_ALL()
for role in all:
if role['name'] not in excepts:
roles.append(role)
if role['name'] not in self.security_roles:
self.security_roles[role['name']] = role
else:
for role_name in security_roles:
if role_name in self.security_roles:
roles.append(self.security_roles[role_name])
continue
try:
query = 'SecurityRole where name is "{}"'.format(role_name)
role = self.session.query(query).one()
self.security_roles[role_name] = role
roles.append(role)
except Exception as e:
raise CustAttrException("Securit role '{}' does not exist".format(role_name))
return roles
def get_default(self, attr):
type = attr['type']
default = attr['default']
err_msg = 'Default value is not'
if type == 'number':
if not isinstance(default, (float, int)):
raise CustAttrException('{} integer'.format(err_msg))
elif type == 'text':
if not isinstance(default, str):
raise CustAttrException('{} string'.format(err_msg))
elif type == 'boolean':
if not isinstance(default, bool):
raise CustAttrException('{} boolean'.format(err_msg))
elif type == 'enumerator':
if not isinstance(default, array):
raise CustAttrException('{} array with strings'.format(err_msg))
# TODO check if multiSelect is available and if default is one of data menu
if not isinstance(default[0], str):
raise CustAttrException('{} array of strings'.format(err_msg))
elif type == 'date':
date_items = default.split(" ")
try:
if len(date_items) == 1:
default = arrow.get(default, 'YY.M.D')
elif len(date_items) == 2:
default = arrow.get(default, 'YY.M.D H:m:s')
else:
raise Exception
except Exception as e:
raise CustAttrException('Date is not in proper format')
elif type == 'dynamic enumerator':
raise CustAttrException('Dynamic enumerator can\'t have default')
return default
def get_optional(self, attr):
output = {}
if 'group' in attr:
output['group'] = self.get_group(attr)
if 'default' in attr:
output['default'] = self.get_default(attr)
roles_read = []
roles_write = []
if 'read_security_roles' in output:
roles_read = attr['read_security_roles']
if 'read_security_roles' in output:
roles_write = attr['write_security_roles']
output['read_security_roles'] = self.get_security_role(roles_read)
output['write_security_roles'] = self.get_security_role(roles_write)
return output
def get_type(self, type_name):
if type_name in self.types:
return self.types[type_name]
query = 'CustomAttributeType where name is "{}"'.format(type_name)
type = self.session.query(query).one()
self.types[type_name] = type
return type
def get_entity_type(self, attr):
if 'is_hierarchical' in attr:
if attr['is_hierarchical'] is True:
return {'is_hierarchical':True}
if 'entity_type' not in attr:
raise CustAttrException('Missing entity_type')
if attr['entity_type'].lower() != 'task':
return {'entity_type':attr['entity_type']}
if 'object_type' not in attr:
raise CustAttrException('Missing object_type')
object_type_name = attr['object_type']
if object_type_name not in self.object_type_ids:
try:
query = 'ObjectType where name is "{}"'.format(object_type_name)
object_type_id = self.session.query(query).one()['id']
except Exception as e:
raise CustAttrException('Object type with name "{}" don\'t exist'.format(object_type_name))
self.object_type_ids[object_type_name] = object_type_id
else:
object_type_id = self.object_type_ids[object_type_name]
return {
'entity_type': attr['entity_type'],
'object_type_id': object_type_id
}
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 = CustomAttributes(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:]))

View file

@ -309,7 +309,7 @@ def register(session, **kw):
return return
action_handler = SyncToAvalon(session) action_handler = SyncToAvalon(session)
action_handler.register() action_handler.register(200)
def main(arguments=None): def main(arguments=None):

View file

@ -27,8 +27,17 @@ class TestAction(BaseAction):
def discover(self, session, entities, event): def discover(self, session, entities, event):
''' Validation ''' ''' Validation '''
discover = False
roleList = ['Pypeclub']
userId = event['source']['user']['id']
user = session.query('User where id is ' + userId).one()
return True for role in user['user_security_roles']:
if role['security_role']['name'] in roleList:
discover = True
break
return discover
def launch(self, session, entities, event): def launch(self, session, entities, event):
@ -70,7 +79,7 @@ def register(session, **kw):
return return
action_handler = TestAction(session) action_handler = TestAction(session)
action_handler.register() action_handler.register(10000)
def main(arguments=None): def main(arguments=None):

View file

@ -54,12 +54,12 @@ class AppAction(object):
'''Return current session.''' '''Return current session.'''
return self._session return self._session
def register(self): def register(self, priority = 100):
'''Registers the action, subscribing the discover and launch topics.''' '''Registers the action, subscribing the discover and launch topics.'''
self.session.event_hub.subscribe( self.session.event_hub.subscribe(
'topic=ftrack.action.discover and source.user.username={0}'.format( 'topic=ftrack.action.discover and source.user.username={0}'.format(
self.session.api_user self.session.api_user
), self._discover ), self._discover,priority=priority
) )
self.session.event_hub.subscribe( self.session.event_hub.subscribe(
@ -467,12 +467,15 @@ class BaseAction(object):
def reset_session(self): def reset_session(self):
self.session.reset() self.session.reset()
def register(self): def register(self, priority = 100):
'''Registers the action, subscribing the the discover and launch topics.''' '''
Registers the action, subscribing the the discover and launch topics.
- highest priority event will show last
'''
self.session.event_hub.subscribe( self.session.event_hub.subscribe(
'topic=ftrack.action.discover and source.user.username={0}'.format( 'topic=ftrack.action.discover and source.user.username={0}'.format(
self.session.api_user self.session.api_user
), self._discover ), self._discover, priority=priority
) )
self.session.event_hub.subscribe( self.session.event_hub.subscribe(
@ -632,6 +635,37 @@ class BaseAction(object):
''' '''
return None return None
def show_message(self, event, input_message, result = False):
"""
Shows message to user who triggered event
- event - just source of user id
- input_message - message that is shown to user
- result - changes color of message (based on ftrack settings)
- True = Violet
- False = Red
"""
if not isinstance(result, bool):
result = False
try:
message = str(input_message)
except:
return
user_id = event['source']['user']['id']
self.session.event_hub.publish(
ftrack_api.event.base.Event(
topic='ftrack.action.trigger-user-interface',
data=dict(
type='message',
success=result,
message=message
),
target='applicationId=ftrack.client.web and user.id="{0}"'.format(user_id)
),
on_error='ignore'
)
def _handle_result(self, session, result, entities, event): def _handle_result(self, session, result, entities, event):
'''Validate the returned result from the action callback''' '''Validate the returned result from the action callback'''
if isinstance(result, bool): if isinstance(result, bool):