mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-26 05:42:15 +01:00
538 lines
17 KiB
Python
538 lines
17 KiB
Python
import os
|
|
import re
|
|
import json
|
|
from pype import lib as pypelib
|
|
from pype.lib import get_avalon_database
|
|
from bson.objectid import ObjectId
|
|
import avalon
|
|
import avalon.api
|
|
from avalon import schema
|
|
from avalon.vendor import toml, jsonschema
|
|
from app.api import Logger
|
|
|
|
ValidationError = jsonschema.ValidationError
|
|
|
|
log = Logger.getLogger(__name__)
|
|
|
|
|
|
def get_ca_mongoid():
|
|
# returns name of Custom attribute that stores mongo_id
|
|
return 'avalon_mongo_id'
|
|
|
|
|
|
def import_to_avalon(
|
|
session, entity, ft_project, av_project, custom_attributes
|
|
):
|
|
database = get_avalon_database()
|
|
project_name = ft_project['full_name']
|
|
output = {}
|
|
errors = []
|
|
|
|
ca_mongoid = get_ca_mongoid()
|
|
# Validate if entity has custom attribute avalon_mongo_id
|
|
if ca_mongoid not in entity['custom_attributes']:
|
|
msg = (
|
|
'Custom attribute "{}" for "{}" is not created'
|
|
' or don\'t have set permissions for API'
|
|
).format(ca_mongoid, entity['name'])
|
|
errors.append({'Custom attribute error': msg})
|
|
output['errors'] = errors
|
|
return output
|
|
|
|
# Validate if entity name match REGEX in schema
|
|
try:
|
|
avalon_check_name(entity)
|
|
except ValidationError:
|
|
msg = '"{}" includes unsupported symbols like "dash" or "space"'
|
|
errors.append({'Unsupported character': msg})
|
|
output['errors'] = errors
|
|
return output
|
|
|
|
entity_type = entity.entity_type
|
|
# Project ////////////////////////////////////////////////////////////////
|
|
if entity_type in ['Project']:
|
|
type = 'project'
|
|
|
|
config = get_project_config(entity)
|
|
schema.validate(config)
|
|
|
|
av_project_code = None
|
|
if av_project is not None and 'code' in av_project['data']:
|
|
av_project_code = av_project['data']['code']
|
|
ft_project_code = ft_project['name']
|
|
|
|
if av_project is None:
|
|
project_schema = pypelib.get_avalon_project_template_schema()
|
|
item = {
|
|
'schema': project_schema,
|
|
'type': type,
|
|
'name': project_name,
|
|
'data': dict(),
|
|
'config': config,
|
|
'parent': None,
|
|
}
|
|
schema.validate(item)
|
|
|
|
database[project_name].insert_one(item)
|
|
|
|
av_project = database[project_name].find_one(
|
|
{'type': type}
|
|
)
|
|
|
|
elif (
|
|
av_project['name'] != project_name or
|
|
(
|
|
av_project_code is not None and
|
|
av_project_code != ft_project_code
|
|
)
|
|
):
|
|
msg = (
|
|
'You can\'t change {0} "{1}" to "{2}"'
|
|
', avalon wouldn\'t work properly!'
|
|
'\n{0} was changed back!'
|
|
)
|
|
if av_project['name'] != project_name:
|
|
entity['full_name'] = av_project['name']
|
|
errors.append(
|
|
{'Changed name error': msg.format(
|
|
'Project name', av_project['name'], project_name
|
|
)}
|
|
)
|
|
if (
|
|
av_project_code is not None and
|
|
av_project_code != ft_project_code
|
|
):
|
|
entity['name'] = av_project_code
|
|
errors.append(
|
|
{'Changed name error': msg.format(
|
|
'Project code', av_project_code, ft_project_code
|
|
)}
|
|
)
|
|
|
|
session.commit()
|
|
|
|
output['errors'] = errors
|
|
return output
|
|
|
|
else:
|
|
# not override existing templates!
|
|
templates = av_project['config'].get('template', None)
|
|
if templates is not None:
|
|
for key, value in config['template'].items():
|
|
if (
|
|
key in templates and
|
|
templates[key] is not None and
|
|
templates[key] != value
|
|
):
|
|
config['template'][key] = templates[key]
|
|
|
|
projectId = av_project['_id']
|
|
|
|
data = get_data(
|
|
entity, session, custom_attributes
|
|
)
|
|
|
|
database[project_name].update_many(
|
|
{'_id': ObjectId(projectId)},
|
|
{'$set': {
|
|
'name': project_name,
|
|
'config': config,
|
|
'data': data,
|
|
}})
|
|
|
|
entity['custom_attributes'][ca_mongoid] = str(projectId)
|
|
session.commit()
|
|
|
|
output['project'] = av_project
|
|
|
|
return output
|
|
|
|
# Asset - /////////////////////////////////////////////////////////////
|
|
if av_project is None:
|
|
result = import_to_avalon(
|
|
session, ft_project, ft_project, av_project, custom_attributes
|
|
)
|
|
|
|
if 'errors' in result:
|
|
output['errors'] = result['errors']
|
|
return output
|
|
|
|
elif 'project' not in result:
|
|
msg = 'During project import went something wrong'
|
|
errors.append({'Unexpected error': msg})
|
|
output['errors'] = errors
|
|
return output
|
|
|
|
av_project = result['project']
|
|
output['project'] = result['project']
|
|
|
|
projectId = av_project['_id']
|
|
data = get_data(
|
|
entity, session, custom_attributes
|
|
)
|
|
|
|
# 1. hierarchical entity have silo set to None
|
|
silo = None
|
|
if len(data['parents']) > 0:
|
|
silo = data['parents'][0]
|
|
|
|
name = entity['name']
|
|
|
|
avalon_asset = None
|
|
# existence of this custom attr is already checked
|
|
if ca_mongoid not in entity['custom_attributes']:
|
|
msg = '"{}" don\'t have "{}" custom attribute'
|
|
errors.append({'Missing Custom attribute': msg.format(
|
|
entity_type, ca_mongoid
|
|
)})
|
|
output['errors'] = errors
|
|
return output
|
|
|
|
mongo_id = entity['custom_attributes'][ca_mongoid]
|
|
mongo_id = mongo_id.replace(' ', '').replace('\n', '')
|
|
try:
|
|
ObjectId(mongo_id)
|
|
except Exception:
|
|
mongo_id = ''
|
|
|
|
if mongo_id is not '':
|
|
avalon_asset = database[project_name].find_one(
|
|
{'_id': ObjectId(mongo_id)}
|
|
)
|
|
|
|
if avalon_asset is None:
|
|
avalon_asset = database[project_name].find_one(
|
|
{'type': 'asset', 'name': name}
|
|
)
|
|
if avalon_asset is None:
|
|
asset_schema = pypelib.get_avalon_asset_template_schema()
|
|
item = {
|
|
'schema': asset_schema,
|
|
'name': name,
|
|
'silo': silo,
|
|
'parent': ObjectId(projectId),
|
|
'type': 'asset',
|
|
'data': data
|
|
}
|
|
schema.validate(item)
|
|
mongo_id = database[project_name].insert_one(item).inserted_id
|
|
|
|
# Raise error if it seems to be different ent. with same name
|
|
elif (
|
|
avalon_asset['data']['parents'] != data['parents'] or
|
|
avalon_asset['silo'] != silo
|
|
):
|
|
msg = (
|
|
'In Avalon DB already exists entity with name "{0}"'
|
|
).format(name)
|
|
errors.append({'Entity name duplication': msg})
|
|
output['errors'] = errors
|
|
return output
|
|
|
|
# Store new ID (in case that asset was removed from DB)
|
|
else:
|
|
mongo_id = avalon_asset['_id']
|
|
else:
|
|
if avalon_asset['name'] != entity['name']:
|
|
if silo is None or changeability_check_childs(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()
|
|
errors.append({'Changed name error': 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.'
|
|
'\nEntity "{}" was moved from "{}" to "{}"'
|
|
'\n\nAvalon won\'t work properly, {}!'
|
|
)
|
|
|
|
moved_back = False
|
|
if 'visualParent' in avalon_asset['data']:
|
|
if silo is None:
|
|
asset_parent_id = avalon_asset['parent']
|
|
else:
|
|
asset_parent_id = avalon_asset['data']['visualParent']
|
|
|
|
asset_parent = database[project_name].find_one(
|
|
{'_id': ObjectId(asset_parent_id)}
|
|
)
|
|
ft_parent_id = asset_parent['data']['ftrackId']
|
|
try:
|
|
entity['parent_id'] = ft_parent_id
|
|
session.commit()
|
|
msg = msg.format(
|
|
avalon_asset['name'], old_path, new_path,
|
|
'entity was moved back'
|
|
)
|
|
moved_back = True
|
|
|
|
except Exception:
|
|
moved_back = False
|
|
|
|
if moved_back is False:
|
|
msg = msg.format(
|
|
avalon_asset['name'], old_path, new_path,
|
|
'please move it back'
|
|
)
|
|
|
|
errors.append({'Hierarchy change error': msg})
|
|
|
|
if len(errors) > 0:
|
|
output['errors'] = errors
|
|
return output
|
|
|
|
database[project_name].update_many(
|
|
{'_id': ObjectId(mongo_id)},
|
|
{'$set': {
|
|
'name': name,
|
|
'silo': silo,
|
|
'data': data,
|
|
'parent': ObjectId(projectId)
|
|
}})
|
|
|
|
entity['custom_attributes'][ca_mongoid] = str(mongo_id)
|
|
session.commit()
|
|
|
|
return output
|
|
|
|
|
|
def get_avalon_attr(session):
|
|
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_' not in cust_attr['key']:
|
|
custom_attributes.append(cust_attr)
|
|
return custom_attributes
|
|
|
|
|
|
def changeability_check_childs(entity):
|
|
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 = 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:
|
|
available_statuses = []
|
|
ent_status = child['status']['name'].lower()
|
|
if ent_status not in available_statuses:
|
|
return False
|
|
# If not task go deeper
|
|
elif changeability_check_childs(child) is False:
|
|
return False
|
|
# If everything is allright
|
|
return True
|
|
|
|
|
|
def get_data(entity, session, custom_attributes):
|
|
database = get_avalon_database()
|
|
|
|
entity_type = entity.entity_type
|
|
|
|
if entity_type.lower() == 'project':
|
|
ft_project = entity
|
|
elif entity_type.lower() != 'project':
|
|
ft_project = entity['project']
|
|
av_project = get_avalon_project(ft_project)
|
|
|
|
project_name = ft_project['full_name']
|
|
|
|
data = {}
|
|
data['ftrackId'] = entity['id']
|
|
data['entityType'] = entity_type
|
|
|
|
for cust_attr in custom_attributes:
|
|
key = cust_attr['key']
|
|
if cust_attr['entity_type'].lower() in ['asset']:
|
|
data[key] = entity['custom_attributes'][key]
|
|
|
|
elif (
|
|
cust_attr['entity_type'].lower() in ['show'] and
|
|
entity_type.lower() == 'project'
|
|
):
|
|
data[key] = entity['custom_attributes'][key]
|
|
|
|
elif (
|
|
cust_attr['entity_type'].lower() in ['task'] and
|
|
entity_type.lower() != 'project'
|
|
):
|
|
# Put space between capitals (e.g. 'AssetBuild' -> 'Asset Build')
|
|
entity_type_full = re.sub(r"(\w)([A-Z])", r"\1 \2", entity_type)
|
|
# Get object id of entity type
|
|
query = 'ObjectType where name is "{}"'.format(entity_type_full)
|
|
ent_obj_type_id = session.query(query).one()['id']
|
|
|
|
if cust_attr['object_type_id'] == ent_obj_type_id:
|
|
data[key] = entity['custom_attributes'][key]
|
|
|
|
if entity_type in ['Project']:
|
|
data['code'] = entity['name']
|
|
return data
|
|
|
|
# Get info for 'Data' in Avalon DB
|
|
tasks = []
|
|
for child in entity['children']:
|
|
if child.entity_type in ['Task']:
|
|
tasks.append(child['name'])
|
|
|
|
# Get list of parents without project
|
|
parents = []
|
|
folderStruct = []
|
|
for i in range(1, len(entity['link'])-1):
|
|
parEnt = session.get(
|
|
entity['link'][i]['type'],
|
|
entity['link'][i]['id']
|
|
)
|
|
parName = parEnt['name']
|
|
folderStruct.append(parName)
|
|
parents.append(parEnt)
|
|
|
|
parentId = None
|
|
|
|
for parent in parents:
|
|
parentId = database[project_name].find_one(
|
|
{'type': 'asset', 'name': parName}
|
|
)['_id']
|
|
if parent['parent'].entity_type != 'project' and parentId is None:
|
|
import_to_avalon(
|
|
session, parent, ft_project, av_project, custom_attributes
|
|
)
|
|
parentId = database[project_name].find_one(
|
|
{'type': 'asset', 'name': parName}
|
|
)['_id']
|
|
|
|
hierarchy = ""
|
|
if len(folderStruct) > 0:
|
|
hierarchy = os.path.sep.join(folderStruct)
|
|
|
|
data['visualParent'] = parentId
|
|
data['parents'] = folderStruct
|
|
data['tasks'] = tasks
|
|
data['hierarchy'] = hierarchy
|
|
|
|
return data
|
|
|
|
|
|
def get_avalon_project(ft_project):
|
|
database = get_avalon_database()
|
|
project_name = ft_project['full_name']
|
|
ca_mongoid = get_ca_mongoid()
|
|
if ca_mongoid not in ft_project['custom_attributes']:
|
|
return None
|
|
|
|
# try to find by Id
|
|
project_id = ft_project['custom_attributes'][ca_mongoid]
|
|
try:
|
|
avalon_project = database[project_name].find_one({
|
|
'_id': ObjectId(project_id)
|
|
})
|
|
except Exception:
|
|
avalon_project = None
|
|
|
|
if avalon_project is None:
|
|
avalon_project = database[project_name].find_one({
|
|
'type': 'project'
|
|
})
|
|
|
|
return avalon_project
|
|
|
|
|
|
def get_project_config(entity):
|
|
config = {}
|
|
config['schema'] = pypelib.get_avalon_project_config_schema()
|
|
config['tasks'] = [{'name': ''}]
|
|
config['apps'] = get_project_apps(entity)
|
|
config['template'] = pypelib.get_avalon_project_template()
|
|
|
|
return config
|
|
|
|
|
|
def get_project_apps(entity):
|
|
""" Get apps from project
|
|
Requirements:
|
|
'Entity' MUST be object of ftrack entity with entity_type 'Project'
|
|
Checking if app from ftrack is available in Templates/bin/{app_name}.toml
|
|
|
|
Returns:
|
|
Array with dictionaries with app Name and Label
|
|
"""
|
|
apps = []
|
|
for app in entity['custom_attributes']['applications']:
|
|
try:
|
|
app_config = {}
|
|
app_config['name'] = app
|
|
app_config['label'] = toml.load(avalon.lib.which_app(app))['label']
|
|
|
|
apps.append(app_config)
|
|
|
|
except Exception as e:
|
|
log.warning('Error with application {0} - {1}'.format(app, e))
|
|
return apps
|
|
|
|
|
|
def avalon_check_name(entity, inSchema=None):
|
|
ValidationError = jsonschema.ValidationError
|
|
alright = True
|
|
name = entity['name']
|
|
if " " in name:
|
|
alright = False
|
|
|
|
data = {}
|
|
data['data'] = {}
|
|
data['type'] = 'asset'
|
|
schema = "avalon-core:asset-2.0"
|
|
# TODO have project any REGEX check?
|
|
if entity.entity_type in ['Project']:
|
|
# data['type'] = 'project'
|
|
name = entity['full_name']
|
|
# schema = get_avalon_project_template_schema()
|
|
|
|
data['silo'] = 'Film'
|
|
|
|
if inSchema is not None:
|
|
schema = inSchema
|
|
data['schema'] = schema
|
|
data['name'] = name
|
|
try:
|
|
avalon.schema.validate(data)
|
|
except ValidationError:
|
|
alright = False
|
|
|
|
if alright is False:
|
|
msg = '"{}" includes unsupported symbols like "dash" or "space"'
|
|
raise ValueError(msg.format(name))
|
|
|
|
|
|
def get_config_data():
|
|
path_items = [pypelib.get_presets_path(), '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.'
|
|
)
|
|
log.warning("{} - {}".format(msg, str(e)))
|
|
|
|
return data
|