Merge pull request #848 from pypeclub/feature/ftrack_actions_with_settings

Ftrack actions with settings
This commit is contained in:
Milan Kolar 2020-12-21 22:43:16 +01:00 committed by GitHub
commit d4f18fedba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 227 additions and 174 deletions

View file

@ -9,7 +9,6 @@ class CleanHierarchicalAttrsAction(BaseAction):
label = "Pype Admin"
variant = "- Clean hierarchical custom attributes"
description = "Unset empty hierarchical attribute values."
role_list = ["Pypeclub", "Administrator", "Project Manager"]
icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg")
all_project_entities_query = (
@ -20,12 +19,17 @@ class CleanHierarchicalAttrsAction(BaseAction):
"select value, entity_id from CustomAttributeValue "
"where entity_id in ({}) and configuration_id is \"{}\""
)
settings_key = "clean_hierarchical_attr"
def discover(self, session, entities, event):
"""Show only on project entity."""
if len(entities) == 1 and entities[0].entity_type.lower() == "project":
return True
return False
if (
len(entities) != 1
or entities[0].entity_type.lower() != "project"
):
return False
return self.valid_roles(session, entities, event)
def launch(self, session, entities, event):
project = entities[0]

View file

@ -131,9 +131,8 @@ class CustomAttributes(BaseAction):
variant = '- Create/Update Avalon Attributes'
#: Action description.
description = 'Creates Avalon/Mongo ID for double check'
#: roles that are allowed to register this action
role_list = ['Pypeclub', 'Administrator']
icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg")
settings_key = "create_update_attributes"
required_keys = ("key", "label", "type")
@ -150,7 +149,7 @@ class CustomAttributes(BaseAction):
Validation
- action is only for Administrators
'''
return True
return self.valid_roles(session, entities, event)
def launch(self, session, entities, event):
# JOB SETTINGS

View file

@ -18,8 +18,8 @@ class DeleteAssetSubset(BaseAction):
#: Action description.
description = "Removes from Avalon with all childs and asset from Ftrack"
icon = statics_icon("ftrack", "action_icons", "DeleteAsset.svg")
#: roles that are allowed to register this action
role_list = ["Pypeclub", "Administrator", "Project Manager"]
settings_key = "delete_asset_subset"
#: Db connection
dbcon = AvalonMongoDB()
@ -32,17 +32,21 @@ class DeleteAssetSubset(BaseAction):
""" Validation """
task_ids = []
for ent_info in event["data"]["selection"]:
entType = ent_info.get("entityType", "")
if entType == "task":
if ent_info.get("entityType") == "task":
task_ids.append(ent_info["entityId"])
is_valid = False
for entity in entities:
ftrack_id = entity["id"]
if ftrack_id not in task_ids:
continue
if entity.entity_type.lower() != "task":
return True
return False
if (
entity["id"] in task_ids
and entity.entity_type.lower() != "task"
):
is_valid = True
break
if is_valid:
is_valid = self.valid_roles(session, entities, event)
return is_valid
def _launch(self, event):
try:

View file

@ -21,7 +21,6 @@ class DeleteOldVersions(BaseAction):
"Delete files from older publishes so project can be"
" archived with only lates versions."
)
role_list = ["Pypeclub", "Project Manager", "Administrator"]
icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg")
dbcon = AvalonMongoDB()
@ -31,13 +30,16 @@ class DeleteOldVersions(BaseAction):
sequence_splitter = "__sequence_splitter__"
def discover(self, session, entities, event):
''' Validation '''
selection = event["data"].get("selection") or []
for entity in selection:
entity_type = (entity.get("entityType") or "").lower()
if entity_type == "assetversion":
return True
return False
""" Validation. """
is_valid = False
for entity in entities:
if entity.entity_type.lower() == "assetversion":
is_valid = True
break
if is_valid:
is_valid = self.valid_roles(session, entities, event)
return is_valid
def interface(self, session, entities, event):
# TODO Add roots existence validation

View file

@ -23,6 +23,7 @@ class Delivery(BaseAction):
description = "Deliver data to client"
role_list = ["Pypeclub", "Administrator", "Project manager"]
icon = statics_icon("ftrack", "action_icons", "Delivery.svg")
settings_key = "delivery_action"
def __init__(self, *args, **kwargs):
self.db_con = AvalonMongoDB()
@ -30,11 +31,15 @@ class Delivery(BaseAction):
super(Delivery, self).__init__(*args, **kwargs)
def discover(self, session, entities, event):
is_valid = False
for entity in entities:
if entity.entity_type.lower() == "assetversion":
return True
is_valid = True
break
return False
if is_valid:
is_valid = self.valid_roles(session, entities, event)
return is_valid
def interface(self, session, entities, event):
if event["data"].get("values", {}):

View file

@ -13,13 +13,12 @@ class JobKiller(BaseAction):
#: Action description.
description = 'Killing selected running jobs'
#: roles that are allowed to register this action
role_list = ['Pypeclub', 'Administrator']
icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg")
settings_key = "job_killer"
def discover(self, session, entities, event):
''' Validation '''
return True
return self.valid_roles(session, entities, event)
def interface(self, session, entities, event):
if not event['data'].get('values', {}):

View file

@ -16,22 +16,23 @@ class PrepareProject(BaseAction):
#: Action description.
description = 'Set basic attributes on the project'
#: roles that are allowed to register this action
role_list = ["Pypeclub", "Administrator", "Project manager"]
icon = statics_icon("ftrack", "action_icons", "PrepareProject.svg")
settings_key = "prepare_project"
# Key to store info about trigerring create folder structure
create_project_structure_key = "create_folder_structure"
item_splitter = {'type': 'label', 'value': '---'}
def discover(self, session, entities, event):
''' Validation '''
if len(entities) != 1:
if (
len(entities) != 1
or entities[0].entity_type.lower() != "project"
):
return False
if entities[0].entity_type.lower() != "project":
return False
return True
return self.valid_roles(session, entities, event)
def interface(self, session, entities, event):
if event['data'].get('values', {}):

View file

@ -15,7 +15,6 @@ class SeedDebugProject(BaseAction):
#: priority
priority = 100
#: roles that are allowed to register this action
role_list = ["Pypeclub"]
icon = statics_icon("ftrack", "action_icons", "SeedProject.svg")
# Asset names which will be created in `Assets` entity
@ -58,9 +57,12 @@ class SeedDebugProject(BaseAction):
existing_projects = None
new_project_item = "< New Project >"
current_project_item = "< Current Project >"
settings_key = "seed_project"
def discover(self, session, entities, event):
''' Validation '''
if not self.valid_roles(session, entities, event):
return False
return True
def interface(self, session, entities, event):

View file

@ -21,8 +21,8 @@ class StoreThumbnailsToAvalon(BaseAction):
# Action description
description = 'Test action'
# roles that are allowed to register this action
role_list = ["Pypeclub", "Administrator", "Project Manager"]
icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg")
settings_key = "store_thubmnail_to_avalon"
thumbnail_key = "AVALON_THUMBNAIL_ROOT"
@ -31,10 +31,15 @@ class StoreThumbnailsToAvalon(BaseAction):
super(StoreThumbnailsToAvalon, self).__init__(*args, **kwargs)
def discover(self, session, entities, event):
is_valid = False
for entity in entities:
if entity.entity_type.lower() == "assetversion":
return True
return False
is_valid = True
break
if is_valid:
is_valid = self.valid_roles(session, entities, event)
return is_valid
def launch(self, session, entities, event):
user = session.query(

View file

@ -41,20 +41,26 @@ class SyncToAvalonLocal(BaseAction):
#: priority
priority = 200
#: roles that are allowed to register this action
role_list = ["Pypeclub"]
icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg")
settings_key = "sync_to_avalon_local"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.entities_factory = SyncEntitiesFactory(self.log, self.session)
def discover(self, session, entities, event):
''' Validation '''
""" Validate selection. """
is_valid = False
for ent in event["data"]["selection"]:
# Ignore entities that are not tasks or projects
if ent["entityType"].lower() in ["show", "task"]:
return True
return False
is_valid = True
break
if is_valid:
is_valid = self.valid_roles(session, entities, event)
return is_valid
def launch(self, session, in_entities, event):
time_start = time.time()

View file

@ -15,11 +15,9 @@ class ThumbToChildren(BaseAction):
icon = statics_icon("ftrack", "action_icons", "Thumbnail.svg")
def discover(self, session, entities, event):
''' Validation '''
if (len(entities) != 1 or entities[0].entity_type in ['Project']):
"""Show only on project."""
if (len(entities) != 1 or entities[0].entity_type in ["Project"]):
return False
return True
def launch(self, session, entities, event):

View file

@ -61,8 +61,8 @@ class TaskStatusToParent(BaseEvent):
session, event, project_id
)
# Load settings
project_settings = self.get_settings_for_project(
session, event, project_entity=project_entity
project_settings = self.get_project_settings_from_event(
event, project_entity
)
# Prepare loaded settings and check if can be processed

View file

@ -102,8 +102,8 @@ class TaskToVersionStatus(BaseEvent):
project_entity = self.get_project_entity_from_event(
session, event, project_id
)
project_settings = self.get_settings_for_project(
session, event, project_entity=project_entity
project_settings = self.get_project_settings_from_event(
event, project_entity
)
project_name = project_entity["full_name"]

View file

@ -22,8 +22,8 @@ class ThumbnailEvents(BaseEvent):
project_entity = self.get_project_entity_from_event(
session, event, project_id
)
project_settings = self.get_settings_for_project(
session, event, project_entity=project_entity
project_settings = self.get_project_settings_from_event(
event, project_entity
)
project_name = project_entity["full_name"]

View file

@ -51,8 +51,8 @@ class VersionToTaskStatus(BaseEvent):
project_entity = self.get_project_entity_from_event(
session, event, project_id
)
project_settings = self.get_settings_for_project(
session, event, project_entity=project_entity
project_settings = self.get_project_settings_from_event(
event, project_entity
)
project_name = project_entity["full_name"]

View file

@ -29,6 +29,8 @@ class BaseAction(BaseHandler):
icon = None
type = 'Action'
settings_frack_subkey = "user_handlers"
def __init__(self, session):
'''Expects a ftrack_api.Session instance'''
if self.label is None:
@ -67,6 +69,9 @@ class BaseAction(BaseHandler):
def _discover(self, event):
entities = self._translate_event(event)
if not entities:
return
accepts = self.discover(self.session, entities, event)
if not accepts:
return
@ -146,21 +151,18 @@ class BaseAction(BaseHandler):
def _launch(self, event):
entities = self._translate_event(event)
if not entities:
return
preactions_launched = self._handle_preactions(self.session, event)
if preactions_launched is False:
return
interface = self._interface(
self.session, entities, event
)
interface = self._interface(self.session, entities, event)
if interface:
return interface
response = self.launch(
self.session, entities, event
)
response = self.launch(self.session, entities, event)
return self._handle_result(response)
@ -196,50 +198,29 @@ class BaseAction(BaseHandler):
return result
@staticmethod
def roles_check(settings_roles, user_roles, default=True):
"""Compare roles from setting and user's roles.
class ServerAction(BaseAction):
"""Action class meant to be used on event server.
Args:
settings_roles(list): List of role names from settings.
user_roles(list): User's lowered role names.
default(bool): If `settings_roles` is empty list.
Unlike the `BaseAction` roles are not checked on register but on discover.
For the same reason register is modified to not filter topics by username.
"""
Returns:
bool: `True` if user has at least one role from settings or
default if `settings_roles` is empty.
"""
if not settings_roles:
return default
def __init__(self, *args, **kwargs):
if not self.role_list:
self.role_list = set()
else:
self.role_list = set(
role_name.lower()
for role_name in self.role_list
)
super(ServerAction, self).__init__(*args, **kwargs)
def _register_role_check(self):
# Skip register role check.
return
def _discover(self, event):
"""Check user discover availability."""
if not self._check_user_discover(event):
return
return super(ServerAction, self)._discover(event)
def _check_user_discover(self, event):
"""Should be action discovered by user trying to show actions."""
if not self.role_list:
return True
user_entity = self._get_user_entity(event)
if not user_entity:
return False
for role in user_entity["user_security_roles"]:
lowered_role = role["security_role"]["name"].lower()
if lowered_role in self.role_list:
for role_name in settings_roles:
if role_name.lower() in user_roles:
return True
return False
def _get_user_entity(self, event):
@classmethod
def get_user_entity_from_event(cls, session, event):
"""Query user entity from event."""
not_set = object()
@ -251,17 +232,88 @@ class ServerAction(BaseAction):
user_id = user_info.get("id")
username = user_info.get("username")
if user_id:
user_entity = self.session.query(
user_entity = session.query(
"User where id is {}".format(user_id)
).first()
if not user_entity and username:
user_entity = self.session.query(
user_entity = session.query(
"User where username is {}".format(username)
).first()
event["data"]["user_entity"] = user_entity
return user_entity
@classmethod
def get_user_roles_from_event(cls, session, event):
"""Query user entity from event."""
not_set = object()
user_roles = event["data"].get("user_roles", not_set)
if user_roles is not_set:
user_roles = []
user_entity = cls.get_user_entity_from_event(session, event)
for role in user_entity["user_security_roles"]:
user_roles.append(role["security_role"]["name"].lower())
event["data"]["user_roles"] = user_roles
return user_roles
def get_project_entity_from_event(self, session, event, entities):
"""Load or query and fill project entity from/to event data.
Project data are stored by ftrack id because in most cases it is
easier to access project id than project name.
Args:
session (ftrack_api.Session): Current session.
event (ftrack_api.Event): Processed event by session.
entities (list): Ftrack entities of selection.
"""
# Try to get project entity from event
project_entity = event["data"].get("project_entity")
if not project_entity:
project_entity = self.get_project_from_entity(
entities[0], session
)
event["data"]["project_entity"] = project_entity
return project_entity
def get_ftrack_settings(self, session, event, entities):
project_entity = self.get_project_entity_from_event(
session, event, entities
)
project_settings = self.get_project_settings_from_event(
event, project_entity
)
return project_settings["ftrack"]
def valid_roles(self, session, entities, event):
"""Validate user roles by settings.
Method requires to have set `settings_key` attribute.
"""
ftrack_settings = self.get_ftrack_settings(session, event, entities)
settings = (
ftrack_settings[self.settings_frack_subkey][self.settings_key]
)
if not settings.get("enabled", True):
return False
user_role_list = self.get_user_roles_from_event(session, event)
if not self.roles_check(settings.get("role_list"), user_role_list):
return False
return True
class ServerAction(BaseAction):
"""Action class meant to be used on event server.
Unlike the `BaseAction` roles are not checked on register but on discover.
For the same reason register is modified to not filter topics by username.
"""
settings_frack_subkey = "events"
def register(self):
"""Register subcription to Ftrack event hub."""
self.session.event_hub.subscribe(

View file

@ -37,7 +37,6 @@ class BaseHandler(object):
type = 'No-type'
ignore_me = False
preactions = []
role_list = []
@staticmethod
def join_query_keys(keys):
@ -142,28 +141,7 @@ class BaseHandler(object):
def reset_session(self):
self.session.reset()
def _register_role_check(self):
if not self.role_list or not isinstance(self.role_list, (list, tuple)):
return
user_entity = self.session.query(
"User where username is \"{}\"".format(self.session.api_user)
).one()
available = False
lowercase_rolelist = [
role_name.lower()
for role_name in self.role_list
]
for role in user_entity["user_security_roles"]:
if role["security_role"]["name"].lower() in lowercase_rolelist:
available = True
break
if available is False:
raise MissingPermision
def _preregister(self):
self._register_role_check()
# Custom validations
result = self.preregister()
if result is None:
@ -550,7 +528,7 @@ class BaseHandler(object):
"Publishing event: {}"
).format(str(event.__dict__)))
def get_project_from_entity(self, entity):
def get_project_from_entity(self, entity, session=None):
low_entity_type = entity.entity_type.lower()
if low_entity_type == "project":
return entity
@ -571,61 +549,30 @@ class BaseHandler(object):
return parent["project"]
project_data = entity["link"][0]
return self.session.query(
if session is None:
session = self.session
return session.query(
"Project where id is {}".format(project_data["id"])
).one()
def get_project_entity_from_event(self, session, event, project_id):
"""Load or query and fill project entity from/to event data.
Project data are stored by ftrack id because in most cases it is
easier to access project id than project name.
Args:
session (ftrack_api.Session): Current session.
event (ftrack_api.Event): Processed event by session.
project_id (str): Ftrack project id.
"""
if not project_id:
raise ValueError(
"Entered `project_id` is not valid. {} ({})".format(
str(project_id), str(type(project_id))
)
)
# Try to get project entity from event
project_entities = event["data"].get("project_entities")
if not project_entities:
project_entities = {}
event["data"]["project_entities"] = project_entities
project_entity = project_entities.get(project_id)
if not project_entity:
# Get project entity from task and store to event
project_entity = session.get("Project", project_id)
event["data"]["project_entities"][project_id] = project_entity
return project_entity
def get_settings_for_project(
self, session, event, project_id=None, project_entity=None
):
def get_project_settings_from_event(self, event, project_entity):
"""Load or fill pype's project settings from event data.
Project data are stored by ftrack id because in most cases it is
easier to access project id than project name.
Args:
session (ftrack_api.Session): Current session.
event (ftrack_api.Event): Processed event by session.
project_id (str): Ftrack project id. Must be entered if
project_entity is not.
project_entity (ftrack_api.Entity): Project entity. Must be entered
if project_id is not.
project_entity (ftrack_api.Entity): Project entity.
"""
if not project_entity:
project_entity = self.get_project_entity_from_event(
session, event, project_id
)
raise AssertionError((
"Invalid arguments entered. Project entity or project id"
"must be entered."
))
project_id = project_entity["id"]
project_name = project_entity["full_name"]
project_settings_by_id = event["data"].get("project_settings")

View file

@ -46,3 +46,33 @@ class BaseEvent(BaseHandler):
session,
ignore=['socialfeed', 'socialnotification']
)
def get_project_entity_from_event(self, session, event, project_id):
"""Load or query and fill project entity from/to event data.
Project data are stored by ftrack id because in most cases it is
easier to access project id than project name.
Args:
session (ftrack_api.Session): Current session.
event (ftrack_api.Event): Processed event by session.
project_id (str): Ftrack project id.
"""
if not project_id:
raise ValueError(
"Entered `project_id` is not valid. {} ({})".format(
str(project_id), str(type(project_id))
)
)
# Try to get project entity from event
project_entities = event["data"].get("project_entities")
if not project_entities:
project_entities = {}
event["data"]["project_entities"] = project_entities
project_entity = project_entities.get(project_id)
if not project_entity:
# Get project entity from task and store to event
project_entity = session.get("Project", project_id)
event["data"]["project_entities"][project_id] = project_entity
return project_entity

View file

@ -94,13 +94,11 @@
"ignored_statuses": [
"In Progress",
"Omitted",
"On hold"
"On hold",
"Approved"
],
"status_change": {
"In Progress": [],
"Ready": [
"Not Ready"
]
"In Progress": []
}
},
"create_update_attributes": {
@ -167,7 +165,8 @@
"sync_to_avalon_local": {
"enabled": true,
"role_list": [
"Pypeclub"
"Pypeclub",
"Administrator"
]
},
"seed_project": {