From 5be4bf72e92f93e16be3bfb5f2da8f0ff648ac47 Mon Sep 17 00:00:00 2001 From: Jesin Roy Date: Mon, 7 May 2018 06:08:36 +0100 Subject: [PATCH] Feature 565 (#26) custom validations for integ --- .../maya/publish_files_integ.py | 381 ++++++++++++++++++ 1 file changed, 381 insertions(+) create mode 100644 hooks/tk-multi-publish2/maya/publish_files_integ.py diff --git a/hooks/tk-multi-publish2/maya/publish_files_integ.py b/hooks/tk-multi-publish2/maya/publish_files_integ.py new file mode 100644 index 000000000..546887419 --- /dev/null +++ b/hooks/tk-multi-publish2/maya/publish_files_integ.py @@ -0,0 +1,381 @@ +# 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 maya.cmds as cmds +import maya.mel as mel +import sgtk +from dd.runtime import api +api.load('frangetools') +import frangetools + +HookBaseClass = sgtk.get_hook_baseclass() + +GROUP_NODES = ['WORLDSCALE', + 'SET_TO_WORLD', + 'TRACK_GEO'] + +CAMERA_NAME = 'CAM' + +DEFAULT_CAMERAS = ['persp', + 'top', + 'front', + 'side'] + + +class MayaPublishFilesDDIntegValidationPlugin(HookBaseClass): + """ + Inherits from MayaPublishPlugin + """ + + @property + def description(self): + """ + Verbose, multi-line description of what the plugin does. This can + contain simple html for formatting. + """ + + desc = super(MayaPublishFilesDDIntegValidationPlugin, self).description + + return desc + "

" + """ + Validation checks before a file is published. + """ + + def _build_dict(self, seq, key): + """ + Creating a dictionary based on a key. + + :param seq: list of dictionaries + :param key: dictionary key from which to create the dictionary + :return: dict with information arranged based on that particular key + """ + return dict((d[key], dict(d, index=index)) for (index, d) in enumerate(seq)) + + def _framerange_of_sequence(self, item): + """ + Since users have the option to render only a subset of frames, + adding validation to check if the full frame range is being published. + + :param item: Item to process + :return: True if yes false otherwise + """ + lss_path = item.properties['path'] + lss_data = frangetools.getSequence(lss_path) + + info_by_path = self._build_dict(lss_data, key="path") + missing_frames = info_by_path.get(lss_path)['missing_frames'] + + if missing_frames: + self.logger.error("Incomplete playblast! All the frames are not the playblast.") + return False + else: + # If there are no missing frames, checking if the start and end frames match with playblast settings. + # This is being directly checked with playblast settings in the scene since + # _sync_frame_range_with_shotgun() will ensure playblast frame range is synced with shotgun + import pymel.core as pm + playback_start = pm.playbackOptions(q=True, minTime=True) + playback_end = pm.playbackOptions(q=True, maxTime=True) + collected_playblast_firstframe = info_by_path.get(lss_path)['frame_range'][0] + collected_playblast_lastframe = info_by_path.get(lss_path)['frame_range'][1] + if (collected_playblast_firstframe != playback_start) or (collected_playblast_lastframe != playback_end): + self.logger.error("Incomplete playblast! All the frames are not in the playblast.") + return False + return True + + + def _sync_frame_range_with_shotgun(self, item): + """ + Checks whether frame range is in sync with shotgun. + + :param item: Item to process + :return: True if yes false otherwise + """ + context = item.context + entity = context.entity + + # checking entity validity + if entity: + + frame_range_app = self.parent.engine.apps.get("tk-multi-setframerange") + + sg_entity_type = entity["type"] + sg_filters = [["id", "is", entity["id"]]] + in_field = frame_range_app.get_setting("sg_in_frame_field") + out_field = frame_range_app.get_setting("sg_out_frame_field") + fields = [in_field, out_field] + + # get the field information from shotgun based on Shot + # sg_cut_in and sg_cut_out info will be on Shot entity, so skip in case this info is not present + # or if the sg_head_in or sg_tail_out is empty, skip the check + data = self.sgtk.shotgun.find_one(sg_entity_type, filters=sg_filters, fields=fields) + if in_field not in data or out_field not in data: + return True + elif in_field is None or out_field is None: + return True + + # Check if playback_start or animation_start is not in sync with shotgun + # Similarly if animation_start or animation_start is not in sync with shotgun + import pymel.core as pm + playback_start = pm.playbackOptions(q=True, minTime=True) + playback_end = pm.playbackOptions(q=True, maxTime=True) + animation_start = pm.playbackOptions(q=True, animationStartTime=True) + animation_end = pm.playbackOptions(q=True, animationEndTime=True) + if playback_start != data[in_field] or playback_end != data[out_field]: + self.logger.error("Frame range not synced with Shotgun.") + return False + if animation_start != data[in_field] or animation_end != data[out_field]: + self.logger.error("Frame range not synced with Shotgun.") + return False + return True + return True + + + def _extra_nodes_outside_track_geo(self): + """ + Check for nodes, apart from groups and camera lying outside of TRACK_GEO node + :return: True if yes false otherwise + """ + children = cmds.listRelatives('TRACK_GEO', c=True) + # Subtracting group nodes, cameras and child nodes of TRACK_GEO from the list of dag nodes. + # This is to get extra nodes present outside TRACK_GEO + if children: + extras = list(set(cmds.ls(tr=True, dag=True)) - set(GROUP_NODES) - set(cmds.listCameras()) - set(children)) + else: + extras = list(set(cmds.ls(tr=True, dag=True)) - set(GROUP_NODES) - set(cmds.listCameras())) + + if extras: + self.logger.error("Nodes present outside TRACK_GEO.", + extra={ + "action_show_more_info": { + "label": "Show Info", + "tooltip": "Show the extra nodes", + "text": "Nodes outside TRACK_GEO:\n{}".format("\n".join(extras)) + } + } + ) + return False + return True + + + def _track_geo_locked_channels(self): + """Check for locked channels for all nodes under the group TRACK_GEO. + :param: + nodes: list of nodes under TRACK_GEO + :return: True if yes false otherwise + """ + children = cmds.listRelatives('TRACK_GEO', c=True) + if children: + locked = "" + for node in children: + # For each node, list out attributes which are locked + lock_per_node = cmds.listAttr(node, l=True) + if lock_per_node: + locked += "\n" + node + " ---> " + ", ".join(lock_per_node) + # If there are locked channels, error message with node name and locked attribute name(s). + if locked: + self.logger.error("Locked channels detected.", + extra={ + "action_show_more_info": { + "label": "Show Info", + "tooltip": "Show the node and locked channels", + "text": "Locked channels:\n{}".format(locked) + } + } + ) + return False + return True + return True + + + def _track_geo_child_naming(self): + """Checks if the name of nodes under TRACK_GEO are prefixed with 'integ_'. + :param: + track_geo: nodes under TRACK_GEO + :return: True if yes false otherwise + """ + # Nodes under TRACK_GEO group + children = cmds.listRelatives('TRACK_GEO', c=True) + error_names = "" + # if there are nodes under TRACK_GEO, check for one without prefix "integ_" + if children: + for child in children: + # If the name doesn't start with integ_ add node name to errorNames + if child[:6] != "integ_": + error_names += "\n" + child + if error_names: + self.logger.error("Incorrect Naming! Node name should start with integ_.", + extra={ + "action_show_more_info": { + "label": "Show Info", + "tooltip": "Show the node with incorrect naming", + "text": "Nodes with incorrect naming:\n{}".format(error_names) + } + } + ) + return False + return True + + + def _check_hierarchy(self, group_nodes): + """Checks the hierarchy of group nodes in the scene. + :param: + group_nodes: the list of nodes in the scene + :return: True if yes false otherwise + """ + for name in range(len(group_nodes) - 1): + # Listing children of group nodes + children = cmds.listRelatives(group_nodes[name], c=True) + # group_nodes is arranged in hierarchical order i.e. the next node should be the child of previous + if children and (group_nodes[name + 1] in children): + if name == 'SET_TO_WORLD' and 'CAM' in children: + continue + else: + hierarchy = "WORLDSCALE\n|__SET_TO_WORLD\n" + " " + "|__TRACK_GEO\n" + " " + "|__CAM" + self.logger.error("Incorrect hierarchy.", + extra={ + "action_show_more_info": { + "label": "Show Info", + "tooltip": "Show the required hierarchy", + "text": "Required hierarchy:\n\n{}".format(hierarchy) + } + } + ) + return False + return True + + + def _connected_image_plane(self): + camshape = cmds.listRelatives(CAMERA_NAME, s=True, c=True)[0] + connections = cmds.listConnections(camshape + '.imagePlane', source=True, type='imagePlane') + if not connections: + self.logger.error("Image plane not attached to CAM.") + return False + return True + + + def _camera_naming(self): + """Checks the naming of the camera. + :param: + group_nodes: The list of nodes that should be in the scene. This will be + used to check node hierarchy once camera naming is validated. + :return: True if yes false otherwise + """ + # Look for all the cameras present in the scene + all_cameras = cmds.listCameras() + # Remove the default_cameras from the list + main_cam = list(set(all_cameras) - set(DEFAULT_CAMERAS)) + if main_cam: + if len(main_cam) > 1: + # Checking if more than one CAM present + self.logger.error("More the one camera detected. Only CAM should be present.") + return False + elif main_cam[0] != CAMERA_NAME: + # Validating camera name + self.logger.error("Incorrect camera name! Should be CAM.") + return False + else: + self.logger.error("Camera (CAM) not present in the scene.") + return False + return True + + + def _node_naming(self, groups): + """Checking if the established group node names have not been tampered with. + :param: + group_nodes: group nodes to be present in the scene + groups: group nodes that are actually present + :return: True if yes false otherwise + """ + # Check for extra group nodes apart from the ones in group_nodes + extras = list(set(groups) - set(GROUP_NODES)) + # check for any group nodes apart from the once mentioned + if extras: + self.logger.error("Incorrect naming or extra group nodes present in the scene.", + extra={ + "action_show_more_info": { + "label": "Show Info", + "tooltip": "Show the conflicting group nodes", + "text": "Please check the group nodes:\n{}".format("\n".join(extras)) + + "\n\nOnly following group nodes should be present:\n{}".format( + "\n".join(GROUP_NODES)) + } + } + ) + return False + # check if any of the required group nodes are missing + elif not set(GROUP_NODES).issubset(set(groups)): + self.logger.error("Please ensure all the group nodes are present.", + extra={ + "action_show_more_info": { + "label": "Show Info", + "tooltip": "Group nodes", + "text": "Following group nodes should be present:\n{}".format( + "\n".join(GROUP_NODES)) + } + } + ) + return False + return True + + + @staticmethod + def _is_group(node=None): + """Check for all the group nodes present in the scene. + :param: + node: all the nodes in the scene + :return: True if yes false otherwise + """ + if cmds.nodeType(node) != "transform": + return False + + children = cmds.listRelatives(node, c=True) + if not children: + return True + + for c in children: + if cmds.nodeType(c) != 'transform': + return False + else: + return True + + + def validate(self, task_settings, item): + """ + Validates the given item to check that it is ok to publish. Returns a + boolean to indicate validity. + + :param task_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. + """ + all_dag_nodes = cmds.ls(dag=True, sn=True) + groups = [g for g in all_dag_nodes if self._is_group(g)] + + status = True + # Checks for the scene file, i.e if the item is not a sequence or a cache file + if item.type == "file.maya": + nodes = self._node_naming(groups) and \ + self._check_hierarchy(groups) and \ + self._track_geo_child_naming() and \ + self._track_geo_locked_channels()and \ + self._extra_nodes_outside_track_geo() and \ + self._sync_frame_range_with_shotgun(item) + cam = self._camera_naming() and self._connected_image_plane() + status = nodes and cam and status + elif item.properties['is_sequence']: + sequences = self._framerange_of_sequence(item) + status = sequences and status + + if not status: + return status + + return super(MayaPublishFilesDDIntegValidationPlugin, self).validate(task_settings, item)