{}".format(e))
- else:
- self._close_widget()
-
- def save_credentials(self, username, password):
- self.module.get_auth_token(username, password)
-
- def showEvent(self, event):
- super(MusterLogin, self).showEvent(event)
-
- # Make btns same width
- max_width = max(
- self.btn_ok.sizeHint().width(),
- self.btn_cancel.sizeHint().width()
- )
- self.btn_ok.setMinimumWidth(max_width)
- self.btn_cancel.setMinimumWidth(max_width)
-
- def closeEvent(self, event):
- event.ignore()
- self._close_widget()
-
- def _close_widget(self):
- self.hide()
diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py
index 8a92697920..3d6f76ad55 100644
--- a/openpype/modules/sync_server/sync_server_module.py
+++ b/openpype/modules/sync_server/sync_server_module.py
@@ -7,7 +7,6 @@ import copy
import signal
from collections import deque, defaultdict
-import click
from bson.objectid import ObjectId
from openpype.client import (
@@ -15,7 +14,12 @@ from openpype.client import (
get_representations,
get_representation_by_id,
)
-from openpype.modules import OpenPypeModule, ITrayModule, IPluginPaths
+from openpype.modules import (
+ OpenPypeModule,
+ ITrayModule,
+ IPluginPaths,
+ click_wrap,
+)
from openpype.settings import (
get_project_settings,
get_system_settings,
@@ -2405,7 +2409,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule, IPluginPaths):
return presets[project_name]['sites'][site_name]['root']
def cli(self, click_group):
- click_group.add_command(cli_main)
+ click_group.add_command(cli_main.to_click_obj())
# Webserver module implementation
def webserver_initialization(self, server_manager):
@@ -2417,13 +2421,15 @@ class SyncServerModule(OpenPypeModule, ITrayModule, IPluginPaths):
)
-@click.group(SyncServerModule.name, help="SyncServer module related commands.")
+@click_wrap.group(
+ SyncServerModule.name,
+ help="SyncServer module related commands.")
def cli_main():
pass
@cli_main.command()
-@click.option(
+@click_wrap.option(
"-a",
"--active_site",
required=True,
diff --git a/openpype/modules/timers_manager/widget_user_idle.py b/openpype/modules/timers_manager/widget_user_idle.py
index 9df328e6b2..8cc78cf102 100644
--- a/openpype/modules/timers_manager/widget_user_idle.py
+++ b/openpype/modules/timers_manager/widget_user_idle.py
@@ -17,6 +17,7 @@ class WidgetUserIdle(QtWidgets.QWidget):
self.setWindowFlags(
QtCore.Qt.WindowCloseButtonHint
| QtCore.Qt.WindowMinimizeButtonHint
+ | QtCore.Qt.WindowStaysOnTopHint
)
self._is_showed = False
diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py
index 54ff2627e1..975fdd31cc 100644
--- a/openpype/pipeline/farm/pyblish_functions.py
+++ b/openpype/pipeline/farm/pyblish_functions.py
@@ -582,16 +582,17 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data,
group_name = subset
# if there are multiple cameras, we need to add camera name
- if isinstance(col, (list, tuple)):
- cam = [c for c in cameras if c in col[0]]
- else:
- # in case of single frame
- cam = [c for c in cameras if c in col]
- if cam:
- if aov:
- subset_name = '{}_{}_{}'.format(group_name, cam, aov)
- else:
- subset_name = '{}_{}'.format(group_name, cam)
+ expected_filepath = col[0] if isinstance(col, (list, tuple)) else col
+ cams = [cam for cam in cameras if cam in expected_filepath]
+ if cams:
+ for cam in cams:
+ if aov:
+ if not aov.startswith(cam):
+ subset_name = '{}_{}_{}'.format(group_name, cam, aov)
+ else:
+ subset_name = "{}_{}".format(group_name, aov)
+ else:
+ subset_name = '{}_{}'.format(group_name, cam)
else:
if aov:
subset_name = '{}_{}'.format(group_name, aov)
diff --git a/openpype/pipeline/project_folders.py b/openpype/pipeline/project_folders.py
index 1bcba5c320..608344ce03 100644
--- a/openpype/pipeline/project_folders.py
+++ b/openpype/pipeline/project_folders.py
@@ -28,13 +28,20 @@ def concatenate_splitted_paths(split_paths, anatomy):
# backward compatibility
if "__project_root__" in path_items:
for root, root_path in anatomy.roots.items():
- if not os.path.exists(str(root_path)):
- log.debug("Root {} path path {} not exist on \
- computer!".format(root, root_path))
+ if not root_path or not os.path.exists(str(root_path)):
+ log.debug(
+ "Root {} path path {} not exist on computer!".format(
+ root, root_path
+ )
+ )
continue
- clean_items = ["{{root[{}]}}".format(root),
- r"{project[name]}"] + clean_items[1:]
- output.append(os.path.normpath(os.path.sep.join(clean_items)))
+
+ root_items = [
+ "{{root[{}]}}".format(root),
+ "{project[name]}"
+ ]
+ root_items.extend(clean_items[1:])
+ output.append(os.path.normpath(os.path.sep.join(root_items)))
continue
output.append(os.path.normpath(os.path.sep.join(clean_items)))
diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py
index 9dc833061a..3096d22518 100644
--- a/openpype/pipeline/workfile/workfile_template_builder.py
+++ b/openpype/pipeline/workfile/workfile_template_builder.py
@@ -1971,7 +1971,6 @@ class PlaceholderCreateMixin(object):
if not placeholder.data.get("keep_placeholder", True):
self.delete_placeholder(placeholder)
-
def create_failed(self, placeholder, creator_data):
if hasattr(placeholder, "create_failed"):
placeholder.create_failed(creator_data)
@@ -2036,7 +2035,7 @@ class CreatePlaceholderItem(PlaceholderItem):
self._failed_created_publish_instances = []
def get_errors(self):
- if not self._failed_representations:
+ if not self._failed_created_publish_instances:
return []
message = (
"Failed to create {} instance using Creator {}"
diff --git a/openpype/plugins/publish/collect_anatomy_instance_data.py b/openpype/plugins/publish/collect_anatomy_instance_data.py
index 0a34848166..b1b7ecd138 100644
--- a/openpype/plugins/publish/collect_anatomy_instance_data.py
+++ b/openpype/plugins/publish/collect_anatomy_instance_data.py
@@ -200,9 +200,15 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
self._fill_task_data(instance, project_task_types, anatomy_data)
# Define version
+ version_number = None
if self.follow_workfile_version:
version_number = context.data("version")
- else:
+
+ # Even if 'follow_workfile_version' is enabled, it may not be set
+ # because workfile version was not collected to 'context.data'
+ # - that can happen e.g. in 'traypublisher' or other hosts without
+ # a workfile
+ if version_number is None:
version_number = instance.data.get("version")
# use latest version (+1) if already any exist
@@ -404,9 +410,9 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
"""
hierarchy_queue = collections.deque()
- hierarchy_queue.append(hierarchy_context)
+ hierarchy_queue.append(copy.deepcopy(hierarchy_context))
while hierarchy_queue:
- item = hierarchy_context.popleft()
+ item = hierarchy_queue.popleft()
if asset_name in item:
return item[asset_name].get("tasks") or {}
diff --git a/openpype/plugins/publish/collect_farm_target.py b/openpype/plugins/publish/collect_farm_target.py
index adcd842b48..2f77c823d7 100644
--- a/openpype/plugins/publish/collect_farm_target.py
+++ b/openpype/plugins/publish/collect_farm_target.py
@@ -19,7 +19,7 @@ class CollectFarmTarget(pyblish.api.InstancePlugin):
farm_name = ""
op_modules = context.data.get("openPypeModules")
- for farm_renderer in ["deadline", "royalrender", "muster"]:
+ for farm_renderer in ["deadline", "royalrender"]:
op_module = op_modules.get(farm_renderer, False)
if op_module and op_module.enabled:
diff --git a/openpype/plugins/publish/collect_rendered_files.py b/openpype/plugins/publish/collect_rendered_files.py
index 6160b4f5c8..baaf454a11 100644
--- a/openpype/plugins/publish/collect_rendered_files.py
+++ b/openpype/plugins/publish/collect_rendered_files.py
@@ -93,14 +93,6 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin):
assert ctx.get("user") == data.get("user"), ctx_err % "user"
assert ctx.get("version") == data.get("version"), ctx_err % "version"
- # ftrack credentials are passed as environment variables by Deadline
- # to publish job, but Muster doesn't pass them.
- if data.get("ftrack") and not os.environ.get("FTRACK_API_USER"):
- ftrack = data.get("ftrack")
- os.environ["FTRACK_API_USER"] = ftrack["FTRACK_API_USER"]
- os.environ["FTRACK_API_KEY"] = ftrack["FTRACK_API_KEY"]
- os.environ["FTRACK_SERVER"] = ftrack["FTRACK_SERVER"]
-
# now we can just add instances from json file and we are done
any_staging_dir_persistent = False
for instance_data in data.get("instances"):
diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py
index faacb7af2e..922df469fe 100644
--- a/openpype/plugins/publish/extract_color_transcode.py
+++ b/openpype/plugins/publish/extract_color_transcode.py
@@ -189,6 +189,13 @@ class ExtractOIIOTranscode(publish.Extractor):
if len(new_repre["files"]) == 1:
new_repre["files"] = new_repre["files"][0]
+ # If the source representation has "review" tag, but its not
+ # part of the output defintion tags, then both the
+ # representations will be transcoded in ExtractReview and
+ # their outputs will clash in integration.
+ if "review" in repre.get("tags", []):
+ added_review = True
+
new_representations.append(new_repre)
added_representations = True
diff --git a/openpype/plugins/publish/extract_hierarchy_to_ayon.py b/openpype/plugins/publish/extract_hierarchy_to_ayon.py
index b601a3fc29..9e84daca30 100644
--- a/openpype/plugins/publish/extract_hierarchy_to_ayon.py
+++ b/openpype/plugins/publish/extract_hierarchy_to_ayon.py
@@ -30,8 +30,7 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin):
if not AYON_SERVER_ENABLED:
return
- hierarchy_context = context.data.get("hierarchyContext")
- if not hierarchy_context:
+ if not context.data.get("hierarchyContext"):
self.log.debug("Skipping ExtractHierarchyToAYON")
return
diff --git a/openpype/plugins/publish/extract_thumbnail.py b/openpype/plugins/publish/extract_thumbnail.py
index 2b4ea0529a..40e4b23ee0 100644
--- a/openpype/plugins/publish/extract_thumbnail.py
+++ b/openpype/plugins/publish/extract_thumbnail.py
@@ -2,6 +2,7 @@ import copy
import os
import subprocess
import tempfile
+import re
import pyblish.api
from openpype.lib import (
@@ -14,9 +15,10 @@ from openpype.lib import (
path_to_subprocess_arg,
run_subprocess,
)
-from openpype.lib.transcoding import convert_colorspace
-
-from openpype.lib.transcoding import VIDEO_EXTENSIONS
+from openpype.lib.transcoding import (
+ convert_colorspace,
+ VIDEO_EXTENSIONS,
+)
class ExtractThumbnail(pyblish.api.InstancePlugin):
@@ -35,6 +37,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
"traypublisher",
"substancepainter",
"nuke",
+ "aftereffects"
]
enabled = False
@@ -49,6 +52,8 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
# attribute presets from settings
oiiotool_defaults = None
ffmpeg_args = None
+ subsets = []
+ product_names = []
def process(self, instance):
# run main process
@@ -103,6 +108,26 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
self.log.debug("Skipping crypto passes.")
return
+ # We only want to process the subsets needed from settings.
+ def validate_string_against_patterns(input_str, patterns):
+ for pattern in patterns:
+ if re.match(pattern, input_str):
+ return True
+ return False
+
+ product_names = self.subsets + self.product_names
+ if product_names:
+ result = validate_string_against_patterns(
+ instance.data["subset"], product_names
+ )
+ if not result:
+ self.log.debug(
+ "Subset \"{}\" did not match any valid subsets: {}".format(
+ instance.data["subset"], product_names
+ )
+ )
+ return
+
# first check for any explicitly marked representations for thumbnail
explicit_repres = self._get_explicit_repres_for_thumbnail(instance)
if explicit_repres:
@@ -231,7 +256,10 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
"files": jpeg_file,
"stagingDir": dst_staging,
"thumbnail": True,
- "tags": new_repre_tags
+ "tags": new_repre_tags,
+ # If source image is jpg then there can be clash when
+ # integrating to making the output name explicit.
+ "outputName": "thumbnail"
}
# adding representation
@@ -442,7 +470,15 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
# Set video input attributes
max_int = str(2147483647)
video_data = get_ffprobe_data(video_file_path, logger=self.log)
- duration = float(video_data["format"]["duration"])
+ # Use duration of the individual streams since it is returned with
+ # higher decimal precision than 'format.duration'. We need this
+ # more precise value for calculating the correct amount of frames
+ # for higher FPS ranges or decimal ranges, e.g. 29.97 FPS
+ duration = max(
+ float(stream.get("duration", 0))
+ for stream in video_data["streams"]
+ if stream.get("codec_type") == "video"
+ )
cmd_args = [
"-y",
diff --git a/openpype/plugins/publish/extract_thumbnail_from_source.py b/openpype/plugins/publish/extract_thumbnail_from_source.py
index 401a5d615d..33cbf6d9bf 100644
--- a/openpype/plugins/publish/extract_thumbnail_from_source.py
+++ b/openpype/plugins/publish/extract_thumbnail_from_source.py
@@ -65,7 +65,8 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin):
"files": dst_filename,
"stagingDir": dst_staging,
"thumbnail": True,
- "tags": ["thumbnail"]
+ "tags": ["thumbnail"],
+ "outputName": "thumbnail",
}
# adding representation
diff --git a/openpype/plugins/publish/validate_resources.py b/openpype/plugins/publish/validate_resources.py
index 7911c70c2d..ce03515400 100644
--- a/openpype/plugins/publish/validate_resources.py
+++ b/openpype/plugins/publish/validate_resources.py
@@ -17,7 +17,7 @@ class ValidateResources(pyblish.api.InstancePlugin):
"""
order = ValidateContentsOrder
- label = "Resources"
+ label = "Validate Resources"
def process(self, instance):
diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py
index a6d90d1cf0..2c851c054d 100644
--- a/openpype/settings/ayon_settings.py
+++ b/openpype/settings/ayon_settings.py
@@ -220,22 +220,6 @@ def _convert_deadline_system_settings(
output["modules"]["deadline"] = deadline_settings
-def _convert_muster_system_settings(
- ayon_settings, output, addon_versions, default_settings
-):
- enabled = addon_versions.get("muster") is not None
- muster_settings = default_settings["modules"]["muster"]
- muster_settings["enabled"] = enabled
- if enabled:
- ayon_muster = ayon_settings["muster"]
- muster_settings["MUSTER_REST_URL"] = ayon_muster["MUSTER_REST_URL"]
- muster_settings["templates_mapping"] = {
- item["name"]: item["value"]
- for item in ayon_muster["templates_mapping"]
- }
- output["modules"]["muster"] = muster_settings
-
-
def _convert_royalrender_system_settings(
ayon_settings, output, addon_versions, default_settings
):
@@ -261,7 +245,6 @@ def _convert_modules_system(
_convert_timers_manager_system_settings,
_convert_clockify_system_settings,
_convert_deadline_system_settings,
- _convert_muster_system_settings,
_convert_royalrender_system_settings,
):
func(ayon_settings, output, addon_versions, default_settings)
@@ -1236,6 +1219,8 @@ def _convert_global_project_settings(ayon_settings, output, default_settings):
for profile in extract_oiio_transcode_profiles:
new_outputs = {}
name_counter = {}
+ if "product_names" in profile:
+ profile["subsets"] = profile.pop("product_names")
for profile_output in profile["outputs"]:
if "name" in profile_output:
name = profile_output.pop("name")
@@ -1291,12 +1276,6 @@ def _convert_global_project_settings(ayon_settings, output, default_settings):
for extract_burnin_def in extract_burnin_defs
}
- ayon_integrate_hero = ayon_publish["IntegrateHeroVersion"]
- for profile in ayon_integrate_hero["template_name_profiles"]:
- if "product_types" not in profile:
- break
- profile["families"] = profile.pop("product_types")
-
if "IntegrateProductGroup" in ayon_publish:
subset_group = ayon_publish.pop("IntegrateProductGroup")
subset_group_profiles = subset_group.pop("product_grouping_profiles")
@@ -1479,7 +1458,7 @@ class _AyonSettingsCache:
variant = "production"
if is_dev_mode_enabled():
- variant = cls._get_dev_mode_settings_variant()
+ variant = cls._get_bundle_name()
elif is_staging_enabled():
variant = "staging"
@@ -1495,28 +1474,6 @@ class _AyonSettingsCache:
def _get_bundle_name(cls):
return os.environ["AYON_BUNDLE_NAME"]
- @classmethod
- def _get_dev_mode_settings_variant(cls):
- """Develop mode settings variant.
-
- Returns:
- str: Name of settings variant.
- """
-
- con = get_ayon_server_api_connection()
- bundles = con.get_bundles()
- user = con.get_user()
- username = user["name"]
- for bundle in bundles["bundles"]:
- if (
- bundle.get("isDev")
- and bundle.get("activeUser") == username
- ):
- return bundle["name"]
- # Return fake variant - distribution logic will tell user that he
- # does not have set any dev bundle
- return "dev"
-
@classmethod
def get_value_by_project(cls, project_name):
cache_item = _AyonSettingsCache.cache_by_project_name[project_name]
diff --git a/openpype/settings/defaults/project_settings/aftereffects.json b/openpype/settings/defaults/project_settings/aftereffects.json
index 77ccb74410..9e2ab7334b 100644
--- a/openpype/settings/defaults/project_settings/aftereffects.json
+++ b/openpype/settings/defaults/project_settings/aftereffects.json
@@ -15,7 +15,8 @@
"default_variants": [
"Main"
],
- "mark_for_review": true
+ "mark_for_review": true,
+ "force_setting_values": true
}
},
"publish": {
diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json
index 385e97ef91..03a5400ced 100644
--- a/openpype/settings/defaults/project_settings/blender.json
+++ b/openpype/settings/defaults/project_settings/blender.json
@@ -22,7 +22,9 @@
"aov_separator": "underscore",
"image_format": "exr",
"multilayer_exr": true,
- "aov_list": [],
+ "renderer": "CYCLES",
+ "compositing": true,
+ "aov_list": ["combined"],
"custom_passes": []
},
"workfile_builder": {
diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json
index a19464a5c1..0c4b282d10 100644
--- a/openpype/settings/defaults/project_settings/deadline.json
+++ b/openpype/settings/defaults/project_settings/deadline.json
@@ -65,6 +65,8 @@
"group": "",
"department": "",
"use_gpu": true,
+ "workfile_dependency": true,
+ "use_published_workfile": true,
"env_allowed_keys": [],
"env_search_replace_values": {},
"limit_groups": {}
@@ -127,6 +129,7 @@
"deadline_priority": 50,
"publishing_script": "",
"skip_integration_repre_list": [],
+ "families_transfer": ["render3d", "render2d", "ftrack", "slate"],
"aov_filter": {
"maya": [
".*([Bb]eauty).*"
diff --git a/openpype/settings/defaults/project_settings/fusion.json b/openpype/settings/defaults/project_settings/fusion.json
index 8579442625..f890f94b6f 100644
--- a/openpype/settings/defaults/project_settings/fusion.json
+++ b/openpype/settings/defaults/project_settings/fusion.json
@@ -31,7 +31,21 @@
"reviewable",
"farm_rendering"
],
- "image_format": "exr"
+ "image_format": "exr",
+ "default_frame_range_option": "asset_db"
+ },
+ "CreateImageSaver": {
+ "temp_rendering_path_template": "{workdir}/renders/fusion/{subset}/{subset}.{ext}",
+ "default_variants": [
+ "Main",
+ "Mask"
+ ],
+ "instance_attributes": [
+ "reviewable",
+ "farm_rendering"
+ ],
+ "image_format": "exr",
+ "default_frame": 0
}
},
"publish": {
diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json
index bb7e3266bd..782fff1052 100644
--- a/openpype/settings/defaults/project_settings/global.json
+++ b/openpype/settings/defaults/project_settings/global.json
@@ -70,6 +70,7 @@
},
"ExtractThumbnail": {
"enabled": true,
+ "subsets": [],
"integrate_thumbnail": false,
"background_color": [
0,
diff --git a/openpype/settings/defaults/project_settings/hiero.json b/openpype/settings/defaults/project_settings/hiero.json
index 9c83733b09..efd80a8876 100644
--- a/openpype/settings/defaults/project_settings/hiero.json
+++ b/openpype/settings/defaults/project_settings/hiero.json
@@ -69,6 +69,10 @@
"tags_addition": [
"review"
]
+ },
+ "CollectClipEffects": {
+ "enabled": true,
+ "effect_categories": {}
}
},
"filters": {},
diff --git a/openpype/settings/defaults/project_settings/max.json b/openpype/settings/defaults/project_settings/max.json
index d1610610dc..a0a4fcf83d 100644
--- a/openpype/settings/defaults/project_settings/max.json
+++ b/openpype/settings/defaults/project_settings/max.json
@@ -56,6 +56,16 @@
"enabled": false,
"attributes": {}
},
+ "ValidateCameraAttributes": {
+ "enabled": true,
+ "optional": true,
+ "active": false,
+ "fov": 45.0,
+ "nearrange": 0.0,
+ "farrange": 1000.0,
+ "nearclip": 1.0,
+ "farclip": 1000.0
+ },
"ValidateLoadedPlugin": {
"enabled": false,
"optional": true,
diff --git a/openpype/settings/defaults/system_settings/modules.json b/openpype/settings/defaults/system_settings/modules.json
index bb943524f1..22daae3b34 100644
--- a/openpype/settings/defaults/system_settings/modules.json
+++ b/openpype/settings/defaults/system_settings/modules.json
@@ -164,23 +164,6 @@
"default": "http://127.0.0.1:8082"
}
},
- "muster": {
- "enabled": false,
- "MUSTER_REST_URL": "http://127.0.0.1:9890",
- "templates_mapping": {
- "file_layers": 7,
- "mentalray": 2,
- "mentalray_sf": 6,
- "redshift": 55,
- "renderman": 29,
- "software": 1,
- "software_sf": 5,
- "turtle": 10,
- "vector": 4,
- "vray": 37,
- "ffmpeg": 48
- }
- },
"royalrender": {
"enabled": false,
"rr_paths": {
diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md
index c333628b25..eb74dd7a9c 100644
--- a/openpype/settings/entities/schemas/README.md
+++ b/openpype/settings/entities/schemas/README.md
@@ -645,7 +645,7 @@ How output of the schema could look like on save:
},
"is_group": true,
"key": "templates_mapping",
- "label": "Muster - Templates mapping",
+ "label": "Deadline - Templates mapping",
"is_file": true
}
```
@@ -657,7 +657,7 @@ How output of the schema could look like on save:
"object_type": "text",
"is_group": true,
"key": "templates_mapping",
- "label": "Muster - Templates mapping",
+ "label": "Deadline - Templates mapping",
"is_file": true
}
```
diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json
index 72f09a641d..b0f8a7357f 100644
--- a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json
+++ b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json
@@ -42,6 +42,12 @@
"key": "mark_for_review",
"label": "Review",
"default": true
+ },
+ {
+ "type": "boolean",
+ "key": "force_setting_values",
+ "label": "Force resolution and duration values from Asset",
+ "default": true
}
]
}
diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json
index 535d9434a3..2ffdc6070d 100644
--- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json
+++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json
@@ -103,6 +103,22 @@
"type": "label",
"label": "Note: Multilayer EXR is only used when output format type set to EXR."
},
+ {
+ "key": "renderer",
+ "label": "Renderer",
+ "type": "enum",
+ "multiselection": false,
+ "defaults": "CYCLES",
+ "enum_items": [
+ {"CYCLES": "Cycles"},
+ {"BLENDER_EEVEE": "Eevee"}
+ ]
+ },
+ {
+ "key": "compositing",
+ "type": "boolean",
+ "label": "Enable Compositing"
+ },
{
"key": "aov_list",
"label": "AOVs to create",
@@ -110,23 +126,38 @@
"multiselection": true,
"defaults": "empty",
"enum_items": [
- {"empty": "< empty >"},
{"combined": "Combined"},
{"z": "Z"},
{"mist": "Mist"},
{"normal": "Normal"},
- {"diffuse_light": "Diffuse Light"},
+ {"position": "Position (Cycles Only)"},
+ {"vector": "Vector (Cycles Only)"},
+ {"uv": "UV (Cycles Only)"},
+ {"denoising": "Denoising Data (Cycles Only)"},
+ {"object_index": "Object Index (Cycles Only)"},
+ {"material_index": "Material Index (Cycles Only)"},
+ {"sample_count": "Sample Count (Cycles Only)"},
+ {"diffuse_light": "Diffuse Light/Direct"},
+ {"diffuse_indirect": "Diffuse Indirect (Cycles Only)"},
{"diffuse_color": "Diffuse Color"},
- {"specular_light": "Specular Light"},
- {"specular_color": "Specular Color"},
- {"volume_light": "Volume Light"},
+ {"specular_light": "Specular (Glossy) Light/Direct"},
+ {"specular_indirect": "Specular (Glossy) Indirect (Cycles Only)"},
+ {"specular_color": "Specular (Glossy) Color"},
+ {"transmission_light": "Transmission Light/Direct (Cycles Only)"},
+ {"transmission_indirect": "Transmission Indirect (Cycles Only)"},
+ {"transmission_color": "Transmission Color (Cycles Only)"},
+ {"volume_light": "Volume Light/Direct"},
+ {"volume_indirect": "Volume Indirect (Cycles Only)"},
{"emission": "Emission"},
{"environment": "Environment"},
- {"shadow": "Shadow"},
+ {"shadow": "Shadow/Shadow Catcher"},
{"ao": "Ambient Occlusion"},
- {"denoising": "Denoising"},
- {"volume_direct": "Direct Volumetric Scattering"},
- {"volume_indirect": "Indirect Volumetric Scattering"}
+ {"bloom": "Bloom (Eevee Only)"},
+ {"transparent": "Transparent (Eevee Only)"},
+ {"cryptomatte_object": "Cryptomatte Object"},
+ {"cryptomatte_material": "Cryptomatte Material"},
+ {"cryptomatte_asset": "Cryptomatte Asset"},
+ {"cryptomatte_accurate": "Cryptomatte Accurate Mode (Eevee Only)"}
]
},
{
diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json
index 1aea778e32..bb8e0b5cd4 100644
--- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json
+++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json
@@ -362,6 +362,16 @@
"key": "use_gpu",
"label": "Use GPU"
},
+ {
+ "type": "boolean",
+ "key": "workfile_dependency",
+ "label": "Workfile Dependency"
+ },
+ {
+ "type": "boolean",
+ "key": "use_published_workfile",
+ "label": "Use Published Workfile"
+ },
{
"type": "list",
"key": "env_allowed_keys",
@@ -683,6 +693,14 @@
"type": "text"
}
},
+ {
+ "type": "list",
+ "key": "families_transfer",
+ "label": "List of family names to transfer\nto generated instances (AOVs for example).",
+ "object_type": {
+ "type": "text"
+ }
+ },
{
"type": "dict-modifiable",
"docstring": "Regular expression to filter for which subset review should be created in publish job.",
diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json
index fbd856b895..84d1efae78 100644
--- a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json
+++ b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json
@@ -74,7 +74,7 @@
"type": "dict",
"collapsible": true,
"key": "CreateSaver",
- "label": "Create Saver",
+ "label": "Create Render Saver",
"is_group": true,
"children": [
{
@@ -116,6 +116,71 @@
{"tif": "tif"},
{"jpg": "jpg"}
]
+ },
+ {
+ "key": "default_frame_range_option",
+ "label": "Default frame range source",
+ "type": "enum",
+ "multiselect": false,
+ "enum_items": [
+ {"asset_db": "Current asset context"},
+ {"render_range": "From render in/out"},
+ {"comp_range": "From composition timeline"}
+ ]
+ }
+ ]
+ },
+ {
+ "type": "dict",
+ "collapsible": true,
+ "key": "CreateImageSaver",
+ "label": "Create Image Saver",
+ "is_group": true,
+ "children": [
+ {
+ "type": "text",
+ "key": "temp_rendering_path_template",
+ "label": "Temporary rendering path template"
+ },
+ {
+ "type": "list",
+ "key": "default_variants",
+ "label": "Default variants",
+ "object_type": {
+ "type": "text"
+ }
+ },
+ {
+ "key": "instance_attributes",
+ "label": "Instance attributes",
+ "type": "enum",
+ "multiselection": true,
+ "enum_items": [
+ {
+ "reviewable": "Reviewable"
+ },
+ {
+ "farm_rendering": "Farm rendering"
+ }
+ ]
+ },
+ {
+ "key": "image_format",
+ "label": "Output Image Format",
+ "type": "enum",
+ "multiselect": false,
+ "enum_items": [
+ {"exr": "exr"},
+ {"tga": "tga"},
+ {"png": "png"},
+ {"tif": "tif"},
+ {"jpg": "jpg"}
+ ]
+ },
+ {
+ "type": "number",
+ "key": "default_frame",
+ "label": "Default rendered frame"
}
]
}
diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json b/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json
index d80edf902b..73bd475815 100644
--- a/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json
+++ b/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json
@@ -312,6 +312,31 @@
"label": "Tags addition"
}
]
+ },
+ {
+ "type": "dict",
+ "collapsible": true,
+ "checkbox_key": "enabled",
+ "key": "CollectClipEffects",
+ "label": "Collect Clip Effects",
+ "is_group": true,
+ "children": [
+ {
+ "type": "boolean",
+ "key": "enabled",
+ "label": "Enabled"
+ },
+ {
+ "type": "dict-modifiable",
+ "key": "effect_categories",
+ "label": "Effect Categories",
+ "object_type": {
+ "type": "list",
+ "key": "effects_classes",
+ "object_type": "text"
+ }
+ }
+ ]
}
]
},
diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json
index 64f292a140..226a190dd4 100644
--- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json
+++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json
@@ -202,6 +202,12 @@
"key": "enabled",
"label": "Enabled"
},
+ {
+ "type": "list",
+ "object_type": "text",
+ "key": "subsets",
+ "label": "Subsets"
+ },
{
"type": "boolean",
"key": "integrate_thumbnail",
diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json
index b4d85bda98..1e7a7c0c73 100644
--- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json
+++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json
@@ -48,6 +48,76 @@
}
]
},
+ {
+ "type": "dict",
+ "collapsible": true,
+ "checkbox_key": "enabled",
+ "key": "ValidateCameraAttributes",
+ "label": "Validate Camera Attributes",
+ "is_group": true,
+ "children": [
+ {
+ "type": "boolean",
+ "key": "enabled",
+ "label": "Enabled"
+ },
+ {
+ "type": "boolean",
+ "key": "optional",
+ "label": "Optional"
+ },
+ {
+ "type": "boolean",
+ "key": "active",
+ "label": "Active"
+ },
+ {
+ "type": "number",
+ "key": "fov",
+ "label": "Focal Length",
+ "decimal": 1,
+ "minimum": 0,
+ "maximum": 100.0
+ },
+ {
+ "type": "label",
+ "label": "If the value of the camera attributes set to 0, the system automatically skips checking it"
+ },
+ {
+ "type": "number",
+ "key": "nearrange",
+ "label": "Near Range",
+ "decimal": 1,
+ "minimum": 0,
+ "maximum": 100.0
+ },
+ {
+ "type": "number",
+ "key": "farrange",
+ "label": "Far Range",
+ "decimal": 1,
+ "minimum": 0,
+ "maximum": 2000.0
+ },
+ {
+ "type": "number",
+ "key": "nearclip",
+ "label": "Near Clip",
+ "decimal": 1,
+ "minimum": 0,
+ "maximum": 100.0
+ },
+ {
+ "type": "number",
+ "key": "farclip",
+ "label": "Far Clip",
+ "decimal": 1,
+ "minimum": 0,
+ "maximum": 2000.0
+ }
+ ]
+ },
+
{
"type": "dict",
"collapsible": true,
diff --git a/openpype/settings/entities/schemas/system_schema/schema_modules.json b/openpype/settings/entities/schemas/system_schema/schema_modules.json
index 5b189eae88..88ef6f7515 100644
--- a/openpype/settings/entities/schemas/system_schema/schema_modules.json
+++ b/openpype/settings/entities/schemas/system_schema/schema_modules.json
@@ -207,37 +207,6 @@
}
]
},
- {
- "type": "dict",
- "key": "muster",
- "label": "Muster",
- "require_restart": true,
- "collapsible": true,
- "checkbox_key": "enabled",
- "children": [
- {
- "type": "boolean",
- "key": "enabled",
- "label": "Enabled"
- },
- {
- "type": "text",
- "key": "MUSTER_REST_URL",
- "label": "Muster Rest URL"
- },
- {
- "type": "dict-modifiable",
- "object_type": {
- "type": "number",
- "minimum": 0,
- "maximum": 300
- },
- "is_group": true,
- "key": "templates_mapping",
- "label": "Templates mapping"
- }
- ]
- },
{
"type": "dict",
"key": "royalrender",
diff --git a/openpype/tools/ayon_loader/abstract.py b/openpype/tools/ayon_loader/abstract.py
index bf3e81d485..1d93716e07 100644
--- a/openpype/tools/ayon_loader/abstract.py
+++ b/openpype/tools/ayon_loader/abstract.py
@@ -531,6 +531,9 @@ class FrontendLoaderController(_BaseLoaderController):
Product types have defined if are checked for filtering or not.
+ Args:
+ project_name (Union[str, None]): Project name.
+
Returns:
list[ProductTypeItem]: List of product type items for a project.
"""
diff --git a/openpype/tools/ayon_loader/models/products.py b/openpype/tools/ayon_loader/models/products.py
index 135f28df97..40b6474d12 100644
--- a/openpype/tools/ayon_loader/models/products.py
+++ b/openpype/tools/ayon_loader/models/products.py
@@ -179,12 +179,15 @@ class ProductsModel:
"""Product type items for project.
Args:
- project_name (str): Project name.
+ project_name (Union[str, None]): Project name.
Returns:
list[ProductTypeItem]: Product type items.
"""
+ if not project_name:
+ return []
+
cache = self._product_type_items_cache[project_name]
if not cache.is_valid:
product_types = ayon_api.get_project_product_types(project_name)
diff --git a/openpype/tools/ayon_loader/ui/window.py b/openpype/tools/ayon_loader/ui/window.py
index a6d40d52e7..8982d92c0f 100644
--- a/openpype/tools/ayon_loader/ui/window.py
+++ b/openpype/tools/ayon_loader/ui/window.py
@@ -322,6 +322,7 @@ class LoaderWindow(QtWidgets.QWidget):
)
def refresh(self):
+ self._reset_on_show = False
self._controller.reset()
def showEvent(self, event):
@@ -332,6 +333,13 @@ class LoaderWindow(QtWidgets.QWidget):
self._show_timer.start()
+ def closeEvent(self, event):
+ super(LoaderWindow, self).closeEvent(event)
+ # Deselect project so current context will be selected
+ # on next 'showEvent'
+ self._controller.set_selected_project(None)
+ self._reset_on_show = True
+
def keyPressEvent(self, event):
modifiers = event.modifiers()
ctrl_pressed = QtCore.Qt.ControlModifier & modifiers
@@ -378,8 +386,7 @@ class LoaderWindow(QtWidgets.QWidget):
self._show_timer.stop()
if self._reset_on_show:
- self._reset_on_show = False
- self._controller.reset()
+ self.refresh()
def _show_group_dialog(self):
project_name = self._projects_combobox.get_selected_project_name()
diff --git a/openpype/tools/ayon_sceneinventory/control.py b/openpype/tools/ayon_sceneinventory/control.py
index 6111d7e43b..3b063ff72e 100644
--- a/openpype/tools/ayon_sceneinventory/control.py
+++ b/openpype/tools/ayon_sceneinventory/control.py
@@ -84,9 +84,9 @@ class SceneInventoryController:
def get_containers(self):
host = self._host
if isinstance(host, ILoadHost):
- return host.get_containers()
+ return list(host.get_containers())
elif hasattr(host, "ls"):
- return host.ls()
+ return list(host.ls())
return []
# Site Sync methods
diff --git a/openpype/tools/ayon_sceneinventory/model.py b/openpype/tools/ayon_sceneinventory/model.py
index 16924b0a7e..f4450f0ac3 100644
--- a/openpype/tools/ayon_sceneinventory/model.py
+++ b/openpype/tools/ayon_sceneinventory/model.py
@@ -23,6 +23,7 @@ from openpype.pipeline import (
)
from openpype.style import get_default_entity_icon_color
from openpype.tools.utils.models import TreeModel, Item
+from openpype.tools.ayon_utils.widgets import get_qt_icon
def walk_hierarchy(node):
@@ -71,8 +72,8 @@ class InventoryModel(TreeModel):
site_icons = self._controller.get_site_provider_icons()
self._site_icons = {
- provider: QtGui.QIcon(icon_path)
- for provider, icon_path in site_icons.items()
+ provider: get_qt_icon(icon_def)
+ for provider, icon_def in site_icons.items()
}
def outdated(self, item):
diff --git a/openpype/tools/ayon_sceneinventory/models/site_sync.py b/openpype/tools/ayon_sceneinventory/models/site_sync.py
index 0101f6c88e..bd65ad1778 100644
--- a/openpype/tools/ayon_sceneinventory/models/site_sync.py
+++ b/openpype/tools/ayon_sceneinventory/models/site_sync.py
@@ -150,23 +150,23 @@ class SiteSyncModel:
return self._remote_site_provider
def _cache_sites(self):
- site_sync = self._get_sync_server_module()
active_site = None
remote_site = None
active_site_provider = None
remote_site_provider = None
- if site_sync is not None:
+ if self.is_sync_server_enabled():
+ site_sync = self._get_sync_server_module()
project_name = self._controller.get_current_project_name()
active_site = site_sync.get_active_site(project_name)
remote_site = site_sync.get_remote_site(project_name)
active_site_provider = "studio"
remote_site_provider = "studio"
if active_site != "studio":
- active_site_provider = site_sync.get_active_provider(
+ active_site_provider = site_sync.get_provider_for_site(
project_name, active_site
)
if remote_site != "studio":
- remote_site_provider = site_sync.get_active_provider(
+ remote_site_provider = site_sync.get_provider_for_site(
project_name, remote_site
)
diff --git a/openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py b/openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py
index 2ebed7f89b..1d1bd1adbc 100644
--- a/openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py
+++ b/openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py
@@ -1212,12 +1212,12 @@ class SwitchAssetDialog(QtWidgets.QDialog):
))
version_ids = set()
- version_docs_by_parent_id = {}
+ version_docs_by_parent_id_and_name = collections.defaultdict(dict)
for version_doc in version_docs:
- parent_id = version_doc["parent"]
- if parent_id not in version_docs_by_parent_id:
- version_ids.add(version_doc["_id"])
- version_docs_by_parent_id[parent_id] = version_doc
+ version_ids.add(version_doc["_id"])
+ product_id = version_doc["parent"]
+ name = version_doc["name"]
+ version_docs_by_parent_id_and_name[product_id][name] = version_doc
hero_version_docs_by_parent_id = {}
for hero_version_doc in hero_version_docs:
@@ -1242,7 +1242,7 @@ class SwitchAssetDialog(QtWidgets.QDialog):
selected_product_name,
selected_representation,
product_docs_by_parent_and_name,
- version_docs_by_parent_id,
+ version_docs_by_parent_id_and_name,
hero_version_docs_by_parent_id,
repre_docs_by_parent_id_by_name,
)
@@ -1256,10 +1256,10 @@ class SwitchAssetDialog(QtWidgets.QDialog):
container,
loader,
selected_folder_id,
- product_name,
+ selected_product_name,
selected_representation,
product_docs_by_parent_and_name,
- version_docs_by_parent_id,
+ version_docs_by_parent_id_and_name,
hero_version_docs_by_parent_id,
repre_docs_by_parent_id_by_name,
):
@@ -1272,15 +1272,18 @@ class SwitchAssetDialog(QtWidgets.QDialog):
container_product_id = container_version["parent"]
container_product = self._product_docs_by_id[container_product_id]
+ container_product_name = container_product["name"]
+
+ container_folder_id = container_product["parent"]
if selected_folder_id:
folder_id = selected_folder_id
else:
- folder_id = container_product["parent"]
+ folder_id = container_folder_id
products_by_name = product_docs_by_parent_and_name[folder_id]
- if product_name:
- product_doc = products_by_name[product_name]
+ if selected_product_name:
+ product_doc = products_by_name[selected_product_name]
else:
product_doc = products_by_name[container_product["name"]]
@@ -1300,7 +1303,26 @@ class SwitchAssetDialog(QtWidgets.QDialog):
repre_doc = _repres.get(container_repre_name)
if not repre_doc:
- version_doc = version_docs_by_parent_id[product_id]
+ version_docs_by_name = (
+ version_docs_by_parent_id_and_name[product_id]
+ )
+ # If asset or subset are selected for switching, we use latest
+ # version else we try to keep the current container version.
+ version_name = None
+ if (
+ selected_folder_id in (None, container_folder_id)
+ and selected_product_name in (None, container_product_name)
+ ):
+ version_name = container_version.get("name")
+
+ version_doc = None
+ if version_name is not None:
+ version_doc = version_docs_by_name.get(version_name)
+
+ if version_doc is None:
+ version_name = max(version_docs_by_name)
+ version_doc = version_docs_by_name[version_name]
+
version_id = version_doc["_id"]
repres_by_name = repre_docs_by_parent_id_by_name[version_id]
if selected_representation:
diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py
index b02d83e4f6..f9b8bcc512 100644
--- a/openpype/tools/publisher/control.py
+++ b/openpype/tools/publisher/control.py
@@ -10,6 +10,7 @@ import inspect
from abc import ABCMeta, abstractmethod
import six
+import arrow
import pyblish.api
from openpype import AYON_SERVER_ENABLED
@@ -285,6 +286,8 @@ class PublishReportMaker:
def get_report(self, publish_plugins=None):
"""Report data with all details of current state."""
+
+ now = arrow.utcnow().to("local")
instances_details = {}
for instance in self._all_instances_by_id.values():
instances_details[instance.id] = self._extract_instance_data(
@@ -334,7 +337,8 @@ class PublishReportMaker:
"context": self._extract_context_data(self._current_context),
"crashed_file_paths": crashed_file_paths,
"id": uuid.uuid4().hex,
- "report_version": "1.0.0"
+ "created_at": now.isoformat(),
+ "report_version": "1.0.1",
}
def _extract_context_data(self, context):
@@ -2325,8 +2329,29 @@ class PublisherController(BasePublisherController):
result = pyblish.plugin.process(
plugin, self._publish_context, None, action.id
)
+ exception = result.get("error")
+ if exception:
+ self._emit_event(
+ "publish.action.failed",
+ {
+ "title": "Action failed",
+ "message": "Action failed.",
+ "traceback": "".join(
+ traceback.format_exception(
+ type(exception),
+ exception,
+ exception.__traceback__
+ )
+ ),
+ "label": action.__name__,
+ "identifier": action.id
+ }
+ )
+
self._publish_report.add_action_result(action, result)
+ self.emit_card_message("Action finished.")
+
def _publish_next_process(self):
# Validations of progress before using iterator
# - same conditions may be inside iterator but they may be used
diff --git a/openpype/tools/publisher/publish_report_viewer/model.py b/openpype/tools/publisher/publish_report_viewer/model.py
index 663a67ac70..460a269f1a 100644
--- a/openpype/tools/publisher/publish_report_viewer/model.py
+++ b/openpype/tools/publisher/publish_report_viewer/model.py
@@ -26,14 +26,14 @@ class InstancesModel(QtGui.QStandardItemModel):
return self._items_by_id
def set_report(self, report_item):
- self.clear()
+ root_item = self.invisibleRootItem()
+ if root_item.rowCount() > 0:
+ root_item.removeRows(0, root_item.rowCount())
self._items_by_id.clear()
self._plugin_items_by_id.clear()
if not report_item:
return
- root_item = self.invisibleRootItem()
-
families = set(report_item.instance_items_by_family.keys())
families.remove(None)
all_families = list(sorted(families))
@@ -125,14 +125,14 @@ class PluginsModel(QtGui.QStandardItemModel):
return self._items_by_id
def set_report(self, report_item):
- self.clear()
+ root_item = self.invisibleRootItem()
+ if root_item.rowCount() > 0:
+ root_item.removeRows(0, root_item.rowCount())
self._items_by_id.clear()
self._plugin_items_by_id.clear()
if not report_item:
return
- root_item = self.invisibleRootItem()
-
labels_iter = iter(self.order_label_mapping)
cur_order, cur_label = next(labels_iter)
cur_plugin_items = []
diff --git a/openpype/tools/publisher/publish_report_viewer/window.py b/openpype/tools/publisher/publish_report_viewer/window.py
index dc4ad70934..f9c8c05802 100644
--- a/openpype/tools/publisher/publish_report_viewer/window.py
+++ b/openpype/tools/publisher/publish_report_viewer/window.py
@@ -4,6 +4,7 @@ import six
import uuid
import appdirs
+import arrow
from qtpy import QtWidgets, QtCore, QtGui
from openpype import style
@@ -25,6 +26,7 @@ else:
ITEM_ID_ROLE = QtCore.Qt.UserRole + 1
+ITEM_CREATED_AT_ROLE = QtCore.Qt.UserRole + 2
def get_reports_dir():
@@ -47,47 +49,77 @@ class PublishReportItem:
"""Report item representing one file in report directory."""
def __init__(self, content):
- item_id = content.get("id")
- changed = False
- if not item_id:
- item_id = str(uuid.uuid4())
- changed = True
- content["id"] = item_id
+ changed = self._fix_content(content)
- if not content.get("report_version"):
- changed = True
- content["report_version"] = "0.0.1"
-
- report_path = os.path.join(get_reports_dir(), item_id)
+ report_path = os.path.join(get_reports_dir(), content["id"])
file_modified = None
if os.path.exists(report_path):
file_modified = os.path.getmtime(report_path)
+
+ created_at_obj = arrow.get(content["created_at"]).to("local")
+ created_at = created_at_obj.float_timestamp
+
self.content = content
self.report_path = report_path
self.file_modified = file_modified
+ self.created_at = float(created_at)
self._loaded_label = content.get("label")
self._changed = changed
self.publish_report = PublishReport(content)
@property
def version(self):
+ """Publish report version.
+
+ Returns:
+ str: Publish report version.
+ """
return self.content["report_version"]
@property
def id(self):
+ """Publish report id.
+
+ Returns:
+ str: Publish report id.
+ """
+
return self.content["id"]
def get_label(self):
+ """Publish report label.
+
+ Returns:
+ str: Publish report label showed in UI.
+ """
+
return self.content.get("label") or "Unfilled label"
def set_label(self, label):
+ """Set publish report label.
+
+ Args:
+ label (str): New publish report label.
+ """
+
if not label:
self.content.pop("label", None)
self.content["label"] = label
label = property(get_label, set_label)
+ @property
+ def loaded_label(self):
+ return self._loaded_label
+
+ def mark_as_changed(self):
+ """Mark report as changed."""
+
+ self._changed = True
+
def save(self):
+ """Save publish report to file."""
+
save = False
if (
self._changed
@@ -109,6 +141,15 @@ class PublishReportItem:
@classmethod
def from_filepath(cls, filepath):
+ """Create report item from file.
+
+ Args:
+ filepath (str): Path to report file. Content must be json.
+
+ Returns:
+ PublishReportItem: Report item.
+ """
+
if not os.path.exists(filepath):
return None
@@ -116,15 +157,25 @@ class PublishReportItem:
with open(filepath, "r") as stream:
content = json.load(stream)
- return cls(content)
+ file_modified = os.path.getmtime(filepath)
+ changed = cls._fix_content(content, file_modified=file_modified)
+ obj = cls(content)
+ if changed:
+ obj.mark_as_changed()
+ return obj
+
except Exception:
return None
def remove_file(self):
+ """Remove report file."""
+
if os.path.exists(self.report_path):
os.remove(self.report_path)
def update_file_content(self):
+ """Update report content in file."""
+
if not os.path.exists(self.report_path):
return
@@ -148,9 +199,57 @@ class PublishReportItem:
self.content = content
self.file_modified = file_modified
+ @classmethod
+ def _fix_content(cls, content, file_modified=None):
+ """Fix content for backward compatibility of older report items.
+
+ Args:
+ content (dict[str, Any]): Report content.
+ file_modified (Optional[float]): File modification time.
+
+ Returns:
+ bool: True if content was changed, False otherwise.
+ """
+
+ # Fix created_at key
+ changed = cls._fix_created_at(content, file_modified)
+
+ # NOTE backward compatibility for 'id' and 'report_version' is from
+ # 28.10.2022 https://github.com/ynput/OpenPype/pull/4040
+ # We can probably safely remove it
+
+ # Fix missing 'id'
+ item_id = content.get("id")
+ if not item_id:
+ item_id = str(uuid.uuid4())
+ changed = True
+ content["id"] = item_id
+
+ # Fix missing 'report_version'
+ if not content.get("report_version"):
+ changed = True
+ content["report_version"] = "0.0.1"
+ return changed
+
+ @classmethod
+ def _fix_created_at(cls, content, file_modified):
+ # Key 'create_at' was added in report version 1.0.1
+ created_at = content.get("created_at")
+ if created_at:
+ return False
+
+ # Auto fix 'created_at', use file modification time if it is not set
+ # or current time if modification could not be received.
+ if file_modified is not None:
+ created_at_obj = arrow.Arrow.fromtimestamp(file_modified)
+ else:
+ created_at_obj = arrow.utcnow()
+ content["created_at"] = created_at_obj.to("local").isoformat()
+ return True
+
class PublisherReportHandler:
- """Class handling storing publish report tool."""
+ """Class handling storing publish report items."""
def __init__(self):
self._reports = None
@@ -173,14 +272,23 @@ class PublisherReportHandler:
continue
filepath = os.path.join(report_dir, filename)
item = PublishReportItem.from_filepath(filepath)
- reports.append(item)
- reports_by_id[item.id] = item
+ if item is not None:
+ reports.append(item)
+ reports_by_id[item.id] = item
self._reports = reports
self._reports_by_id = reports_by_id
return reports
- def remove_report_items(self, item_id):
+ def remove_report_item(self, item_id):
+ """Remove report item by id.
+
+ Remove from cache and also remove the file with the content.
+
+ Args:
+ item_id (str): Report item id.
+ """
+
item = self._reports_by_id.get(item_id)
if item:
try:
@@ -191,9 +299,16 @@ class PublisherReportHandler:
class LoadedFilesModel(QtGui.QStandardItemModel):
+ header_labels = ("Reports", "Created")
+
def __init__(self, *args, **kwargs):
super(LoadedFilesModel, self).__init__(*args, **kwargs)
+ # Column count must be set before setting header data
+ self.setColumnCount(len(self.header_labels))
+ for col, label in enumerate(self.header_labels):
+ self.setHeaderData(col, QtCore.Qt.Horizontal, label)
+
self._items_by_id = {}
self._report_items_by_id = {}
@@ -202,10 +317,14 @@ class LoadedFilesModel(QtGui.QStandardItemModel):
self._loading_registry = False
def refresh(self):
- self._handler.reset()
+ root_item = self.invisibleRootItem()
+ if root_item.rowCount() > 0:
+ root_item.removeRows(0, root_item.rowCount())
self._items_by_id = {}
self._report_items_by_id = {}
+ self._handler.reset()
+
new_items = []
for report_item in self._handler.list_reports():
item = self._create_item(report_item)
@@ -217,26 +336,26 @@ class LoadedFilesModel(QtGui.QStandardItemModel):
root_item = self.invisibleRootItem()
root_item.appendRows(new_items)
- def headerData(self, section, orientation, role):
- if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
- if section == 0:
- return "Exports"
- if section == 1:
- return "Modified"
- return ""
- super(LoadedFilesModel, self).headerData(section, orientation, role)
-
def data(self, index, role=None):
if role is None:
role = QtCore.Qt.DisplayRole
col = index.column()
+ if col == 1:
+ if role in (
+ QtCore.Qt.DisplayRole, QtCore.Qt.InitialSortOrderRole
+ ):
+ role = ITEM_CREATED_AT_ROLE
+
if col != 0:
index = self.index(index.row(), 0, index.parent())
return super(LoadedFilesModel, self).data(index, role)
- def setData(self, index, value, role):
+ def setData(self, index, value, role=None):
+ if role is None:
+ role = QtCore.Qt.EditRole
+
if role == QtCore.Qt.EditRole:
item_id = index.data(ITEM_ID_ROLE)
report_item = self._report_items_by_id.get(item_id)
@@ -247,6 +366,12 @@ class LoadedFilesModel(QtGui.QStandardItemModel):
return super(LoadedFilesModel, self).setData(index, value, role)
+ def flags(self, index):
+ # Allow editable flag only for first column
+ if index.column() > 0:
+ return QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled
+ return super(LoadedFilesModel, self).flags(index)
+
def _create_item(self, report_item):
if report_item.id in self._items_by_id:
return None
@@ -254,6 +379,7 @@ class LoadedFilesModel(QtGui.QStandardItemModel):
item = QtGui.QStandardItem(report_item.label)
item.setColumnCount(self.columnCount())
item.setData(report_item.id, ITEM_ID_ROLE)
+ item.setData(report_item.created_at, ITEM_CREATED_AT_ROLE)
return item
@@ -278,16 +404,16 @@ class LoadedFilesModel(QtGui.QStandardItemModel):
new_items = []
for normalized_path in filtered_paths:
- try:
- with open(normalized_path, "r") as stream:
- data = json.load(stream)
- report_item = PublishReportItem(data)
- except Exception:
- # TODO handle errors
+ report_item = PublishReportItem.from_filepath(normalized_path)
+ if report_item is None:
continue
- label = data.get("label")
- if not label:
+ # Skip already added report items
+ # QUESTION: Should we replace existing or skip the item?
+ if report_item.id in self._items_by_id:
+ continue
+
+ if not report_item.loaded_label:
report_item.label = (
os.path.splitext(os.path.basename(filepath))[0]
)
@@ -306,15 +432,13 @@ class LoadedFilesModel(QtGui.QStandardItemModel):
root_item.appendRows(new_items)
def remove_item_by_id(self, item_id):
- report_item = self._report_items_by_id.get(item_id)
- if not report_item:
- return
+ self._handler.remove_report_item(item_id)
- self._handler.remove_report_items(item_id)
- item = self._items_by_id.get(item_id)
-
- parent = self.invisibleRootItem()
- parent.removeRow(item.row())
+ self._report_items_by_id.pop(item_id, None)
+ item = self._items_by_id.pop(item_id, None)
+ if item is not None:
+ parent = self.invisibleRootItem()
+ parent.removeRow(item.row())
def get_report_by_id(self, item_id):
report_item = self._report_items_by_id.get(item_id)
@@ -335,13 +459,18 @@ class LoadedFilesView(QtWidgets.QTreeView):
)
self.setIndentation(0)
self.setAlternatingRowColors(True)
+ self.setSortingEnabled(True)
model = LoadedFilesModel()
- self.setModel(model)
+ proxy_model = QtCore.QSortFilterProxyModel()
+ proxy_model.setSourceModel(model)
+ self.setModel(proxy_model)
time_delegate = PrettyTimeDelegate()
self.setItemDelegateForColumn(1, time_delegate)
+ self.sortByColumn(1, QtCore.Qt.AscendingOrder)
+
remove_btn = IconButton(self)
remove_icon_path = resources.get_icon_path("delete")
loaded_remove_image = QtGui.QImage(remove_icon_path)
@@ -356,6 +485,7 @@ class LoadedFilesView(QtWidgets.QTreeView):
)
self._model = model
+ self._proxy_model = proxy_model
self._time_delegate = time_delegate
self._remove_btn = remove_btn
@@ -403,7 +533,8 @@ class LoadedFilesView(QtWidgets.QTreeView):
if index.isValid():
return
- index = self._model.index(0, 0)
+ model = self.model()
+ index = model.index(0, 0)
if index.isValid():
self.setCurrentIndex(index)
diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py
index 5dd6998b24..dcfbbde851 100644
--- a/openpype/tools/publisher/window.py
+++ b/openpype/tools/publisher/window.py
@@ -42,7 +42,7 @@ from .widgets import (
)
-class PublisherWindow(QtWidgets.QWidget):
+class PublisherWindow(QtWidgets.QDialog):
"""Main window of publisher."""
default_width = 1300
default_height = 800
@@ -50,7 +50,7 @@ class PublisherWindow(QtWidgets.QWidget):
publish_footer_spacer = 2
def __init__(self, parent=None, controller=None, reset_on_show=None):
- super(PublisherWindow, self).__init__()
+ super(PublisherWindow, self).__init__(parent)
self.setObjectName("PublishWindow")
@@ -294,12 +294,6 @@ class PublisherWindow(QtWidgets.QWidget):
controller.event_system.add_callback(
"publish.process.stopped", self._on_publish_stop
)
- controller.event_system.add_callback(
- "publish.process.instance.changed", self._on_instance_change
- )
- controller.event_system.add_callback(
- "publish.process.plugin.changed", self._on_plugin_change
- )
controller.event_system.add_callback(
"show.card.message", self._on_overlay_message
)
@@ -321,6 +315,9 @@ class PublisherWindow(QtWidgets.QWidget):
controller.event_system.add_callback(
"convertors.find.failed", self._on_convertor_error
)
+ controller.event_system.add_callback(
+ "publish.action.failed", self._on_action_error
+ )
controller.event_system.add_callback(
"export_report.request", self._export_report
)
@@ -328,7 +325,6 @@ class PublisherWindow(QtWidgets.QWidget):
"copy_report.request", self._copy_report
)
-
# Store extra header widget for TrayPublisher
# - can be used to add additional widgets to header between context
# label and help button
@@ -491,8 +487,14 @@ class PublisherWindow(QtWidgets.QWidget):
app.removeEventFilter(self)
def keyPressEvent(self, event):
- # Ignore escape button to close window
- if event.key() == QtCore.Qt.Key_Escape:
+ if event.key() in {
+ # Ignore escape button to close window
+ QtCore.Qt.Key_Escape,
+ # Ignore enter keyboard event which by default triggers
+ # first available button in QDialog
+ QtCore.Qt.Key_Enter,
+ QtCore.Qt.Key_Return,
+ }:
event.accept()
return
@@ -558,18 +560,6 @@ class PublisherWindow(QtWidgets.QWidget):
self._reset_on_show = False
self.reset()
- def _make_sure_on_top(self):
- """Raise window to top and activate it.
-
- This may not work for some DCCs without Qt.
- """
-
- if not self._window_is_visible:
- self.show()
-
- self.setWindowState(QtCore.Qt.WindowActive)
- self.raise_()
-
def _checks_before_save(self, explicit_save):
"""Save of changes may trigger some issues.
@@ -882,12 +872,6 @@ class PublisherWindow(QtWidgets.QWidget):
if self._is_on_create_tab():
self._go_to_publish_tab()
- def _on_instance_change(self):
- self._make_sure_on_top()
-
- def _on_plugin_change(self):
- self._make_sure_on_top()
-
def _on_publish_validated_change(self, event):
if event["value"]:
self._validate_btn.setEnabled(False)
@@ -898,7 +882,6 @@ class PublisherWindow(QtWidgets.QWidget):
self._comment_input.setText("")
def _on_publish_stop(self):
- self._make_sure_on_top()
self._set_publish_overlay_visibility(False)
self._reset_btn.setEnabled(True)
self._stop_btn.setEnabled(False)
@@ -1012,6 +995,18 @@ class PublisherWindow(QtWidgets.QWidget):
event["title"], new_failed_info, "Convertor:"
)
+ def _on_action_error(self, event):
+ self.add_error_message_dialog(
+ event["title"],
+ [{
+ "message": event["message"],
+ "traceback": event["traceback"],
+ "label": event["label"],
+ "identifier": event["identifier"]
+ }],
+ "Action:"
+ )
+
def _update_create_overlay_size(self):
metrics = self._create_overlay_button.fontMetrics()
height = int(metrics.height())
diff --git a/openpype/tools/sceneinventory/switch_dialog.py b/openpype/tools/sceneinventory/switch_dialog.py
index 150e369678..695f47b4d4 100644
--- a/openpype/tools/sceneinventory/switch_dialog.py
+++ b/openpype/tools/sceneinventory/switch_dialog.py
@@ -1299,15 +1299,21 @@ class SwitchAssetDialog(QtWidgets.QDialog):
# If asset or subset are selected for switching, we use latest
# version else we try to keep the current container version.
+ version_name = None
if (
- selected_asset not in (None, container_asset_name)
- or selected_subset not in (None, container_subset_name)
+ selected_asset in (None, container_asset_name)
+ and selected_subset in (None, container_subset_name)
):
- version_name = max(version_docs_by_name)
- else:
- version_name = container_version["name"]
+ version_name = container_version.get("name")
+
+ version_doc = None
+ if version_name is not None:
+ version_doc = version_docs_by_name.get(version_name)
+
+ if version_doc is None:
+ version_name = max(version_docs_by_name)
+ version_doc = version_docs_by_name[version_name]
- version_doc = version_docs_by_name[version_name]
version_id = version_doc["_id"]
repres_docs_by_name = repre_docs_by_parent_id_by_name[
version_id
diff --git a/openpype/tools/settings/settings/README.md b/openpype/tools/settings/settings/README.md
index c29664a907..8f4ec00a76 100644
--- a/openpype/tools/settings/settings/README.md
+++ b/openpype/tools/settings/settings/README.md
@@ -334,7 +334,7 @@
},
"is_group": true,
"key": "templates_mapping",
- "label": "Muster - Templates mapping",
+ "label": "Deadline - Templates mapping",
"is_file": true
}
```
@@ -346,7 +346,7 @@
"object_type": "text",
"is_group": true,
"key": "templates_mapping",
- "label": "Muster - Templates mapping",
+ "label": "Deadline - Templates mapping",
"is_file": true
}
```
diff --git a/openpype/tools/utils/__init__.py b/openpype/tools/utils/__init__.py
index 50d50f467a..74702a2a10 100644
--- a/openpype/tools/utils/__init__.py
+++ b/openpype/tools/utils/__init__.py
@@ -32,6 +32,7 @@ from .lib import (
set_style_property,
DynamicQThread,
qt_app_context,
+ get_qt_app,
get_openpype_qt_app,
get_asset_icon,
get_asset_icon_by_name,
diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py
index 723e71e7aa..c7f92dd26e 100644
--- a/openpype/tools/utils/lib.py
+++ b/openpype/tools/utils/lib.py
@@ -91,7 +91,8 @@ def set_style_property(widget, property_name, property_value):
if cur_value == property_value:
return
widget.setProperty(property_name, property_value)
- widget.style().polish(widget)
+ style = widget.style()
+ style.polish(widget)
def paint_image_with_color(image, color):
@@ -153,11 +154,15 @@ def qt_app_context():
yield app
-def get_openpype_qt_app():
- """Main Qt application initialized for OpenPype processed.
+def get_qt_app():
+ """Get Qt application.
- This function should be used only inside OpenPype process and never inside
- other processes.
+ The function initializes new Qt application if it is not already
+ initialized. It also sets some attributes to the application to
+ ensure that it will work properly on high DPI displays.
+
+ Returns:
+ QtWidgets.QApplication: Current Qt application.
"""
app = QtWidgets.QApplication.instance()
@@ -183,6 +188,17 @@ def get_openpype_qt_app():
app = QtWidgets.QApplication(sys.argv)
+ return app
+
+
+def get_openpype_qt_app():
+ """Main Qt application initialized for OpenPype processed.
+
+ This function should be used only inside OpenPype process and never inside
+ other processes.
+ """
+
+ app = get_qt_app()
app.setWindowIcon(QtGui.QIcon(get_app_icon_path()))
return app
diff --git a/openpype/version.py b/openpype/version.py
index 279575d110..95203e17c9 100644
--- a/openpype/version.py
+++ b/openpype/version.py
@@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring Pype version."""
-__version__ = "3.18.3-nightly.2"
+__version__ = "3.18.8-nightly.1"
diff --git a/pyproject.toml b/pyproject.toml
index ee8e8017e3..eef6a2e978 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "OpenPype"
-version = "3.18.2" # OpenPype
+version = "3.18.7" # OpenPype
description = "Open VFX and Animation pipeline with support."
authors = ["OpenPype Team - Pipeline is the technical backbone of your production. It means, that whatever solution you use, it will cause vendor-lock to some extend. + Pipeline is the technical backbone of your production. It means, that whatever solution you use, it will cause vendor-lock to some extend. You can mitigate this risk by developing purely in-house tools, however, that just shifts the problem from a software vendor to your developers. Sooner or later, you'll hit the limits of such solution. In-house tools tend to be undocumented, narrow focused and heavily dependent on a very few or even a single developer.
@@ -332,7 +332,7 @@ function Home() {
Planned or in development by us and OpenPype community.
Maya
-
+
Flame
@@ -422,7 +422,7 @@ function Home() {
Deadline
-
+
Royal Render
@@ -443,17 +443,12 @@ function Home() {