[Automated] Merged develop into main

This commit is contained in:
ynbot 2023-03-22 04:25:03 +01:00 committed by GitHub
commit b7cfc6aaf4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 424 additions and 138 deletions

View file

@ -146,6 +146,8 @@ class CreatorWidget(QtWidgets.QDialog):
return " ".join([str(m.group(0)).capitalize() for m in matches])
def create_row(self, layout, type, text, **kwargs):
value_keys = ["setText", "setCheckState", "setValue", "setChecked"]
# get type attribute from qwidgets
attr = getattr(QtWidgets, type)
@ -167,14 +169,27 @@ class CreatorWidget(QtWidgets.QDialog):
# assign the created attribute to variable
item = getattr(self, attr_name)
# set attributes to item which are not values
for func, val in kwargs.items():
if func in value_keys:
continue
if getattr(item, func):
log.debug("Setting {} to {}".format(func, val))
func_attr = getattr(item, func)
if isinstance(val, tuple):
func_attr(*val)
else:
func_attr(val)
# set values to item
for value_item in value_keys:
if value_item not in kwargs:
continue
if getattr(item, value_item):
getattr(item, value_item)(kwargs[value_item])
# add to layout
layout.addRow(label, item)
@ -276,8 +291,11 @@ class CreatorWidget(QtWidgets.QDialog):
elif v["type"] == "QSpinBox":
data[k]["value"] = self.create_row(
content_layout, "QSpinBox", v["label"],
setValue=v["value"], setMinimum=0,
setValue=v["value"],
setDisplayIntegerBase=10000,
setRange=(0, 99999), setMinimum=0,
setMaximum=100000, setToolTip=tool_tip)
return data

View file

@ -30,36 +30,6 @@ def _has_arnold():
return False
def escape_space(path):
"""Ensure path is enclosed by quotes to allow paths with spaces"""
return '"{}"'.format(path) if " " in path else path
def get_ocio_config_path(profile_folder):
"""Path to OpenPype vendorized OCIO.
Vendorized OCIO config file path is grabbed from the specific path
hierarchy specified below.
"{OPENPYPE_ROOT}/vendor/OpenColorIO-Configs/{profile_folder}/config.ocio"
Args:
profile_folder (str): Name of folder to grab config file from.
Returns:
str: Path to vendorized config file.
"""
return os.path.join(
os.environ["OPENPYPE_ROOT"],
"vendor",
"bin",
"ocioconfig",
"OpenColorIOConfigs",
profile_folder,
"config.ocio"
)
def find_paths_by_hash(texture_hash):
"""Find the texture hash key in the dictionary.

View file

@ -25,7 +25,7 @@ class ExtractReviewData(publish.Extractor):
# review can be removed since `ProcessSubmittedJobOnFarm` will create
# reviewable representation if needed
if (
"render.farm" in instance.data["families"]
instance.data.get("farm")
and "review" in instance.data["families"]
):
instance.data["families"].remove("review")

View file

@ -49,7 +49,12 @@ class ExtractReviewDataLut(publish.Extractor):
exporter.stagingDir, exporter.file).replace("\\", "/")
instance.data["representations"] += data["representations"]
if "render.farm" in families:
# review can be removed since `ProcessSubmittedJobOnFarm` will create
# reviewable representation if needed
if (
instance.data.get("farm")
and "review" in instance.data["families"]
):
instance.data["families"].remove("review")
self.log.debug(

View file

@ -105,10 +105,7 @@ class ExtractReviewDataMov(publish.Extractor):
self, instance, o_name, o_data["extension"],
multiple_presets)
if (
"render.farm" in families or
"prerender.farm" in families
):
if instance.data.get("farm"):
if "review" in instance.data["families"]:
instance.data["families"].remove("review")

View file

@ -31,7 +31,7 @@ class ExtractThumbnail(publish.Extractor):
def process(self, instance):
if "render.farm" in instance.data["families"]:
if instance.data.get("farm"):
return
with napi.maintained_selection():

View file

@ -5,29 +5,38 @@ import re
import subprocess
from distutils import dir_util
from pathlib import Path
from typing import List
from typing import List, Union
import openpype.hosts.unreal.lib as ue_lib
from qtpy import QtCore
def parse_comp_progress(line: str, progress_signal: QtCore.Signal(int)) -> int:
match = re.search('\[[1-9]+/[0-9]+\]', line)
def parse_comp_progress(line: str, progress_signal: QtCore.Signal(int)):
match = re.search(r"\[[1-9]+/[0-9]+]", line)
if match is not None:
split: list[str] = match.group().split('/')
split: list[str] = match.group().split("/")
curr: float = float(split[0][1:])
total: float = float(split[1][:-1])
progress_signal.emit(int((curr / total) * 100.0))
def parse_prj_progress(line: str, progress_signal: QtCore.Signal(int)) -> int:
match = re.search('@progress', line)
def parse_prj_progress(line: str, progress_signal: QtCore.Signal(int)):
match = re.search("@progress", line)
if match is not None:
percent_match = re.search('\d{1,3}', line)
percent_match = re.search(r"\d{1,3}", line)
progress_signal.emit(int(percent_match.group()))
def retrieve_exit_code(line: str):
match = re.search(r"ExitCode=\d+", line)
if match is not None:
split: list[str] = match.group().split("=")
return int(split[1])
return None
class UEProjectGenerationWorker(QtCore.QObject):
finished = QtCore.Signal(str)
failed = QtCore.Signal(str)
@ -77,16 +86,19 @@ class UEProjectGenerationWorker(QtCore.QObject):
if self.dev_mode:
stage_count = 4
self.stage_begin.emit(f'Generating a new UE project ... 1 out of '
f'{stage_count}')
self.stage_begin.emit(
("Generating a new UE project ... 1 out of "
f"{stage_count}"))
commandlet_cmd = [f'{ue_editor_exe.as_posix()}',
f'{cmdlet_project.as_posix()}',
f'-run=OPGenerateProject',
f'{project_file.resolve().as_posix()}']
commandlet_cmd = [
f"{ue_editor_exe.as_posix()}",
f"{cmdlet_project.as_posix()}",
"-run=OPGenerateProject",
f"{project_file.resolve().as_posix()}",
]
if self.dev_mode:
commandlet_cmd.append('-GenerateCode')
commandlet_cmd.append("-GenerateCode")
gen_process = subprocess.Popen(commandlet_cmd,
stdout=subprocess.PIPE,
@ -94,24 +106,27 @@ class UEProjectGenerationWorker(QtCore.QObject):
for line in gen_process.stdout:
decoded_line = line.decode(errors="replace")
print(decoded_line, end='')
print(decoded_line, end="")
self.log.emit(decoded_line)
gen_process.stdout.close()
return_code = gen_process.wait()
if return_code and return_code != 0:
msg = 'Failed to generate ' + self.project_name \
+ f' project! Exited with return code {return_code}'
msg = (
f"Failed to generate {self.project_name} "
f"project! Exited with return code {return_code}"
)
self.failed.emit(msg, return_code)
raise RuntimeError(msg)
print("--- Project has been generated successfully.")
self.stage_begin.emit(f'Writing the Engine ID of the build UE ... 1'
f' out of {stage_count}')
self.stage_begin.emit(
(f"Writing the Engine ID of the build UE ... 1"
f" out of {stage_count}"))
if not project_file.is_file():
msg = "Failed to write the Engine ID into .uproject file! Can " \
"not read!"
msg = ("Failed to write the Engine ID into .uproject file! Can "
"not read!")
self.failed.emit(msg)
raise RuntimeError(msg)
@ -125,13 +140,14 @@ class UEProjectGenerationWorker(QtCore.QObject):
pf.seek(0)
json.dump(pf_json, pf, indent=4)
pf.truncate()
print(f'--- Engine ID has been written into the project file')
print("--- Engine ID has been written into the project file")
self.progress.emit(90)
if self.dev_mode:
# 2nd stage
self.stage_begin.emit(f'Generating project files ... 2 out of '
f'{stage_count}')
self.stage_begin.emit(
(f"Generating project files ... 2 out of "
f"{stage_count}"))
self.progress.emit(0)
ubt_path = ue_lib.get_path_to_ubt(self.engine_path,
@ -154,8 +170,8 @@ class UEProjectGenerationWorker(QtCore.QObject):
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
for line in gen_proc.stdout:
decoded_line: str = line.decode(errors='replace')
print(decoded_line, end='')
decoded_line: str = line.decode(errors="replace")
print(decoded_line, end="")
self.log.emit(decoded_line)
parse_prj_progress(decoded_line, self.progress)
@ -163,13 +179,13 @@ class UEProjectGenerationWorker(QtCore.QObject):
return_code = gen_proc.wait()
if return_code and return_code != 0:
msg = 'Failed to generate project files! ' \
f'Exited with return code {return_code}'
msg = ("Failed to generate project files! "
f"Exited with return code {return_code}")
self.failed.emit(msg, return_code)
raise RuntimeError(msg)
self.stage_begin.emit(f'Building the project ... 3 out of '
f'{stage_count}')
self.stage_begin.emit(
f"Building the project ... 3 out of {stage_count}")
self.progress.emit(0)
# 3rd stage
build_prj_cmd = [ubt_path.as_posix(),
@ -177,16 +193,16 @@ class UEProjectGenerationWorker(QtCore.QObject):
arch,
"Development",
"-TargetType=Editor",
f'-Project={project_file}',
f'{project_file}',
f"-Project={project_file}",
f"{project_file}",
"-IgnoreJunk"]
build_prj_proc = subprocess.Popen(build_prj_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
for line in build_prj_proc.stdout:
decoded_line: str = line.decode(errors='replace')
print(decoded_line, end='')
decoded_line: str = line.decode(errors="replace")
print(decoded_line, end="")
self.log.emit(decoded_line)
parse_comp_progress(decoded_line, self.progress)
@ -194,16 +210,17 @@ class UEProjectGenerationWorker(QtCore.QObject):
return_code = build_prj_proc.wait()
if return_code and return_code != 0:
msg = 'Failed to build project! ' \
f'Exited with return code {return_code}'
msg = ("Failed to build project! "
f"Exited with return code {return_code}")
self.failed.emit(msg, return_code)
raise RuntimeError(msg)
# ensure we have PySide2 installed in engine
self.progress.emit(0)
self.stage_begin.emit(f'Checking PySide2 installation... {stage_count}'
f' out of {stage_count}')
self.stage_begin.emit(
(f"Checking PySide2 installation... {stage_count} "
f" out of {stage_count}"))
python_path = None
if platform.system().lower() == "windows":
python_path = self.engine_path / ("Engine/Binaries/ThirdParty/"
@ -225,9 +242,30 @@ class UEProjectGenerationWorker(QtCore.QObject):
msg = f"Unreal Python not found at {python_path}"
self.failed.emit(msg, 1)
raise RuntimeError(msg)
subprocess.check_call(
[python_path.as_posix(), "-m", "pip", "install", "pyside2"]
)
pyside_cmd = [python_path.as_posix(),
"-m",
"pip",
"install",
"pyside2"]
pyside_install = subprocess.Popen(pyside_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
for line in pyside_install.stdout:
decoded_line: str = line.decode(errors="replace")
print(decoded_line, end="")
self.log.emit(decoded_line)
pyside_install.stdout.close()
return_code = pyside_install.wait()
if return_code and return_code != 0:
msg = ("Failed to create the project! "
"The installation of PySide2 has failed!")
self.failed.emit(msg, return_code)
raise RuntimeError(msg)
self.progress.emit(100)
self.finished.emit("Project successfully built!")
@ -266,26 +304,30 @@ class UEPluginInstallWorker(QtCore.QObject):
# in order to successfully build the plugin,
# It must be built outside the Engine directory and then moved
build_plugin_cmd: List[str] = [f'{uat_path.as_posix()}',
'BuildPlugin',
f'-Plugin={uplugin_path.as_posix()}',
f'-Package={temp_dir.as_posix()}']
build_plugin_cmd: List[str] = [f"{uat_path.as_posix()}",
"BuildPlugin",
f"-Plugin={uplugin_path.as_posix()}",
f"-Package={temp_dir.as_posix()}"]
build_proc = subprocess.Popen(build_plugin_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
return_code: Union[None, int] = None
for line in build_proc.stdout:
decoded_line: str = line.decode(errors='replace')
print(decoded_line, end='')
decoded_line: str = line.decode(errors="replace")
print(decoded_line, end="")
self.log.emit(decoded_line)
if return_code is None:
return_code = retrieve_exit_code(decoded_line)
parse_comp_progress(decoded_line, self.progress)
build_proc.stdout.close()
return_code = build_proc.wait()
build_proc.wait()
if return_code and return_code != 0:
msg = 'Failed to build plugin' \
f' project! Exited with return code {return_code}'
msg = ("Failed to build plugin"
f" project! Exited with return code {return_code}")
dir_util.remove_tree(temp_dir.as_posix())
self.failed.emit(msg, return_code)
raise RuntimeError(msg)

View file

@ -13,6 +13,16 @@ else:
from shutil import copyfile
class DuplicateDestinationError(ValueError):
"""Error raised when transfer destination already exists in queue.
The error is only raised if `allow_queue_replacements` is False on the
FileTransaction instance and the added file to transfer is of a different
src file than the one already detected in the queue.
"""
class FileTransaction(object):
"""File transaction with rollback options.
@ -44,7 +54,7 @@ class FileTransaction(object):
MODE_COPY = 0
MODE_HARDLINK = 1
def __init__(self, log=None):
def __init__(self, log=None, allow_queue_replacements=False):
if log is None:
log = logging.getLogger("FileTransaction")
@ -60,6 +70,8 @@ class FileTransaction(object):
# Backup file location mapping to original locations
self._backup_to_original = {}
self._allow_queue_replacements = allow_queue_replacements
def add(self, src, dst, mode=MODE_COPY):
"""Add a new file to transfer queue.
@ -82,6 +94,14 @@ class FileTransaction(object):
src, dst))
return
else:
if not self._allow_queue_replacements:
raise DuplicateDestinationError(
"Transfer to destination is already in queue: "
"{} -> {}. It's not allowed to be replaced by "
"a new transfer from {}".format(
queued_src, dst, src
))
self.log.warning("File transfer in queue replaced..")
self.log.debug(
"Removed from queue: {} -> {} replaced by {} -> {}".format(

View file

@ -32,7 +32,7 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin,
label = "Submit Nuke to Deadline"
order = pyblish.api.IntegratorOrder + 0.1
hosts = ["nuke"]
families = ["render", "prerender.farm"]
families = ["render", "prerender"]
optional = True
targets = ["local"]
@ -80,6 +80,10 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin,
]
def process(self, instance):
if not instance.data.get("farm"):
self.log.info("Skipping local instance.")
return
instance.data["attributeValues"] = self.get_attr_values_from_data(
instance.data)
@ -168,10 +172,10 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin,
resp.json()["_id"])
# redefinition of families
if "render.farm" in families:
if "render" in instance.data["family"]:
instance.data['family'] = 'write'
families.insert(0, "render2d")
elif "prerender.farm" in families:
elif "prerender" in instance.data["family"]:
instance.data['family'] = 'write'
families.insert(0, "prerender")
instance.data["families"] = families

View file

@ -756,6 +756,10 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
instance (pyblish.api.Instance): Instance data.
"""
if not instance.data.get("farm"):
self.log.info("Skipping local instance.")
return
data = instance.data.copy()
context = instance.context
self.context = context

View file

@ -21,6 +21,10 @@ class ValidateDeadlinePools(OptionalPyblishPluginMixin,
optional = True
def process(self, instance):
if not instance.data.get("farm"):
self.log.info("Skipping local instance.")
return
# get default deadline webservice url from deadline module
deadline_url = instance.context.data["defaultDeadline"]
self.log.info("deadline_url::{}".format(deadline_url))

View file

@ -124,6 +124,11 @@ class AppplicationsAction(BaseAction):
if not avalon_project_apps:
return False
settings = self.get_project_settings_from_event(
event, avalon_project_doc["name"])
only_available = settings["applications"]["only_available"]
items = []
for app_name in avalon_project_apps:
app = self.application_manager.applications.get(app_name)
@ -133,6 +138,10 @@ class AppplicationsAction(BaseAction):
if app.group.name in CUSTOM_LAUNCH_APP_GROUPS:
continue
# Skip applications without valid executables
if only_available and not app.find_executable():
continue
app_icon = app.icon
if app_icon and self.icon_url:
try:

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import gazu
import pyblish.api
import re
class IntegrateKitsuNote(pyblish.api.ContextPlugin):
@ -9,27 +10,66 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin):
order = pyblish.api.IntegratorOrder
label = "Kitsu Note and Status"
families = ["render", "kitsu"]
# status settings
set_status_note = False
note_status_shortname = "wfa"
status_conditions = list()
# comment settings
custom_comment_template = {
"enabled": False,
"comment_template": "{comment}",
}
def format_publish_comment(self, instance):
"""Format the instance's publish comment
Formats `instance.data` against the custom template.
"""
def replace_missing_key(match):
"""If key is not found in kwargs, set None instead"""
key = match.group(1)
if key not in instance.data:
self.log.warning(
"Key '{}' was not found in instance.data "
"and will be rendered as an empty string "
"in the comment".format(key)
)
return ""
else:
return str(instance.data[key])
template = self.custom_comment_template["comment_template"]
pattern = r"\{([^}]*)\}"
return re.sub(pattern, replace_missing_key, template)
def process(self, context):
# Get comment text body
publish_comment = context.data.get("comment")
if not publish_comment:
self.log.info("Comment is not set.")
self.log.debug("Comment is `{}`".format(publish_comment))
for instance in context:
# Check if instance is a review by checking its family
if "review" not in instance.data["families"]:
continue
kitsu_task = instance.data.get("kitsu_task")
if kitsu_task is None:
continue
# Get note status, by default uses the task status for the note
# if it is not specified in the configuration
note_status = kitsu_task["task_status"]["id"]
shortname = kitsu_task["task_status"]["short_name"].upper()
note_status = kitsu_task["task_status_id"]
if self.set_status_note:
# Check if any status condition is not met
allow_status_change = True
for status_cond in self.status_conditions:
condition = status_cond["condition"] == "equal"
match = status_cond["short_name"].upper() == shortname
if match and not condition or condition and not match:
allow_status_change = False
break
if self.set_status_note and allow_status_change:
kitsu_status = gazu.task.get_task_status_by_short_name(
self.note_status_shortname
)
@ -42,11 +82,22 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin):
"changed!".format(self.note_status_shortname)
)
# Get comment text body
publish_comment = instance.data.get("comment")
if self.custom_comment_template["enabled"]:
publish_comment = self.format_publish_comment(instance)
if not publish_comment:
self.log.info("Comment is not set.")
else:
self.log.debug("Comment is `{}`".format(publish_comment))
# Add comment to kitsu task
task_id = kitsu_task["id"]
self.log.debug("Add new note in taks id {}".format(task_id))
self.log.debug(
"Add new note in tasks id {}".format(kitsu_task["id"])
)
kitsu_comment = gazu.task.add_comment(
task_id, note_status, comment=publish_comment
kitsu_task, note_status, comment=publish_comment
)
instance.data["kitsu_comment"] = kitsu_comment

View file

@ -129,7 +129,7 @@ def update_op_assets(
frame_out = frame_in + frames_duration - 1
else:
frame_out = project_doc["data"].get("frameEnd", frame_in)
item_data["frameEnd"] = frame_out
item_data["frameEnd"] = int(frame_out)
# Fps, fallback to project's value or default value (25.0)
try:
fps = float(item_data.get("fps"))
@ -147,33 +147,37 @@ def update_op_assets(
item_data["resolutionWidth"] = int(match_res.group(1))
item_data["resolutionHeight"] = int(match_res.group(2))
else:
item_data["resolutionWidth"] = project_doc["data"].get(
"resolutionWidth"
item_data["resolutionWidth"] = int(
project_doc["data"].get("resolutionWidth")
)
item_data["resolutionHeight"] = project_doc["data"].get(
"resolutionHeight"
item_data["resolutionHeight"] = int(
project_doc["data"].get("resolutionHeight")
)
# Properties that doesn't fully exist in Kitsu.
# Guessing those property names below:
# Pixel Aspect Ratio
item_data["pixelAspect"] = item_data.get(
"pixel_aspect", project_doc["data"].get("pixelAspect")
item_data["pixelAspect"] = float(
item_data.get(
"pixel_aspect", project_doc["data"].get("pixelAspect")
)
)
# Handle Start
item_data["handleStart"] = item_data.get(
"handle_start", project_doc["data"].get("handleStart")
item_data["handleStart"] = int(
item_data.get(
"handle_start", project_doc["data"].get("handleStart")
)
)
# Handle End
item_data["handleEnd"] = item_data.get(
"handle_end", project_doc["data"].get("handleEnd")
item_data["handleEnd"] = int(
item_data.get("handle_end", project_doc["data"].get("handleEnd"))
)
# Clip In
item_data["clipIn"] = item_data.get(
"clip_in", project_doc["data"].get("clipIn")
item_data["clipIn"] = int(
item_data.get("clip_in", project_doc["data"].get("clipIn"))
)
# Clip Out
item_data["clipOut"] = item_data.get(
"clip_out", project_doc["data"].get("clipOut")
item_data["clipOut"] = int(
item_data.get("clip_out", project_doc["data"].get("clipOut"))
)
# Tasks

View file

@ -24,7 +24,10 @@ from openpype.client import (
get_version_by_name,
)
from openpype.lib import source_hash
from openpype.lib.file_transaction import FileTransaction
from openpype.lib.file_transaction import (
FileTransaction,
DuplicateDestinationError
)
from openpype.pipeline.publish import (
KnownPublishError,
get_publish_template_name,
@ -170,9 +173,18 @@ class IntegrateAsset(pyblish.api.InstancePlugin):
).format(instance.data["family"]))
return
file_transactions = FileTransaction(log=self.log)
file_transactions = FileTransaction(log=self.log,
# Enforce unique transfers
allow_queue_replacements=False)
try:
self.register(instance, file_transactions, filtered_repres)
except DuplicateDestinationError as exc:
# Raise DuplicateDestinationError as KnownPublishError
# and rollback the transactions
file_transactions.rollback()
six.reraise(KnownPublishError,
KnownPublishError(exc),
sys.exc_info()[2])
except Exception:
# clean destination
# todo: preferably we'd also rollback *any* changes to the database

View file

@ -60,6 +60,8 @@ class PreIntegrateThumbnails(pyblish.api.InstancePlugin):
if not found_profile:
return
thumbnail_repre.setdefault("tags", [])
if not found_profile["integrate_thumbnail"]:
if "delete" not in thumbnail_repre["tags"]:
thumbnail_repre["tags"].append("delete")

View file

@ -0,0 +1,3 @@
{
"only_available": false
}

View file

@ -7,7 +7,12 @@
"publish": {
"IntegrateKitsuNote": {
"set_status_note": false,
"note_status_shortname": "wfa"
"note_status_shortname": "wfa",
"status_conditions": [],
"custom_comment_template": {
"enabled": false,
"comment_template": "{comment}\n\n| | |\n|--|--|\n| version| `{version}` |\n| family | `{family}` |\n| name | `{name}` |"
}
}
}
}

View file

@ -82,6 +82,10 @@
"type": "schema",
"name": "schema_project_slack"
},
{
"type": "schema",
"name": "schema_project_applications"
},
{
"type": "schema",
"name": "schema_project_max"

View file

@ -0,0 +1,14 @@
{
"type": "dict",
"key": "applications",
"label": "Applications",
"collapsible": true,
"is_file": true,
"children": [
{
"type": "boolean",
"key": "only_available",
"label": "Show only available applications"
}
]
}

View file

@ -46,12 +46,62 @@
{
"type": "boolean",
"key": "set_status_note",
"label": "Set status on note"
"label": "Set status with note"
},
{
"type": "text",
"key": "note_status_shortname",
"label": "Note shortname"
},
{
"type": "list",
"key": "status_conditions",
"label": "Status conditions",
"object_type": {
"type": "dict",
"key": "conditions_dict",
"children": [
{
"type": "enum",
"key": "condition",
"label": "Condition",
"enum_items": [
{"equal": "Equal"},
{"not_equal": "Not equal"}
]
},
{
"type": "text",
"key": "short_name",
"label": "Short name"
}
]
},
"label": "Status shortname"
},
{
"type": "dict",
"collapsible": true,
"checkbox_key": "enabled",
"key": "custom_comment_template",
"label": "Custom Comment Template",
"children": [
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
},
{
"type": "label",
"label": "Kitsu supports markdown and here you can create a custom comment template.<br/>You can use data from your publishing instance's data."
},
{
"key": "comment_template",
"type": "text",
"multiline": true,
"label": "Custom comment"
}
]
}
]
}

View file

@ -19,6 +19,7 @@ from openpype.lib.applications import (
CUSTOM_LAUNCH_APP_GROUPS,
ApplicationManager
)
from openpype.settings import get_project_settings
from openpype.pipeline import discover_launcher_actions
from openpype.tools.utils.lib import (
DynamicQThread,
@ -94,6 +95,8 @@ class ActionModel(QtGui.QStandardItemModel):
if not project_doc:
return actions
project_settings = get_project_settings(project_name)
only_available = project_settings["applications"]["only_available"]
self.application_manager.refresh()
for app_def in project_doc["config"]["apps"]:
app_name = app_def["name"]
@ -104,6 +107,9 @@ class ActionModel(QtGui.QStandardItemModel):
if app.group.name in CUSTOM_LAUNCH_APP_GROUPS:
continue
if only_available and not app.find_executable():
continue
# Get from app definition, if not there from app in project
action = type(
"app_{}".format(app_name),

View file

@ -327,7 +327,7 @@ class InventoryModel(TreeModel):
project_name, repre_id
)
if not representation:
not_found["representation"].append(group_items)
not_found["representation"].extend(group_items)
not_found_ids.append(repre_id)
continue
@ -335,7 +335,7 @@ class InventoryModel(TreeModel):
project_name, representation["parent"]
)
if not version:
not_found["version"].append(group_items)
not_found["version"].extend(group_items)
not_found_ids.append(repre_id)
continue
@ -348,13 +348,13 @@ class InventoryModel(TreeModel):
subset = get_subset_by_id(project_name, version["parent"])
if not subset:
not_found["subset"].append(group_items)
not_found["subset"].extend(group_items)
not_found_ids.append(repre_id)
continue
asset = get_asset_by_id(project_name, subset["parent"])
if not asset:
not_found["asset"].append(group_items)
not_found["asset"].extend(group_items)
not_found_ids.append(repre_id)
continue
@ -380,11 +380,11 @@ class InventoryModel(TreeModel):
self.add_child(group_node, parent=parent)
for _group_items in group_items:
for item in group_items:
item_node = Item()
item_node["Name"] = ", ".join(
[item["objectName"] for item in _group_items]
)
item_node.update(item)
item_node["Name"] = item.get("objectName", "NO NAME")
item_node["isNotFound"] = True
self.add_child(item_node, parent=group_node)
for repre_id, group_dict in sorted(grouped.items()):

View file

@ -80,9 +80,16 @@ class SceneInventoryView(QtWidgets.QTreeView):
self.setStyleSheet("QTreeView {}")
def _build_item_menu_for_selection(self, items, menu):
# Exclude items that are "NOT FOUND" since setting versions, updating
# and removal won't work for those items.
items = [item for item in items if not item.get("isNotFound")]
if not items:
return
# An item might not have a representation, for example when an item
# is listed as "NOT FOUND"
repre_ids = {
item["representation"]
for item in items

View file

@ -30,9 +30,11 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
def displayText(self, value, locale):
if isinstance(value, HeroVersionType):
return lib.format_version(value, True)
assert isinstance(value, numbers.Integral), (
"Version is not integer. \"{}\" {}".format(value, str(type(value)))
)
if not isinstance(value, numbers.Integral):
# For cases where no version is resolved like NOT FOUND cases
# where a representation might not exist in current database
return
return lib.format_version(value)
def paint(self, painter, option, index):

View file

@ -49,9 +49,7 @@ class SplashScreen(QtWidgets.QDialog):
self.init_ui()
def was_proc_successful(self) -> bool:
if self.thread_return_code == 0:
return True
return False
return self.thread_return_code == 0
def start_thread(self, q_thread: QtCore.QThread):
"""Saves the reference to this thread and starts it.
@ -80,8 +78,14 @@ class SplashScreen(QtWidgets.QDialog):
"""
self.thread_return_code = 0
self.q_thread.quit()
if not self.q_thread.wait(5000):
raise RuntimeError("Failed to quit the QThread! "
"The deadline has been reached! The thread "
"has not finished it's execution!.")
self.close()
@QtCore.Slot()
def toggle_log(self):
if self.is_log_visible:
@ -256,3 +260,4 @@ class SplashScreen(QtWidgets.QDialog):
self.close_btn.show()
self.thread_return_code = return_code
self.q_thread.exit(return_code)
self.q_thread.wait()

View file

@ -71,12 +71,30 @@ class TestDeadlinePublishInNuke(NukeDeadlinePublishTestClass):
failures.append(
DBAssert.count_of_types(dbcon, "representation", 4))
additional_args = {"context.subset": "workfileTest_task",
"context.ext": "nk"}
failures.append(
DBAssert.count_of_types(dbcon, "representation", 1,
additional_args=additional_args))
additional_args = {"context.subset": "renderTest_taskMain",
"context.ext": "exr"}
failures.append(
DBAssert.count_of_types(dbcon, "representation", 1,
additional_args=additional_args))
additional_args = {"context.subset": "renderTest_taskMain",
"name": "thumbnail"}
failures.append(
DBAssert.count_of_types(dbcon, "representation", 1,
additional_args=additional_args))
additional_args = {"context.subset": "renderTest_taskMain",
"name": "h264_mov"}
failures.append(
DBAssert.count_of_types(dbcon, "representation", 1,
additional_args=additional_args))
assert not any(failures)

View file

@ -15,7 +15,7 @@ class TestPublishInNuke(NukeLocalPublishTestClass):
!!!
It expects modified path in WriteNode,
use '[python {nuke.script_directory()}]' instead of regular root
dir (eg. instead of `c:/projects/test_project/test_asset/test_task`).
dir (eg. instead of `c:/projects`).
Access file path by selecting WriteNode group, CTRL+Enter, update file
input
!!!
@ -70,12 +70,30 @@ class TestPublishInNuke(NukeLocalPublishTestClass):
failures.append(
DBAssert.count_of_types(dbcon, "representation", 4))
additional_args = {"context.subset": "workfileTest_task",
"context.ext": "nk"}
failures.append(
DBAssert.count_of_types(dbcon, "representation", 1,
additional_args=additional_args))
additional_args = {"context.subset": "renderTest_taskMain",
"context.ext": "exr"}
failures.append(
DBAssert.count_of_types(dbcon, "representation", 1,
additional_args=additional_args))
additional_args = {"context.subset": "renderTest_taskMain",
"name": "thumbnail"}
failures.append(
DBAssert.count_of_types(dbcon, "representation", 1,
additional_args=additional_args))
additional_args = {"context.subset": "renderTest_taskMain",
"name": "h264_mov"}
failures.append(
DBAssert.count_of_types(dbcon, "representation", 1,
additional_args=additional_args))
assert not any(failures)

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View file

@ -38,6 +38,18 @@ This functionality cannot deal with all cases and is not error proof, some inter
openpype_console module kitsu push-to-zou -l me@domain.ext -p my_password
```
## Integrate Kitsu Note
Task status can be automatically set during publish thanks to `Integrate Kitsu Note`. This feature can be configured in:
`Admin -> Studio Settings -> Project Settings -> Kitsu -> Integrate Kitsu Note`.
There are three settings available:
- `Set status on note` -> turns on and off this integrator.
- `Note shortname` -> Which status shortname should be set automatically (Case sensitive).
- `Status conditions` -> Conditions that need to be met for kitsu status to be changed. You can add as many conditions as you like. There are two fields to each conditions: `Condition` (Whether current status should be equal or not equal to the condition status) and `Short name` (Kitsu Shortname of the condition status).
![Integrate Kitsu Note project settings](assets/integrate_kitsu_note_settings.png)
## Q&A
### Is it safe to rename an entity from Kitsu?
Absolutely! Entities are linked by their unique IDs between the two databases.