mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-01 16:34:53 +01:00
591 lines
18 KiB
Python
591 lines
18 KiB
Python
import os
|
|
import re
|
|
import json
|
|
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 pypeapp import Logger, Anatomy, config
|
|
|
|
ValidationError = jsonschema.ValidationError
|
|
|
|
log = Logger().get_logger(__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'
|
|
|
|
proj_config = get_project_config(entity)
|
|
schema.validate(proj_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:
|
|
item = {
|
|
'schema': "avalon-core:project-2.0",
|
|
'type': type,
|
|
'name': project_name,
|
|
'data': dict(),
|
|
'config': proj_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 proj_config['template'].items():
|
|
if (
|
|
key in templates and
|
|
templates[key] is not None and
|
|
templates[key] != value
|
|
):
|
|
proj_config['template'][key] = templates[key]
|
|
|
|
projectId = av_project['_id']
|
|
|
|
data = get_data(
|
|
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': proj_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 != '':
|
|
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:
|
|
item = {
|
|
'schema': "avalon-core:asset-2.0",
|
|
'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
|
|
|
|
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': {
|
|
'name': name,
|
|
'silo': silo,
|
|
'data': enter_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':
|
|
available_statuses = config.get_presets().get(
|
|
"ftrack", {}).get(
|
|
"ftrack_config", {}).get(
|
|
"sync_to_avalon", {}).get(
|
|
"statuses_name_change", []
|
|
)
|
|
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:
|
|
# 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]
|
|
|
|
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:
|
|
if key in entity['custom_attributes']:
|
|
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_avalon_project_template():
|
|
"""Get avalon template
|
|
|
|
Returns:
|
|
dictionary with templates
|
|
"""
|
|
templates = Anatomy().templates
|
|
return {
|
|
'workfile': templates["avalon"]["workfile"],
|
|
'work': templates["avalon"]["work"],
|
|
'publish': templates["avalon"]["publish"]
|
|
}
|
|
|
|
|
|
def get_project_config(entity):
|
|
proj_config = {}
|
|
proj_config['schema'] = 'avalon-core:config-1.0'
|
|
proj_config['tasks'] = get_tasks(entity)
|
|
proj_config['apps'] = get_project_apps(entity)
|
|
proj_config['template'] = get_avalon_project_template()
|
|
|
|
return proj_config
|
|
|
|
|
|
def get_tasks(project):
|
|
task_types = project['project_schema']['_task_type_schema']['types']
|
|
return [{'name': task_type['name']} for task_type in task_types]
|
|
|
|
|
|
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:
|
|
toml_path = avalon.lib.which_app(app)
|
|
if not toml_path:
|
|
log.warning((
|
|
'Missing config file for application "{}"'
|
|
).format(app))
|
|
continue
|
|
|
|
apps.append({
|
|
'name': app,
|
|
'label': toml.load(toml_path)['label']
|
|
})
|
|
|
|
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 = "avalon-core:project-2.0"
|
|
|
|
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 show_errors(obj, event, errors):
|
|
title = 'Hey You! You raised few Errors! (*look below*)'
|
|
items = []
|
|
splitter = {'type': 'label', 'value': '---'}
|
|
for error in errors:
|
|
for key, message in error.items():
|
|
error_title = {
|
|
'type': 'label',
|
|
'value': '# {}'.format(key)
|
|
}
|
|
error_message = {
|
|
'type': 'label',
|
|
'value': '<p>{}</p>'.format(message)
|
|
}
|
|
if len(items) > 0:
|
|
items.append(splitter)
|
|
items.append(error_title)
|
|
items.append(error_message)
|
|
obj.log.error(
|
|
'{}: {}'.format(key, message)
|
|
)
|
|
obj.show_interface(items, title, event=event)
|