diff --git a/pype/hosts/nuke/lib.py b/pype/hosts/nuke/lib.py
index 8c0e37b15d..19a0784327 100644
--- a/pype/hosts/nuke/lib.py
+++ b/pype/hosts/nuke/lib.py
@@ -1,7 +1,6 @@
import os
import re
import sys
-import getpass
from collections import OrderedDict
from avalon import api, io, lib
@@ -1060,310 +1059,6 @@ def get_write_node_template_attr(node):
return avalon.nuke.lib.fix_data_for_node_create(correct_data)
-class BuildWorkfile(WorkfileSettings):
- """
- Building first version of workfile.
-
- Settings are taken from presets and db. It will add all subsets
- in last version for defined representaions
-
- Arguments:
- variable (type): description
-
- """
- xpos = 0
- ypos = 0
- xpos_size = 80
- ypos_size = 90
- xpos_gap = 50
- ypos_gap = 50
- pos_layer = 10
-
- def __init__(self,
- root_path=None,
- root_node=None,
- nodes=None,
- to_script=None,
- **kwargs):
- """
- A short description.
-
- A bit longer description.
-
- Argumetns:
- root_path (str): description
- root_node (nuke.Node): description
- nodes (list): list of nuke.Node
- nodes_effects (dict): dictionary with subsets
-
- Example:
- nodes_effects = {
- "plateMain": {
- "nodes": [
- [("Class", "Reformat"),
- ("resize", "distort"),
- ("flip", True)],
-
- [("Class", "Grade"),
- ("blackpoint", 0.5),
- ("multiply", 0.4)]
- ]
- },
- }
-
- """
-
- WorkfileSettings.__init__(self,
- root_node=root_node,
- nodes=nodes,
- **kwargs)
- self.to_script = to_script
- # collect data for formating
- self.data_tmp = {
- "project": {"name": self._project["name"],
- "code": self._project["data"].get("code", "")},
- "asset": self._asset or os.environ["AVALON_ASSET"],
- "task": kwargs.get("task") or api.Session["AVALON_TASK"],
- "hierarchy": kwargs.get("hierarchy") or pype.get_hierarchy(),
- "version": kwargs.get("version", {}).get("name", 1),
- "user": getpass.getuser(),
- "comment": "firstBuild",
- "ext": "nk"
- }
-
- # get presets from anatomy
- anatomy = get_anatomy()
- # format anatomy
- anatomy_filled = anatomy.format(self.data_tmp)
-
- # get dir and file for workfile
- self.work_dir = anatomy_filled["work"]["folder"]
- self.work_file = anatomy_filled["work"]["file"]
-
- def save_script_as(self, path=None):
- # first clear anything in open window
- nuke.scriptClear()
-
- if not path:
- dir = self.work_dir
- path = os.path.join(
- self.work_dir,
- self.work_file).replace("\\", "/")
- else:
- dir = os.path.dirname(path)
-
- # check if folder is created
- if not os.path.exists(dir):
- os.makedirs(dir)
-
- # save script to path
- nuke.scriptSaveAs(path)
-
- def process(self,
- regex_filter=None,
- version=None,
- representations=["exr", "dpx", "lutJson", "mov",
- "preview", "png", "jpeg", "jpg"]):
- """
- A short description.
-
- A bit longer description.
-
- Args:
- regex_filter (raw string): regex pattern to filter out subsets
- version (int): define a particular version, None gets last
- representations (list):
-
- Returns:
- type: description
-
- Raises:
- Exception: description
-
- """
-
- if not self.to_script:
- # save the script
- self.save_script_as()
-
- # create viewer and reset frame range
- viewer = self.get_nodes(nodes_filter=["Viewer"])
- if not viewer:
- vn = nuke.createNode("Viewer")
- vn["xpos"].setValue(self.xpos)
- vn["ypos"].setValue(self.ypos)
- else:
- vn = viewer[-1]
-
- # move position
- self.position_up()
-
- wn = self.write_create()
- wn["xpos"].setValue(self.xpos)
- wn["ypos"].setValue(self.ypos)
- wn["render"].setValue(True)
- vn.setInput(0, wn)
-
- # adding backdrop under write
- self.create_backdrop(label="Render write \n\n\n\nOUTPUT",
- color='0xcc1102ff', layer=-1,
- nodes=[wn])
-
- # move position
- self.position_up(4)
-
- # set frame range for new viewer
- self.reset_frame_range_handles()
-
- # get all available representations
- subsets = pype.get_subsets(self._asset,
- regex_filter=regex_filter,
- version=version,
- representations=representations)
-
- for name, subset in subsets.items():
- log.debug("___________________")
- log.debug(name)
- log.debug(subset["version"])
-
- nodes_backdrop = list()
- for name, subset in subsets.items():
- if "lut" in name:
- continue
- log.info("Building Loader to: `{}`".format(name))
- version = subset["version"]
- log.info("Version to: `{}`".format(version["name"]))
- representations = subset["representaions"]
- for repr in representations:
- rn = self.read_loader(repr)
- rn["xpos"].setValue(self.xpos)
- rn["ypos"].setValue(self.ypos)
- wn.setInput(0, rn)
-
- # get editional nodes
- lut_subset = [s for n, s in subsets.items()
- if "lut{}".format(name.lower()) in n.lower()]
- log.debug(">> lut_subset: `{}`".format(lut_subset))
-
- if len(lut_subset) > 0:
- lsub = lut_subset[0]
- fxn = self.effect_loader(lsub["representaions"][-1])
- fxn_ypos = fxn["ypos"].value()
- fxn["ypos"].setValue(fxn_ypos - 100)
- nodes_backdrop.append(fxn)
-
- nodes_backdrop.append(rn)
- # move position
- self.position_right()
-
- # adding backdrop under all read nodes
- self.create_backdrop(label="Loaded Reads",
- color='0x2d7702ff', layer=-1,
- nodes=nodes_backdrop)
-
- def read_loader(self, representation):
- """
- Gets Loader plugin for image sequence or mov
-
- Arguments:
- representation (dict): avalon db entity
-
- """
- context = representation["context"]
-
- loader_name = "LoadSequence"
- if "mov" in context["representation"]:
- loader_name = "LoadMov"
-
- loader_plugin = None
- for Loader in api.discover(api.Loader):
- if Loader.__name__ != loader_name:
- continue
-
- loader_plugin = Loader
-
- return api.load(Loader=loader_plugin,
- representation=representation["_id"])
-
- def effect_loader(self, representation):
- """
- Gets Loader plugin for effects
-
- Arguments:
- representation (dict): avalon db entity
-
- """
- loader_name = "LoadLuts"
-
- loader_plugin = None
- for Loader in api.discover(api.Loader):
- if Loader.__name__ != loader_name:
- continue
-
- loader_plugin = Loader
-
- return api.load(Loader=loader_plugin,
- representation=representation["_id"])
-
- def write_create(self):
- """
- Create render write
-
- Arguments:
- representation (dict): avalon db entity
-
- """
- task = self.data_tmp["task"]
- sanitized_task = re.sub('[^0-9a-zA-Z]+', '', task)
- subset_name = "render{}Main".format(
- sanitized_task.capitalize())
-
- Create_name = "CreateWriteRender"
-
- creator_plugin = None
- for Creator in api.discover(api.Creator):
- if Creator.__name__ != Create_name:
- continue
-
- creator_plugin = Creator
-
- # return api.create()
- return creator_plugin(subset_name, self._asset).process()
-
- def create_backdrop(self, label="", color=None, layer=0,
- nodes=None):
- """
- Create Backdrop node
-
- Arguments:
- color (str): nuke compatible string with color code
- layer (int): layer of node usually used (self.pos_layer - 1)
- label (str): the message
- nodes (list): list of nodes to be wrapped into backdrop
-
- """
- assert isinstance(nodes, list), "`nodes` should be a list of nodes"
- layer = self.pos_layer + layer
-
- create_backdrop(label=label, color=color, layer=layer, nodes=nodes)
-
- def position_reset(self, xpos=0, ypos=0):
- self.xpos = xpos
- self.ypos = ypos
-
- def position_right(self, multiply=1):
- self.xpos += (self.xpos_size * multiply) + self.xpos_gap
-
- def position_left(self, multiply=1):
- self.xpos -= (self.xpos_size * multiply) + self.xpos_gap
-
- def position_down(self, multiply=1):
- self.ypos -= (self.ypos_size * multiply) + self.ypos_gap
-
- def position_up(self, multiply=1):
- self.ypos -= (self.ypos_size * multiply) + self.ypos_gap
-
-
class ExporterReview:
"""
Base class object for generating review data from Nuke
diff --git a/pype/hosts/nuke/menu.py b/pype/hosts/nuke/menu.py
index 7306add9fe..b1ef7f47c4 100644
--- a/pype/hosts/nuke/menu.py
+++ b/pype/hosts/nuke/menu.py
@@ -2,10 +2,12 @@ import nuke
from avalon.api import Session
from pype.hosts.nuke import lib
+from ...lib import BuildWorkfile
from pype.api import Logger
log = Logger().get_logger(__name__, "nuke")
+
def install():
menubar = nuke.menu("Nuke")
menu = menubar.findItem(Session["AVALON_LABEL"])
@@ -20,7 +22,11 @@ def install():
log.debug("Changing Item: {}".format(rm_item))
# rm_item[1].setEnabled(False)
menu.removeItem(rm_item[1].name())
- menu.addCommand(new_name, lambda: workfile_settings().reset_resolution(), index=(rm_item[0]))
+ menu.addCommand(
+ new_name,
+ lambda: workfile_settings().reset_resolution(),
+ index=(rm_item[0])
+ )
# replace reset frame range from avalon core to pype's
name = "Reset Frame Range"
@@ -31,33 +37,38 @@ def install():
log.debug("Changing Item: {}".format(rm_item))
# rm_item[1].setEnabled(False)
menu.removeItem(rm_item[1].name())
- menu.addCommand(new_name, lambda: workfile_settings().reset_frame_range_handles(), index=(rm_item[0]))
+ menu.addCommand(
+ new_name,
+ lambda: workfile_settings().reset_frame_range_handles(),
+ index=(rm_item[0])
+ )
# add colorspace menu item
- name = "Set colorspace"
+ name = "Set Colorspace"
menu.addCommand(
name, lambda: workfile_settings().set_colorspace(),
- index=(rm_item[0]+2)
+ index=(rm_item[0] + 2)
)
log.debug("Adding menu item: {}".format(name))
# add workfile builder menu item
- name = "Build First Workfile.."
+ name = "Build Workfile"
menu.addCommand(
- name, lambda: lib.BuildWorkfile().process(),
- index=(rm_item[0]+7)
+ name, lambda: BuildWorkfile().process(),
+ index=(rm_item[0] + 7)
)
log.debug("Adding menu item: {}".format(name))
# add item that applies all setting above
- name = "Apply all settings"
+ name = "Apply All Settings"
menu.addCommand(
- name, lambda: workfile_settings().set_context_settings(), index=(rm_item[0]+3)
+ name,
+ lambda: workfile_settings().set_context_settings(),
+ index=(rm_item[0] + 3)
)
log.debug("Adding menu item: {}".format(name))
-
def uninstall():
menubar = nuke.menu("Nuke")
diff --git a/pype/lib.py b/pype/lib.py
index 7cf4e2f1a5..601c85f521 100644
--- a/pype/lib.py
+++ b/pype/lib.py
@@ -746,8 +746,9 @@ class PypeHook:
def get_linked_assets(asset_entity):
"""Return linked assets for `asset_entity`."""
- # TODO implement
- return []
+ inputs = asset_entity["data"].get("inputs", [])
+ inputs = [io.find_one({"_id": x}) for x in inputs]
+ return inputs
def map_subsets_by_family(subsets):
diff --git a/pype/modules/ftrack/actions/action_thumbnail_to_parent.py b/pype/modules/ftrack/actions/action_thumbnail_to_parent.py
index 8710fa9dcf..fb473f9aa5 100644
--- a/pype/modules/ftrack/actions/action_thumbnail_to_parent.py
+++ b/pype/modules/ftrack/actions/action_thumbnail_to_parent.py
@@ -41,9 +41,9 @@ class ThumbToParent(BaseAction):
parent = None
thumbid = None
if entity.entity_type.lower() == 'assetversion':
- try:
- parent = entity['task']
- except Exception:
+ parent = entity['task']
+
+ if parent is None:
par_ent = entity['link'][-2]
parent = session.get(par_ent['type'], par_ent['id'])
else:
@@ -51,7 +51,7 @@ class ThumbToParent(BaseAction):
parent = entity['parent']
except Exception as e:
msg = (
- "Durin Action 'Thumb to Parent'"
+ "During Action 'Thumb to Parent'"
" went something wrong"
)
self.log.error(msg)
@@ -62,7 +62,10 @@ class ThumbToParent(BaseAction):
parent['thumbnail_id'] = thumbid
status = 'done'
else:
- status = 'failed'
+ raise Exception(
+ "Parent or thumbnail id not found. Parent: {}. "
+ "Thumbnail id: {}".format(parent, thumbid)
+ )
# inform the user that the job is done
job['status'] = status or 'done'
diff --git a/pype/modules/ftrack/events/action_push_frame_values_to_task.py b/pype/modules/ftrack/events/action_push_frame_values_to_task.py
new file mode 100644
index 0000000000..a55c1e46a6
--- /dev/null
+++ b/pype/modules/ftrack/events/action_push_frame_values_to_task.py
@@ -0,0 +1,437 @@
+import json
+import collections
+import ftrack_api
+from pype.modules.ftrack.lib import BaseAction
+
+
+class PushFrameValuesToTaskAction(BaseAction):
+ """Action for testing purpose or as base for new actions."""
+
+ # Ignore event handler by default
+ ignore_me = True
+
+ identifier = "admin.push_frame_values_to_task"
+ label = "Pype Admin"
+ variant = "- Push Frame values to Task"
+
+ entities_query = (
+ "select id, name, parent_id, link from TypedContext"
+ " where project_id is \"{}\" and object_type_id in ({})"
+ )
+ cust_attrs_query = (
+ "select id, key, object_type_id, is_hierarchical, default"
+ " from CustomAttributeConfiguration"
+ " where key in ({})"
+ )
+ cust_attr_value_query = (
+ "select value, entity_id from CustomAttributeValue"
+ " where entity_id in ({}) and configuration_id in ({})"
+ )
+
+ pushing_entity_types = {"Shot"}
+ hierarchical_custom_attribute_keys = {"frameStart", "frameEnd"}
+ custom_attribute_mapping = {
+ "frameStart": "fstart",
+ "frameEnd": "fend"
+ }
+ discover_role_list = {"Pypeclub", "Administrator", "Project Manager"}
+
+ def register(self):
+ modified_role_names = set()
+ for role_name in self.discover_role_list:
+ modified_role_names.add(role_name.lower())
+ self.discover_role_list = modified_role_names
+
+ self.session.event_hub.subscribe(
+ "topic=ftrack.action.discover",
+ self._discover,
+ priority=self.priority
+ )
+
+ launch_subscription = (
+ "topic=ftrack.action.launch and data.actionIdentifier={0}"
+ ).format(self.identifier)
+ self.session.event_hub.subscribe(launch_subscription, self._launch)
+
+ def discover(self, session, entities, event):
+ """ Validation """
+ # Check if selection is valid
+ valid_selection = False
+ for ent in event["data"]["selection"]:
+ # Ignore entities that are not tasks or projects
+ if ent["entityType"].lower() == "show":
+ valid_selection = True
+ break
+
+ if not valid_selection:
+ return False
+
+ # Get user and check his roles
+ user_id = event.get("source", {}).get("user", {}).get("id")
+ if not user_id:
+ return False
+
+ user = session.query("User where id is \"{}\"".format(user_id)).first()
+ if not user:
+ return False
+
+ for role in user["user_security_roles"]:
+ lowered_role = role["security_role"]["name"].lower()
+ if lowered_role in self.discover_role_list:
+ return True
+ return False
+
+ def launch(self, session, entities, event):
+ self.log.debug("{}: Creating job".format(self.label))
+
+ user_entity = session.query(
+ "User where id is {}".format(event["source"]["user"]["id"])
+ ).one()
+ job = session.create("Job", {
+ "user": user_entity,
+ "status": "running",
+ "data": json.dumps({
+ "description": "Propagation of Frame attribute values to task."
+ })
+ })
+ session.commit()
+
+ try:
+ project_entity = self.get_project_from_entity(entities[0])
+ result = self.propagate_values(session, project_entity, event)
+ job["status"] = "done"
+ session.commit()
+
+ return result
+
+ except Exception:
+ session.rollback()
+ job["status"] = "failed"
+ session.commit()
+
+ msg = "Pushing Custom attribute values to task Failed"
+ self.log.warning(msg, exc_info=True)
+ return {
+ "success": False,
+ "message": msg
+ }
+
+ finally:
+ if job["status"] == "running":
+ job["status"] = "failed"
+ session.commit()
+
+ def task_attributes(self, session):
+ task_object_type = session.query(
+ "ObjectType where name is \"Task\""
+ ).one()
+
+ hier_attr_names = list(
+ self.custom_attribute_mapping.keys()
+ )
+ entity_type_specific_names = list(
+ self.custom_attribute_mapping.values()
+ )
+ joined_keys = self.join_keys(
+ hier_attr_names + entity_type_specific_names
+ )
+ attribute_entities = session.query(
+ self.cust_attrs_query.format(joined_keys)
+ ).all()
+
+ hier_attrs = []
+ task_attrs = {}
+ for attr in attribute_entities:
+ attr_key = attr["key"]
+ if attr["is_hierarchical"]:
+ if attr_key in hier_attr_names:
+ hier_attrs.append(attr)
+ elif attr["object_type_id"] == task_object_type["id"]:
+ if attr_key in entity_type_specific_names:
+ task_attrs[attr_key] = attr["id"]
+ return task_attrs, hier_attrs
+
+ def join_keys(self, items):
+ return ",".join(["\"{}\"".format(item) for item in items])
+
+ def propagate_values(self, session, project_entity, event):
+ self.log.debug("Querying project's entities \"{}\".".format(
+ project_entity["full_name"]
+ ))
+ pushing_entity_types = tuple(
+ ent_type.lower()
+ for ent_type in self.pushing_entity_types
+ )
+ destination_object_types = []
+ all_object_types = session.query("ObjectType").all()
+ for object_type in all_object_types:
+ lowered_name = object_type["name"].lower()
+ if (
+ lowered_name == "task"
+ or lowered_name in pushing_entity_types
+ ):
+ destination_object_types.append(object_type)
+
+ destination_object_type_ids = tuple(
+ obj_type["id"]
+ for obj_type in destination_object_types
+ )
+ entities = session.query(self.entities_query.format(
+ project_entity["id"],
+ self.join_keys(destination_object_type_ids)
+ )).all()
+
+ entities_by_id = {
+ entity["id"]: entity
+ for entity in entities
+ }
+
+ self.log.debug("Filtering Task entities.")
+ task_entities_by_parent_id = collections.defaultdict(list)
+ non_task_entities = []
+ non_task_entity_ids = []
+ for entity in entities:
+ if entity.entity_type.lower() != "task":
+ non_task_entities.append(entity)
+ non_task_entity_ids.append(entity["id"])
+ continue
+
+ parent_id = entity["parent_id"]
+ if parent_id in entities_by_id:
+ task_entities_by_parent_id[parent_id].append(entity)
+
+ task_attr_id_by_keys, hier_attrs = self.task_attributes(session)
+
+ self.log.debug("Getting Custom attribute values from tasks' parents.")
+ hier_values_by_entity_id = self.get_hier_values(
+ session,
+ hier_attrs,
+ non_task_entity_ids
+ )
+
+ self.log.debug("Setting parents' values to task.")
+ task_missing_keys = self.set_task_attr_values(
+ session,
+ task_entities_by_parent_id,
+ hier_values_by_entity_id,
+ task_attr_id_by_keys
+ )
+
+ self.log.debug("Setting values to entities themselves.")
+ missing_keys_by_object_name = self.push_values_to_entities(
+ session,
+ non_task_entities,
+ hier_values_by_entity_id
+ )
+ if task_missing_keys:
+ missing_keys_by_object_name["Task"] = task_missing_keys
+ if missing_keys_by_object_name:
+ self.report(missing_keys_by_object_name, event)
+ return True
+
+ def report(self, missing_keys_by_object_name, event):
+ splitter = {"type": "label", "value": "---"}
+
+ title = "Push Custom Attribute values report:"
+
+ items = []
+ items.append({
+ "type": "label",
+ "value": "# Pushing values was not complete"
+ })
+ items.append({
+ "type": "label",
+ "value": (
+ "
It was due to missing custom"
+ " attribute configurations for specific entity type/s."
+ " These configurations are not created automatically.
"
+ )
+ })
+
+ log_message_items = []
+ log_message_item_template = (
+ "Entity type \"{}\" does not have created Custom Attribute/s: {}"
+ )
+ for object_name, missing_attr_names in (
+ missing_keys_by_object_name.items()
+ ):
+ log_message_items.append(log_message_item_template.format(
+ object_name, self.join_keys(missing_attr_names)
+ ))
+
+ items.append(splitter)
+ items.append({
+ "type": "label",
+ "value": "## Entity type: {}".format(object_name)
+ })
+
+ items.append({
+ "type": "label",
+ "value": "{}
".format("
".join(missing_attr_names))
+ })
+
+ self.log.warning((
+ "Couldn't finish pushing attribute values because"
+ " few entity types miss Custom attribute configurations:\n{}"
+ ).format("\n".join(log_message_items)))
+
+ self.show_interface(items, title, event)
+
+ def get_hier_values(self, session, hier_attrs, focus_entity_ids):
+ joined_entity_ids = self.join_keys(focus_entity_ids)
+ hier_attr_ids = self.join_keys(
+ tuple(hier_attr["id"] for hier_attr in hier_attrs)
+ )
+ hier_attrs_key_by_id = {
+ hier_attr["id"]: hier_attr["key"]
+ for hier_attr in hier_attrs
+ }
+ call_expr = [{
+ "action": "query",
+ "expression": self.cust_attr_value_query.format(
+ joined_entity_ids, hier_attr_ids
+ )
+ }]
+ if hasattr(session, "call"):
+ [values] = session.call(call_expr)
+ else:
+ [values] = session._call(call_expr)
+
+ values_per_entity_id = {}
+ for item in values["data"]:
+ entity_id = item["entity_id"]
+ key = hier_attrs_key_by_id[item["configuration_id"]]
+
+ if entity_id not in values_per_entity_id:
+ values_per_entity_id[entity_id] = {}
+ value = item["value"]
+ if value is not None:
+ values_per_entity_id[entity_id][key] = value
+
+ output = {}
+ for entity_id in focus_entity_ids:
+ value = values_per_entity_id.get(entity_id)
+ if value:
+ output[entity_id] = value
+
+ return output
+
+ def set_task_attr_values(
+ self,
+ session,
+ task_entities_by_parent_id,
+ hier_values_by_entity_id,
+ task_attr_id_by_keys
+ ):
+ missing_keys = set()
+ for parent_id, values in hier_values_by_entity_id.items():
+ task_entities = task_entities_by_parent_id[parent_id]
+ for hier_key, value in values.items():
+ key = self.custom_attribute_mapping[hier_key]
+ if key not in task_attr_id_by_keys:
+ missing_keys.add(key)
+ continue
+
+ for task_entity in task_entities:
+ _entity_key = collections.OrderedDict({
+ "configuration_id": task_attr_id_by_keys[key],
+ "entity_id": task_entity["id"]
+ })
+
+ session.recorded_operations.push(
+ ftrack_api.operation.UpdateEntityOperation(
+ "ContextCustomAttributeValue",
+ _entity_key,
+ "value",
+ ftrack_api.symbol.NOT_SET,
+ value
+ )
+ )
+ session.commit()
+
+ return missing_keys
+
+ def push_values_to_entities(
+ self,
+ session,
+ non_task_entities,
+ hier_values_by_entity_id
+ ):
+ object_types = session.query(
+ "ObjectType where name in ({})".format(
+ self.join_keys(self.pushing_entity_types)
+ )
+ ).all()
+ object_type_names_by_id = {
+ object_type["id"]: object_type["name"]
+ for object_type in object_types
+ }
+ joined_keys = self.join_keys(
+ self.custom_attribute_mapping.values()
+ )
+ attribute_entities = session.query(
+ self.cust_attrs_query.format(joined_keys)
+ ).all()
+
+ attrs_by_obj_id = {}
+ for attr in attribute_entities:
+ if attr["is_hierarchical"]:
+ continue
+
+ obj_id = attr["object_type_id"]
+ if obj_id not in object_type_names_by_id:
+ continue
+
+ if obj_id not in attrs_by_obj_id:
+ attrs_by_obj_id[obj_id] = {}
+
+ attr_key = attr["key"]
+ attrs_by_obj_id[obj_id][attr_key] = attr["id"]
+
+ entities_by_obj_id = collections.defaultdict(list)
+ for entity in non_task_entities:
+ entities_by_obj_id[entity["object_type_id"]].append(entity)
+
+ missing_keys_by_object_id = collections.defaultdict(set)
+ for obj_type_id, attr_keys in attrs_by_obj_id.items():
+ entities = entities_by_obj_id.get(obj_type_id)
+ if not entities:
+ continue
+
+ for entity in entities:
+ values = hier_values_by_entity_id.get(entity["id"])
+ if not values:
+ continue
+
+ for hier_key, value in values.items():
+ key = self.custom_attribute_mapping[hier_key]
+ if key not in attr_keys:
+ missing_keys_by_object_id[obj_type_id].add(key)
+ continue
+
+ _entity_key = collections.OrderedDict({
+ "configuration_id": attr_keys[key],
+ "entity_id": entity["id"]
+ })
+
+ session.recorded_operations.push(
+ ftrack_api.operation.UpdateEntityOperation(
+ "ContextCustomAttributeValue",
+ _entity_key,
+ "value",
+ ftrack_api.symbol.NOT_SET,
+ value
+ )
+ )
+ session.commit()
+
+ missing_keys_by_object_name = {}
+ for obj_id, missing_keys in missing_keys_by_object_id.items():
+ obj_name = object_type_names_by_id[obj_id]
+ missing_keys_by_object_name[obj_name] = missing_keys
+
+ return missing_keys_by_object_name
+
+
+def register(session, plugins_presets={}):
+ PushFrameValuesToTaskAction(session, plugins_presets).register()
diff --git a/pype/modules/ftrack/events/event_push_frame_values_to_task.py b/pype/modules/ftrack/events/event_push_frame_values_to_task.py
new file mode 100644
index 0000000000..32993ef938
--- /dev/null
+++ b/pype/modules/ftrack/events/event_push_frame_values_to_task.py
@@ -0,0 +1,230 @@
+import collections
+import ftrack_api
+from pype.modules.ftrack import BaseEvent
+
+
+class PushFrameValuesToTaskEvent(BaseEvent):
+ # Ignore event handler by default
+ ignore_me = True
+
+ cust_attrs_query = (
+ "select id, key, object_type_id, is_hierarchical, default"
+ " from CustomAttributeConfiguration"
+ " where key in ({}) and object_type_id in ({})"
+ )
+
+ interest_entity_types = {"Shot"}
+ interest_attributes = {"frameStart", "frameEnd"}
+ interest_attr_mapping = {
+ "frameStart": "fstart",
+ "frameEnd": "fend"
+ }
+ _cached_task_object_id = None
+ _cached_interest_object_ids = None
+
+ @staticmethod
+ def join_keys(keys):
+ return ",".join(["\"{}\"".format(key) for key in keys])
+
+ @classmethod
+ def task_object_id(cls, session):
+ if cls._cached_task_object_id is None:
+ task_object_type = session.query(
+ "ObjectType where name is \"Task\""
+ ).one()
+ cls._cached_task_object_id = task_object_type["id"]
+ return cls._cached_task_object_id
+
+ @classmethod
+ def interest_object_ids(cls, session):
+ if cls._cached_interest_object_ids is None:
+ object_types = session.query(
+ "ObjectType where name in ({})".format(
+ cls.join_keys(cls.interest_entity_types)
+ )
+ ).all()
+ cls._cached_interest_object_ids = tuple(
+ object_type["id"]
+ for object_type in object_types
+ )
+ return cls._cached_interest_object_ids
+
+ def launch(self, session, event):
+ interesting_data = self.extract_interesting_data(session, event)
+ if not interesting_data:
+ return
+
+ entities = self.get_entities(session, interesting_data)
+ if not entities:
+ return
+
+ entities_by_id = {
+ entity["id"]: entity
+ for entity in entities
+ }
+ for entity_id in tuple(interesting_data.keys()):
+ if entity_id not in entities_by_id:
+ interesting_data.pop(entity_id)
+
+ task_entities = self.get_task_entities(session, interesting_data)
+
+ attrs_by_obj_id = self.attrs_configurations(session)
+ if not attrs_by_obj_id:
+ self.log.warning((
+ "There is not created Custom Attributes {}"
+ " for \"Task\" entity type."
+ ).format(self.join_keys(self.interest_attributes)))
+ return
+
+ task_entities_by_parent_id = collections.defaultdict(list)
+ for task_entity in task_entities:
+ task_entities_by_parent_id[task_entity["parent_id"]].append(
+ task_entity
+ )
+
+ missing_keys_by_object_name = collections.defaultdict(set)
+ for parent_id, values in interesting_data.items():
+ entities = task_entities_by_parent_id.get(parent_id) or []
+ entities.append(entities_by_id[parent_id])
+
+ for hier_key, value in values.items():
+ changed_ids = []
+ for entity in entities:
+ key = self.interest_attr_mapping[hier_key]
+ entity_attrs_mapping = (
+ attrs_by_obj_id.get(entity["object_type_id"])
+ )
+ if not entity_attrs_mapping:
+ missing_keys_by_object_name[entity.entity_type].add(
+ key
+ )
+ continue
+
+ configuration_id = entity_attrs_mapping.get(key)
+ if not configuration_id:
+ missing_keys_by_object_name[entity.entity_type].add(
+ key
+ )
+ continue
+
+ changed_ids.append(entity["id"])
+ entity_key = collections.OrderedDict({
+ "configuration_id": configuration_id,
+ "entity_id": entity["id"]
+ })
+ if value is None:
+ op = ftrack_api.operation.DeleteEntityOperation(
+ "CustomAttributeValue",
+ entity_key
+ )
+ else:
+ op = ftrack_api.operation.UpdateEntityOperation(
+ "ContextCustomAttributeValue",
+ entity_key,
+ "value",
+ ftrack_api.symbol.NOT_SET,
+ value
+ )
+
+ session.recorded_operations.push(op)
+ self.log.info((
+ "Changing Custom Attribute \"{}\" to value"
+ " \"{}\" on entities: {}"
+ ).format(key, value, self.join_keys(changed_ids)))
+ try:
+ session.commit()
+ except Exception:
+ session.rollback()
+ self.log.warning(
+ "Changing of values failed.",
+ exc_info=True
+ )
+ if not missing_keys_by_object_name:
+ return
+
+ msg_items = []
+ for object_name, missing_keys in missing_keys_by_object_name.items():
+ msg_items.append(
+ "{}: ({})".format(object_name, self.join_keys(missing_keys))
+ )
+
+ self.log.warning((
+ "Missing Custom Attribute configuration"
+ " per specific object types: {}"
+ ).format(", ".join(msg_items)))
+
+ def extract_interesting_data(self, session, event):
+ # Filter if event contain relevant data
+ entities_info = event["data"].get("entities")
+ if not entities_info:
+ return
+
+ interesting_data = {}
+ for entity_info in entities_info:
+ # Care only about tasks
+ if entity_info.get("entityType") != "task":
+ continue
+
+ # Care only about changes of status
+ changes = entity_info.get("changes") or {}
+ if not changes:
+ continue
+
+ # Care only about changes if specific keys
+ entity_changes = {}
+ for key in self.interest_attributes:
+ if key in changes:
+ entity_changes[key] = changes[key]["new"]
+
+ if not entity_changes:
+ continue
+
+ # Do not care about "Task" entity_type
+ task_object_id = self.task_object_id(session)
+ if entity_info.get("objectTypeId") == task_object_id:
+ continue
+
+ interesting_data[entity_info["entityId"]] = entity_changes
+ return interesting_data
+
+ def get_entities(self, session, interesting_data):
+ entities = session.query(
+ "TypedContext where id in ({})".format(
+ self.join_keys(interesting_data.keys())
+ )
+ ).all()
+
+ output = []
+ interest_object_ids = self.interest_object_ids(session)
+ for entity in entities:
+ if entity["object_type_id"] in interest_object_ids:
+ output.append(entity)
+ return output
+
+ def get_task_entities(self, session, interesting_data):
+ return session.query(
+ "Task where parent_id in ({})".format(
+ self.join_keys(interesting_data.keys())
+ )
+ ).all()
+
+ def attrs_configurations(self, session):
+ object_ids = list(self.interest_object_ids(session))
+ object_ids.append(self.task_object_id(session))
+
+ attrs = session.query(self.cust_attrs_query.format(
+ self.join_keys(self.interest_attr_mapping.values()),
+ self.join_keys(object_ids)
+ )).all()
+
+ output = {}
+ for attr in attrs:
+ obj_id = attr["object_type_id"]
+ if obj_id not in output:
+ output[obj_id] = {}
+ output[obj_id][attr["key"]] = attr["id"]
+ return output
+
+
+def register(session, plugins_presets):
+ PushFrameValuesToTaskEvent(session, plugins_presets).register()
diff --git a/pype/modules/ftrack/tray/ftrack_module.py b/pype/modules/ftrack/tray/ftrack_module.py
index 674e8cbd4f..0b011c5b33 100644
--- a/pype/modules/ftrack/tray/ftrack_module.py
+++ b/pype/modules/ftrack/tray/ftrack_module.py
@@ -2,7 +2,7 @@ import os
import time
import datetime
import threading
-from Qt import QtCore, QtWidgets
+from Qt import QtCore, QtWidgets, QtGui
import ftrack_api
from ..ftrack_server.lib import check_ftrack_url
@@ -10,7 +10,7 @@ from ..ftrack_server import socket_thread
from ..lib import credentials
from . import login_dialog
-from pype.api import Logger
+from pype.api import Logger, resources
log = Logger().get_logger("FtrackModule", "ftrack")
@@ -19,7 +19,7 @@ log = Logger().get_logger("FtrackModule", "ftrack")
class FtrackModule:
def __init__(self, main_parent=None, parent=None):
self.parent = parent
- self.widget_login = login_dialog.Login_Dialog_ui(self)
+
self.thread_action_server = None
self.thread_socket_server = None
self.thread_timer = None
@@ -29,8 +29,22 @@ class FtrackModule:
self.bool_action_thread_running = False
self.bool_timer_event = False
+ self.widget_login = login_dialog.CredentialsDialog()
+ self.widget_login.login_changed.connect(self.on_login_change)
+ self.widget_login.logout_signal.connect(self.on_logout)
+
+ self.action_credentials = None
+ self.icon_logged = QtGui.QIcon(
+ resources.get_resource("icons", "circle_green.png")
+ )
+ self.icon_not_logged = QtGui.QIcon(
+ resources.get_resource("icons", "circle_orange.png")
+ )
+
def show_login_widget(self):
self.widget_login.show()
+ self.widget_login.activateWindow()
+ self.widget_login.raise_()
def validate(self):
validation = False
@@ -39,9 +53,10 @@ class FtrackModule:
ft_api_key = cred.get("api_key")
validation = credentials.check_credentials(ft_user, ft_api_key)
if validation:
+ self.widget_login.set_credentials(ft_user, ft_api_key)
credentials.set_env(ft_user, ft_api_key)
log.info("Connected to Ftrack successfully")
- self.loginChange()
+ self.on_login_change()
return validation
@@ -60,15 +75,28 @@ class FtrackModule:
return validation
# Necessary - login_dialog works with this method after logging in
- def loginChange(self):
+ def on_login_change(self):
self.bool_logged = True
+
+ if self.action_credentials:
+ self.action_credentials.setIcon(self.icon_logged)
+ self.action_credentials.setToolTip(
+ "Logged as user \"{}\"".format(
+ self.widget_login.user_input.text()
+ )
+ )
+
self.set_menu_visibility()
self.start_action_server()
- def logout(self):
+ def on_logout(self):
credentials.clear_credentials()
self.stop_action_server()
+ if self.action_credentials:
+ self.action_credentials.setIcon(self.icon_not_logged)
+ self.action_credentials.setToolTip("Logged out")
+
log.info("Logged out of Ftrack")
self.bool_logged = False
self.set_menu_visibility()
@@ -218,43 +246,45 @@ class FtrackModule:
# Definition of Tray menu
def tray_menu(self, parent_menu):
# Menu for Tray App
- self.menu = QtWidgets.QMenu('Ftrack', parent_menu)
- self.menu.setProperty('submenu', 'on')
-
- # Actions - server
- self.smActionS = self.menu.addMenu("Action server")
-
- self.aRunActionS = QtWidgets.QAction(
- "Run action server", self.smActionS
- )
- self.aResetActionS = QtWidgets.QAction(
- "Reset action server", self.smActionS
- )
- self.aStopActionS = QtWidgets.QAction(
- "Stop action server", self.smActionS
- )
-
- self.aRunActionS.triggered.connect(self.start_action_server)
- self.aResetActionS.triggered.connect(self.reset_action_server)
- self.aStopActionS.triggered.connect(self.stop_action_server)
-
- self.smActionS.addAction(self.aRunActionS)
- self.smActionS.addAction(self.aResetActionS)
- self.smActionS.addAction(self.aStopActionS)
+ tray_menu = QtWidgets.QMenu("Ftrack", parent_menu)
# Actions - basic
- self.aLogin = QtWidgets.QAction("Login", self.menu)
- self.aLogin.triggered.connect(self.validate)
- self.aLogout = QtWidgets.QAction("Logout", self.menu)
- self.aLogout.triggered.connect(self.logout)
+ action_credentials = QtWidgets.QAction("Credentials", tray_menu)
+ action_credentials.triggered.connect(self.show_login_widget)
+ if self.bool_logged:
+ icon = self.icon_logged
+ else:
+ icon = self.icon_not_logged
+ action_credentials.setIcon(icon)
+ tray_menu.addAction(action_credentials)
+ self.action_credentials = action_credentials
- self.menu.addAction(self.aLogin)
- self.menu.addAction(self.aLogout)
+ # Actions - server
+ tray_server_menu = tray_menu.addMenu("Action server")
+ self.action_server_run = QtWidgets.QAction(
+ "Run action server", tray_server_menu
+ )
+ self.action_server_reset = QtWidgets.QAction(
+ "Reset action server", tray_server_menu
+ )
+ self.action_server_stop = QtWidgets.QAction(
+ "Stop action server", tray_server_menu
+ )
+
+ self.action_server_run.triggered.connect(self.start_action_server)
+ self.action_server_reset.triggered.connect(self.reset_action_server)
+ self.action_server_stop.triggered.connect(self.stop_action_server)
+
+ tray_server_menu.addAction(self.action_server_run)
+ tray_server_menu.addAction(self.action_server_reset)
+ tray_server_menu.addAction(self.action_server_stop)
+
+ self.tray_server_menu = tray_server_menu
self.bool_logged = False
self.set_menu_visibility()
- parent_menu.addMenu(self.menu)
+ parent_menu.addMenu(tray_menu)
def tray_start(self):
self.validate()
@@ -264,19 +294,15 @@ class FtrackModule:
# Definition of visibility of each menu actions
def set_menu_visibility(self):
-
- self.smActionS.menuAction().setVisible(self.bool_logged)
- self.aLogin.setVisible(not self.bool_logged)
- self.aLogout.setVisible(self.bool_logged)
-
+ self.tray_server_menu.menuAction().setVisible(self.bool_logged)
if self.bool_logged is False:
if self.bool_timer_event is True:
self.stop_timer_thread()
return
- self.aRunActionS.setVisible(not self.bool_action_server_running)
- self.aResetActionS.setVisible(self.bool_action_thread_running)
- self.aStopActionS.setVisible(self.bool_action_server_running)
+ self.action_server_run.setVisible(not self.bool_action_server_running)
+ self.action_server_reset.setVisible(self.bool_action_thread_running)
+ self.action_server_stop.setVisible(self.bool_action_server_running)
if self.bool_timer_event is False:
self.start_timer_thread()
diff --git a/pype/modules/ftrack/tray/login_dialog.py b/pype/modules/ftrack/tray/login_dialog.py
index e0614513a3..7730ee1609 100644
--- a/pype/modules/ftrack/tray/login_dialog.py
+++ b/pype/modules/ftrack/tray/login_dialog.py
@@ -7,309 +7,314 @@ from pype.api import resources
from Qt import QtCore, QtGui, QtWidgets
-class Login_Dialog_ui(QtWidgets.QWidget):
-
+class CredentialsDialog(QtWidgets.QDialog):
SIZE_W = 300
SIZE_H = 230
- loginSignal = QtCore.Signal(object, object, object)
- _login_server_thread = None
- inputs = []
- buttons = []
- labels = []
+ login_changed = QtCore.Signal()
+ logout_signal = QtCore.Signal()
- def __init__(self, parent=None, is_event=False):
+ def __init__(self, parent=None):
+ super(CredentialsDialog, self).__init__(parent)
- super(Login_Dialog_ui, self).__init__()
+ self.setWindowTitle("Pype - Ftrack Login")
- self.parent = parent
- self.is_event = is_event
+ self._login_server_thread = None
+ self._is_logged = False
+ self._in_advance_mode = False
- if hasattr(parent, 'icon'):
- self.setWindowIcon(self.parent.icon)
- elif hasattr(parent, 'parent') and hasattr(parent.parent, 'icon'):
- self.setWindowIcon(self.parent.parent.icon)
- else:
- icon = QtGui.QIcon(resources.pype_icon_filepath())
- self.setWindowIcon(icon)
+ icon = QtGui.QIcon(resources.pype_icon_filepath())
+ self.setWindowIcon(icon)
self.setWindowFlags(
QtCore.Qt.WindowCloseButtonHint |
QtCore.Qt.WindowMinimizeButtonHint
)
- self.loginSignal.connect(self.loginWithCredentials)
- self._translate = QtCore.QCoreApplication.translate
-
- self.font = QtGui.QFont()
- self.font.setFamily("DejaVu Sans Condensed")
- self.font.setPointSize(9)
- self.font.setBold(True)
- self.font.setWeight(50)
- self.font.setKerning(True)
-
- self.resize(self.SIZE_W, self.SIZE_H)
self.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H))
- self.setMaximumSize(QtCore.QSize(self.SIZE_W+100, self.SIZE_H+100))
+ self.setMaximumSize(QtCore.QSize(self.SIZE_W + 100, self.SIZE_H + 100))
self.setStyleSheet(style.load_stylesheet())
- self.setLayout(self._main())
- self.setWindowTitle('Pype - Ftrack Login')
+ self.login_changed.connect(self._on_login)
- def _main(self):
- self.main = QtWidgets.QVBoxLayout()
- self.main.setObjectName("main")
+ self.ui_init()
- self.form = QtWidgets.QFormLayout()
- self.form.setContentsMargins(10, 15, 10, 5)
- self.form.setObjectName("form")
-
- self.ftsite_label = QtWidgets.QLabel("FTrack URL:")
- self.ftsite_label.setFont(self.font)
- self.ftsite_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor))
- self.ftsite_label.setTextFormat(QtCore.Qt.RichText)
- self.ftsite_label.setObjectName("user_label")
+ def ui_init(self):
+ self.ftsite_label = QtWidgets.QLabel("Ftrack URL:")
+ self.user_label = QtWidgets.QLabel("Username:")
+ self.api_label = QtWidgets.QLabel("API Key:")
self.ftsite_input = QtWidgets.QLineEdit()
- self.ftsite_input.setEnabled(True)
- self.ftsite_input.setFrame(True)
- self.ftsite_input.setEnabled(False)
self.ftsite_input.setReadOnly(True)
- self.ftsite_input.setObjectName("ftsite_input")
-
- self.user_label = QtWidgets.QLabel("Username:")
- self.user_label.setFont(self.font)
- self.user_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor))
- self.user_label.setTextFormat(QtCore.Qt.RichText)
- self.user_label.setObjectName("user_label")
+ self.ftsite_input.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor))
self.user_input = QtWidgets.QLineEdit()
- self.user_input.setEnabled(True)
- self.user_input.setFrame(True)
- self.user_input.setObjectName("user_input")
- self.user_input.setPlaceholderText(
- self._translate("main", "user.name")
- )
+ self.user_input.setPlaceholderText("user.name")
self.user_input.textChanged.connect(self._user_changed)
- self.api_label = QtWidgets.QLabel("API Key:")
- self.api_label.setFont(self.font)
- self.api_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor))
- self.api_label.setTextFormat(QtCore.Qt.RichText)
- self.api_label.setObjectName("api_label")
-
self.api_input = QtWidgets.QLineEdit()
- self.api_input.setEnabled(True)
- self.api_input.setFrame(True)
- self.api_input.setObjectName("api_input")
- self.api_input.setPlaceholderText(self._translate(
- "main", "e.g. xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
- ))
+ self.api_input.setPlaceholderText(
+ "e.g. xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ )
self.api_input.textChanged.connect(self._api_changed)
+ input_layout = QtWidgets.QFormLayout()
+ input_layout.setContentsMargins(10, 15, 10, 5)
+
+ input_layout.addRow(self.ftsite_label, self.ftsite_input)
+ input_layout.addRow(self.user_label, self.user_input)
+ input_layout.addRow(self.api_label, self.api_input)
+
+ self.btn_advanced = QtWidgets.QPushButton("Advanced")
+ self.btn_advanced.clicked.connect(self._on_advanced_clicked)
+
+ self.btn_simple = QtWidgets.QPushButton("Simple")
+ self.btn_simple.clicked.connect(self._on_simple_clicked)
+
+ self.btn_login = QtWidgets.QPushButton("Login")
+ self.btn_login.setToolTip(
+ "Set Username and API Key with entered values"
+ )
+ self.btn_login.clicked.connect(self._on_login_clicked)
+
+ self.btn_ftrack_login = QtWidgets.QPushButton("Ftrack login")
+ self.btn_ftrack_login.setToolTip("Open browser for Login to Ftrack")
+ self.btn_ftrack_login.clicked.connect(self._on_ftrack_login_clicked)
+
+ self.btn_logout = QtWidgets.QPushButton("Logout")
+ self.btn_logout.clicked.connect(self._on_logout_clicked)
+
+ self.btn_close = QtWidgets.QPushButton("Close")
+ self.btn_close.setToolTip("Close this window")
+ self.btn_close.clicked.connect(self._close_widget)
+
+ btns_layout = QtWidgets.QHBoxLayout()
+ btns_layout.addWidget(self.btn_advanced)
+ btns_layout.addWidget(self.btn_simple)
+ btns_layout.addStretch(1)
+ btns_layout.addWidget(self.btn_ftrack_login)
+ btns_layout.addWidget(self.btn_login)
+ btns_layout.addWidget(self.btn_logout)
+ btns_layout.addWidget(self.btn_close)
+
+ self.note_label = QtWidgets.QLabel((
+ "NOTE: Click on \"{}\" button to log with your default browser"
+ " or click on \"{}\" button to enter API key manually."
+ ).format(self.btn_ftrack_login.text(), self.btn_advanced.text()))
+
+ self.note_label.setWordWrap(True)
+ self.note_label.hide()
+
self.error_label = QtWidgets.QLabel("")
- self.error_label.setFont(self.font)
- self.error_label.setTextFormat(QtCore.Qt.RichText)
- self.error_label.setObjectName("error_label")
self.error_label.setWordWrap(True)
self.error_label.hide()
- self.form.addRow(self.ftsite_label, self.ftsite_input)
- self.form.addRow(self.user_label, self.user_input)
- self.form.addRow(self.api_label, self.api_input)
- self.form.addRow(self.error_label)
+ label_layout = QtWidgets.QVBoxLayout()
+ label_layout.setContentsMargins(10, 5, 10, 5)
+ label_layout.addWidget(self.note_label)
+ label_layout.addWidget(self.error_label)
- self.btnGroup = QtWidgets.QHBoxLayout()
- self.btnGroup.addStretch(1)
- self.btnGroup.setObjectName("btnGroup")
+ main = QtWidgets.QVBoxLayout(self)
+ main.addLayout(input_layout)
+ main.addLayout(label_layout)
+ main.addStretch(1)
+ main.addLayout(btns_layout)
- self.btnEnter = QtWidgets.QPushButton("Login")
- self.btnEnter.setToolTip(
- 'Set Username and API Key with entered values'
- )
- self.btnEnter.clicked.connect(self.enter_credentials)
+ self.fill_ftrack_url()
- self.btnClose = QtWidgets.QPushButton("Close")
- self.btnClose.setToolTip('Close this window')
- self.btnClose.clicked.connect(self._close_widget)
+ self.set_is_logged(self._is_logged)
- self.btnFtrack = QtWidgets.QPushButton("Ftrack")
- self.btnFtrack.setToolTip('Open browser for Login to Ftrack')
- self.btnFtrack.clicked.connect(self.open_ftrack)
+ self.setLayout(main)
- self.btnGroup.addWidget(self.btnFtrack)
- self.btnGroup.addWidget(self.btnEnter)
- self.btnGroup.addWidget(self.btnClose)
+ def fill_ftrack_url(self):
+ url = os.getenv("FTRACK_SERVER")
+ checked_url = self.check_url(url)
- self.main.addLayout(self.form)
- self.main.addLayout(self.btnGroup)
+ if checked_url is None:
+ checked_url = ""
+ self.btn_login.setEnabled(False)
+ self.btn_ftrack_login.setEnabled(False)
- self.inputs.append(self.api_input)
- self.inputs.append(self.user_input)
- self.inputs.append(self.ftsite_input)
+ self.api_input.setEnabled(False)
+ self.user_input.setEnabled(False)
+ self.ftsite_input.setEnabled(False)
- self.enter_site()
- return self.main
+ self.ftsite_input.setText(checked_url)
- def enter_site(self):
- try:
- url = os.getenv('FTRACK_SERVER')
- newurl = self.checkUrl(url)
+ def set_advanced_mode(self, is_advanced):
+ self._in_advance_mode = is_advanced
- if newurl is None:
- self.btnEnter.setEnabled(False)
- self.btnFtrack.setEnabled(False)
- for input in self.inputs:
- input.setEnabled(False)
- newurl = url
+ self.error_label.setVisible(False)
- self.ftsite_input.setText(newurl)
+ is_logged = self._is_logged
- except Exception:
- self.setError("FTRACK_SERVER is not set in templates")
- self.btnEnter.setEnabled(False)
- self.btnFtrack.setEnabled(False)
- for input in self.inputs:
- input.setEnabled(False)
+ self.note_label.setVisible(not is_logged and not is_advanced)
+ self.btn_ftrack_login.setVisible(not is_logged and not is_advanced)
+ self.btn_advanced.setVisible(not is_logged and not is_advanced)
- def setError(self, msg):
+ self.btn_login.setVisible(not is_logged and is_advanced)
+ self.btn_simple.setVisible(not is_logged and is_advanced)
+
+ self.user_label.setVisible(is_logged or is_advanced)
+ self.user_input.setVisible(is_logged or is_advanced)
+ self.api_label.setVisible(is_logged or is_advanced)
+ self.api_input.setVisible(is_logged or is_advanced)
+ if is_advanced:
+ self.user_input.setFocus()
+ else:
+ self.btn_ftrack_login.setFocus()
+
+ def set_is_logged(self, is_logged):
+ self._is_logged = is_logged
+
+ self.user_input.setReadOnly(is_logged)
+ self.api_input.setReadOnly(is_logged)
+ self.user_input.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor))
+ self.api_input.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor))
+
+ self.btn_logout.setVisible(is_logged)
+
+ self.set_advanced_mode(self._in_advance_mode)
+
+ def set_error(self, msg):
self.error_label.setText(msg)
self.error_label.show()
+ def _on_logout_clicked(self):
+ self.user_input.setText("")
+ self.api_input.setText("")
+ self.set_is_logged(False)
+ self.logout_signal.emit()
+
+ def _on_simple_clicked(self):
+ self.set_advanced_mode(False)
+
+ def _on_advanced_clicked(self):
+ self.set_advanced_mode(True)
+
def _user_changed(self):
- self.user_input.setStyleSheet("")
+ self._not_invalid_input(self.user_input)
def _api_changed(self):
- self.api_input.setStyleSheet("")
+ self._not_invalid_input(self.api_input)
- def _invalid_input(self, entity):
- entity.setStyleSheet("border: 1px solid red;")
+ def _not_invalid_input(self, input_widget):
+ input_widget.setStyleSheet("")
- def enter_credentials(self):
+ def _invalid_input(self, input_widget):
+ input_widget.setStyleSheet("border: 1px solid red;")
+
+ def _on_login(self):
+ self.set_is_logged(True)
+ self._close_widget()
+
+ def _on_login_clicked(self):
username = self.user_input.text().strip()
- apiKey = self.api_input.text().strip()
- msg = "You didn't enter "
+ api_key = self.api_input.text().strip()
missing = []
if username == "":
missing.append("Username")
self._invalid_input(self.user_input)
- if apiKey == "":
+ if api_key == "":
missing.append("API Key")
self._invalid_input(self.api_input)
if len(missing) > 0:
- self.setError("{0} {1}".format(msg, " and ".join(missing)))
+ self.set_error("You didn't enter {}".format(" and ".join(missing)))
return
- verification = credentials.check_credentials(username, apiKey)
-
- if verification:
- credentials.save_credentials(username, apiKey, self.is_event)
- credentials.set_env(username, apiKey)
- if self.parent is not None:
- self.parent.loginChange()
- self._close_widget()
- else:
+ if not self.login_with_credentials(username, api_key):
self._invalid_input(self.user_input)
self._invalid_input(self.api_input)
- self.setError(
+ self.set_error(
"We're unable to sign in to Ftrack with these credentials"
)
- def open_ftrack(self):
- url = self.ftsite_input.text()
- self.loginWithCredentials(url, None, None)
-
- def checkUrl(self, url):
- url = url.strip('/ ')
-
+ def _on_ftrack_login_clicked(self):
+ url = self.check_url(self.ftsite_input.text())
if not url:
- self.setError("There is no URL set in Templates")
- return
-
- if 'http' not in url:
- if url.endswith('ftrackapp.com'):
- url = 'https://' + url
- else:
- url = 'https://{0}.ftrackapp.com'.format(url)
- try:
- result = requests.get(
- url,
- # Old python API will not work with redirect.
- allow_redirects=False
- )
- except requests.exceptions.RequestException:
- self.setError(
- 'The server URL set in Templates could not be reached.'
- )
- return
-
- if (
- result.status_code != 200 or 'FTRACK_VERSION' not in result.headers
- ):
- self.setError(
- 'The server URL set in Templates is not a valid ftrack server.'
- )
- return
- return url
-
- def loginWithCredentials(self, url, username, apiKey):
- url = url.strip('/ ')
-
- if not url:
- self.setError(
- 'You need to specify a valid server URL, '
- 'for example https://server-name.ftrackapp.com'
- )
- return
-
- if 'http' not in url:
- if url.endswith('ftrackapp.com'):
- url = 'https://' + url
- else:
- url = 'https://{0}.ftrackapp.com'.format(url)
- try:
- result = requests.get(
- url,
- # Old python API will not work with redirect.
- allow_redirects=False
- )
- except requests.exceptions.RequestException:
- self.setError(
- 'The server URL you provided could not be reached.'
- )
- return
-
- if (
- result.status_code != 200 or 'FTRACK_VERSION' not in result.headers
- ):
- self.setError(
- 'The server URL you provided is not a valid ftrack server.'
- )
return
# If there is an existing server thread running we need to stop it.
if self._login_server_thread:
- self._login_server_thread.quit()
+ self._login_server_thread.join()
self._login_server_thread = None
# If credentials are not properly set, try to get them using a http
# server.
- if not username or not apiKey:
- self._login_server_thread = login_tools.LoginServerThread()
- self._login_server_thread.loginSignal.connect(self.loginSignal)
- self._login_server_thread.start(url)
+ self._login_server_thread = login_tools.LoginServerThread(
+ url, self._result_of_ftrack_thread
+ )
+ self._login_server_thread.start()
+
+ def _result_of_ftrack_thread(self, username, api_key):
+ if not self.login_with_credentials(username, api_key):
+ self._invalid_input(self.api_input)
+ self.set_error((
+ "Somthing happened with Ftrack login."
+ " Try enter Username and API key manually."
+ ))
+
+ def login_with_credentials(self, username, api_key):
+ verification = credentials.check_credentials(username, api_key)
+ if verification:
+ credentials.save_credentials(username, api_key, False)
+ credentials.set_env(username, api_key)
+ self.set_credentials(username, api_key)
+ self.login_changed.emit()
+ return verification
+
+ def set_credentials(self, username, api_key, is_logged=True):
+ self.user_input.setText(username)
+ self.api_input.setText(api_key)
+
+ self.error_label.hide()
+
+ self._not_invalid_input(self.ftsite_input)
+ self._not_invalid_input(self.user_input)
+ self._not_invalid_input(self.api_input)
+
+ if is_logged is not None:
+ self.set_is_logged(is_logged)
+
+ def check_url(self, url):
+ if url is not None:
+ url = url.strip("/ ")
+
+ if not url:
+ self.set_error((
+ "You need to specify a valid server URL, "
+ "for example https://server-name.ftrackapp.com"
+ ))
return
- verification = credentials.check_credentials(username, apiKey)
+ if "http" not in url:
+ if url.endswith("ftrackapp.com"):
+ url = "https://" + url
+ else:
+ url = "https://{}.ftrackapp.com".format(url)
+ try:
+ result = requests.get(
+ url,
+ # Old python API will not work with redirect.
+ allow_redirects=False
+ )
+ except requests.exceptions.RequestException:
+ self.set_error(
+ "Specified URL could not be reached."
+ )
+ return
- if verification is True:
- credentials.save_credentials(username, apiKey, self.is_event)
- credentials.set_env(username, apiKey)
- if self.parent is not None:
- self.parent.loginChange()
- self._close_widget()
+ if (
+ result.status_code != 200
+ or "FTRACK_VERSION" not in result.headers
+ ):
+ self.set_error(
+ "Specified URL does not lead to a valid Ftrack server."
+ )
+ return
+ return url
def closeEvent(self, event):
event.ignore()
diff --git a/pype/modules/ftrack/tray/login_tools.py b/pype/modules/ftrack/tray/login_tools.py
index 02982294f2..e7d22fbc19 100644
--- a/pype/modules/ftrack/tray/login_tools.py
+++ b/pype/modules/ftrack/tray/login_tools.py
@@ -2,7 +2,7 @@ from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib import parse
import webbrowser
import functools
-from Qt import QtCore
+import threading
from pype.api import resources
@@ -55,20 +55,17 @@ class LoginServerHandler(BaseHTTPRequestHandler):
)
-class LoginServerThread(QtCore.QThread):
+class LoginServerThread(threading.Thread):
'''Login server thread.'''
- # Login signal.
- loginSignal = QtCore.Signal(object, object, object)
-
- def start(self, url):
- '''Start thread.'''
+ def __init__(self, url, callback):
self.url = url
- super(LoginServerThread, self).start()
+ self.callback = callback
+ super(LoginServerThread, self).__init__()
def _handle_login(self, api_user, api_key):
'''Login to server with *api_user* and *api_key*.'''
- self.loginSignal.emit(self.url, api_user, api_key)
+ self.callback(api_user, api_key)
def run(self):
'''Listen for events.'''
diff --git a/pype/plugins/maya/load/load_audio.py b/pype/plugins/maya/load/load_audio.py
index e1860d0ca6..ca38082ed0 100644
--- a/pype/plugins/maya/load/load_audio.py
+++ b/pype/plugins/maya/load/load_audio.py
@@ -1,6 +1,9 @@
from maya import cmds, mel
+import pymel.core as pc
from avalon import api
+from avalon.maya.pipeline import containerise
+from avalon.maya import lib
class AudioLoader(api.Loader):
@@ -24,4 +27,48 @@ class AudioLoader(api.Loader):
displaySound=True
)
- return [sound_node]
+ asset = context["asset"]["name"]
+ namespace = namespace or lib.unique_namespace(
+ asset + "_",
+ prefix="_" if asset[0].isdigit() else "",
+ suffix="_",
+ )
+
+ return containerise(
+ name=name,
+ namespace=namespace,
+ nodes=[sound_node],
+ context=context,
+ loader=self.__class__.__name__
+ )
+
+ def update(self, container, representation):
+ audio_node = None
+ for node in pc.PyNode(container["objectName"]).members():
+ if node.nodeType() == "audio":
+ audio_node = node
+
+ assert audio_node is not None, "Audio node not found."
+
+ path = api.get_representation_path(representation)
+ audio_node.filename.set(path)
+ cmds.setAttr(
+ container["objectName"] + ".representation",
+ str(representation["_id"]),
+ type="string"
+ )
+
+ def switch(self, container, representation):
+ self.update(container, representation)
+
+ def remove(self, container):
+ members = cmds.sets(container['objectName'], query=True)
+ cmds.lockNode(members, lock=False)
+ cmds.delete([container['objectName']] + members)
+
+ # Clean up the namespace
+ try:
+ cmds.namespace(removeNamespace=container['namespace'],
+ deleteNamespaceContent=True)
+ except RuntimeError:
+ pass
diff --git a/pype/plugins/maya/load/load_image_plane.py b/pype/plugins/maya/load/load_image_plane.py
index 08f7c99156..17a6866f80 100644
--- a/pype/plugins/maya/load/load_image_plane.py
+++ b/pype/plugins/maya/load/load_image_plane.py
@@ -12,7 +12,7 @@ class ImagePlaneLoader(api.Loader):
families = ["plate", "render"]
label = "Create imagePlane on selected camera."
- representations = ["mov", "exr"]
+ representations = ["mov", "exr", "preview"]
icon = "image"
color = "orange"
@@ -83,7 +83,8 @@ class ImagePlaneLoader(api.Loader):
image_plane_shape.frameOut.set(end_frame)
image_plane_shape.useFrameExtension.set(1)
- if context["representation"]["name"] == "mov":
+ movie_representations = ["mov", "preview"]
+ if context["representation"]["name"] in movie_representations:
# Need to get "type" by string, because its a method as well.
pc.Attribute(image_plane_shape + ".type").set(2)
diff --git a/pype/plugins/nukestudio/publish/collect_reviews.py b/pype/plugins/nukestudio/publish/collect_reviews.py
index aa8c60767c..3167c66170 100644
--- a/pype/plugins/nukestudio/publish/collect_reviews.py
+++ b/pype/plugins/nukestudio/publish/collect_reviews.py
@@ -63,10 +63,14 @@ class CollectReviews(api.InstancePlugin):
self.log.debug("Track item on plateMain")
rev_inst = None
for inst in instance.context[:]:
- if inst.data["track"] in track:
- rev_inst = inst
- self.log.debug("Instance review: {}".format(
- rev_inst.data["name"]))
+ if inst.data["track"] != track:
+ continue
+
+ if inst.data["item"].name() != instance.data["item"].name():
+ continue
+
+ rev_inst = inst
+ break
if rev_inst is None:
raise RuntimeError((
@@ -82,7 +86,7 @@ class CollectReviews(api.InstancePlugin):
ext = os.path.splitext(file)[-1][1:]
# change label
- instance.data["label"] = "{0} - {1} - ({2}) - review".format(
+ instance.data["label"] = "{0} - {1} - ({2})".format(
instance.data['asset'], instance.data["subset"], ext
)
@@ -99,7 +103,7 @@ class CollectReviews(api.InstancePlugin):
"step": 1,
"fps": rev_inst.data.get("fps"),
"name": "preview",
- "tags": ["preview"],
+ "tags": ["preview", "ftrackreview"],
"ext": ext
}
diff --git a/pype/tools/pyblish_pype/model.py b/pype/tools/pyblish_pype/model.py
index fdcdffd33f..3c9d4806ac 100644
--- a/pype/tools/pyblish_pype/model.py
+++ b/pype/tools/pyblish_pype/model.py
@@ -870,13 +870,18 @@ class ArtistProxy(QtCore.QAbstractProxyModel):
self.rowsInserted.emit(self.parent(), new_from, new_to + 1)
def _remove_rows(self, parent_row, from_row, to_row):
- removed_rows = []
increment_num = self.mapping_from[parent_row][from_row]
+
+ to_end_index = len(self.mapping_from[parent_row]) - 1
+ for _idx in range(0, parent_row):
+ to_end_index += len(self.mapping_from[_idx])
+
+ removed_rows = 0
_emit_last = None
for row_num in reversed(range(from_row, to_row + 1)):
row = self.mapping_from[parent_row].pop(row_num)
_emit_last = row
- removed_rows.append(row)
+ removed_rows += 1
_emit_first = int(increment_num)
mapping_from_len = len(self.mapping_from)
@@ -896,11 +901,8 @@ class ArtistProxy(QtCore.QAbstractProxyModel):
self.mapping_from[idx_i][idx_j] = increment_num
increment_num += 1
- first_to_row = None
- for row in removed_rows:
- if first_to_row is None:
- first_to_row = row
- self.mapping_to.pop(row)
+ for idx in range(removed_rows):
+ self.mapping_to.pop(to_end_index - idx)
return (_emit_first, _emit_last)
diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py
index 9537b62581..a4cf4eabfe 100644
--- a/pype/tools/tray/pype_tray.py
+++ b/pype/tools/tray/pype_tray.py
@@ -537,6 +537,14 @@ class PypeTrayApplication(QtWidgets.QApplication):
super(self.__class__, self).__init__(sys.argv)
# Allows to close widgets without exiting app
self.setQuitOnLastWindowClosed(False)
+
+ # Allow show icon istead of python icon in task bar (Windows)
+ if os.name == "nt":
+ import ctypes
+ ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(
+ u"pype_tray"
+ )
+
# Sets up splash
splash_widget = self.set_splash()