Hooks
The Alias Engine defines hooks to customize the Toolkit Apps that it uses. See below for App specific hook details.
tk-multi-data-validation
AliasDataValidationHook
This hook will get the validation rule data from the AliasDataValidator and pass it to the tk-multi-data-validation App.
# Copyright (c) 2022 Autodesk, Inc.
#
# CONFIDENTIAL AND PROPRIETARY
#
# This work is provided "AS IS" and subject to the ShotGrid Pipeline Toolkit
# Source Code License included in this distribution package. See LICENSE.
# By accessing, using, copying or modifying this work you indicate your
# agreement to the ShotGrid Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Autodesk, Inc.
from typing import Optional, List
import sgtk
import alias_api
HookBaseClass = sgtk.get_hook_baseclass()
class AliasDataValidationHook(HookBaseClass):
"""Hook to define Alias data validation functionality."""
class AliasDataValidationError(Exception):
"""Custom exception class to report Alias Data Validation errors."""
class SanitizedResult:
"""Class to represent a sanitized check result object."""
def __init__(
self,
is_valid: Optional[bool] = None,
errors: Optional[List] = None,
error_count: int = 0,
):
"""
Initialize the object with the given data.
:param is_valid: The success status that the check function
reported. If not provided, the validity will be determined
based on if there are any errors.
:param errors: The data errors the check function found. This
should be a list of Alias objects, but can be a list ofobject
as long as they follow the expected format.
:param error_count: The number of errors found by the check
function. This is useful when the errors list is not provided.
"""
if is_valid is None:
self.is_valid = not errors
else:
self.is_valid = is_valid
if errors:
error_list = []
for item in errors:
if not item:
continue
if isinstance(item, str):
error_list.append(
{
"id": item,
"name": item,
"type": "",
}
)
elif isinstance(item, dict):
if "name" not in item:
raise self.AliasDataValidationError(
"Error dict missing 'name' key"
)
error_list.append(item)
else:
try:
error_list.append(
{
"id": item.name or "",
"name": item.name or "",
"type": item.type(),
}
)
except AttributeError:
raise self.AliasDataValidationError(
f"Cannot sanitize error object {item}"
)
self.errors = sorted(error_list, key=lambda x: x["name"].lower())
else:
self.errors = []
self.error_count = len(errors) if errors else error_count
def get_validation_data(self):
"""
Return the validation rule data set to validate an Alias scene.
This method will retrieve the default validation rules returned by
:meth:`AliasDataValidator.get_validation_data`. To customize the default
validation rules, override this hook method to modify the returned data dictionary.
The dictionary returned by this function should be formated such that it can be passed
to the :class:`~tk-multi-data-validation:api.data.ValidationRule` class constructor to
create a new validation rule object.
:return: The validation rules data set.
:rtype: dict
"""
data = self.parent.engine.data_validator.get_validation_data()
# -------------------------------------------------------------------------------------------------------
#
# Example:
# How to add a custom rule to the default list of validation rules (the data above)
#
# This example assume that the tk-multi-data-validation App is being used to display the validation
# data, and to perform the validation functionality.
#
# -------------------------------------------------------------------------------------------------------
#
# Step (1) - define the necessary check, fix, and action callbacks that are required by your new rule.
# Step (2) - add your custom rule entry into the validation data dictionary `data`
# Step (3) - add your custom rule id to your tk-multi-data-validation.yml config settings
#
# -------------------------------------------------------------------------------------------------------
#
# # Define a global variable to toggle the custom rule valid state, for demonstrations purposes
# global custom_rule_is_valid
# custom_rule_is_valid = False
#
# def check_my_custom_rule(fail_fast=False):
# """
# This callback method will execute when the "Validate" button is clicked for this
# rule, or validate all is initiated.
#
# NOTE that the check function takes one parameter, `fail_fast`, even if it is not
# used, it should still be defined to follow the "check"functions guidelines (see
# AliasDataValidator class for more info).
#
# For examples of more advanced check functions, see the AliasDataValidator class
# methods prefixed with `check_`.
# """
#
# if fail_fast:
# # In a fail fast context, just return True or False indicating if the rule is valid
# return custom_rule_is_valid
#
# if custom_rule_is_valid:
# # Do not report any errors if the rule is valid
# errors = None
# else:
# # The rule is not valid, return the Alias objects that do not pass the check.
# # In this example, we using a namedtuple to return a list of Alias objects, but
# # a list of Alias objects retrieved from the Alias Python API can also be used
# # directly.
# from collections import namedtuple
# AliasObject = namedtuple("AliasObject", ["name", "type"])
# errors = [
# AliasObject("node#1", lambda: "AlSurfaceNode()"),
# AliasObject("node#2", lambda: "AlSurfaceNode()"),
# ]
#
# # If not fail faist, check functions should return the list of error objects
# return errors
#
# def fix_my_custom_rule(errors=None):
# """
# This callback method will execute when the "Fix" button is clicked for this rule,
# or the fix all is initiated.
#
# This fix function just sets the global valid state to True so that the next time
# the check function is executed, it will have a valid state of True.
#
# NOTE that the fix function takes one parameter, `errors`, this is not used here
# for simplicity, but it should still be defined to follow the "fix" function
# guidelines (see AliasDataValidator class for more info).
#
# For examples of more advanced fix functions, see the AliasDataValidator class
# methods prefixed with `fix_`.
# """
# global custom_rule_is_valid
# custom_rule_is_valid = True
#
# def action_callback(errors=None):
# """
# This callback method will execute when the action item is clicked for this rule.
#
# The action item can be found by right-clicking the rule to see the menu actions,
# or by clicking the "..." on the rule item row. It is the action called
#
# 'Click me! Revalidate to see what happend >:)"
#
# This action function just sets the global valid state to False so that the next time
# the check function is executed, it will have a valid state of False.
#
# NOTE that the fix function takes one parameter, `errors`, this is not used here
# for simplicity, but it should still be defined to follow the action function
# guidelines (see AliasDataValidator class for more info).
#
# For examples of more advanced fix functions, see the AliasDataValidator class
# methods.
# """
# global custom_rule_is_valid
# custom_rule_is_valid = False
#
# # Step (2)
# data["my_custom_rule"] = {
# "name": "My Custom Validation Rule",
# "description": """
# This is an example to demonstrate how to add a custom rule. Try validating it ----------><br/>
# Right-click and 'Show Details' to open the right-hand panel to see more.
# """,
# "error_msg": "An error has been found by this rule. Let's fix it!",
# "fix_name": "Fix it!",
# "check_func": check_my_custom_rule,
# "fix_func": fix_my_custom_rule,
# "actions": [
# {"name": "Click me! Revalidate to see what happend >:)", "callback": action_callback}
# ]
# }
return data
def sanitize_check_result(self, result):
"""
Sanitize the value returned by any validate function to conform to the standard format.
Convert the incoming list of Alias objects (that are errors) to conform to the standard
format that the Data Validation App requires:
is_valid:
type: bool
description: True if the validate function succeed with the current data, else
False.
errors:
type: list
description: The list of error objects (found by the validate function). None
or empty list if the current data is valid.
items:
type: dict
key-values:
id:
type: str | int
description: A unique identifier for the error object.
optional: False
name:
type: str
description: The display name for the error object.
optional: False
type:
type: str
description: The display name of the error object type.
optional: True
This method will be called by the Data Validation App after any validate function is
called, in order to receive the validate result in the required format.
:param result: The result returned by a validation rule ``check_func``. This is
should be a list of Alias objects or a boolean value indicating the validity of
the check.
:type result: list
:return: The result of a ``check_func`` in the Data Validation standardized format.
:rtype: dict
"""
if isinstance(result, bool):
return self.SanitizedResult(is_valid=result)
if (
isinstance(result, tuple)
and len(result) == 2
and isinstance(result[0], bool)
and isinstance(result[1], int)
):
return self.SanitizedResult(is_valid=result[0], error_count=result[1])
if isinstance(result, list):
return self.SanitizedResult(errors=result)
raise self.AliasDataValidationError(
"Cannot sanitize result type '{}'".format(type(result))
)
def post_fix_action(self, rules):
"""
Called once an individual fix has been resolved
:param rules: List of rule IDs linked to the executed fix
"""
rule_ids = [rule.id for rule in rules]
# force Alias to refresh its viewport for a specific set of fixes
if not {"cos_unused", "references_exist", "node_pivots_at_origin"}.isdisjoint(
rule_ids
):
alias_api.redraw_screen()
tk-multi-loader2
AliasActions
# Copyright (c) 2015 Shotgun Software Inc.
#
# CONFIDENTIAL AND PROPRIETARY
#
# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit
# Source Code License included in this distribution package. See LICENSE.
# By accessing, using, copying or modifying this work you indicate your
# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Shotgun Software Inc.
"""
Hook that loads defines all the available actions, broken down by publish type.
"""
import os
import sgtk
import alias_api
HookBaseClass = sgtk.get_hook_baseclass()
class AliasActions(HookBaseClass):
def generate_actions(self, sg_publish_data, actions, ui_area):
"""
Returns a list of action instances for a particular publish.
This method is called each time a user clicks a publish somewhere in the UI.
The data returned from this hook will be used to populate the actions menu for a publish.
The mapping between Publish types and actions are kept in a different place
(in the configuration) so at the point when this hook is called, the loader app
has already established *which* actions are appropriate for this object.
The hook should return at least one action for each item passed in via the
actions parameter.
This method needs to return detailed data for those actions, in the form of a list
of dictionaries, each with name, params, caption and description keys.
Because you are operating on a particular publish, you may tailor the output
(caption, tooltip etc) to contain custom information suitable for this publish.
The ui_area parameter is a string and indicates where the publish is to be shown.
- If it will be shown in the main browsing area, "main" is passed.
- If it will be shown in the details area, "details" is passed.
- If it will be shown in the history area, "history" is passed.
Please note that it is perfectly possible to create more than one action "instance" for
an action! You can for example do scene introspection - if the action passed in
is "character_attachment" you may for example scan the scene, figure out all the nodes
where this object can be attached and return a list of action instances:
"attach to left hand", "attach to right hand" etc. In this case, when more than
one object is returned for an action, use the params key to pass additional
data into the run_action hook.
:param sg_publish_data: Flow Production Tracking data dictionary with all the standard publish fields.
:param actions: List of action strings which have been defined in the app configuration.
:param ui_area: String denoting the UI Area (see above).
:returns List of dictionaries, each with keys name, params, caption and description
"""
self.logger.debug(
"Generate actions called for UI element %s. "
"Actions: %s. Publish Data: %s" % (ui_area, actions, sg_publish_data)
)
action_instances = []
if "reference" in actions:
action_instances.append(
{
"name": "reference",
"params": None,
"caption": "Create Reference",
"description": "This will add the item to the universe as a standard reference.",
}
)
if "import" in actions:
action_instances.append(
{
"name": "import",
"params": None,
"caption": "Import into Scene",
"description": "This will import the item into the current universe.",
}
)
if "import_as_reference" in actions:
action_instances.append(
{
"name": "import_as_reference",
"params": None,
"caption": "Import as Reference",
"description": "This will import the item as a reference into the current universe.",
}
)
if "texture_node" in actions:
action_instances.append(
{
"name": "texture_node",
"params": None,
"caption": "Create Canvas",
"description": "This will import the item into the current universe.",
}
)
if "import_subdiv" in actions:
action_instances.append(
{
"name": "import_subdiv",
"params": None,
"caption": "Import Subdiv file into Scene",
"description": "This will import the subdiv item into the current universe.",
}
)
return action_instances
def execute_multiple_actions(self, actions):
"""
Executes the specified action on a list of items.
The default implementation dispatches each item from ``actions`` to
the ``execute_action`` method.
The ``actions`` is a list of dictionaries holding all the actions to execute.
Each entry will have the following values:
name: Name of the action to execute
sg_publish_data: Publish information coming from Flow Production Tracking
params: Parameters passed down from the generate_actions hook.
.. note::
This is the default entry point for the hook. It reuses the ``execute_action``
method for backward compatibility with hooks written for the previous
version of the loader.
.. note::
The hook will stop applying the actions on the selection if an error
is raised midway through.
:param list actions: Action dictionaries.
"""
for single_action in actions:
name = single_action["name"]
sg_publish_data = single_action["sg_publish_data"]
params = single_action["params"]
self.execute_action(name, params, sg_publish_data)
def execute_action(self, name, params, sg_publish_data):
"""
Execute a given action. The data sent to this be method will
represent one of the actions enumerated by the generate_actions method.
:param name: Action name string representing one of the items returned by generate_actions.
:param params: Params data, as specified by generate_actions.
:param sg_publish_data: Flow Production Tracking data dictionary with all the standard publish fields.
:returns: No return value expected.
"""
self.logger.debug(
"Execute action called for action %s. "
"Parameters: %s. Publish Data: %s" % (name, params, sg_publish_data)
)
# resolve path
path = self.get_publish_path(sg_publish_data)
if name == "reference":
self._create_reference(path)
elif name == "import":
self._import_file(path)
elif name == "import_as_reference":
self._import_file_as_reference(path, sg_publish_data)
elif name == "texture_node":
self._create_texture_node(path)
elif name == "import_subdiv":
self._import_subdivision(path)
def _create_reference(self, path):
"""
Create an Alias reference.
:param path: Path to the file.
"""
if not os.path.exists(path):
raise Exception("File not found on disk - '%s'" % path)
alias_api.create_reference(path)
def _import_file(self, path):
"""
Import the file into the current Alias session.
:param path: Path to file.
"""
if not os.path.exists(path):
raise Exception("File not found on disk - '%s'" % path)
alias_api.import_file(path)
def _import_file_as_reference(self, path, sg_publish_data):
"""
Import the file as an Alias reference, converting it on the fly as wref.
:param path: Path to the file.
:param sg_publish_data: Flow Production Tracking data dictionary with all the standard publish fields
"""
# get the tank of the project the file we're trying to import belongs to
# this will be useful to manipulate configuration settings and templates
tk = self.parent.engine.get_tk_from_project(sg_publish_data["project"])
# then, get the reference template and the source template to be able to extract fields and build the path to
# the translated file
try:
reference_template = self.parent.engine.get_reference_template(
tk, sg_publish_data
)
except AttributeError:
raise Exception(
"There is an issue with Pipeline Configurations for the Linked Project<br>"
" Please see the guidelines <a href=https://github.com/shotgunsoftware/tk-alias/wiki/Loading#import-as-reference-from-another-project><b>here.</b></a>"
)
return
source_template = tk.template_from_path(path)
# get the path to the reference, using the templates if it's possible otherwise using the source path
# location
if reference_template and source_template:
template_fields = source_template.get_fields(path)
template_fields["alias.extension"] = os.path.splitext(path)[-1][1:]
reference_path = reference_template.apply_fields(template_fields)
else:
output_path, output_ext = os.path.splitext(path)
reference_path = "{output_path}_{output_ext}.wref".format(
output_path=output_path, output_ext=output_ext[1:]
)
# if the reference file doesn't exist on disk yet, run the translation
if not os.path.exists(reference_path):
framework = self.load_framework("tk-framework-aliastranslations_v0.x.x")
if not framework:
raise Exception("Couldn't find tk-framework-aliastranslations_v0.x.x")
tk_framework_aliastranslations = framework.import_module(
"tk_framework_aliastranslations"
)
translator = tk_framework_aliastranslations.Translator(path, reference_path)
translator.execute()
alias_api.create_reference(reference_path)
def _create_texture_node(self, path):
"""
Import an image as Canvas in Alias
:param path: Path to the image.
"""
if not os.path.exists(path):
raise Exception("File not found on disk - '%s'" % path)
alias_api.create_texture_node(path, True)
def _import_subdivision(self, path):
"""
Import a file as subdivision in the current Alias session.
:param path: Path to the file.
"""
if not os.path.exists(path):
raise Exception("File not found on disk - '%s'" % path)
try:
alias_api.import_subdiv(path)
except alias_api.AliasPythonException as api_error:
err_msg = "{api_error}<br/><br/>For more information, click {help_link}.".format(
api_error=str(api_error),
help_link="<a href='https://help.autodesk.com/view/ALIAS/2023/ENU/?guid=GUID-667410AD-CF4D-43B3-AE96-0C1331CB80B2'>here</a>",
)
raise alias_api.AliasPythonException(err_msg)
tk-multi-publish2
PublishAnnotationsPlugin
# Copyright (c) 2017 Shotgun Software Inc.
#
# CONFIDENTIAL AND PROPRIETARY
#
# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit
# Source Code License included in this distribution package. See LICENSE.
# By accessing, using, copying or modifying this work you indicate your
# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Shotgun Software Inc.
import sgtk
import alias_api
HookBaseClass = sgtk.get_hook_baseclass()
class PublishAnnotationsPlugin(HookBaseClass):
"""
Plugin for publishing annotations of the current alias open session
"""
@property
def name(self):
"""
One line display name describing the plugin
"""
return "Publish Annotations to Flow Production Tracking"
@property
def description(self):
return """
<p>
This plugin exports all annotations created using the Locator Annotation tool in Alias.
</p>
<p>
Each annotation will create a Note in Flow Production Tracking. All Notes are linked to this version and file. Use this to
sync all review notes made in Alias with Flow Production Tracking.
</p>
"""
@property
def item_filters(self):
"""
List of item types that this plugin is interested in.
Only items matching entries in this list will be presented to the
accept() method. Strings can contain glob patters such as *, for example
["maya.*", "file.maya"]
"""
return ["alias.session"]
def accept(self, settings, item):
"""
Method called by the publisher to determine if an item is of any
interest to this plugin. Only items matching the filters defined via the
item_filters property will be presented to this method.
A publish task will be generated for each item accepted here. Returns a
dictionary with the following booleans:
- accepted: Indicates if the plugin is interested in this value at
all. Required.
- enabled: If True, the plugin will be enabled in the UI, otherwise
it will be disabled. Optional, True by default.
- visible: If True, the plugin will be visible in the UI, otherwise
it will be hidden. Optional, True by default.
- checked: If True, the plugin will be checked in the UI, otherwise
it will be unchecked. Optional, True by default.
:param settings: Dictionary of Settings. The keys are strings, matching
the keys returned in the settings property. The values are `Setting`
instances.
:param item: Item to process
:returns: dictionary with boolean keys accepted, required and enabled
"""
if not alias_api.has_annotation_locator():
self.logger.debug("There are not annotations to export")
return {"accepted": False}
return {"accepted": True, "checked": False}
def validate(self, settings, item):
"""
Validates the given item to check that it is ok to publish. Returns a
boolean to indicate validity.
:param settings: Dictionary of Settings. The keys are strings, matching
the keys returned in the settings property. The values are `Setting`
instances.
:param item: Item to process
:returns: True if item is valid, False otherwise.
"""
return True
def publish(self, settings, item):
"""
Executes the publish logic for the given item and settings.
:param settings: Dictionary of Settings. The keys are strings, matching
the keys returned in the settings property. The values are `Setting`
instances.
:param item: Item to process
"""
# get the publish "mode" stored inside of the root item properties
bg_processing = item.parent.properties.get("bg_processing", False)
in_bg_process = item.parent.properties.get("in_bg_process", False)
if not bg_processing or (bg_processing and in_bg_process):
self.logger.info("Publishing annotations")
# Links, the note will be attached to published file by default
# if a version is created the note will be attached to this too
publish_data = item.properties["sg_publish_data"]
version_data = item.properties.get("sg_version_data")
note_links = [publish_data]
if version_data is not None:
note_links.append(version_data)
annotations = alias_api.get_annotation_locator_strings()
batch_data = []
for annotation in annotations:
note_data = {
"project": item.context.project,
"user": item.context.user,
"subject": "Alias Annotation",
"content": annotation,
"note_links": note_links,
}
if item.context.task:
note_data["tasks"] = [item.context.task]
batch_data.append(
{"request_type": "create", "entity_type": "Note", "data": note_data}
)
if batch_data:
self.parent.shotgun.batch(batch_data)
def finalize(self, settings, item):
"""
Execute the finalization pass. This pass executes once all the publish
tasks have completed, and can for example be used to version up files.
:param settings: Dictionary of Settings. The keys are strings, matching
the keys returned in the settings property. The values are `Setting`
instances.
:param item: Item to process
"""
# get the publish "mode" stored inside of the root item properties
bg_processing = item.parent.properties.get("bg_processing", False)
in_bg_process = item.parent.properties.get("in_bg_process", False)
if not bg_processing or (bg_processing and in_bg_process):
self.logger.info("Annotations published successfully")
AliasSessionPublishPlugin
# Copyright (c) 2017 Shotgun Software Inc.
#
# CONFIDENTIAL AND PROPRIETARY
#
# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit
# Source Code License included in this distribution package. See LICENSE.
# By accessing, using, copying or modifying this work you indicate your
# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Shotgun Software Inc.
import os
import sgtk
import alias_api
HookBaseClass = sgtk.get_hook_baseclass()
class AliasSessionPublishPlugin(HookBaseClass):
"""
Plugin for publishing an open alias session.
This hook relies on functionality found in the base file publisher hook in
the publish2 app and should inherit from it in the configuration. The hook
setting for this plugin should look something like this::
hook: "{self}/publish_file.py:{engine}/tk-multi-publish2/basic/publish_session.py"
"""
# Publish mode string constants
PUBLISH_MODE_DEFAULT = "Default"
PUBLISH_MODE_EXPORT_SELECTION = "Export Selection"
# Publish mode options
PUBLISH_MODE_OPTIONS = [
PUBLISH_MODE_DEFAULT,
PUBLISH_MODE_EXPORT_SELECTION,
]
@property
def description(self):
"""
Verbose, multi-line description of what the plugin does. This can
contain simple html for formatting.
"""
loader_url = "https://help.autodesk.com/view/SGDEV/ENU/?contextId=PC_APP_LOADER"
return """
Publishes the file to Flow Production Tracking. A <b>Publish</b>
entry will be created in Flow Production Tracking which will
include a reference to the file's current path on disk. If a
publish template is configured, a copy of the current session
will be copied to the publish template path which will be the
file that is published. Other users will be able to access the
published file via the <b><a href='%s'>Loader</a></b> so long as
they have access to the file's location on disk.
If the session has not been saved, validation will fail and a button
will be provided in the logging output to save the file.
<h3>File versioning</h3>
If the filename contains a version number, the process will bump the
file to the next version after publishing.
The <code>version</code> field of the resulting <b>Publish</b> in
Flow Production Tracking will also reflect the version number identified
in the filename. The basic worklfow recognizes the following version
formats by default:
<ul>
<li><code>filename.v###.ext</code></li>
<li><code>filename_v###.ext</code></li>
<li><code>filename-v###.ext</code></li>
</ul>
After publishing, if a version number is detected in the work file, the
work file will automatically be saved to the next incremental version
number. For example, <code>filename.v001.ext</code> will be published
and copied to <code>filename.v002.ext</code>
If the next incremental version of the file already exists on disk, the
validation step will produce a warning, and a button will be provided in
the logging output which will allow saving the session to the next
available version number prior to publishing.
<br><br><i>NOTE: any amount of version number padding is supported. for
non-template based workflows.</i>
<h3>Overwriting an existing publish</h3>
In non-template workflows, a file can be published multiple times,
however only the most recent publish will be available to other users.
Warnings will be provided during validation if there are previous
publishes.
""" % (
loader_url,
)
@property
def settings(self):
"""
Dictionary defining the settings that this plugin expects to receive
through the settings parameter in the accept, validate, publish and
finalize methods.
A dictionary on the following form::
{
"Settings Name": {
"type": "settings_type",
"default": "default_value",
"description": "One line description of the setting"
}
The type string should be one of the data types that toolkit accepts as
part of its environment configuration.
"""
# inherit the settings from the base publish plugin
base_settings = super(AliasSessionPublishPlugin, self).settings or {}
# settings specific to this class
alias_publish_settings = {
"Publish Template": {
"type": "template",
"default": None,
"description": "Template path for published work files. Should"
"correspond to a template defined in "
"templates.yml.",
},
"Publish Mode": {
"type": "str",
"default": self.PUBLISH_MODE_DEFAULT,
"description": "The mode to use when publishing the session. User can choose between 'Default' and 'Export Selection'.",
},
}
# update the base settings
base_settings.update(alias_publish_settings)
return base_settings
@property
def item_filters(self):
"""
List of item types that this plugin is interested in.
Only items matching entries in this list will be presented to the
accept() method. Strings can contain glob patters such as *, for example
["maya.*", "file.maya"]
"""
return ["alias.session"]
def accept(self, settings, item):
"""
Method called by the publisher to determine if an item is of any
interest to this plugin. Only items matching the filters defined via the
item_filters property will be presented to this method.
A publish task will be generated for each item accepted here. Returns a
dictionary with the following booleans:
- accepted: Indicates if the plugin is interested in this value at
all. Required.
- enabled: If True, the plugin will be enabled in the UI, otherwise
it will be disabled. Optional, True by default.
- visible: If True, the plugin will be visible in the UI, otherwise
it will be hidden. Optional, True by default.
- checked: If True, the plugin will be checked in the UI, otherwise
it will be unchecked. Optional, True by default.
:param settings: Dictionary of Settings. The keys are strings, matching
the keys returned in the settings property. The values are `Setting`
instances.
:param item: Item to process
:returns: dictionary with boolean keys accepted, required and enabled
"""
# if a publish template is configured, disable context change. This
# is a temporary measure until the publisher handles context switching
# natively.
if settings.get("Publish Template").value:
item.context_change_allowed = False
path = _session_path()
if not path:
# the session has not been saved before (no path determined).
# provide a save button. the session will need to be saved before
# validation will succeed.
self.logger.warning(
"The Alias session has not been saved.",
extra=_get_save_as_action(),
)
self.logger.info(
"Alias '%s' plugin accepted the current Alias session." % (self.name,)
)
return {"accepted": True, "checked": True}
def validate(self, settings, item):
"""
Validates the given item to check that it is ok to publish. Returns a
boolean to indicate validity.
:param settings: Dictionary of Settings. The keys are strings, matching
the keys returned in the settings property. The values are `Setting`
instances.
:param item: Item to process
:returns: True if item is valid, False otherwise.
"""
publisher = self.parent
path = _session_path()
# ---- ensure the valid publish mode
publish_mode = settings.get("Publish Mode").value
if publish_mode not in self.PUBLISH_MODE_OPTIONS:
self.logger.error(f"Unsupported Publish Mode {publish_mode}")
return False
if publish_mode == self.PUBLISH_MODE_EXPORT_SELECTION:
bg_processing = item.parent.get_property("bg_processing", False)
if bg_processing:
error_msg = "Export Selection mode is not supported with Background Publishing. Please change the Publish Mode or turn off Background Publishing."
self.logger.error(error_msg)
return False
# ---- ensure the session has been saved
if not path:
# the session still requires saving. provide a save button.
# validation fails.
error_msg = "The Alias session has not been saved."
self.logger.error(
error_msg,
extra=_get_save_as_action(),
)
raise Exception(error_msg)
# ---- check that references exist, display warning for invalid refs
for reference in alias_api.get_references():
ref_path = reference.path
if not os.path.exists(ref_path):
self.logger.warning(
"Reference path does not exist '{}'".format(ref_path)
)
# ---- check the session against any attached work template
# get the path in a normalized state. no trailing separator,
# separators are appropriate for current os, no double separators,
# etc.
path = sgtk.util.ShotgunPath.normalize(path)
# if the session item has a known work template, see if the path
# matches. if not, warn the user and provide a way to save the file to
# a different path
work_template = item.properties.get("work_template")
if work_template:
if not work_template.validate(path):
error_msg = "The current session does not match the configured work file template."
self.logger.warning(
error_msg,
extra={
"action_button": {
"label": "Save File",
"tooltip": "Save the current Alias session to a "
"different file name",
"callback": sgtk.platform.current_engine().open_save_as_dialog,
}
},
)
raise Exception(error_msg)
else:
self.logger.debug("Work template configured and matches session file.")
else:
self.logger.debug("No work template configured.")
# ---- see if the version can be bumped post-publish
# check to see if the next version of the work file already exists on
# disk. if so, warn the user and provide the ability to jump to save
# to that version now
(next_version_path, version) = self._get_next_version_info(path, item)
if next_version_path and os.path.exists(next_version_path):
# determine the next available version_number. just keep asking for
# the next one until we get one that doesn't exist.
while os.path.exists(next_version_path):
(next_version_path, version) = self._get_next_version_info(
next_version_path, item
)
error_msg = "The next version of this file already exists on disk."
self.logger.error(
error_msg,
extra={
"action_button": {
"label": "Save to v%s" % (version,),
"tooltip": "Save to the next available version number, "
"v%s" % (version,),
"callback": lambda: publisher.engine.save_file_as(
next_version_path
),
}
},
)
raise Exception(error_msg)
# ---- populate the necessary properties and call base class validation
# populate the publish template on the item if found
publish_template_setting = settings.get("Publish Template")
publish_template = publisher.engine.get_template_by_name(
publish_template_setting.value
)
if publish_template:
item.properties["publish_template"] = publish_template
# set the session path on the item for use by the base plugin validation
# step. NOTE: this path could change prior to the publish phase.
item.properties["path"] = path
# run the base class validation
return super(AliasSessionPublishPlugin, self).validate(settings, item)
def publish(self, settings, item):
"""
Executes the publish logic for the given item and settings.
:param settings: Dictionary of Settings. The keys are strings, matching
the keys returned in the settings property. The values are `Setting`
instances.
:param item: Item to process
"""
# get the publish "mode" stored inside of the root item properties
bg_processing = item.parent.properties.get("bg_processing", False)
in_bg_process = item.parent.properties.get("in_bg_process", False)
# get the path in a normalized state. no trailing separator, separators
# are appropriate for current os, no double separators, etc.
path = sgtk.util.ShotgunPath.normalize(_session_path())
# ensure the session is saved
# we need to do this action locally to be sure the background process could access the work file
if not bg_processing or (bg_processing and not in_bg_process):
# Save the working file before publishing
self.parent.engine.save_file()
# store the current session path in the root item properties
# it will be used later in the background process to open the file before running the publishing actions
if bg_processing and "session_path" not in item.parent.properties:
item.parent.properties["session_path"] = path
item.parent.properties[
"session_name"
] = "Alias Session - {task_name}, {entity_type} {entity_name} - {file_name}".format(
task_name=item.context.task["name"],
entity_type=item.context.entity["type"],
entity_name=item.context.entity["name"],
file_name=os.path.basename(path),
)
# update the item with the saved session path
item.properties["path"] = path
if not bg_processing or (bg_processing and in_bg_process):
# add dependencies for the base class to register when publishing
item.properties[
"publish_dependencies"
] = _alias_find_additional_session_dependencies()
# let the base class register the publish
super(AliasSessionPublishPlugin, self).publish(settings, item)
def finalize(self, settings, item):
"""
Execute the finalization pass. This pass executes once all the publish
tasks have completed, and can for example be used to version up files.
:param settings: Dictionary of Settings. The keys are strings, matching
the keys returned in the settings property. The values are `Setting`
instances.
:param item: Item to process
"""
# get the publish "mode" stored inside of the root item properties
bg_processing = item.parent.properties.get("bg_processing", False)
in_bg_process = item.parent.properties.get("in_bg_process", False)
if not bg_processing or (bg_processing and in_bg_process):
# do the base class finalization
super(AliasSessionPublishPlugin, self).finalize(settings, item)
# bump the session file to the next version
if not bg_processing or (bg_processing and not in_bg_process):
self._save_to_next_version(
item.properties["path"], item, self.parent.engine.save_file_as
)
############################################################################
# Methods for creating/displaying custom plugin interface
def create_settings_widget(self, parent, items=None):
"""
Creates a Qt widget, for the supplied parent widget (a container widget
on the right side of the publish UI).
:param parent: The parent to use for the widget being created.
:param items: A list of PublishItems the selected publish tasks are parented to.
:return: A QtGui.QWidget or subclass that displays information about
the plugin and/or editable widgets for modifying the plugin's
settings.
"""
# defer Qt-related imports
from sgtk.platform.qt import QtGui
# The main widget
widget = QtGui.QWidget(parent)
widget_layout = QtGui.QVBoxLayout()
# The description widget
description_groupbox = super().create_settings_widget(parent, items)
# Add a combobox to edit the publish mode
publish_mode_label = QtGui.QLabel("Publish Mode:")
publish_mode_combobox = QtGui.QComboBox(widget)
publish_mode_combobox.setAccessibleName("Publish mode selection dropdown")
publish_mode_combobox.addItems(self.PUBLISH_MODE_OPTIONS)
publish_mode_widget = QtGui.QWidget(widget)
publish_mode_widget.setSizePolicy(
QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred
)
publish_mode_layout = QtGui.QHBoxLayout()
publish_mode_layout.setContentsMargins(0, 0, 0, 0)
publish_mode_layout.addWidget(publish_mode_label)
publish_mode_layout.addWidget(publish_mode_combobox)
publish_mode_layout.addStretch()
publish_mode_widget.setLayout(publish_mode_layout)
# Add all the minor widgets to the main widget
widget_layout.addWidget(publish_mode_widget)
widget_layout.addWidget(description_groupbox)
widget.setLayout(widget_layout)
# Set the widget property to store the combobox to access in get_ui_settings and set_ui_settings
widget.setProperty("publish_mode_combobox", publish_mode_combobox)
return widget
def get_ui_settings(self, widget, items=None):
"""
This method is required to be defined in order for the custom UI to show up in the app.
Invoked by the Publisher when the selection changes. This method gathers the settings
on the previously selected task, so that they can be later used to repopulate the
custom UI if the task gets selected again. They will also be passed to the accept, validate,
publish and finalize methods, so that the settings can be used to drive the publish process.
The widget argument is the widget that was previously created by
`create_settings_widget`.
The method returns a dictionary, where the key is the name of a
setting that should be updated and the value is the new value of that
setting. Note that it is up to you how you want to store the UI's state as
settings and you don't have to necessarily to return all the values from
the UI. This is to allow the publisher to update a subset of settings
when multiple tasks have been selected.
Example::
{
"setting_a": "/path/to/a/file"
}
:param widget: The widget that was created by `create_settings_widget`
"""
ui_settings = {}
# Get the Publish Mode settings value from the UI combobox
publish_mode_combobox = widget.property("publish_mode_combobox")
if publish_mode_combobox:
mode_index = publish_mode_combobox.currentIndex()
if 0 <= mode_index < len(self.PUBLISH_MODE_OPTIONS):
ui_settings["Publish Mode"] = self.PUBLISH_MODE_OPTIONS[mode_index]
else:
self.logger.debug(f"Invalid Publish Mode index {mode_index}")
return ui_settings
def set_ui_settings(self, widget, settings, items=None):
"""
This method is required to be defined in order for the custom UI to show up in the app.
Allows the custom UI to populate its fields with the settings from the
currently selected tasks.
The widget is the widget created and returned by
`create_settings_widget`.
A list of settings dictionaries are supplied representing the current
values of the settings for selected tasks. The settings dictionaries
correspond to the dictionaries returned by the settings property of the
hook.
Example::
settings = [
{
"seeting_a": "/path/to/a/file"
"setting_b": False
},
{
"setting_a": "/path/to/a/file"
"setting_b": False
}]
The default values for the settings will be the ones specified in the
environment file. Each task has its own copy of the settings.
When invoked with multiple settings dictionaries, it is the
responsibility of the custom UI to decide how to display the
information. If you do not wish to implement the editing of multiple
tasks at the same time, you can raise a ``NotImplementedError`` when
there is more than one item in the list and the publisher will inform
the user than only one task of that type can be edited at a time.
:param widget: The widget that was created by `create_settings_widget`.
:param settings: a list of dictionaries of settings for each selected
task.
:param items: A list of PublishItems the selected publish tasks are parented to.
"""
if not settings:
return
if len(settings) > 1:
raise NotImplementedError
publish_mode_combobox = widget.property("publish_mode_combobox")
if not publish_mode_combobox:
self.logger.debug(
"Failed to retrieve Publish Mode combobox to set custom UI"
)
return
# Get the default setting for publish mode
default_value = self.settings.get("Publish Mode", {}).get("default")
# Get the publish mode value from the settings, and set the combobox accordingly
publish_mode = settings[0].get("Publish Mode", default_value)
try:
publish_mode_index = max(self.PUBLISH_MODE_OPTIONS.index(publish_mode), 0)
except ValueError:
publish_mode_index = 0
# Set the publish mode combobox
current_version_index = publish_mode_combobox.currentIndex()
if current_version_index == publish_mode_index:
return # Nothing to do
publish_mode_combobox.setCurrentIndex(publish_mode_index)
############################################################################
# protected methods
def _copy_to_publish(self, settings, item):
"""
Copy the item file to the publish location.
:param settings: This plugin instance's configured settings.
:param item: The item containing the file to copy.
"""
publish_mode = settings.get("Publish Mode").value
if publish_mode == self.PUBLISH_MODE_EXPORT_SELECTION:
publish_path = self.get_publish_path(settings, item)
self.parent.engine.alias_py.store_active(publish_path)
else:
super()._copy_to_publish(settings, item)
def _alias_find_additional_session_dependencies():
"""
Find additional dependencies from the session
"""
references = []
for reference in alias_api.get_references():
path = reference.path
if path not in references and os.path.exists(path):
references.append(path)
return references
def _session_path():
"""
Return the path to the current session
:return:
"""
return alias_api.get_current_path()
def _get_save_as_action():
"""Simple helper for returning a log action to show the "File Save As" dialog"""
return {
"action_button": {
"label": "Save As...",
"tooltip": "Save the current session",
"callback": sgtk.platform.current_engine().open_save_as_dialog,
}
}
AliasTranslationPublishPlugin
# Copyright (c) 2017 Shotgun Software Inc.
#
# CONFIDENTIAL AND PROPRIETARY
#
# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit
# Source Code License included in this distribution package. See LICENSE.
# By accessing, using, copying or modifying this work you indicate your
# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Shotgun Software Inc.
import os
import sgtk
import time
import alias_api
HookBaseClass = sgtk.get_hook_baseclass()
class AliasTranslationPublishPlugin(HookBaseClass):
"""
Plugin for publishing an open alias session.
This hook relies on functionality found in the base file publisher hook in
the publish2 app and should inherit from it in the configuration. The hook
setting for this plugin should look something like this::
hook: "{self}/publish_file.py:{engine}/tk-multi-publish2/basic/publish_session.py"
"""
# NOTE: The plugin icon and name are defined by the base file plugin.
@property
def description(self):
"""
Verbose, multi-line description of what the plugin does. This can
contain simple html for formatting.
"""
loader_url = "https://help.autodesk.com/view/SGDEV/ENU/?contextId=PC_APP_LOADER"
return """
Publishes the file to Flow Production Tracking. A <b>Publish</b>
entry will be created in Flow Production Tracking which will include
a reference to the file's current path on disk. If a publish template
is configured, a copy of the current session will be copied to the
publish template path which will be the file that is published. Other
users will be able to access the published file via the <b>
<a href='%s'>Loader</a></b> so long as they have access to the file's
location on disk.
<br><br><b color='red'>NOTE:</b> it's not possible to publish a WREF file
if you already have WREF files loaded in your current session.
""" % (
loader_url,
)
@property
def settings(self):
"""
Dictionary defining the settings that this plugin expects to receive
through the settings parameter in the accept, validate, publish and
finalize methods.
A dictionary on the following form::
{
"Settings Name": {
"type": "settings_type",
"default": "default_value",
"description": "One line description of the setting"
}
The type string should be one of the data types that toolkit accepts as
part of its environment configuration.
"""
# inherit the settings from the base publish plugin
base_settings = super(AliasTranslationPublishPlugin, self).settings or {}
# settings specific to this class
alias_publish_settings = {
"Publish Template": {
"type": "template",
"default": None,
"description": "Template path for published work files. Should"
"correspond to a template defined in "
"templates.yml.",
}
}
# update the base settings
base_settings.update(alias_publish_settings)
# translator settings
translator_settings = {
"Translator Settings": {
"type": "list",
"default": [],
"description": "Translator settings used to set values like file release number for CATPArt, among "
"others. To see all the available options per format, you can look at the command line "
"parameters",
}
}
# update the base settings
base_settings.update(translator_settings)
return base_settings
@property
def item_filters(self):
"""
List of item types that this plugin is interested in.
Only items matching entries in this list will be presented to the
accept() method. Strings can contain glob patters such as *, for example
["maya.*", "file.maya"]
"""
return ["alias.session.translation"]
def accept(self, settings, item):
"""
Method called by the publisher to determine if an item is of any
interest to this plugin. Only items matching the filters defined via the
item_filters property will be presented to this method.
A publish task will be generated for each item accepted here. Returns a
dictionary with the following booleans:
- accepted: Indicates if the plugin is interested in this value at
all. Required.
- enabled: If True, the plugin will be enabled in the UI, otherwise
it will be disabled. Optional, True by default.
- visible: If True, the plugin will be visible in the UI, otherwise
it will be hidden. Optional, True by default.
- checked: If True, the plugin will be checked in the UI, otherwise
it will be unchecked. Optional, True by default.
:param settings: Dictionary of Settings. The keys are strings, matching
the keys returned in the settings property. The values are `Setting`
instances.
:param item: Item to process
:returns: dictionary with boolean keys accepted, required and enabled
"""
publisher = self.parent
publish_template_setting = settings.get("Publish Template").value
if publish_template_setting:
# if a publish template is configured, disable context change. This
# is a temporary measure until the publisher handles context switching
# natively.
item.context_change_allowed = False
# get the publish template definition to determine if we are trying to publish a WREF file.
# If so, disable the plugin if some references are loaded in the current session
publish_template = publisher.engine.get_template_by_name(
publish_template_setting
)
if publish_template and "wref" in publish_template.definition:
alias_references = alias_api.get_references()
if alias_references:
return {"accepted": True, "enabled": False, "checked": False}
path = _session_path()
if not path:
# the session has not been saved before (no path determined).
# provide a save button. the session will need to be saved before
# validation will succeed.
self.logger.warn(
"The Alias session has not been saved.",
extra=_get_save_as_action(),
)
self.logger.info(
"Alias '%s' plugin accepted the current Alias session." % (self.name,)
)
return {"accepted": True, "checked": False}
def validate(self, settings, item):
"""
Validates the given item to check that it is ok to publish. Returns a
boolean to indicate validity.
:param settings: Dictionary of Settings. The keys are strings, matching
the keys returned in the settings property. The values are `Setting`
instances.
:param item: Item to process
:returns: True if item is valid, False otherwise.
"""
publisher = self.parent
path = _session_path()
# ---- ensure the session has been saved
if not path:
# the session still requires saving. provide a save button.
# validation fails.
error_msg = "The Alias session has not been saved."
self.logger.error(
error_msg,
extra=_get_save_as_action(),
)
raise Exception(error_msg)
# ---- check the session against any attached work template
# get the path in a normalized state. no trailing separator,
# separators are appropriate for current os, no double separators,
# etc.
path = sgtk.util.ShotgunPath.normalize(path)
# if the session item has a known work template, see if the path
# matches. if not, warn the user and provide a way to save the file to
# a different path
work_template = item.properties.get("work_template")
if not work_template or not work_template.validate(path):
self.logger.warning(
"The current session does not match the configured work "
"file template.",
extra={
"action_button": {
"label": "Save File",
"tooltip": "Save the current VRED session to a "
"different file name",
"callback": sgtk.platform.current_engine().open_save_as_dialog,
}
},
)
return False
else:
self.logger.debug("Work template configured and matches session file.")
# ---- populate the necessary properties and call base class validation
# set the session path on the item for use by the base plugin validation
# step. NOTE: this path could change prior to the publish phase.
item.properties["path"] = path
# if we don't have a publish path, we can't publish
publish_path = self.get_publish_path(settings, item)
if not publish_path:
self.logger.warning(
"Couldn't find a valid publish path for the translation file."
)
return False
# use the framework to get the Alias translator
framework = self.load_framework("tk-framework-aliastranslations_v0.x.x")
if not framework:
self.logger.warning("Couldn't find the Alias Translations Framework")
return False
tk_framework_aliastranslations = framework.import_module(
"tk_framework_aliastranslations"
)
translator = tk_framework_aliastranslations.Translator(path, publish_path)
# if we don't match valid conditions for the translator, exit
if not translator.is_valid():
self.logger.warning("Invalid conditions for translator.")
return
# if we don't have translator settings, we can't publish
if not translator.translation_type:
self.logger.warning(
f"Couldn't find the translation type {translator.translator_type}."
)
return False
if not translator.translator_path:
self.logger.warning(f"Couldn't determine which translator to use.")
return False
if not os.path.exists(translator.translator_path):
self.logger.warning(
f"Translator path does not exist {translator.translator_path}."
)
return False
self.logger.info(f"Translator in use: {translator.translator_path}.")
# store the licensing information in the item properties so that the translation could be run in
# background mode
alias_info = alias_api.get_product_information()
item.local_properties.license_settings = {
"product_key": alias_info.get("product_key"),
"product_version": alias_info.get("product_version"),
"product_license_type": alias_info.get("product_license_type"),
"product_license_path": alias_info.get("product_license_path"),
}
return super(AliasTranslationPublishPlugin, self).validate(settings, item)
def publish(self, settings, item):
"""
Executes the publish logic for the given item and settings.
:param settings: Dictionary of Settings. The keys are strings, matching
the keys returned in the settings property. The values are `Setting`
instances.
:param item: Item to process
"""
# get the publish "mode" stored inside of the root item properties
bg_processing = item.parent.parent.properties.get("bg_processing", False)
in_bg_process = item.parent.parent.properties.get("in_bg_process", False)
if not bg_processing or (bg_processing and in_bg_process):
publisher = self.parent
# get the path to create and publish
publish_path = self.get_publish_path(settings, item)
# ensure the publish folder exists:
publish_folder = os.path.dirname(publish_path)
self.parent.ensure_folder_exists(publish_folder)
# need to build a new instance of the translator for each translation because the type is changing
framework = self.load_framework("tk-framework-aliastranslations_v0.x.x")
tk_framework_aliastranslations = framework.import_module(
"tk_framework_aliastranslations"
)
translator = tk_framework_aliastranslations.Translator(
item.properties.path, publish_path
)
# set the license information
translator.translator_settings.license_settings = item.get_property(
"license_settings"
)
if (
settings.get("Translator Settings")
and settings.get("Translator Settings").value
):
for setting in settings.get("Translator Settings").value:
translator.add_extra_param(
setting.get("name"), setting.get("value")
)
try:
translator.execute()
except Exception as e:
self.logger.error("Failed to run translation: %s" % e)
return
parent_sg_publish_data = item.parent.properties.get("sg_publish_data")
if parent_sg_publish_data and not item.description:
item.description = parent_sg_publish_data["description"]
super(AliasTranslationPublishPlugin, self).publish(settings, item)
# If we have some parent publish data, share the thumbnail between the parent publish and it child
if parent_sg_publish_data:
request_timeout = 60
start_time = time.perf_counter()
self.logger.debug("Sharing the thumbnail")
thumbnail_shared = False
while time.perf_counter() - start_time <= request_timeout:
try:
publisher.shotgun.share_thumbnail(
entities=[item.properties.get("sg_publish_data")],
source_entity=parent_sg_publish_data,
)
self.logger.debug("Thumbnail shared successfully")
thumbnail_shared = True
break
except Exception as e:
pass
time.sleep(1)
if not thumbnail_shared:
self.logger.debug("Thumbnail couln't be shared")
def finalize(self, settings, item):
"""
Execute the finalization pass. This pass executes once all the publish
tasks have completed, and can for example be used to version up files.
:param settings: Dictionary of Settings. The keys are strings, matching
the keys returned in the settings property. The values are `Setting`
instances.
:param item: Item to process
"""
# get the publish "mode" stored inside of the root item properties
bg_processing = item.parent.parent.properties.get("bg_processing", False)
in_bg_process = item.parent.parent.properties.get("in_bg_process", False)
if not bg_processing or (bg_processing and in_bg_process):
self.logger.info(
"Translation published successfully",
extra={
"action_show_in_shotgun": {
"label": "Show Publish",
"tooltip": "Reveal the published file in Flow Production Tracking.",
"entity": item.properties["sg_publish_data"],
}
},
)
def get_publish_template(self, settings, item):
"""
Get a publish template for the supplied settings and item.
:param settings: This plugin instance's configured settings
:param item: The item to determine the publish template for
:return: A template representing the publish path of the item or
None if no template could be identified.
"""
publisher = self.parent
# here we can't use the item.properties.publish_path value as it can store the current session publish template
publish_template_setting = settings.get("Publish Template")
publish_template = publisher.engine.get_template_by_name(
publish_template_setting.value
)
return publish_template
def get_publish_type(self, settings, item):
"""
Get a publish type for the supplied settings and item.
:param settings: This plugin instance's configured settings
:param item: The item to determine the publish type for
:return: A publish type or None if one could not be found.
"""
publisher = self.parent
# get the publish type from the publish path extension as the item will have the session publish type
publish_path = self.get_publish_path(settings, item)
path_info = publisher.util.get_file_path_components(publish_path)
extension = path_info["extension"]
# ensure lowercase and no dot
if extension:
extension = extension.lstrip(".").lower()
for type_def in settings["File Types"].value:
publish_type = type_def[0]
file_extensions = type_def[1:]
if extension in file_extensions:
# found a matching type in settings. use it!
return publish_type
# --- no pre-defined publish type found...
if extension:
# publish type is based on extension
publish_type = "%s File" % extension.capitalize()
else:
# no extension, assume it is a folder
publish_type = "Folder"
return publish_type
def get_publish_name(self, settings, item):
"""
Get the publish name for the supplied settings and item.
:param settings: This plugin instance's configured settings
:param item: The item to determine the publish name for
Uses the path info hook to retrieve the publish name.
"""
publisher = self.parent
publish_path = self.get_publish_path(settings, item)
return publisher.util.get_publish_name(publish_path, sequence=False)
def _copy_work_to_publish(self, settings, item):
"""
This method handles copying work file path(s) to a designated publish
location.
This method requires a "work_template" and a "publish_template" be set
on the supplied item.
The method will handle copying the "path" property to the corresponding
publish location assuming the path corresponds to the "work_template"
and the fields extracted from the "work_template" are sufficient to
satisfy the "publish_template".
The method will not attempt to copy files if any of the above
requirements are not met. If the requirements are met, the file will
ensure the publish path folder exists and then copy the file to that
location.
If the item has "sequence_paths" set, it will attempt to copy all paths
assuming they meet the required criteria with respect to the templates.
"""
# here, as we inherit from the publish_plugin, we have to remove all the actions done in _copy_work_to_publish
# otherwise the translation will be erased by the wire work file
pass
def _session_path():
"""
Return the path to the current session
:return:
"""
return alias_api.get_current_path()
def _get_save_as_action():
"""Simple helper for returning a log action to show the "File Save As" dialog"""
return {
"action_button": {
"label": "Save As...",
"tooltip": "Save the current session",
"callback": sgtk.platform.current_engine().open_save_as_dialog,
}
}
AliasPublishVariantsPlugin
# Copyright (c) 2017 Shotgun Software Inc.
#
# CONFIDENTIAL AND PROPRIETARY
#
# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit
# Source Code License included in this distribution package. See LICENSE.
# By accessing, using, copying or modifying this work you indicate your
# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Shotgun Software Inc.
import os
import sgtk
import alias_api
HookBaseClass = sgtk.get_hook_baseclass()
class AliasPublishVariantsPlugin(HookBaseClass):
"""
Plugin for publishing variants of the current alias open session
"""
@property
def name(self):
"""
One line display name describing the plugin
"""
return "Publish Variants to Flow Production Tracking"
@property
def description(self):
return """
<p>
This plugin exports all Variant images created in Alias and makes a Note in Flow Production Tracking for each one.
</p>
<p>
All Notes are linked this version & file. Use this to sync all review notes made in Alias with Flow Production Tracking.
</p>
<p>
To see the Variant images that will be exported, check the Alias Variant Lister.
</p>
"""
@property
def item_filters(self):
"""
List of item types that this plugin is interested in.
Only items matching entries in this list will be presented to the
accept() method. Strings can contain glob patters such as *, for example
["maya.*", "file.maya"]
"""
return ["alias.session"]
def accept(self, settings, item):
"""
Method called by the publisher to determine if an item is of any
interest to this plugin. Only items matching the filters defined via the
item_filters property will be presented to this method.
A publish task will be generated for each item accepted here. Returns a
dictionary with the following booleans:
- accepted: Indicates if the plugin is interested in this value at
all. Required.
- enabled: If True, the plugin will be enabled in the UI, otherwise
it will be disabled. Optional, True by default.
- visible: If True, the plugin will be visible in the UI, otherwise
it will be hidden. Optional, True by default.
- checked: If True, the plugin will be checked in the UI, otherwise
it will be unchecked. Optional, True by default.
:param settings: Dictionary of Settings. The keys are strings, matching
the keys returned in the settings property. The values are `Setting`
instances.
:param item: Item to process
:returns: dictionary with boolean keys accepted, required and enabled
"""
if not alias_api.has_variants():
self.logger.debug("There are not variants to export")
return {"accepted": False}
return {"accepted": True, "checked": False}
def validate(self, settings, item):
"""
Validates the given item to check that it is ok to publish. Returns a
boolean to indicate validity.
:param settings: Dictionary of Settings. The keys are strings, matching
the keys returned in the settings property. The values are `Setting`
instances.
:param item: Item to process
:returns: True if item is valid, False otherwise.
"""
return True
def publish(self, settings, item):
"""
Executes the publish logic for the given item and settings.
:param settings: Dictionary of Settings. The keys are strings, matching
the keys returned in the settings property. The values are `Setting`
instances.
:param item: Item to process
"""
# get the publish "mode" stored inside of the root item properties
bg_processing = item.parent.properties.get("bg_processing", False)
in_bg_process = item.parent.properties.get("in_bg_process", False)
# as the alias_api.get_variants() method doesn't work with OpenModel
# we need to get the variants locally
if not bg_processing or (bg_processing and not in_bg_process):
variants = []
for variant in alias_api.get_variants():
variants.append((variant.name, variant.path))
item.properties["alias_variants"] = variants
if not bg_processing or (bg_processing and in_bg_process):
publisher = self.parent
version_data = item.properties.get("sg_version_data")
publish_data = item.properties["sg_publish_data"]
# Links, the note will be attached to published file by default
# if a version is created the note will be attached to this too
note_links = [publish_data]
if version_data is not None:
note_links.append(version_data)
for variant in item.properties["alias_variants"]:
data = {
"project": item.context.project,
"user": item.context.user,
"subject": "Alias Variant",
"content": variant[0],
"note_links": note_links,
}
if item.context.task:
data["tasks"] = [item.context.task]
note = publisher.shotgun.create("Note", data)
publisher.shotgun.upload_thumbnail(
entity_type="Note", entity_id=note.get("id"), path=variant[1]
)
variant_filepath = variant[1]
_, file_ext = os.path.splitext(variant_filepath)
publisher.shotgun.upload(
entity_type="Note",
entity_id=note.get("id"),
path=variant_filepath,
field_name="attachments",
display_name="{name}{ext}".format(name=variant[0], ext=file_ext),
)
def finalize(self, settings, item):
"""
Execute the finalization pass. This pass executes once all the publish
tasks have completed, and can for example be used to version up files.
:param settings: Dictionary of Settings. The keys are strings, matching
the keys returned in the settings property. The values are `Setting`
instances.
:param item: Item to process
"""
# get the publish "mode" stored inside of the root item properties
bg_processing = item.parent.properties.get("bg_processing", False)
in_bg_process = item.parent.properties.get("in_bg_process", False)
if not bg_processing or (bg_processing and in_bg_process):
self.logger.info("Variants published successfully")
AliasStartVersionControlPlugin
# Copyright (c) 2017 Shotgun Software Inc.
#
# CONFIDENTIAL AND PROPRIETARY
#
# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit
# Source Code License included in this distribution package. See LICENSE.
# By accessing, using, copying or modifying this work you indicate your
# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Shotgun Software Inc.
import os
import sgtk
import alias_api
HookBaseClass = sgtk.get_hook_baseclass()
class AliasStartVersionControlPlugin(HookBaseClass):
"""
Simple plugin to insert a version number into the alias file path if one
does not exist.
"""
@property
def icon(self):
"""
Path to an png icon on disk
"""
# look for icon one level up from this hook's folder in "icons" folder
return os.path.join(self.disk_location, os.pardir, "icons", "version_up.png")
@property
def name(self):
"""
One line display name describing the plugin
"""
return "Begin file versioning"
@property
def description(self):
"""
Verbose, multi-line description of what the plugin does. This can
contain simple html for formatting.
"""
return """
Adds a version number to the filename.<br><br>
Once a version number exists in the file, the publishing will
automatically bump the version number. For example,
<code>filename.ext</code> will be saved to
<code>filename.v001.ext</code>.<br><br>
If the session has not been saved, validation will fail and a button
will be provided in the logging output to save the file.<br><br>
If a file already exists on disk with a version number, validation will
fail and the logging output will include button to save the file to a
different name.<br><br>
"""
@property
def item_filters(self):
"""
List of item types that this plugin is interested in.
Only items matching entries in this list will be presented to the
accept() method. Strings can contain glob patters such as *, for example
["maya.*", "file.maya"]
"""
return ["alias.session"]
@property
def settings(self):
"""
Dictionary defining the settings that this plugin expects to receive
through the settings parameter in the accept, validate, publish and
finalize methods.
A dictionary on the following form::
{
"Settings Name": {
"type": "settings_type",
"default": "default_value",
"description": "One line description of the setting"
}
The type string should be one of the data types that toolkit accepts as
part of its environment configuration.
"""
return {}
def accept(self, settings, item):
"""
Method called by the publisher to determine if an item is of any
interest to this plugin. Only items matching the filters defined via the
item_filters property will be presented to this method.
A publish task will be generated for each item accepted here. Returns a
dictionary with the following booleans:
- accepted: Indicates if the plugin is interested in this value at
all. Required.
- enabled: If True, the plugin will be enabled in the UI, otherwise
it will be disabled. Optional, True by default.
- visible: If True, the plugin will be visible in the UI, otherwise
it will be hidden. Optional, True by default.
- checked: If True, the plugin will be checked in the UI, otherwise
it will be unchecked. Optional, True by default.
:param settings: Dictionary of Settings. The keys are strings, matching
the keys returned in the settings property. The values are `Setting`
instances.
:param item: Item to process
:returns: dictionary with boolean keys accepted, required and enabled
"""
path = _session_path()
if path:
version_number = self._get_version_number(path, item)
if version_number is not None:
self.logger.info(
"Alias '%s' plugin rejected the current session..." % (self.name,)
)
self.logger.info(" There is already a version number in the file...")
self.logger.info(" Alias file path: %s" % (path,))
return {"accepted": False}
else:
# the session has not been saved before (no path determined).
# provide a save button. the session will need to be saved before
# validation will succeed.
self.logger.warn(
"The Alias session has not been saved.",
extra=_get_save_as_action(),
)
self.logger.info(
"Alias '%s' plugin accepted the current session." % (self.name,),
extra=_get_version_docs_action(),
)
# accept the plugin, but don't force the user to add a version number
# (leave it unchecked)
return {"accepted": True, "checked": False}
def validate(self, settings, item):
"""
Validates the given item to check that it is ok to publish.
Returns a boolean to indicate validity.
:param settings: Dictionary of Settings. The keys are strings, matching
the keys returned in the settings property. The values are `Setting`
instances.
:param item: Item to process
:returns: True if item is valid, False otherwise.
"""
publisher = self.parent
path = _session_path()
if not path:
# the session still requires saving. provide a save button.
# validation fails
error_msg = "The Alias session has not been saved."
self.logger.error(error_msg, extra=_get_save_as_action())
raise Exception(error_msg)
# NOTE: If the plugin is attached to an item, that means no version
# number could be found in the path. If that's the case, the work file
# template won't be much use here as it likely has a version number
# field defined within it. Simply use the path info hook to inject a
# version number into the current file path
# get the path to a versioned copy of the file.
version_path = publisher.util.get_version_path(path, "v001")
if os.path.exists(version_path):
error_msg = (
"A file already exists with a version number. Please "
"choose another name."
)
self.logger.error(error_msg, extra=_get_save_as_action())
raise Exception(error_msg)
return True
def publish(self, settings, item):
"""
Executes the publish logic for the given item and settings.
:param settings: Dictionary of Settings. The keys are strings, matching
the keys returned in the settings property. The values are `Setting`
instances.
:param item: Item to process
"""
publisher = self.parent
# get the path in a normalized state. no trailing separator, separators
# are appropriate for current os, no double separators, etc.
path = sgtk.util.ShotgunPath.normalize(_session_path())
# ensure the session is saved in its current state
publisher.engine.save_file()
# get the path to a versioned copy of the file.
version_path = publisher.util.get_version_path(path, "v001")
# save to the new version path
publisher.engine.save_file_as(version_path)
self.logger.info("A version number has been added to the Alias file...")
self.logger.info(" Alias file path: %s" % (version_path,))
def finalize(self, settings, item):
"""
Execute the finalization pass. This pass executes once
all the publish tasks have completed, and can for example
be used to version up files.
:param settings: Dictionary of Settings. The keys are strings, matching
the keys returned in the settings property. The values are `Setting`
instances.
:param item: Item to process
"""
pass
def _get_version_number(self, path, item):
"""
Try to extract and return a version number for the supplied path.
:param path: The path to the current session
:return: The version number as an `int` if it can be determined, else
None.
NOTE: This method will use the work template provided by the
session collector, if configured, to determine the version number. If
not configured, the version number will be extracted using the zero
config path_info hook.
"""
publisher = self.parent
version_number = None
work_template = item.properties.get("work_template")
if work_template:
if work_template.validate(path):
self.logger.debug("Using work template to determine version number.")
work_fields = work_template.get_fields(path)
if "version" in work_fields:
version_number = work_fields.get("version")
else:
self.logger.debug("Work template did not match path")
else:
self.logger.debug("Work template unavailable for version extraction.")
if version_number is None:
self.logger.debug("Using path info hook to determine version number.")
version_number = publisher.util.get_version_number(path)
return version_number
def _session_path():
"""
Return the path to the current session
:return:
"""
return alias_api.get_current_path()
def _get_save_as_action():
"""Simple helper for returning a log action to show the "File Save As" dialog"""
return {
"action_button": {
"label": "Save As...",
"tooltip": "Save the current session",
"callback": sgtk.platform.current_engine().open_save_as_dialog,
}
}
def _get_version_docs_action():
"""
Simple helper for returning a log action to show version docs
"""
return {
"action_open_url": {
"label": "Version Docs",
"tooltip": "Show docs for version formats",
"url": "https://help.autodesk.com/view/SGSUB/ENU/?guid=SG_Supervisor_Artist_sa_integrations_sa_integrations_user_guide_html",
}
}
UploadVersionPlugin
# Copyright (c) 2017 Shotgun Software Inc.
#
# CONFIDENTIAL AND PROPRIETARY
#
# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit
# Source Code License included in this distribution package. See LICENSE.
# By accessing, using, copying or modifying this work you indicate your
# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Shotgun Software Inc.
import os
import shutil
import tempfile
import sgtk
HookBaseClass = sgtk.get_hook_baseclass()
class UploadVersionPlugin(HookBaseClass):
"""Plugin for uploading Versions to Flow Production Tracking for review."""
# Version Type string constants
VERSION_TYPE_2D = "2D Version"
VERSION_TYPE_3D = "3D Version"
# Version Type Options
VERSION_TYPE_OPTIONS = [
VERSION_TYPE_2D,
VERSION_TYPE_3D,
]
# Descriptions for Version Types
VERSION_TYPE_DESCRIPTIONS = {
VERSION_TYPE_2D: """
Create a Version in Flow Production Tracking for Review.<br/><br/>
A 2D Version (image or video representation of your file/scene) will be created in Flow Production Tracking.
This Version can then be reviewed via Flow Production Tracking's many review apps.
""",
VERSION_TYPE_3D: """
Create a Version in Flow Production Tracking for Review.<br/><br/>
A 3D Version (LMV translation of your file/scene's geometry) will be created in
Flow Production Tracking. This Version can then be reviewed via Flow Production Tracking's
many review apps.<br/><br/> References in your file will not be included in the 3D version.
""",
}
@property
def icon(self):
"""Path to an png icon on disk."""
return os.path.join(self.disk_location, os.pardir, "icons", "review.png")
@property
def settings(self):
"""
Dictionary defining the settings that this plugin expects to recieve
through the settings parameter in the accept, validate, publish and
finalize methods.
A dictionary on the following form::
{
"Settings Name": {
"type": "settings_type",
"default": "default_value",
"description": "One line description of the setting"
}
The type string should be one of the data types that toolkit accepts as
part of its environment configuration.
"""
# inherit the settings from the base publish plugin
base_settings = super(UploadVersionPlugin, self).settings or {}
# settings specific to this class
upload_version_settings = {
"Version Type": {
"type": "str",
"default": self.VERSION_TYPE_2D,
"description": "Generate a {options} or {last_option} Version".format(
options=", ".join(self.VERSION_TYPE_OPTIONS[:-1]),
last_option=self.VERSION_TYPE_OPTIONS[-1],
),
},
"Upload": {
"type": "bool",
"default": False,
"description": "Upload content to Flow Production Tracking?",
},
}
# update the base settings
base_settings.update(upload_version_settings)
return base_settings
@property
def item_filters(self):
"""
List of item types that this plugin is interested in.
Only items matching entries in this list will be presented to the
accept() method. Strings can contain glob patters such as *, for example
["maya.*", "file.maya"]
"""
return ["alias.session"]
def accept(self, settings, item):
"""
Method called by the publisher to determine if an item is of any
interest to this plugin. Only items matching the filters defined via the
item_filters property will be presented to this method.
A publish task will be generated for each item accepted here. Returns a
dictionary with the following booleans:
- accepted: Indicates if the plugin is interested in this value at
all. Required.
- enabled: If True, the plugin will be enabled in the UI, otherwise
it will be disabled. Optional, True by default.
- visible: If True, the plugin will be visible in the UI, otherwise
it will be hidden. Optional, True by default.
- checked: If True, the plugin will be checked in the UI, otherwise
it will be unchecked. Optional, True by default.
:param settings: Dictionary of Settings. The keys are strings, matching
the keys returned in the settings property. The values are `Setting`
instances.
:param item: Item to process
:returns: dictionary with boolean keys accepted, required and enabled
"""
return {"accepted": True, "checked": True}
def validate(self, settings, item):
"""
Validates the given item to check that it is ok to publish. Returns a
boolean to indicate validity.
:param settings: Dictionary of Settings. The keys are strings, matching
the keys returned in the settings property. The values are `Setting`
instances.
:param item: Item to process
:returns: True if item is valid, False otherwise.
"""
path = item.get_property("path")
if not path:
self.logger.error("No path found for item")
return False
# Validate fails if the Version Type is not supported
version_type = settings.get("Version Type").value
if version_type not in self.VERSION_TYPE_OPTIONS:
self.logger.error("Unsupported Version Type '{}'".format(version_type))
return False
# Check the site pref for 3D Review enabled. Provide warning messages if the user is
# attempting to create a 3D Version but may not have 3D Review enabled on their site, but
# do not block the user from publishing.
if version_type == self.VERSION_TYPE_3D:
is_3d_viewer_enabled = self._is_3d_viewer_enabled()
if is_3d_viewer_enabled is None:
self.logger.warning(
"Failed to check if 3D Review is enabled for your site."
)
self.logger.warning(
"Please contact Autodesk support to access your site preference for 3D Review or use the 2D Version publish option instead."
)
elif not is_3d_viewer_enabled:
self.logger.warning("Your site does not have 3D Review enabled.")
self.logger.warning(
"Please contact Autodesk support to have 3D Review enabled on your Flow Production Tracking site or use the 2D Version publish option instead."
)
framework_lmv = self.load_framework("tk-framework-lmv_v1.x.x")
if not framework_lmv:
self.logger.error("Missing required framework tk-framework-lmv v1.x.x")
return False
translator = framework_lmv.import_module("translator")
lmv_translator = translator.LMVTranslator(
path, self.parent.sgtk, item.context
)
lmv_translator_path = lmv_translator.get_translator_path()
if not lmv_translator_path:
self.logger.error(
"Missing translator for Alias. Alias must be installed locally to run LMV translation."
)
return False
return True
def publish(self, settings, item):
"""
Executes the publish logic for the given item and settings.
:param settings: Dictionary of Settings. The keys are strings, matching
the keys returned in the settings property. The values are `Setting`
instances.
:param item: Item to process
"""
# Get the publish "mode" stored inside of the root item properties
bg_processing = item.parent.properties.get("bg_processing", False)
in_bg_process = item.parent.properties.get("in_bg_process", False)
if not bg_processing or (bg_processing and in_bg_process):
publisher = self.parent
path = item.properties["path"]
# Be sure to strip the extension from the publish name
path_components = publisher.util.get_file_path_components(path)
filename = path_components["filename"]
(publish_name, _) = os.path.splitext(filename)
item.properties["publish_name"] = publish_name
# Create the Version in Flow Production Tracking
super(UploadVersionPlugin, self).publish(settings, item)
# Generate media content and upload to Flow Production Tracking
version_type = item.properties["sg_version_data"]["type"]
version_id = item.properties["sg_version_data"]["id"]
thumbnail_path = item.get_thumbnail_as_path()
media_package_path = None
media_version_type = settings.get("Version Type").value
if media_version_type == self.VERSION_TYPE_3D:
# Pass the thumbnail retrieved to override the LMV thumbnail, and ignore the
# LMV thumbnail output
media_package_path, _, _ = self._translate_file_to_lmv(
item, thumbnail_path=thumbnail_path
)
self.logger.info("Translated file to LMV")
if media_package_path:
# For 3D media, a media package path will be generated. Set the translation
# type on the Version in order to view 3D media in Flow Production Tracking Web.
self.parent.shotgun.update(
entity_type=version_type,
entity_id=version_id,
data={"sg_translation_type": "LMV"},
)
self.logger.info("Set Version translation type to LMV")
uploaded_movie_path = media_package_path or thumbnail_path
if uploaded_movie_path:
# Uplod to the `sg_uploaded_movie` field on the Version so that the Version
# thumbnail shows the "play" button on hover from Flow Production Tracking Web
self.parent.shotgun.upload(
entity_type=version_type,
entity_id=version_id,
path=uploaded_movie_path,
field_name="sg_uploaded_movie",
)
self.logger.info(
f"Uploaded Version media from path {uploaded_movie_path}"
)
if thumbnail_path:
self.parent.shotgun.upload_thumbnail(
entity_type=version_type,
entity_id=version_id,
path=thumbnail_path,
)
self.logger.info(
f"Uploaded Version thumbnail from path {thumbnail_path}"
)
# Remove the temporary directory or files created to generate media content
self._cleanup_temp_files(media_package_path)
def finalize(self, settings, item):
"""
Execute the finalization pass. This pass executes once all the publish
tasks have completed, and can for example be used to version up files.
:param settings: Dictionary of Settings. The keys are strings, matching
the keys returned in the settings property. The values are `Setting`
instances.
:param item: Item to process
"""
# get the publish "mode" stored inside of the root item properties
bg_processing = item.parent.properties.get("bg_processing", False)
in_bg_process = item.parent.properties.get("in_bg_process", False)
if not bg_processing or (bg_processing and in_bg_process):
super(UploadVersionPlugin, self).finalize(settings, item)
############################################################################
# Methods for creating/displaying custom plugin interface
def create_settings_widget(self, parent, items=None):
"""
Creates a Qt widget, for the supplied parent widget (a container widget
on the right side of the publish UI).
:param parent: The parent to use for the widget being created.
:param items: A list of PublishItems the selected publish tasks are parented to.
:return: A QtGui.QWidget or subclass that displays information about
the plugin and/or editable widgets for modifying the plugin's
settings.
"""
# defer Qt-related imports
from sgtk.platform.qt import QtCore, QtGui
# The main widget
widget = QtGui.QWidget(parent)
widget_layout = QtGui.QVBoxLayout()
# create a group box to display the description
description_group_box = QtGui.QGroupBox(widget)
description_group_box.setTitle("Description:")
# Defer setting the description text, this will be updated when
# the version type combobox value is changed
description_label = QtGui.QLabel()
description_label.setWordWrap(True)
description_label.setOpenExternalLinks(True)
description_label.setTextFormat(QtCore.Qt.RichText)
# create the layout to use within the group box
description_layout = QtGui.QVBoxLayout()
description_layout.addWidget(description_label)
description_layout.addStretch()
description_group_box.setLayout(description_layout)
# Add a combobox to edit the version type option
version_type_combobox = QtGui.QComboBox(widget)
version_type_combobox.setAccessibleName("Version type selection dropdown")
version_type_combobox.addItems(self.VERSION_TYPE_OPTIONS)
# Hook up the signal/slot to update the description according to the current version type
version_type_combobox.currentIndexChanged.connect(
lambda index: self._on_version_type_changed(
version_type_combobox.currentText(), description_label
)
)
# Add all the minor widgets to the main widget
widget_layout.addWidget(description_group_box)
widget_layout.addWidget(version_type_combobox)
widget.setLayout(widget_layout)
# Set the widget property to store the combobox to access in get_ui_settings and set_ui_settings
widget.setProperty("description_label", description_label)
widget.setProperty("version_type_combobox", version_type_combobox)
return widget
def get_ui_settings(self, widget, items=None):
"""
This method is required to be defined in order for the custom UI to show up in the app.
Invoked by the Publisher when the selection changes. This method gathers the settings
on the previously selected task, so that they can be later used to repopulate the
custom UI if the task gets selected again. They will also be passed to the accept, validate,
publish and finalize methods, so that the settings can be used to drive the publish process.
The widget argument is the widget that was previously created by
`create_settings_widget`.
The method returns a dictionary, where the key is the name of a
setting that should be updated and the value is the new value of that
setting. Note that it is up to you how you want to store the UI's state as
settings and you don't have to necessarily to return all the values from
the UI. This is to allow the publisher to update a subset of settings
when multiple tasks have been selected.
Example::
{
"setting_a": "/path/to/a/file"
}
:param widget: The widget that was created by `create_settings_widget`
"""
ui_settings = {}
# Get the Version Type settings value from the UI combobox
version_type_combobox = widget.property("version_type_combobox")
if version_type_combobox:
version_type_index = version_type_combobox.currentIndex()
if 0 <= version_type_index < len(self.VERSION_TYPE_OPTIONS):
self.VERSION_TYPE_OPTIONS[version_type_index]
ui_settings["Version Type"] = self.VERSION_TYPE_OPTIONS[
version_type_index
]
else:
self.logger.debug(
"Invalid Version Type index {}".format(version_type_index)
)
return ui_settings
def set_ui_settings(self, widget, settings, items=None):
"""
This method is required to be defined in order for the custom UI to show up in the app.
Allows the custom UI to populate its fields with the settings from the
currently selected tasks.
The widget is the widget created and returned by
`create_settings_widget`.
A list of settings dictionaries are supplied representing the current
values of the settings for selected tasks. The settings dictionaries
correspond to the dictionaries returned by the settings property of the
hook.
Example::
settings = [
{
"seeting_a": "/path/to/a/file"
"setting_b": False
},
{
"setting_a": "/path/to/a/file"
"setting_b": False
}]
The default values for the settings will be the ones specified in the
environment file. Each task has its own copy of the settings.
When invoked with multiple settings dictionaries, it is the
responsibility of the custom UI to decide how to display the
information. If you do not wish to implement the editing of multiple
tasks at the same time, you can raise a ``NotImplementedError`` when
there is more than one item in the list and the publisher will inform
the user than only one task of that type can be edited at a time.
:param widget: The widget that was created by `create_settings_widget`.
:param settings: a list of dictionaries of settings for each selected
task.
:param items: A list of PublishItems the selected publish tasks are parented to.
"""
if not settings:
return
if len(settings) > 1:
raise NotImplementedError
version_type_combobox = widget.property("version_type_combobox")
if not version_type_combobox:
self.logger.debug(
"Failed to retrieve Version Type combobox to set custom UI"
)
return
description_label = widget.property("description_label")
if not description_label:
self.logger.debug(
"Failed to retrieve Version Type combobox to set custom UI"
)
# Get the default setting for version type
default_value = self.settings.get("Version Type", {}).get(
"default", self.VERSION_TYPE_OPTIONS[0]
)
# Get the version type value from the settings, and set the combobox accordingly
version_type_value = settings[0].get("Version Type", default_value)
version_type_index = max(self.VERSION_TYPE_OPTIONS.index(version_type_value), 0)
# Set the version type combobox
current_version_index = version_type_combobox.currentIndex()
if current_version_index == version_type_index:
# Combobox already has the correct verstion type - manually trigger the 'currentIndexChanged'
# slot to update the description label based on the version
self._on_version_type_changed(version_type_value, description_label)
else:
version_type_combobox.setCurrentIndex(version_type_index)
def _on_version_type_changed(self, version_type, description_label):
"""
Slot called when the Version Type combobox selector index changes.
Update the description based on the current Version Type.
:param version_type: The current Version Type.
:type version_type: str
:param description_label: The label widget to set the description on
:type description_label: QLabel
"""
if not description_label:
return
note = ""
if version_type == self.VERSION_TYPE_3D:
is_3d_viewer_enabled = self._is_3d_viewer_enabled()
if is_3d_viewer_enabled is None:
note = """
<br/><br/>
<b>NOTE:</b>
<br/>
<b>
Failed to check if 3D Review is enabled for your site.
<br/>
You may create a 3D Version for review, but if 3D Review is not enabled,
you will see an error message 'No web playable media available' when trying to open the Version with the Media viewer.
</b>
<br/><br/>
<b>
Please contact Autodesk support to access your site preference for 3D Review or use the 2D Version publish option instead.
</b>
"""
elif not is_3d_viewer_enabled:
note = """
<br/><br/>
<b>NOTE:</b>
<br/>
<b>
Your site does not have 3D Review enabled.
<br/>
You may create a 3D Version for review, but until your site has 3D Review enabled,
you will see an error message 'No web playable media available' when trying to open the Version with the Media viewer.
</b>
<br/><br/>
<b>
Please contact Autodesk support to have 3D Review enabled on your Flow Production Tracking site or use the 2D Version publish option instead.
</b>
"""
text = "{description}{note}".format(
description=self.VERSION_TYPE_DESCRIPTIONS.get(
version_type, self.description
),
note=note,
)
description_label.setText(text)
############################################################################
# Protected functions
def _cleanup_temp_files(self, path, remove_from_root=True):
"""
Remove any temporary directories or files from the given path.
If `remove_from_root` is True, the top most level directory of the given path is
used to remove all sub directories and files.
:param path: The file path to remove temporary files and/or directories from.
:type path: str
:param remove_from_root: True will remove directories and files from the top most level
directory within the root temporary directory, else False will remove the single
file or directory (and its children). Default is True.
:type remove_from_root: bool
"""
if path is None or not os.path.exists(path):
return # Cannot clean up a path that does not exist
tempdir = tempfile.gettempdir()
if os.path.commonpath([path, tempdir]) != tempdir:
return # Not a temporary directory or file
if remove_from_root:
# Get the top most level of the path that is inside the root temp dir
relative_path = os.path.relpath(path, tempdir)
path = os.path.normpath(
os.path.join(tempdir, relative_path.split(os.path.sep)[0])
)
if os.path.isdir(path):
shutil.rmtree(path)
elif os.path.isfile(path):
os.remove(path)
def _translate_file_to_lmv(self, item, thumbnail_path=None):
"""
Translate the current Alias file as an LMV package in order to upload it to Flow Production Tracking as a 3D Version
:param item: Item to process
:type item: PublishItem
:param thumbnail_path: Optionally pass a thumbnail file path to override the LMV
thumbnail (this thumbnail will be included in the LMV packaged zip file).
:type thumbnail_path: str
:returns:
- The path to the LMV zip file
- The path to the LMV thumbnail
- The path to the temporary folder where the LMV files have been processed
"""
path = item.get_property("path")
thumbnail_path = thumbnail_path or item.get_thumbnail_as_path()
# Translate the file to LMV
framework_lmv = self.load_framework("tk-framework-lmv_v1.x.x")
translator = framework_lmv.import_module("translator")
lmv_translator = translator.LMVTranslator(path, self.parent.sgtk, item.context)
lmv_translator.translate()
# Package up the LMV files into a zip file
file_name = str(item.properties["sg_version_data"]["id"])
package_path, lmv_thumbnail_path = lmv_translator.package(
svf_file_name=file_name,
thumbnail_path=thumbnail_path,
)
return package_path, lmv_thumbnail_path, lmv_translator.output_directory
def _is_3d_viewer_enabled(self):
"""
Look up the Flow Production Tracking site preference to check if the 3D Viewer is enabled. Return True
if the 3D Viewer is enabled, False if it is disabled, or None if the 3D Viewer Enabled
site pref could not be accessed.
If the Flow Production Tracking API returns an empty dictionary, the hidden site preference could not be
accessed. Ensure that the hidden site preference "API hidden allowed list of preferences"
contains the "enable_3d_viewer" in its list.
:return: True if the 3D Viewer is enabled for the Flow Production Tracking site, False if it is disabled,
or None if the 3D Viewer site pref could not be accessed.
:rtype: bool
"""
enable_3d_viewer_pref = "enable_3d_viewer"
prefs = self.parent.shotgun.preferences_read(prefs=[enable_3d_viewer_pref])
if not prefs:
# The 'enable_3d_viewer' site pref could not be accessed
return None
return prefs[enable_3d_viewer_pref]
tk-multi-shotgunpanel
AliasActions
# Copyright (c) 2015 Shotgun Software Inc.
#
# CONFIDENTIAL AND PROPRIETARY
#
# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit
# Source Code License included in this distribution package. See LICENSE.
# By accessing, using, copying or modifying this work you indicate your
# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Shotgun Software Inc.
"""
Hook that loads defines all the available actions, broken down by publish type.
"""
import os
import sgtk
import alias_api
import tempfile
HookBaseClass = sgtk.get_hook_baseclass()
class AliasActions(HookBaseClass):
"""
Flow Production Tracking Panel Actions for Alias
"""
def generate_actions(self, sg_data, actions, ui_area):
"""
Returns a list of action instances for a particular publish.
This method is called each time a user clicks a publish somewhere in the UI.
The data returned from this hook will be used to populate the actions menu for a publish.
The mapping between Publish types and actions are kept in a different place
(in the configuration) so at the point when this hook is called, the loader app
has already established *which* actions are appropriate for this object.
The hook should return at least one action for each item passed in via the
actions parameter.
This method needs to return detailed data for those actions, in the form of a list
of dictionaries, each with name, params, caption and description keys.
Because you are operating on a particular publish, you may tailor the output
(caption, tooltip etc) to contain custom information suitable for this publish.
The ui_area parameter is a string and indicates where the publish is to be shown.
- If it will be shown in the main browsing area, "main" is passed.
- If it will be shown in the details area, "details" is passed.
- If it will be shown in the history area, "history" is passed.
Please note that it is perfectly possible to create more than one action "instance" for
an action! You can for example do scene introspection - if the action passed in
is "character_attachment" you may for example scan the scene, figure out all the nodes
where this object can be attached and return a list of action instances:
"attach to left hand", "attach to right hand" etc. In this case, when more than
one object is returned for an action, use the params key to pass additional
data into the run_action hook.
:param sg_data: Flow Production Tracking data dictionary with all the standard publish fields.
:param actions: List of action strings which have been defined in the app configuration.
:param ui_area: String denoting the UI Area (see above).
:returns List of dictionaries, each with keys name, params, caption and description
"""
self.logger.debug(
"Generate actions called for UI element %s. "
"Actions: %s. Publish Data: %s" % (ui_area, actions, sg_data)
)
action_instances = []
try:
# call base class first
action_instances += HookBaseClass.generate_actions(
self, sg_data, actions, ui_area
)
except AttributeError:
# base class doesn't have the method, so ignore and continue
pass
if "reference" in actions:
action_instances.append(
{
"name": "reference",
"params": None,
"caption": "Create Reference",
"description": "This will add the item to the universe as a standard reference.",
}
)
if "import" in actions:
action_instances.append(
{
"name": "import",
"params": None,
"caption": "Import into Scene",
"description": "This will import the item into the current universe.",
}
)
if "import_as_reference" in actions:
action_instances.append(
{
"name": "import_as_reference",
"params": None,
"caption": "Import as Reference",
"description": "This will import the item as a reference into the current universe.",
}
)
if "texture_node" in actions:
action_instances.append(
{
"name": "texture_node",
"params": None,
"caption": "Create Canvas",
"description": "This will import the item into the current universe.",
}
)
if "import_note_attachments" in actions:
action_instances.append(
{
"name": "import_note_attachments",
"params": None,
"caption": "Import Note attachment(s) as canvas image(s)",
"description": "This will create a new canvas for each image attached to the note.",
}
)
if "import_subdiv" in actions:
action_instances.append(
{
"name": "import_subdiv",
"params": None,
"caption": "Import Subdiv file into Scene",
"description": "This will import the subdiv item into the current universe.",
}
)
return action_instances
def execute_action(self, name, params, sg_data):
"""
Execute a given action. The data sent to this be method will
represent one of the actions enumerated by the generate_actions method.
:param name: Action name string representing one of the items returned by generate_actions.
:param params: Params data, as specified by generate_actions.
:param sg_data: Flow Production Tracking data dictionary with all the standard publish fields.
:returns: No return value expected.
"""
self.logger.debug(
"Execute action called for action %s. "
"Parameters: %s. Flow Production Tracking Data: %s"
% (name, params, sg_data)
)
if name == "reference":
path = self.get_publish_path(sg_data)
self._create_reference(path)
elif name == "import":
path = self.get_publish_path(sg_data)
self._import_file(path)
elif name == "import_as_reference":
path = self.get_publish_path(sg_data)
self._import_file_as_reference(path)
elif name == "texture_node":
path = self.get_publish_path(sg_data)
self._create_texture_node(path)
if name == "import_note_attachments":
self._import_note_attachments_as_canvas(sg_data)
elif name == "import_subdiv":
path = self.get_publish_path(sg_data)
self._import_subdivision(path)
else:
try:
HookBaseClass.execute_action(self, name, params, sg_data)
except AttributeError:
# base class doesn't have the method, so ignore and continue
pass
def execute_multiple_actions(self, actions):
"""
Executes the specified action on a list of items.
The default implementation dispatches each item from ``actions`` to
the ``execute_action`` method.
The ``actions`` is a list of dictionaries holding all the actions to execute.
Each entry will have the following values:
name: Name of the action to execute
sg_data: Publish information coming from Flow Production Tracking
params: Parameters passed down from the generate_actions hook.
.. note::
This is the default entry point for the hook. It reuses the ``execute_action``
method for backward compatibility with hooks written for the previous
version of the loader.
.. note::
The hook will stop applying the actions on the selection if an error
is raised midway through.
:param list actions: Action dictionaries.
"""
for single_action in actions:
name = single_action["name"]
sg_data = single_action["sg_data"]
params = single_action["params"]
self.execute_action(name, params, sg_data)
def _create_reference(self, path):
"""
Create an Alias reference.
:param path: Path to the file.
"""
if not os.path.exists(path):
raise Exception("File not found on disk - '%s'" % path)
alias_api.create_reference(path)
def _import_file(self, path):
"""
Import the file into the current Alias session.
:param path: Path to file.
"""
if not os.path.exists(path):
raise Exception("File not found on disk - '%s'" % path)
alias_api.import_file(path)
def _import_file_as_reference(self, path):
"""
Import the file as an Alias reference, converting it on the fly as wref.
:param path: Path to the file.
"""
reference_template = self.parent.engine.get_template("reference_template")
source_template = self.sgtk.template_from_path(path)
# get the path to the reference, using the templates if it's possible otherwise using the source path
# location
if reference_template and source_template:
template_fields = source_template.get_fields(path)
template_fields["alias.extension"] = os.path.splitext(path)[-1][1:]
reference_path = reference_template.apply_fields(template_fields)
else:
output_path, output_ext = os.path.splitext(path)
reference_path = "{output_path}_{output_ext}.wref".format(
output_path=output_path, output_ext=output_ext[1:]
)
# if the reference file doesn't exist on disk yet, run the translation
if not os.path.exists(reference_path):
framework = self.load_framework("tk-framework-aliastranslations_v0.x.x")
if not framework:
raise Exception("Couldn't find tk-framework-aliastranslations_v0.x.x")
tk_framework_aliastranslations = framework.import_module(
"tk_framework_aliastranslations"
)
translator = tk_framework_aliastranslations.Translator(path, reference_path)
translator.execute()
alias_api.create_reference(reference_path)
def _create_texture_node(self, path):
"""
Import an image as Canvas in Alias
:param path: Path to the image.
"""
if not os.path.exists(path):
raise Exception("File not found on disk - '%s'" % path)
alias_api.create_texture_node(path, True)
def _import_note_attachments_as_canvas(self, sg_data):
"""
Import the Note attachments as canvas images.
This will create a new canvas for each image attached to the note.
:param sg_data: The Flow Production Tracking entity dict for the note.
:type sg_data: dict
"""
if not sg_data or not sg_data.get("id"):
return
sg_note = self.parent.shotgun.find_one(
"Note", [["id", "is", sg_data["id"]]], ["attachments"]
)
if not sg_note:
return
with tempfile.TemporaryDirectory() as temp_dir:
for attachment in sg_note["attachments"]:
temp_path = os.path.join(temp_dir, attachment["name"])
self.parent.shotgun.download_attachment(attachment, temp_path)
alias_api.create_texture_node(temp_path)
def _import_subdivision(self, path):
"""
Import a file as subdivision in the current Alias session.
:param path: Path to the file.
"""
if not os.path.exists(path):
raise Exception("File not found on disk - '%s'" % path)
try:
alias_api.import_subdiv(path)
except alias_api.AliasPythonException as api_error:
err_msg = "{api_error}<br/><br/>For more information, click {help_link}.".format(
api_error=str(api_error),
help_link="<a href='https://help.autodesk.com/view/ALIAS/2023/ENU/?guid=GUID-667410AD-CF4D-43B3-AE96-0C1331CB80B2'>here</a>",
)
raise alias_api.AliasPythonException(err_msg)
tk-multi-workfiles2
SceneOperation
# Copyright (c) 2017 Shotgun Software Inc.
#
# CONFIDENTIAL AND PROPRIETARY
#
# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit
# Source Code License included in this distribution package. See LICENSE.
# By accessing, using, copying or modifying this work you indicate your
# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Shotgun Software Inc.
import alias_api
import uuid
import sgtk
from sgtk.platform.qt import QtGui
HookClass = sgtk.get_hook_baseclass()
class SceneOperation(HookClass):
"""
Hook called to perform an operation with the
current file
"""
def execute(
self,
operation,
file_path,
context=None,
parent_action=None,
file_version=None,
read_only=None,
**kwargs
):
"""
Main hook entry point
:param operation: String
Scene operation to perform
:param file_path: String
File path to use if the operation
requires it (e.g. open)
:param context: Context
The context the file operation is being
performed in.
:param parent_action: This is the action that this scene operation is
being executed for. This can be one of:
- open_file
- new_file
- save_file_as
- version_up
:param file_version: The version/revision of the file to be opened. If this is 'None'
then the latest version should be opened.
:param read_only: Specifies if the file should be opened read-only or not
:returns: Depends on operation:
'current_path' - Return the current scene
file path as a String
'open' - True if file was opened, otherwise False
'reset' - True if scene was reset to an empty
state, otherwise False
all others - None
"""
# At the end of the scene operation, indicate if the context needs to be saved for the
# current Alias stage.
save_context = operation in ["save_as", "prepare_new", "open", "reset"]
# Use the event watcher context manager to queue any callbacks triggered by Alias
# events while performing any scene operations. This ensures that all Alias file I/O
# operations are complete before executing any event callbacks that may interfere
# with Alias
with self.parent.engine.event_watcher.create_context_manager():
try:
if operation == "current_path":
return alias_api.get_current_path()
if operation == "open":
if alias_api.is_empty_file():
alias_api.open_file(file_path, new_stage=False)
else:
open_in_current_stage = (
self.parent.engine.open_delete_stages_dialog()
)
if open_in_current_stage == QtGui.QMessageBox.Cancel:
# Do not save the context if the operation was cancelled.
save_context = False
return False
if open_in_current_stage == QtGui.QMessageBox.No:
alias_api.open_file(file_path, new_stage=True)
else:
alias_api.reset()
alias_api.open_file(file_path, new_stage=False)
elif operation == "save":
alias_api.save_file()
elif operation == "save_as":
alias_api.save_file_as(file_path)
elif operation == "reset":
# do not reset the file if we try to open another one as we have to deal with the stages an resetting
# the current session will delete all the stages
if parent_action == "open_file":
save_context = False
return True
if alias_api.is_empty_file() and len(alias_api.get_stages()) == 1:
alias_api.reset()
return True
open_in_current_stage = (
self.parent.engine.open_delete_stages_dialog(new_file=True)
)
if open_in_current_stage == QtGui.QMessageBox.Cancel:
# Do not save the context if the operation was cancelled.
save_context = False
return False
if open_in_current_stage == QtGui.QMessageBox.No:
stage_name = uuid.uuid4().hex
alias_api.create_stage(stage_name)
else:
alias_api.reset()
return True
finally:
if save_context:
# It is important that this method is executed before the event watcher
# context manager exits to ensure that the current context is saved for
# this Alias stage, before any event callbacks are triggered (whcih may
# require the contexts to be updated).
self.parent.engine.save_context_for_stage(context)