Merge branch 'develop' of https://github.com/ynput/ayon-core into bugfix/AY-6775_deliver_versions_udim

# Conflicts:
#	client/ayon_core/plugins/load/delivery.py
This commit is contained in:
Roy Nieterau 2024-09-30 23:37:01 +02:00
commit 89a4d5062f
21 changed files with 531 additions and 335 deletions

12
.github/workflows/release_trigger.yml vendored Normal file
View file

@ -0,0 +1,12 @@
name: 🚀 Release Trigger
on:
workflow_dispatch:
jobs:
call-release-trigger:
uses: ynput/ops-repo-automation/.github/workflows/release_trigger.yml@main
secrets:
token: ${{ secrets.YNPUT_BOT_TOKEN }}
email: ${{ secrets.CI_EMAIL }}
user: ${{ secrets.CI_USER }}

View file

@ -19,7 +19,8 @@ class OCIOEnvHook(PreLaunchHook):
"nuke", "nuke",
"hiero", "hiero",
"resolve", "resolve",
"openrv" "openrv",
"cinema4d"
} }
launch_types = set() launch_types = set()

View file

@ -51,9 +51,10 @@ from .load import (
) )
from .publish import ( from .publish import (
KnownPublishError,
PublishError,
PublishValidationError, PublishValidationError,
PublishXmlValidationError, PublishXmlValidationError,
KnownPublishError,
AYONPyblishPluginMixin, AYONPyblishPluginMixin,
OptionalPyblishPluginMixin, OptionalPyblishPluginMixin,
) )
@ -162,9 +163,10 @@ __all__ = (
"get_repres_contexts", "get_repres_contexts",
# --- Publish --- # --- Publish ---
"KnownPublishError",
"PublishError",
"PublishValidationError", "PublishValidationError",
"PublishXmlValidationError", "PublishXmlValidationError",
"KnownPublishError",
"AYONPyblishPluginMixin", "AYONPyblishPluginMixin",
"OptionalPyblishPluginMixin", "OptionalPyblishPluginMixin",

View file

@ -9,6 +9,7 @@ AVALON_INSTANCE_ID = "pyblish.avalon.instance"
HOST_WORKFILE_EXTENSIONS = { HOST_WORKFILE_EXTENSIONS = {
"blender": [".blend"], "blender": [".blend"],
"celaction": [".scn"], "celaction": [".scn"],
"cinema4d": [".c4d"],
"tvpaint": [".tvpp"], "tvpaint": [".tvpp"],
"fusion": [".comp"], "fusion": [".comp"],
"harmony": [".zip"], "harmony": [".zip"],

View file

@ -788,6 +788,11 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data,
colorspace = product.colorspace colorspace = product.colorspace
break break
if isinstance(files, (list, tuple)):
files = [os.path.basename(f) for f in files]
else:
files = os.path.basename(files)
rep = { rep = {
"name": ext, "name": ext,
"ext": ext, "ext": ext,

View file

@ -4,17 +4,19 @@ AYON is using `pyblish` for publishing process which is a little bit extented an
## Exceptions ## Exceptions
AYON define few specific exceptions that should be used in publish plugins. AYON define few specific exceptions that should be used in publish plugins.
### Publish error
Exception `PublishError` can be raised on known error. The message is shown to artist.
- **message** Error message.
- **title** Short description of error (2-5 words). Title can be used for grouping of exceptions per plugin.
- **description** Override of 'message' for UI, you can add markdown and html. By default, is filled with 'message'.
- **detail** Additional detail message that is hidden under collapsed component.
Arguments `title`, `description` and `detail` are optional. Title is filled with generic message "This is not your fault" if is not passed.
### Validation exception ### Validation exception
Validation plugins should raise `PublishValidationError` to show to an artist what's wrong and give him actions to fix it. The exception says that error happened in plugin can be fixed by artist himself (with or without action on plugin). Any other errors will stop publishing immediately. Exception `PublishValidationError` raised after validation order has same effect as any other exception. Validation plugins should raise `PublishValidationError` to show to an artist what's wrong and give him actions to fix it. The exception says that error happened in plugin can be fixed by artist himself (with or without action on plugin). Any other errors will stop publishing immediately. Exception `PublishValidationError` raised after validation order has same effect as any other exception.
Exception `PublishValidationError` 3 arguments: Exception expect same arguments as `PublishError`. Value of `title` is filled with plugin label if is not passed.
- **message** Which is not used in UI but for headless publishing.
- **title** Short description of error (2-5 words). Title is used for grouping of exceptions per plugin.
- **description** Detailed description of happened issue where markdown and html can be used.
### Known errors
When there is a known error that can't be fixed by user (e.g. can't connect to deadline service, etc.) `KnownPublishError` should be raise. The only difference is that it's message is shown in UI to artist otherwise a neutral message without context is shown.
## Plugin extension ## Plugin extension
Publish plugins can be extended by additional logic when inherits from `AYONPyblishPluginMixin` which can be used as mixin (additional inheritance of class). Publish plugins can be extended by additional logic when inherits from `AYONPyblishPluginMixin` which can be used as mixin (additional inheritance of class).

View file

@ -9,9 +9,11 @@ from .publish_plugins import (
AbstractMetaInstancePlugin, AbstractMetaInstancePlugin,
AbstractMetaContextPlugin, AbstractMetaContextPlugin,
KnownPublishError,
PublishError,
PublishValidationError, PublishValidationError,
PublishXmlValidationError, PublishXmlValidationError,
KnownPublishError,
AYONPyblishPluginMixin, AYONPyblishPluginMixin,
OptionalPyblishPluginMixin, OptionalPyblishPluginMixin,
@ -61,9 +63,11 @@ __all__ = (
"AbstractMetaInstancePlugin", "AbstractMetaInstancePlugin",
"AbstractMetaContextPlugin", "AbstractMetaContextPlugin",
"KnownPublishError",
"PublishError",
"PublishValidationError", "PublishValidationError",
"PublishXmlValidationError", "PublishXmlValidationError",
"KnownPublishError",
"AYONPyblishPluginMixin", "AYONPyblishPluginMixin",
"OptionalPyblishPluginMixin", "OptionalPyblishPluginMixin",

View file

@ -25,27 +25,52 @@ class AbstractMetaContextPlugin(ABCMeta, ExplicitMetaPlugin):
pass pass
class PublishValidationError(Exception): class KnownPublishError(Exception):
"""Validation error happened during publishing. """Publishing crashed because of known error.
This exception should be used when validation publishing failed. Artist can't affect source of the error.
Has additional UI specific attributes that may be handy for artist. Deprecated:
Please use `PublishError` instead. Marked as deprecated 24/09/02.
"""
pass
class PublishError(Exception):
"""Publishing crashed because of known error.
Message will be shown in UI for artist.
Args: Args:
message(str): Message of error. Short explanation an issue. message (str): Message of error. Short explanation an issue.
title(str): Title showed in UI. All instances are grouped under title (Optional[str]): Title showed in UI.
single title. description (Optional[str]): Detailed description of an error.
description(str): Detailed description of an error. It is possible It is possible to use Markdown syntax.
to use Markdown syntax.
"""
"""
def __init__(self, message, title=None, description=None, detail=None): def __init__(self, message, title=None, description=None, detail=None):
self.message = message self.message = message
self.title = title self.title = title
self.description = description or message self.description = description or message
self.detail = detail self.detail = detail
super(PublishValidationError, self).__init__(message) super().__init__(message)
class PublishValidationError(PublishError):
"""Validation error happened during publishing.
This exception should be used when validation publishing failed.
Publishing does not stop during validation order if this
exception is raised.
Has additional UI specific attributes that may be handy for artist.
Argument 'title' is used to group errors.
"""
pass
class PublishXmlValidationError(PublishValidationError): class PublishXmlValidationError(PublishValidationError):
@ -68,15 +93,6 @@ class PublishXmlValidationError(PublishValidationError):
) )
class KnownPublishError(Exception):
"""Publishing crashed because of known error.
Message will be shown in UI for artist.
"""
pass
class AYONPyblishPluginMixin: class AYONPyblishPluginMixin:
# TODO # TODO
# executable_in_thread = False # executable_in_thread = False

View file

@ -17,8 +17,7 @@ from ayon_core.pipeline.load import get_representation_path_with_anatomy
from ayon_core.pipeline.delivery import ( from ayon_core.pipeline.delivery import (
get_format_dict, get_format_dict,
check_destination_path, check_destination_path,
deliver_single_file, deliver_single_file
deliver_sequence,
) )
@ -235,63 +234,51 @@ class DeliveryOptionsDialog(QtWidgets.QDialog):
# that are published along with the publish, because those should # that are published along with the publish, because those should
# not adhere to the template directly but are ingested in a # not adhere to the template directly but are ingested in a
# customized way. For example, maya look textures or any publish # customized way. For example, maya look textures or any publish
# that directly adds files into `instance.data["transfers"]`. # that directly adds files into `instance.data["transfers"]`
if repre.get("files"): src_paths = []
src_paths = [] for repre_file in repre["files"]:
for repre_file in repre["files"]: src_path = self.anatomy.fill_root(repre_file["path"])
src_path = self.anatomy.fill_root(repre_file["path"]) src_paths.append(src_path)
src_paths.append(src_path) sources_and_frames = collect_frames(src_paths)
sources_and_frames = collect_frames(src_paths)
frames = set(sources_and_frames.values()) frames = set(sources_and_frames.values())
frames.discard(None) frames.discard(None)
first_frame = None first_frame = None
if frames: if frames:
first_frame = min(frames) first_frame = min(frames)
for src_path, frame in sources_and_frames.items(): for src_path, frame in sources_and_frames.items():
args[0] = src_path args[0] = src_path
# Renumber frames # Renumber frames
if renumber_frame and frame is not None: if renumber_frame and frame is not None:
# Calculate offset between # Calculate offset between
# first frame and current frame # first frame and current frame
# - '0' for first frame # - '0' for first frame
offset = frame_offset - int(first_frame) offset = frame_offset - int(first_frame)
# Add offset to new frame start # Add offset to new frame start
dst_frame = int(frame) + offset dst_frame = int(frame) + offset
if dst_frame < 0: if dst_frame < 0:
msg = "Renumber frame has a smaller number than original frame" # noqa msg = "Renumber frame has a smaller number than original frame" # noqa
report_items[msg].append(src_path) report_items[msg].append(src_path)
self.log.warning("{} <{}>".format( self.log.warning("{} <{}>".format(
msg, dst_frame)) msg, dst_frame))
continue continue
frame = dst_frame frame = dst_frame
if frame is not None: if frame is not None:
if repre["context"].get("frame"): if repre["context"].get("frame"):
anatomy_data["frame"] = frame anatomy_data["frame"] = frame
elif repre["context"].get("udim"): elif repre["context"].get("udim"):
anatomy_data["udim"] = frame anatomy_data["udim"] = frame
else: else:
# Fallback # Fallback
self.log.warning( self.log.warning(
"Representation context has no frame or udim" "Representation context has no frame or udim"
" data. Supplying sequence frame to '{frame}'" " data. Supplying sequence frame to '{frame}'"
" formatting data." " formatting data."
) )
anatomy_data["frame"] = frame anatomy_data["frame"] = frame
new_report_items, uploaded = deliver_single_file(*args) new_report_items, uploaded = deliver_single_file(*args)
report_items.update(new_report_items)
self._update_progress(uploaded)
else: # fallback for Pype2 and representations without files
frame = repre["context"].get("frame")
if frame:
repre["context"]["frame"] = len(str(frame)) * "#"
if not frame:
new_report_items, uploaded = deliver_single_file(*args)
else:
new_report_items, uploaded = deliver_sequence(*args)
report_items.update(new_report_items) report_items.update(new_report_items)
self._update_progress(uploaded) self._update_progress(uploaded)

View file

@ -509,8 +509,11 @@ class IntegrateAsset(pyblish.api.InstancePlugin):
if not is_sequence_representation: if not is_sequence_representation:
files = [files] files = [files]
if any(os.path.isabs(fname) for fname in files): for fname in files:
raise KnownPublishError("Given file names contain full paths") if os.path.isabs(fname):
raise KnownPublishError(
f"Representation file names contains full paths: {fname}"
)
if not is_sequence_representation: if not is_sequence_representation:
return return

View file

@ -9,7 +9,14 @@ from ayon_api import (
class IntegrateInputLinksAYON(pyblish.api.ContextPlugin): class IntegrateInputLinksAYON(pyblish.api.ContextPlugin):
"""Connecting version level dependency links""" """Connecting version level dependency links
Handles links:
- generative - what gets produced from workfile
- reference - what was loaded into workfile
It expects workfile instance is being published.
"""
order = pyblish.api.IntegratorOrder + 0.2 order = pyblish.api.IntegratorOrder + 0.2
label = "Connect Dependency InputLinks AYON" label = "Connect Dependency InputLinks AYON"
@ -47,6 +54,11 @@ class IntegrateInputLinksAYON(pyblish.api.ContextPlugin):
self.create_links_on_server(context, new_links_by_type) self.create_links_on_server(context, new_links_by_type)
def split_instances(self, context): def split_instances(self, context):
"""Separates published instances into workfile and other
Returns:
(tuple(pyblish.plugin.Instance), list(pyblish.plugin.Instance))
"""
workfile_instance = None workfile_instance = None
other_instances = [] other_instances = []
@ -83,6 +95,15 @@ class IntegrateInputLinksAYON(pyblish.api.ContextPlugin):
def create_workfile_links( def create_workfile_links(
self, workfile_instance, other_instances, new_links_by_type self, workfile_instance, other_instances, new_links_by_type
): ):
"""Adds links (generative and reference) for workfile.
Args:
workfile_instance (pyblish.plugin.Instance): published workfile
other_instances (list[pyblish.plugin.Instance]): other published
instances
new_links_by_type (dict[str, list[str]]): dictionary collecting new
created links by its type
"""
if workfile_instance is None: if workfile_instance is None:
self.log.warn("No workfile in this publish session.") self.log.warn("No workfile in this publish session.")
return return
@ -97,7 +118,7 @@ class IntegrateInputLinksAYON(pyblish.api.ContextPlugin):
instance.data["versionEntity"]["id"], instance.data["versionEntity"]["id"],
) )
loaded_versions = workfile_instance.context.get("loadedVersions") loaded_versions = workfile_instance.context.data.get("loadedVersions")
if not loaded_versions: if not loaded_versions:
return return

View file

@ -36,7 +36,8 @@ class ValidateCurrentSaveFile(pyblish.api.ContextPlugin):
label = "Validate File Saved" label = "Validate File Saved"
order = pyblish.api.ValidatorOrder - 0.1 order = pyblish.api.ValidatorOrder - 0.1
hosts = ["fusion", "houdini", "max", "maya", "nuke", "substancepainter"] hosts = ["fusion", "houdini", "max", "maya", "nuke", "substancepainter",
"cinema4d"]
actions = [SaveByVersionUpAction, ShowWorkfilesAction] actions = [SaveByVersionUpAction, ShowWorkfilesAction]
def process(self, context): def process(self, context):

View file

@ -1118,39 +1118,39 @@ ValidationArtistMessage QLabel {
font-weight: bold; font-weight: bold;
} }
#ValidationActionButton { #PublishActionButton {
border-radius: 0.2em; border-radius: 0.2em;
padding: 4px 6px 4px 6px; padding: 4px 6px 4px 6px;
background: {color:bg-buttons}; background: {color:bg-buttons};
} }
#ValidationActionButton:hover { #PublishActionButton:hover {
background: {color:bg-buttons-hover}; background: {color:bg-buttons-hover};
color: {color:font-hover}; color: {color:font-hover};
} }
#ValidationActionButton:disabled { #PublishActionButton:disabled {
background: {color:bg-buttons-disabled}; background: {color:bg-buttons-disabled};
} }
#ValidationErrorTitleFrame { #PublishErrorTitleFrame {
border-radius: 0.2em; border-radius: 0.2em;
background: {color:bg-buttons}; background: {color:bg-buttons};
} }
#ValidationErrorTitleFrame:hover { #PublishErrorTitleFrame:hover {
background: {color:bg-buttons-hover}; background: {color:bg-buttons-hover};
} }
#ValidationErrorTitleFrame[selected="1"] { #PublishErrorTitleFrame[selected="1"] {
background: {color:bg-view-selection}; background: {color:bg-view-selection};
} }
#ValidationErrorInstanceList { #PublishErrorInstanceList {
border-radius: 0; border-radius: 0;
} }
#ValidationErrorInstanceList::item { #PublishErrorInstanceList::item {
border-bottom: 1px solid {color:border}; border-bottom: 1px solid {color:border};
border-left: 1px solid {color:border}; border-left: 1px solid {color:border};
} }

View file

@ -1,6 +1,9 @@
import collections import collections
from ayon_api import get_representations, get_versions_links from ayon_api import (
get_representations,
get_versions_links,
)
from ayon_core.lib import Logger, NestedCacheItem from ayon_core.lib import Logger, NestedCacheItem
from ayon_core.addon import AddonsManager from ayon_core.addon import AddonsManager
@ -509,18 +512,19 @@ class SiteSyncModel:
"reference" "reference"
) )
for link_repre_id in links: for link_repre_id in links:
try: if not self._sitesync_addon.is_representation_on_site(
project_name,
link_repre_id,
site_name
):
print("Adding {} to linked representation: {}".format( print("Adding {} to linked representation: {}".format(
site_name, link_repre_id)) site_name, link_repre_id))
self._sitesync_addon.add_site( self._sitesync_addon.add_site(
project_name, project_name,
link_repre_id, link_repre_id,
site_name, site_name,
force=False force=True
) )
except Exception:
# do not add/reset working site for references
log.debug("Site present", exc_info=True)
def _get_linked_representation_id( def _get_linked_representation_id(
self, self,
@ -575,7 +579,7 @@ class SiteSyncModel:
project_name, project_name,
versions_to_check, versions_to_check,
link_types=link_types, link_types=link_types,
link_direction="out") link_direction="in") # looking for 'in'puts for version
versions_to_check = set() versions_to_check = set()
for links in versions_links.values(): for links in versions_links.values():
@ -584,9 +588,6 @@ class SiteSyncModel:
if link["entityType"] != "version": if link["entityType"] != "version":
continue continue
entity_id = link["entityId"] entity_id = link["entityId"]
# Skip already found linked version ids
if entity_id in linked_version_ids:
continue
linked_version_ids.add(entity_id) linked_version_ids.add(entity_id)
versions_to_check.add(entity_id) versions_to_check.add(entity_id)

View file

@ -26,7 +26,7 @@ from ayon_core.tools.common_models import (
) )
if TYPE_CHECKING: if TYPE_CHECKING:
from .models import CreatorItem from .models import CreatorItem, PublishErrorInfo
class CardMessageTypes: class CardMessageTypes:
@ -543,14 +543,13 @@ class AbstractPublisherFrontend(AbstractPublisherCommon):
pass pass
@abstractmethod @abstractmethod
def get_publish_error_msg(self) -> Union[str, None]: def get_publish_error_info(self) -> Optional["PublishErrorInfo"]:
"""Current error message which cause fail of publishing. """Current error message which cause fail of publishing.
Returns: Returns:
Union[str, None]: Message which will be showed to artist or Optional[PublishErrorInfo]: Error info or None.
None.
"""
"""
pass pass
@abstractmethod @abstractmethod
@ -558,7 +557,7 @@ class AbstractPublisherFrontend(AbstractPublisherCommon):
pass pass
@abstractmethod @abstractmethod
def get_validation_errors(self): def get_publish_errors_report(self):
pass pass
@abstractmethod @abstractmethod

View file

@ -496,14 +496,14 @@ class PublisherController(
def get_publish_progress(self): def get_publish_progress(self):
return self._publish_model.get_progress() return self._publish_model.get_progress()
def get_publish_error_msg(self): def get_publish_error_info(self):
return self._publish_model.get_error_msg() return self._publish_model.get_error_info()
def get_publish_report(self): def get_publish_report(self):
return self._publish_model.get_publish_report() return self._publish_model.get_publish_report()
def get_validation_errors(self): def get_publish_errors_report(self):
return self._publish_model.get_validation_errors() return self._publish_model.get_publish_errors_report()
def set_comment(self, comment): def set_comment(self, comment):
"""Set comment from ui to pyblish context. """Set comment from ui to pyblish context.

View file

@ -1,5 +1,5 @@
from .create import CreateModel, CreatorItem from .create import CreateModel, CreatorItem
from .publish import PublishModel from .publish import PublishModel, PublishErrorInfo
__all__ = ( __all__ = (
@ -7,4 +7,5 @@ __all__ = (
"CreatorItem", "CreatorItem",
"PublishModel", "PublishModel",
"PublishErrorInfo",
) )

View file

@ -15,7 +15,10 @@ from ayon_core.pipeline import (
OptionalPyblishPluginMixin, OptionalPyblishPluginMixin,
) )
from ayon_core.pipeline.plugin_discover import DiscoverResult from ayon_core.pipeline.plugin_discover import DiscoverResult
from ayon_core.pipeline.publish import get_publish_instance_label from ayon_core.pipeline.publish import (
get_publish_instance_label,
PublishError,
)
from ayon_core.tools.publisher.abstract import AbstractPublisherBackend from ayon_core.tools.publisher.abstract import AbstractPublisherBackend
PUBLISH_EVENT_SOURCE = "publisher.publish.model" PUBLISH_EVENT_SOURCE = "publisher.publish.model"
@ -23,6 +26,53 @@ PUBLISH_EVENT_SOURCE = "publisher.publish.model"
PLUGIN_ORDER_OFFSET = 0.5 PLUGIN_ORDER_OFFSET = 0.5
class PublishErrorInfo:
def __init__(
self,
message: str,
is_unknown_error: bool,
description: Optional[str] = None,
title: Optional[str] = None,
detail: Optional[str] = None,
):
self.message: str = message
self.is_unknown_error = is_unknown_error
self.description: str = description or message
self.title: Optional[str] = title or "Unknown error"
self.detail: Optional[str] = detail
def __eq__(self, other: Any) -> bool:
if not isinstance(other, PublishErrorInfo):
return False
return (
self.description == other.description
and self.is_unknown_error == other.is_unknown_error
and self.title == other.title
and self.detail == other.detail
)
def __ne__(self, other: Any) -> bool:
return not self.__eq__(other)
@classmethod
def from_exception(cls, exc) -> "PublishErrorInfo":
if isinstance(exc, PublishError):
return cls(
exc.message,
False,
exc.description,
title=exc.title,
detail=exc.detail,
)
if isinstance(exc, KnownPublishError):
msg = str(exc)
else:
msg = (
"Something went wrong. Send report"
" to your supervisor or Ynput team."
)
return cls(msg, True)
class PublishReportMaker: class PublishReportMaker:
"""Report for single publishing process. """Report for single publishing process.
@ -479,10 +529,10 @@ class PublishPluginsProxy:
) )
class ValidationErrorItem: class PublishErrorItem:
"""Data driven validation error item. """Data driven publish error item.
Prepared data container with information about validation error and it's Prepared data container with information about publish error and it's
source plugin. source plugin.
Can be converted to raw data and recreated should be used for controller Can be converted to raw data and recreated should be used for controller
@ -490,11 +540,11 @@ class ValidationErrorItem:
Args: Args:
instance_id (Optional[str]): Pyblish instance id to which is instance_id (Optional[str]): Pyblish instance id to which is
validation error connected. publish error connected.
instance_label (Optional[str]): Prepared instance label. instance_label (Optional[str]): Prepared instance label.
plugin_id (str): Pyblish plugin id which triggered the validation plugin_id (str): Pyblish plugin id which triggered the publish
error. Id is generated using 'PublishPluginsProxy'. error. Id is generated using 'PublishPluginsProxy'.
context_validation (bool): Error happened on context. is_context_plugin (bool): Error happened on context.
title (str): Error title. title (str): Error title.
description (str): Error description. description (str): Error description.
detail (str): Error detail. detail (str): Error detail.
@ -505,7 +555,8 @@ class ValidationErrorItem:
instance_id: Optional[str], instance_id: Optional[str],
instance_label: Optional[str], instance_label: Optional[str],
plugin_id: str, plugin_id: str,
context_validation: bool, is_context_plugin: bool,
is_validation_error: bool,
title: str, title: str,
description: str, description: str,
detail: str detail: str
@ -513,7 +564,8 @@ class ValidationErrorItem:
self.instance_id: Optional[str] = instance_id self.instance_id: Optional[str] = instance_id
self.instance_label: Optional[str] = instance_label self.instance_label: Optional[str] = instance_label
self.plugin_id: str = plugin_id self.plugin_id: str = plugin_id
self.context_validation: bool = context_validation self.is_context_plugin: bool = is_context_plugin
self.is_validation_error: bool = is_validation_error
self.title: str = title self.title: str = title
self.description: str = description self.description: str = description
self.detail: str = detail self.detail: str = detail
@ -529,7 +581,8 @@ class ValidationErrorItem:
"instance_id": self.instance_id, "instance_id": self.instance_id,
"instance_label": self.instance_label, "instance_label": self.instance_label,
"plugin_id": self.plugin_id, "plugin_id": self.plugin_id,
"context_validation": self.context_validation, "is_context_plugin": self.is_context_plugin,
"is_validation_error": self.is_validation_error,
"title": self.title, "title": self.title,
"description": self.description, "description": self.description,
"detail": self.detail, "detail": self.detail,
@ -539,13 +592,13 @@ class ValidationErrorItem:
def from_result( def from_result(
cls, cls,
plugin_id: str, plugin_id: str,
error: PublishValidationError, error: PublishError,
instance: Union[pyblish.api.Instance, None] instance: Union[pyblish.api.Instance, None]
): ):
"""Create new object based on resukt from controller. """Create new object based on resukt from controller.
Returns: Returns:
ValidationErrorItem: New object with filled data. PublishErrorItem: New object with filled data.
""" """
instance_label = None instance_label = None
@ -561,6 +614,7 @@ class ValidationErrorItem:
instance_label, instance_label,
plugin_id, plugin_id,
instance is None, instance is None,
isinstance(error, PublishValidationError),
error.title, error.title,
error.description, error.description,
error.detail, error.detail,
@ -571,11 +625,11 @@ class ValidationErrorItem:
return cls(**data) return cls(**data)
class PublishValidationErrorsReport: class PublishErrorsReport:
"""Publish validation errors report that can be parsed to raw data. """Publish errors report that can be parsed to raw data.
Args: Args:
error_items (List[ValidationErrorItem]): List of validation errors. error_items (List[PublishErrorItem]): List of publish errors.
plugin_action_items (Dict[str, List[PublishPluginActionItem]]): Action plugin_action_items (Dict[str, List[PublishPluginActionItem]]): Action
items by plugin id. items by plugin id.
@ -584,7 +638,7 @@ class PublishValidationErrorsReport:
self._error_items = error_items self._error_items = error_items
self._plugin_action_items = plugin_action_items self._plugin_action_items = plugin_action_items
def __iter__(self) -> Iterable[ValidationErrorItem]: def __iter__(self) -> Iterable[PublishErrorItem]:
for item in self._error_items: for item in self._error_items:
yield item yield item
@ -658,7 +712,7 @@ class PublishValidationErrorsReport:
@classmethod @classmethod
def from_data( def from_data(
cls, data: Dict[str, Any] cls, data: Dict[str, Any]
) -> "PublishValidationErrorsReport": ) -> "PublishErrorsReport":
"""Recreate object from data. """Recreate object from data.
Args: Args:
@ -666,11 +720,11 @@ class PublishValidationErrorsReport:
using 'to_data' method. using 'to_data' method.
Returns: Returns:
PublishValidationErrorsReport: New object based on data. PublishErrorsReport: New object based on data.
""" """
error_items = [ error_items = [
ValidationErrorItem.from_data(error_item) PublishErrorItem.from_data(error_item)
for error_item in data["error_items"] for error_item in data["error_items"]
] ]
plugin_action_items = {} plugin_action_items = {}
@ -682,12 +736,12 @@ class PublishValidationErrorsReport:
return cls(error_items, plugin_action_items) return cls(error_items, plugin_action_items)
class PublishValidationErrors: class PublishErrors:
"""Object to keep track about validation errors by plugin.""" """Object to keep track about publish errors by plugin."""
def __init__(self): def __init__(self):
self._plugins_proxy: Union[PublishPluginsProxy, None] = None self._plugins_proxy: Union[PublishPluginsProxy, None] = None
self._error_items: List[ValidationErrorItem] = [] self._error_items: List[PublishErrorItem] = []
self._plugin_action_items: Dict[ self._plugin_action_items: Dict[
str, List[PublishPluginActionItem] str, List[PublishPluginActionItem]
] = {} ] = {}
@ -713,29 +767,29 @@ class PublishValidationErrors:
self._error_items = [] self._error_items = []
self._plugin_action_items = {} self._plugin_action_items = {}
def create_report(self) -> PublishValidationErrorsReport: def create_report(self) -> PublishErrorsReport:
"""Create report based on currently existing errors. """Create report based on currently existing errors.
Returns: Returns:
PublishValidationErrorsReport: Validation error report with all PublishErrorsReport: Publish error report with all
error information and publish plugin action items. error information and publish plugin action items.
""" """
return PublishValidationErrorsReport( return PublishErrorsReport(
self._error_items, self._plugin_action_items self._error_items, self._plugin_action_items
) )
def add_error( def add_error(
self, self,
plugin: pyblish.api.Plugin, plugin: pyblish.api.Plugin,
error: PublishValidationError, error: PublishError,
instance: Union[pyblish.api.Instance, None] instance: Union[pyblish.api.Instance, None]
): ):
"""Add error from pyblish result. """Add error from pyblish result.
Args: Args:
plugin (pyblish.api.Plugin): Plugin which triggered error. plugin (pyblish.api.Plugin): Plugin which triggered error.
error (PublishValidationError): Validation error. error (PublishError): Publish error.
instance (Union[pyblish.api.Instance, None]): Instance on which was instance (Union[pyblish.api.Instance, None]): Instance on which was
error raised or None if was raised on context. error raised or None if was raised on context.
""" """
@ -750,7 +804,7 @@ class PublishValidationErrors:
error.title = plugin_label error.title = plugin_label
self._error_items.append( self._error_items.append(
ValidationErrorItem.from_result(plugin_id, error, instance) PublishErrorItem.from_result(plugin_id, error, instance)
) )
if plugin_id in self._plugin_action_items: if plugin_id in self._plugin_action_items:
return return
@ -801,7 +855,7 @@ class PublishModel:
self._publish_comment_is_set: bool = False self._publish_comment_is_set: bool = False
# Any other exception that happened during publishing # Any other exception that happened during publishing
self._publish_error_msg: Optional[str] = None self._publish_error_info: Optional[PublishErrorInfo] = None
# Publishing is in progress # Publishing is in progress
self._publish_is_running: bool = False self._publish_is_running: bool = False
# Publishing is over validation order # Publishing is over validation order
@ -824,10 +878,8 @@ class PublishModel:
self._publish_context = None self._publish_context = None
# Pyblish report # Pyblish report
self._publish_report: PublishReportMaker = PublishReportMaker() self._publish_report: PublishReportMaker = PublishReportMaker()
# Store exceptions of validation error # Store exceptions of publish error
self._publish_validation_errors: PublishValidationErrors = ( self._publish_errors: PublishErrors = PublishErrors()
PublishValidationErrors()
)
# This information is not much important for controller but for widget # This information is not much important for controller but for widget
# which can change (and set) the comment. # which can change (and set) the comment.
@ -851,7 +903,7 @@ class PublishModel:
self._publish_comment_is_set = False self._publish_comment_is_set = False
self._publish_has_started = False self._publish_has_started = False
self._set_publish_error_msg(None) self._set_publish_error_info(None)
self._set_progress(0) self._set_progress(0)
self._set_is_running(False) self._set_is_running(False)
self._set_has_validated(False) self._set_has_validated(False)
@ -881,7 +933,7 @@ class PublishModel:
) )
for plugin in create_context.publish_plugins_mismatch_targets: for plugin in create_context.publish_plugins_mismatch_targets:
self._publish_report.set_plugin_skipped(plugin.id) self._publish_report.set_plugin_skipped(plugin.id)
self._publish_validation_errors.reset(self._publish_plugins_proxy) self._publish_errors.reset(self._publish_plugins_proxy)
self._set_max_progress(len(publish_plugins)) self._set_max_progress(len(publish_plugins))
@ -974,11 +1026,11 @@ class PublishModel:
self._publish_context self._publish_context
) )
def get_validation_errors(self) -> PublishValidationErrorsReport: def get_publish_errors_report(self) -> PublishErrorsReport:
return self._publish_validation_errors.create_report() return self._publish_errors.create_report()
def get_error_msg(self) -> Optional[str]: def get_error_info(self) -> Optional[PublishErrorInfo]:
return self._publish_error_msg return self._publish_error_info
def set_comment(self, comment: str): def set_comment(self, comment: str):
# Ignore change of comment when publishing started # Ignore change of comment when publishing started
@ -1077,9 +1129,9 @@ class PublishModel:
{"value": value} {"value": value}
) )
def _set_publish_error_msg(self, value: Optional[str]): def _set_publish_error_info(self, value: Optional[PublishErrorInfo]):
if self._publish_error_msg != value: if self._publish_error_info != value:
self._publish_error_msg = value self._publish_error_info = value
self._emit_event( self._emit_event(
"publish.publish_error.changed", "publish.publish_error.changed",
{"value": value} {"value": value}
@ -1225,32 +1277,33 @@ class PublishModel:
exception = result.get("error") exception = result.get("error")
if exception: if exception:
has_validation_error = False
if ( if (
isinstance(exception, PublishValidationError) isinstance(exception, PublishValidationError)
and not self._publish_has_validated and not self._publish_has_validated
): ):
has_validation_error = True result["is_validation_error"] = True
self._add_validation_error(result) self._add_validation_error(result)
else: else:
if isinstance(exception, KnownPublishError): if isinstance(exception, PublishError):
msg = str(exception) if not exception.title:
else: exception.title = plugin.label or plugin.__name__
msg = ( self._add_publish_error_to_report(result)
"Something went wrong. Send report"
" to your supervisor or Ynput team." error_info = PublishErrorInfo.from_exception(exception)
) self._set_publish_error_info(error_info)
self._set_publish_error_msg(msg)
self._set_is_crashed(True) self._set_is_crashed(True)
result["is_validation_error"] = has_validation_error result["is_validation_error"] = False
self._publish_report.add_result(plugin.id, result) self._publish_report.add_result(plugin.id, result)
def _add_validation_error(self, result: Dict[str, Any]): def _add_validation_error(self, result: Dict[str, Any]):
self._set_has_validation_errors(True) self._set_has_validation_errors(True)
self._publish_validation_errors.add_error( self._add_publish_error_to_report(result)
def _add_publish_error_to_report(self, result: Dict[str, Any]):
self._publish_errors.add_error(
result["plugin"], result["plugin"],
result["error"], result["error"],
result["instance"] result["instance"]

View file

@ -411,10 +411,13 @@ class PublishFrame(QtWidgets.QWidget):
"""Show error message to artist on publish crash.""" """Show error message to artist on publish crash."""
self._set_main_label("Error happened") self._set_main_label("Error happened")
error_info = self._controller.get_publish_error_info()
self._message_label_top.setText( error_message = "Unknown error happened"
self._controller.get_publish_error_msg() if error_info is not None:
) error_message = error_info.message
self._message_label_top.setText(error_message)
self._set_success_property(1) self._set_success_property(1)

View file

@ -26,7 +26,7 @@ from ayon_core.tools.publisher.constants import (
CONTEXT_LABEL, CONTEXT_LABEL,
) )
from .widgets import IconValuePixmapLabel from .widgets import PublishPixmapLabel, IconValuePixmapLabel
from .icons import ( from .icons import (
get_pixmap, get_pixmap,
get_image, get_image,
@ -42,7 +42,7 @@ INFO_VISIBLE = 1 << 6
class VerticalScrollArea(QtWidgets.QScrollArea): class VerticalScrollArea(QtWidgets.QScrollArea):
"""Scroll area for validation error titles. """Scroll area for publish error titles.
The biggest difference is that the scroll area has scroll bar on left side The biggest difference is that the scroll area has scroll bar on left side
and resize of content will also resize scrollarea itself. and resize of content will also resize scrollarea itself.
@ -126,7 +126,7 @@ class ActionButton(BaseClickableFrame):
def __init__(self, plugin_action_item, parent): def __init__(self, plugin_action_item, parent):
super().__init__(parent) super().__init__(parent)
self.setObjectName("ValidationActionButton") self.setObjectName("PublishActionButton")
self.plugin_action_item = plugin_action_item self.plugin_action_item = plugin_action_item
@ -155,10 +155,10 @@ class ActionButton(BaseClickableFrame):
) )
class ValidateActionsWidget(QtWidgets.QFrame): class PublishActionsWidget(QtWidgets.QFrame):
"""Wrapper widget for plugin actions. """Wrapper widget for plugin actions.
Change actions based on selected validation error. Change actions based on selected publish error.
""" """
def __init__( def __init__(
@ -243,16 +243,16 @@ class ValidateActionsWidget(QtWidgets.QFrame):
self._controller.run_action(plugin_id, action_id) self._controller.run_action(plugin_id, action_id)
# --- Validation error titles --- # --- Publish error titles ---
class ValidationErrorInstanceList(QtWidgets.QListView): class PublishErrorInstanceList(QtWidgets.QListView):
"""List of publish instances that caused a validation error. """List of publish instances that caused a publish error.
Instances are collected per plugin's validation error title. Instances are collected per plugin's publish error title.
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.setObjectName("ValidationErrorInstanceList") self.setObjectName("PublishErrorInstanceList")
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
@ -270,18 +270,19 @@ class ValidationErrorInstanceList(QtWidgets.QListView):
return result return result
class ValidationErrorTitleWidget(QtWidgets.QWidget): class PublishErrorTitleWidget(QtWidgets.QWidget):
"""Title of validation error. """Title of publish error.
Widget is used as radio button so requires clickable functionality and Widget is used as radio button so requires clickable functionality and
changing style on selection/deselection. changing style on selection/deselection.
Has toggle button to show/hide instances on which validation error happened Has toggle button to show/hide instances on which publish error happened
if there is a list (Valdation error may happen on context). if there is a list (Valdation error may happen on context).
""" """
selected = QtCore.Signal(str) selected = QtCore.Signal(str)
instance_changed = QtCore.Signal(str) instance_changed = QtCore.Signal(str)
_error_pixmap = None
def __init__(self, title_id, error_info, parent): def __init__(self, title_id, error_info, parent):
super().__init__(parent) super().__init__(parent)
@ -290,30 +291,17 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget):
self._error_info = error_info self._error_info = error_info
self._selected = False self._selected = False
title_frame = ClickableFrame(self)
title_frame.setObjectName("ValidationErrorTitleFrame")
toggle_instance_btn = QtWidgets.QToolButton(title_frame)
toggle_instance_btn.setObjectName("ArrowBtn")
toggle_instance_btn.setArrowType(QtCore.Qt.RightArrow)
toggle_instance_btn.setMaximumWidth(14)
label_widget = QtWidgets.QLabel(error_info["title"], title_frame)
title_frame_layout = QtWidgets.QHBoxLayout(title_frame)
title_frame_layout.addWidget(label_widget, 1)
title_frame_layout.addWidget(toggle_instance_btn, 0)
instances_model = QtGui.QStandardItemModel() instances_model = QtGui.QStandardItemModel()
instance_ids = [] instance_ids = []
items = [] items = []
context_validation = False is_context_plugin = False
is_crashing_error = False
for error_item in error_info["error_items"]: for error_item in error_info["error_items"]:
context_validation = error_item.context_validation is_crashing_error = not error_item.is_validation_error
if context_validation: is_context_plugin = error_item.is_context_plugin
toggle_instance_btn.setArrowType(QtCore.Qt.NoArrow) if is_context_plugin:
instance_ids.append(CONTEXT_ID) instance_ids.append(CONTEXT_ID)
# Add fake item to have minimum size hint of view widget # Add fake item to have minimum size hint of view widget
items.append(QtGui.QStandardItem(CONTEXT_LABEL)) items.append(QtGui.QStandardItem(CONTEXT_LABEL))
@ -333,7 +321,33 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget):
root_item = instances_model.invisibleRootItem() root_item = instances_model.invisibleRootItem()
root_item.appendRows(items) root_item.appendRows(items)
instances_view = ValidationErrorInstanceList(self) title_frame = ClickableFrame(self)
title_frame.setObjectName("PublishErrorTitleFrame")
toggle_instance_btn = QtWidgets.QToolButton(title_frame)
toggle_instance_btn.setObjectName("ArrowBtn")
toggle_instance_btn.setArrowType(QtCore.Qt.RightArrow)
toggle_instance_btn.setMaximumWidth(14)
if is_context_plugin:
toggle_instance_btn.setArrowType(QtCore.Qt.NoArrow)
icon_label = None
if is_crashing_error:
error_pixmap = self._get_error_pixmap()
icon_label = PublishPixmapLabel(error_pixmap, self)
label_widget = QtWidgets.QLabel(error_info["title"], title_frame)
title_frame_layout = QtWidgets.QHBoxLayout(title_frame)
title_frame_layout.setContentsMargins(8, 8, 8, 8)
title_frame_layout.setSpacing(0)
if icon_label is not None:
title_frame_layout.addWidget(icon_label, 0)
title_frame_layout.addSpacing(6)
title_frame_layout.addWidget(label_widget, 1)
title_frame_layout.addWidget(toggle_instance_btn, 0)
instances_view = PublishErrorInstanceList(self)
instances_view.setModel(instances_model) instances_view.setModel(instances_model)
self.setLayoutDirection(QtCore.Qt.LeftToRight) self.setLayoutDirection(QtCore.Qt.LeftToRight)
@ -352,7 +366,7 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget):
layout.addWidget(view_widget, 0) layout.addWidget(view_widget, 0)
view_widget.setVisible(False) view_widget.setVisible(False)
if not context_validation: if not is_context_plugin:
toggle_instance_btn.clicked.connect(self._on_toggle_btn_click) toggle_instance_btn.clicked.connect(self._on_toggle_btn_click)
title_frame.clicked.connect(self._mouse_release_callback) title_frame.clicked.connect(self._mouse_release_callback)
@ -369,7 +383,8 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget):
self._instances_model = instances_model self._instances_model = instances_model
self._instances_view = instances_view self._instances_view = instances_view
self._context_validation = context_validation self._is_context_plugin = is_context_plugin
self._is_crashing_error = is_crashing_error
self._instance_ids = instance_ids self._instance_ids = instance_ids
self._expanded = False self._expanded = False
@ -411,6 +426,10 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget):
def id(self): def id(self):
return self._title_id return self._title_id
@property
def is_crashing_error(self):
return self._is_crashing_error
def _change_style_property(self, selected): def _change_style_property(self, selected):
"""Change style of widget based on selection.""" """Change style of widget based on selection."""
@ -438,6 +457,12 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget):
self.selected.emit(self._title_id) self.selected.emit(self._title_id)
self._set_expanded(True) self._set_expanded(True)
@classmethod
def _get_error_pixmap(cls):
if cls._error_pixmap is None:
cls._error_pixmap = get_pixmap("error")
return cls._error_pixmap
def _on_toggle_btn_click(self): def _on_toggle_btn_click(self):
"""Show/hide instances list.""" """Show/hide instances list."""
@ -450,7 +475,7 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget):
elif expanded is self._expanded: elif expanded is self._expanded:
return return
if expanded and self._context_validation: if expanded and self._is_context_plugin:
return return
self._expanded = expanded self._expanded = expanded
@ -464,7 +489,7 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget):
self.instance_changed.emit(self._title_id) self.instance_changed.emit(self._title_id)
def get_selected_instances(self): def get_selected_instances(self):
if self._context_validation: if self._is_context_plugin:
return [CONTEXT_ID] return [CONTEXT_ID]
sel_model = self._instances_view.selectionModel() sel_model = self._instances_view.selectionModel()
return [ return [
@ -477,21 +502,7 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget):
return list(self._instance_ids) return list(self._instance_ids)
class ValidationArtistMessage(QtWidgets.QWidget): class PublishErrorsView(QtWidgets.QWidget):
def __init__(self, message, parent):
super().__init__(parent)
artist_msg_label = QtWidgets.QLabel(message, self)
artist_msg_label.setAlignment(QtCore.Qt.AlignCenter)
main_layout = QtWidgets.QHBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.addWidget(
artist_msg_label, 1, QtCore.Qt.AlignCenter
)
class ValidationErrorsView(QtWidgets.QWidget):
selection_changed = QtCore.Signal() selection_changed = QtCore.Signal()
def __init__(self, parent): def __init__(self, parent):
@ -510,8 +521,9 @@ class ValidationErrorsView(QtWidgets.QWidget):
# scroll widget # scroll widget
errors_layout.setContentsMargins(5, 0, 0, 0) errors_layout.setContentsMargins(5, 0, 0, 0)
layout = QtWidgets.QVBoxLayout(self) main_layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(errors_scroll, 1) main_layout.setContentsMargins(8, 8, 8, 8)
main_layout.addWidget(errors_scroll, 1)
self._errors_widget = errors_widget self._errors_widget = errors_widget
self._errors_layout = errors_layout self._errors_layout = errors_layout
@ -533,28 +545,30 @@ class ValidationErrorsView(QtWidgets.QWidget):
"""Set errors into context and created titles. """Set errors into context and created titles.
Args: Args:
validation_error_report (PublishValidationErrorsReport): Report grouped_error_items (List[Dict[str, Any]]): Report
with information about validation errors and publish plugin with information about publish errors and publish plugin
actions. actions.
""" """
self._clear() self._clear()
first_id = None select_id = None
for title_item in grouped_error_items: for title_item in grouped_error_items:
title_id = title_item["id"] title_id = title_item["id"]
if first_id is None: if select_id is None:
first_id = title_id select_id = title_id
widget = ValidationErrorTitleWidget(title_id, title_item, self) widget = PublishErrorTitleWidget(title_id, title_item, self)
widget.selected.connect(self._on_select) widget.selected.connect(self._on_select)
widget.instance_changed.connect(self._on_instance_change) widget.instance_changed.connect(self._on_instance_change)
if widget.is_crashing_error:
select_id = title_id
self._errors_layout.addWidget(widget) self._errors_layout.addWidget(widget)
self._title_widgets[title_id] = widget self._title_widgets[title_id] = widget
self._errors_layout.addStretch(1) self._errors_layout.addStretch(1)
if first_id: if select_id:
self._title_widgets[first_id].set_selected(True) self._title_widgets[select_id].set_selected(True)
else: else:
self.selection_changed.emit() self.selection_changed.emit()
@ -1319,6 +1333,7 @@ class InstancesLogsView(QtWidgets.QFrame):
content_widget = QtWidgets.QWidget(content_wrap_widget) content_widget = QtWidgets.QWidget(content_wrap_widget)
content_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) content_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
content_layout = QtWidgets.QVBoxLayout(content_widget) content_layout = QtWidgets.QVBoxLayout(content_widget)
content_layout.setContentsMargins(8, 8, 8, 8)
content_layout.setSpacing(15) content_layout.setSpacing(15)
scroll_area.setWidget(content_wrap_widget) scroll_area.setWidget(content_wrap_widget)
@ -1454,6 +1469,78 @@ class InstancesLogsView(QtWidgets.QFrame):
self._update_instances() self._update_instances()
class ErrorDetailWidget(QtWidgets.QWidget):
def __init__(self, parent):
super().__init__(parent)
error_detail_top = ClickableFrame(self)
line_l_widget = SeparatorWidget(1, parent=error_detail_top)
error_detail_expand_btn = ClassicExpandBtn(error_detail_top)
error_detail_expand_label = QtWidgets.QLabel(
"Details", error_detail_top)
line_r_widget = SeparatorWidget(1, parent=error_detail_top)
error_detail_top_l = QtWidgets.QHBoxLayout(error_detail_top)
error_detail_top_l.setContentsMargins(0, 0, 10, 0)
error_detail_top_l.addWidget(line_l_widget, 1)
error_detail_top_l.addWidget(error_detail_expand_btn, 0)
error_detail_top_l.addWidget(error_detail_expand_label, 0)
error_detail_top_l.addWidget(line_r_widget, 9)
error_detail_input = ExpandingTextEdit(self)
error_detail_input.setObjectName("InfoText")
error_detail_input.setTextInteractionFlags(
QtCore.Qt.TextBrowserInteraction
)
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.addWidget(error_detail_top, 0)
main_layout.addWidget(error_detail_input, 0)
main_layout.addStretch(1)
error_detail_input.setVisible(not error_detail_expand_btn.collapsed)
error_detail_top.clicked.connect(self._on_detail_toggle)
self._error_detail_top = error_detail_top
self._error_detail_expand_btn = error_detail_expand_btn
self._error_detail_input = error_detail_input
def set_detail(self, detail):
if not detail:
self._set_visible_inputs(False)
return
if commonmark:
self._error_detail_input.setHtml(
commonmark.commonmark(detail)
)
elif hasattr(self._error_detail_input, "setMarkdown"):
self._error_detail_input.setMarkdown(detail)
else:
self._error_detail_input.setText(detail)
self._set_visible_inputs(True)
def _set_visible_inputs(self, visible):
self._error_detail_top.setVisible(visible)
input_visible = visible
if input_visible:
input_visible = not self._error_detail_expand_btn.collapsed
self._error_detail_input.setVisible(input_visible)
def _on_detail_toggle(self):
self._error_detail_expand_btn.set_collapsed()
self._error_detail_input.setVisible(
not self._error_detail_expand_btn.collapsed
)
class CrashWidget(QtWidgets.QWidget): class CrashWidget(QtWidgets.QWidget):
"""Widget shown when publishing crashes. """Widget shown when publishing crashes.
@ -1488,6 +1575,8 @@ class CrashWidget(QtWidgets.QWidget):
"Save to disk", btns_widget) "Save to disk", btns_widget)
btns_layout = QtWidgets.QHBoxLayout(btns_widget) btns_layout = QtWidgets.QHBoxLayout(btns_widget)
btns_layout.setContentsMargins(0, 0, 0, 0)
btns_layout.setSpacing(0)
btns_layout.addStretch(1) btns_layout.addStretch(1)
btns_layout.addWidget(copy_clipboard_btn, 0) btns_layout.addWidget(copy_clipboard_btn, 0)
btns_layout.addSpacing(20) btns_layout.addSpacing(20)
@ -1495,11 +1584,13 @@ class CrashWidget(QtWidgets.QWidget):
btns_layout.addStretch(1) btns_layout.addStretch(1)
layout = QtWidgets.QVBoxLayout(self) layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(5, 5, 5, 5)
layout.setSpacing(0)
layout.addStretch(1) layout.addStretch(1)
layout.addWidget(main_label, 0) layout.addWidget(main_label, 0)
layout.addSpacing(20) layout.addSpacing(30)
layout.addWidget(report_label, 0) layout.addWidget(report_label, 0)
layout.addSpacing(20) layout.addSpacing(30)
layout.addWidget(btns_widget, 0) layout.addWidget(btns_widget, 0)
layout.addStretch(2) layout.addStretch(2)
@ -1517,7 +1608,7 @@ class CrashWidget(QtWidgets.QWidget):
"export_report.request", {}, "report_page") "export_report.request", {}, "report_page")
class ErrorDetailsWidget(QtWidgets.QWidget): class PublishFailWidget(QtWidgets.QWidget):
def __init__(self, parent): def __init__(self, parent):
super().__init__(parent) super().__init__(parent)
@ -1530,34 +1621,7 @@ class ErrorDetailsWidget(QtWidgets.QWidget):
) )
# Error 'Details' widget -> Collapsible # Error 'Details' widget -> Collapsible
error_details_widget = QtWidgets.QWidget(inputs_widget) error_details_widget = ErrorDetailWidget(inputs_widget)
error_details_top = ClickableFrame(error_details_widget)
error_details_expand_btn = ClassicExpandBtn(error_details_top)
error_details_expand_label = QtWidgets.QLabel(
"Details", error_details_top)
line_widget = SeparatorWidget(1, parent=error_details_top)
error_details_top_l = QtWidgets.QHBoxLayout(error_details_top)
error_details_top_l.setContentsMargins(0, 0, 10, 0)
error_details_top_l.addWidget(error_details_expand_btn, 0)
error_details_top_l.addWidget(error_details_expand_label, 0)
error_details_top_l.addWidget(line_widget, 1)
error_details_input = ExpandingTextEdit(error_details_widget)
error_details_input.setObjectName("InfoText")
error_details_input.setTextInteractionFlags(
QtCore.Qt.TextBrowserInteraction
)
error_details_input.setVisible(not error_details_expand_btn.collapsed)
error_details_layout = QtWidgets.QVBoxLayout(error_details_widget)
error_details_layout.setContentsMargins(0, 0, 0, 0)
error_details_layout.addWidget(error_details_top, 0)
error_details_layout.addWidget(error_details_input, 0)
error_details_layout.addStretch(1)
# Description and Details layout # Description and Details layout
inputs_layout = QtWidgets.QVBoxLayout(inputs_widget) inputs_layout = QtWidgets.QVBoxLayout(inputs_widget)
@ -1570,17 +1634,8 @@ class ErrorDetailsWidget(QtWidgets.QWidget):
main_layout.setContentsMargins(0, 0, 0, 0) main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.addWidget(inputs_widget, 1) main_layout.addWidget(inputs_widget, 1)
error_details_top.clicked.connect(self._on_detail_toggle)
self._error_details_widget = error_details_widget
self._error_description_input = error_description_input self._error_description_input = error_description_input
self._error_details_expand_btn = error_details_expand_btn self._error_details_widget = error_details_widget
self._error_details_input = error_details_input
def _on_detail_toggle(self):
self._error_details_expand_btn.set_collapsed()
self._error_details_input.setVisible(
not self._error_details_expand_btn.collapsed)
def set_error_item(self, error_item): def set_error_item(self, error_item):
detail = "" detail = ""
@ -1589,23 +1644,18 @@ class ErrorDetailsWidget(QtWidgets.QWidget):
description = error_item.description or description description = error_item.description or description
detail = error_item.detail or detail detail = error_item.detail or detail
self._error_details_widget.set_detail(detail)
if commonmark: if commonmark:
self._error_description_input.setHtml( self._error_description_input.setHtml(
commonmark.commonmark(description) commonmark.commonmark(description)
) )
self._error_details_input.setHtml(
commonmark.commonmark(detail)
)
elif hasattr(self._error_details_input, "setMarkdown"): elif hasattr(self._error_description_input, "setMarkdown"):
self._error_description_input.setMarkdown(description) self._error_description_input.setMarkdown(description)
self._error_details_input.setMarkdown(detail)
else: else:
self._error_description_input.setText(description) self._error_description_input.setText(description)
self._error_details_input.setText(detail)
self._error_details_widget.setVisible(bool(detail))
class ReportsWidget(QtWidgets.QWidget): class ReportsWidget(QtWidgets.QWidget):
@ -1622,7 +1672,7 @@ class ReportsWidget(QtWidgets.QWidget):
# Validation errors layout # Publish errors layout
Views Actions Views Actions
Details Details
@ -1641,12 +1691,12 @@ class ReportsWidget(QtWidgets.QWidget):
instances_view = PublishInstancesViewWidget(controller, views_widget) instances_view = PublishInstancesViewWidget(controller, views_widget)
validation_error_view = ValidationErrorsView(views_widget) publish_error_view = PublishErrorsView(views_widget)
views_layout = QtWidgets.QStackedLayout(views_widget) views_layout = QtWidgets.QStackedLayout(views_widget)
views_layout.setContentsMargins(0, 0, 0, 0) views_layout.setContentsMargins(0, 0, 0, 0)
views_layout.addWidget(instances_view) views_layout.addWidget(instances_view)
views_layout.addWidget(validation_error_view) views_layout.addWidget(publish_error_view)
views_layout.setCurrentWidget(instances_view) views_layout.setCurrentWidget(instances_view)
@ -1655,10 +1705,13 @@ class ReportsWidget(QtWidgets.QWidget):
details_widget.setObjectName("PublishInstancesDetails") details_widget.setObjectName("PublishInstancesDetails")
# Actions widget # Actions widget
actions_widget = ValidateActionsWidget(controller, details_widget) actions_widget = PublishActionsWidget(controller, details_widget)
pages_widget = QtWidgets.QWidget(details_widget) pages_widget = QtWidgets.QWidget(details_widget)
# Crash information
crash_widget = CrashWidget(controller, details_widget)
# Logs view # Logs view
logs_view = InstancesLogsView(pages_widget) logs_view = InstancesLogsView(pages_widget)
@ -1671,30 +1724,24 @@ class ReportsWidget(QtWidgets.QWidget):
detail_input_scroll = QtWidgets.QScrollArea(pages_widget) detail_input_scroll = QtWidgets.QScrollArea(pages_widget)
detail_inputs_widget = ErrorDetailsWidget(detail_input_scroll) detail_inputs_widget = PublishFailWidget(detail_input_scroll)
detail_inputs_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) detail_inputs_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
detail_input_scroll.setWidget(detail_inputs_widget) detail_input_scroll.setWidget(detail_inputs_widget)
detail_input_scroll.setWidgetResizable(True) detail_input_scroll.setWidgetResizable(True)
detail_input_scroll.setViewportMargins(0, 0, 0, 0) detail_input_scroll.setViewportMargins(0, 0, 0, 0)
# Crash information
crash_widget = CrashWidget(controller, details_widget)
# Layout pages # Layout pages
pages_layout = QtWidgets.QHBoxLayout(pages_widget) pages_layout = QtWidgets.QHBoxLayout(pages_widget)
pages_layout.setContentsMargins(0, 0, 0, 0) pages_layout.setContentsMargins(0, 0, 0, 0)
pages_layout.addWidget(crash_widget, 1)
pages_layout.addWidget(logs_view, 1) pages_layout.addWidget(logs_view, 1)
pages_layout.addWidget(detail_inputs_spacer, 0) pages_layout.addWidget(detail_inputs_spacer, 0)
pages_layout.addWidget(detail_input_scroll, 1) pages_layout.addWidget(detail_input_scroll, 1)
pages_layout.addWidget(crash_widget, 1)
details_layout = QtWidgets.QVBoxLayout(details_widget) details_layout = QtWidgets.QVBoxLayout(details_widget)
margins = details_layout.contentsMargins() details_layout.setContentsMargins(8, 16, 8, 16)
margins.setTop(margins.top() * 2) details_layout.setSpacing(8)
margins.setBottom(margins.bottom() * 2)
details_layout.setContentsMargins(margins)
details_layout.setSpacing(margins.top())
details_layout.addWidget(actions_widget, 0) details_layout.addWidget(actions_widget, 0)
details_layout.addWidget(pages_widget, 1) details_layout.addWidget(pages_widget, 1)
@ -1704,12 +1751,12 @@ class ReportsWidget(QtWidgets.QWidget):
content_layout.addWidget(details_widget, 1) content_layout.addWidget(details_widget, 1)
instances_view.selection_changed.connect(self._on_instance_selection) instances_view.selection_changed.connect(self._on_instance_selection)
validation_error_view.selection_changed.connect( publish_error_view.selection_changed.connect(
self._on_error_selection) self._on_error_selection)
self._views_layout = views_layout self._views_layout = views_layout
self._instances_view = instances_view self._instances_view = instances_view
self._validation_error_view = validation_error_view self._publish_error_view = publish_error_view
self._actions_widget = actions_widget self._actions_widget = actions_widget
self._detail_inputs_widget = detail_inputs_widget self._detail_inputs_widget = detail_inputs_widget
@ -1720,7 +1767,7 @@ class ReportsWidget(QtWidgets.QWidget):
self._controller: AbstractPublisherFrontend = controller self._controller: AbstractPublisherFrontend = controller
self._validation_errors_by_id = {} self._publish_errors_by_id = {}
def _get_instance_items(self): def _get_instance_items(self):
report = self._controller.get_publish_report() report = self._controller.get_publish_report()
@ -1750,40 +1797,50 @@ class ReportsWidget(QtWidgets.QWidget):
return instance_items return instance_items
def update_data(self): def update_data(self):
view = self._instances_view has_validation_error = self._controller.publish_has_validation_errors()
validation_error_mode = False has_finished = self._controller.publish_has_finished()
if ( has_crashed = self._controller.publish_has_crashed()
not self._controller.publish_has_crashed() error_info = None
and self._controller.publish_has_validation_errors() if has_crashed:
): error_info = self._controller.get_publish_error_info()
view = self._validation_error_view
validation_error_mode = True publish_error_mode = False
if error_info is not None:
publish_error_mode = not error_info.is_unknown_error
elif has_validation_error:
publish_error_mode = True
if publish_error_mode:
view = self._publish_error_view
else:
view = self._instances_view
self._actions_widget.set_visible_mode(validation_error_mode)
self._detail_inputs_spacer.setVisible(validation_error_mode)
self._detail_input_scroll.setVisible(validation_error_mode)
self._views_layout.setCurrentWidget(view) self._views_layout.setCurrentWidget(view)
is_crashed = self._controller.publish_has_crashed() self._actions_widget.set_visible_mode(publish_error_mode)
self._crash_widget.setVisible(is_crashed) self._detail_inputs_spacer.setVisible(publish_error_mode)
self._logs_view.setVisible(not is_crashed) self._detail_input_scroll.setVisible(publish_error_mode)
logs_visible = publish_error_mode or has_finished or not has_crashed
self._logs_view.setVisible(logs_visible)
self._crash_widget.setVisible(not logs_visible)
# Instance view & logs update # Instance view & logs update
instance_items = self._get_instance_items() instance_items = self._get_instance_items()
self._instances_view.update_instances(instance_items) self._instances_view.update_instances(instance_items)
self._logs_view.update_instances(instance_items) self._logs_view.update_instances(instance_items)
# Validation errors # Publish errors
validation_errors = self._controller.get_validation_errors() publish_errors_report = self._controller.get_publish_errors_report()
grouped_error_items = validation_errors.group_items_by_title() grouped_error_items = publish_errors_report.group_items_by_title()
validation_errors_by_id = { publish_errors_by_id = {
title_item["id"]: title_item title_item["id"]: title_item
for title_item in grouped_error_items for title_item in grouped_error_items
} }
self._validation_errors_by_id = validation_errors_by_id self._publish_errors_by_id = publish_errors_by_id
self._validation_error_view.set_errors(grouped_error_items) self._publish_error_view.set_errors(grouped_error_items)
def _on_instance_selection(self): def _on_instance_selection(self):
instance_ids = self._instances_view.get_selected_instance_ids() instance_ids = self._instances_view.get_selected_instance_ids()
@ -1791,8 +1848,8 @@ class ReportsWidget(QtWidgets.QWidget):
def _on_error_selection(self): def _on_error_selection(self):
title_id, instance_ids = ( title_id, instance_ids = (
self._validation_error_view.get_selected_items()) self._publish_error_view.get_selected_items())
error_info = self._validation_errors_by_id.get(title_id) error_info = self._publish_errors_by_id.get(title_id)
if error_info is None: if error_info is None:
self._actions_widget.set_error_info(None) self._actions_widget.set_error_info(None)
self._detail_inputs_widget.set_error_item(None) self._detail_inputs_widget.set_error_item(None)
@ -1820,12 +1877,12 @@ class ReportPageWidget(QtWidgets.QFrame):
2. Publishing is paused. 2. Publishing is paused.
3. Publishing successfully finished. > Instances with logs. 3. Publishing successfully finished. > Instances with logs.
4. Publishing crashed. 4. Publishing crashed.
5. Crashed because of validation error. > Errors with logs. 5. Crashed because of publish error. > Errors with logs.
This widget is shown if validation errors happened during validation part. This widget is shown if publish errors happened.
Shows validation error titles with instances on which they happened Shows publish error titles with instances on which they happened
and validation error detail with possible actions (repair). and publish error detail with possible actions (repair).
""" """
def __init__( def __init__(

View file

@ -540,11 +540,38 @@ class ClassicExpandBtnLabel(ExpandBtnLabel):
right_arrow_path = get_style_image_path("right_arrow") right_arrow_path = get_style_image_path("right_arrow")
down_arrow_path = get_style_image_path("down_arrow") down_arrow_path = get_style_image_path("down_arrow")
def _normalize_pixmap(self, pixmap):
if pixmap.width() == pixmap.height():
return pixmap
width = pixmap.width()
height = pixmap.height()
size = max(width, height)
pos_x = 0
pos_y = 0
if width > height:
pos_y = (size - height) // 2
else:
pos_x = (size - width) // 2
new_pix = QtGui.QPixmap(size, size)
new_pix.fill(QtCore.Qt.transparent)
painter = QtGui.QPainter(new_pix)
render_hints = (
QtGui.QPainter.Antialiasing
| QtGui.QPainter.SmoothPixmapTransform
)
if hasattr(QtGui.QPainter, "HighQualityAntialiasing"):
render_hints |= QtGui.QPainter.HighQualityAntialiasing
painter.setRenderHints(render_hints)
painter.drawPixmap(QtCore.QPoint(pos_x, pos_y), pixmap)
painter.end()
return new_pix
def _create_collapsed_pixmap(self): def _create_collapsed_pixmap(self):
return QtGui.QPixmap(self.right_arrow_path) return self._normalize_pixmap(QtGui.QPixmap(self.right_arrow_path))
def _create_expanded_pixmap(self): def _create_expanded_pixmap(self):
return QtGui.QPixmap(self.down_arrow_path) return self._normalize_pixmap(QtGui.QPixmap(self.down_arrow_path))
class ClassicExpandBtn(ExpandBtn): class ClassicExpandBtn(ExpandBtn):