diff --git a/openpype/hosts/photoshop/plugins/publish/help/validate_instance_asset.xml b/openpype/hosts/photoshop/plugins/publish/help/validate_instance_asset.xml
new file mode 100644
index 0000000000..3b040e8ea8
--- /dev/null
+++ b/openpype/hosts/photoshop/plugins/publish/help/validate_instance_asset.xml
@@ -0,0 +1,25 @@
+
+
+
+Subset context
+
+## Invalid subset context
+
+Asset name found '{found}' in subsets, expected '{expected}'.
+
+### How to repair?
+
+You can fix this with `Repair` button on the right. This will use '{expected}' asset name and overwrite '{found}' asset name in scene metadata.
+
+After that restart `Publish` with a `Reload button`.
+
+If this is unwanted, close workfile and open again, that way different asset value would be used for context information.
+
+
+### __Detailed Info__ (optional)
+
+This might happen if you are reuse old workfile and open it in different context.
+(Eg. you created subset "renderCompositingDefault" from asset "Robot' in "your_project_Robot_compositing.aep", now you opened this workfile in a context "Sloth" but existing subset for "Robot" asset stayed in the workfile.)
+
+
+
\ No newline at end of file
diff --git a/openpype/hosts/photoshop/plugins/publish/help/validate_naming.xml b/openpype/hosts/photoshop/plugins/publish/help/validate_naming.xml
new file mode 100644
index 0000000000..21a7370340
--- /dev/null
+++ b/openpype/hosts/photoshop/plugins/publish/help/validate_naming.xml
@@ -0,0 +1,21 @@
+
+
+
+Invalid name
+
+## Invalid name of subset
+
+Name of subset is created from a layer name. Some characters (whitespace, '/' etc.) are not allowed because of publishing (files couldn't be saved on some OSes).
+
+### How to repair?
+
+You can fix this with `Repair` button on the right. This will remove invalid characters with safe character ('_' by default) in both subset names and matching group names.
+
+After that restart `Publish` with a `Reload button`.
+
+Or you use `Subset Manager` to delete existing subsets, remove created groups, rename layers that are used for their creation and use `Create` option in the Openpype menu to create them again.
+
+Invalid characters and 'safe character' could be configured in Settings. Ask your OpenPype admin to modify them if necessary.
+
+
+
\ No newline at end of file
diff --git a/openpype/hosts/photoshop/plugins/publish/help/validate_unique_subsets.xml b/openpype/hosts/photoshop/plugins/publish/help/validate_unique_subsets.xml
new file mode 100644
index 0000000000..fa7c76a2dd
--- /dev/null
+++ b/openpype/hosts/photoshop/plugins/publish/help/validate_unique_subsets.xml
@@ -0,0 +1,23 @@
+
+
+
+Subsets duplicated
+
+## Some subsets are duplicated
+
+Created subsets must be unique.
+
+Subsets '{duplicates_str}' are duplicated.
+
+### How to repair?
+
+Use `Subset Manager` to delete duplicated subset to have only unique subset names and restart `Publish` with a `Reload button`.
+
+
+### __Detailed Info__ (optional)
+
+Subset names are created from layer names. Layer names are filtered for characters that would break publishing process when files are created.
+This replacement process might result in duplicate names of subsets.
+
+
+
\ No newline at end of file
diff --git a/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py b/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py
index 4dc1972074..8f13cc6b33 100644
--- a/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py
+++ b/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py
@@ -1,8 +1,10 @@
from avalon import api
import pyblish.api
-import openpype.api
from avalon import photoshop
+import openpype.api
+from openpype.pipeline import PublishXmlValidationError
+
class ValidateInstanceAssetRepair(pyblish.api.Action):
"""Repair the instance asset."""
@@ -56,4 +58,10 @@ class ValidateInstanceAsset(pyblish.api.InstancePlugin):
f"If that's not correct value, close workfile and "
f"reopen via Workfiles!"
)
- assert instance_asset == current_asset, msg
+ formatting_data = {
+ "found": instance_asset,
+ "expected": current_asset
+ }
+ if instance_asset != current_asset:
+ raise PublishXmlValidationError(self, msg,
+ formatting_data=formatting_data)
diff --git a/openpype/hosts/photoshop/plugins/publish/validate_naming.py b/openpype/hosts/photoshop/plugins/publish/validate_naming.py
index 1635096f4b..d548992f09 100644
--- a/openpype/hosts/photoshop/plugins/publish/validate_naming.py
+++ b/openpype/hosts/photoshop/plugins/publish/validate_naming.py
@@ -1,9 +1,11 @@
import re
import pyblish.api
-import openpype.api
from avalon import photoshop
+import openpype.api
+from openpype.pipeline import PublishXmlValidationError
+
class ValidateNamingRepair(pyblish.api.Action):
"""Repair the instance asset."""
@@ -69,14 +71,18 @@ class ValidateNaming(pyblish.api.InstancePlugin):
replace_char = ''
def process(self, instance):
- help_msg = ' Use Repair action (A) in Pyblish to fix it.'
- msg = "Name \"{}\" is not allowed.{}".format(instance.data["name"],
- help_msg)
- assert not re.search(self.invalid_chars, instance.data["name"]), msg
+ msg = "Name \"{}\" is not allowed.".format(instance.data["name"])
- msg = "Subset \"{}\" is not allowed.{}".format(instance.data["subset"],
- help_msg)
- assert not re.search(self.invalid_chars, instance.data["subset"]), msg
+ formatting_data = {"error_msg": msg}
+ if re.search(self.invalid_chars, instance.data["name"]):
+ raise PublishXmlValidationError(self, msg,
+ formatting_data=formatting_data)
+
+ msg = "Subset \"{}\" is not allowed.".format(instance.data["subset"])
+ formatting_data = {"error_msg": msg}
+ if re.search(self.invalid_chars, instance.data["subset"]):
+ raise PublishXmlValidationError(self, msg,
+ formatting_data=formatting_data)
@classmethod
def get_replace_chars(cls):
diff --git a/openpype/hosts/photoshop/plugins/publish/validate_unique_subsets.py b/openpype/hosts/photoshop/plugins/publish/validate_unique_subsets.py
index 15ae5fbcea..d41fefa971 100644
--- a/openpype/hosts/photoshop/plugins/publish/validate_unique_subsets.py
+++ b/openpype/hosts/photoshop/plugins/publish/validate_unique_subsets.py
@@ -1,5 +1,8 @@
+import collections
+
import pyblish.api
import openpype.api
+from openpype.pipeline import PublishXmlValidationError
class ValidateSubsetUniqueness(pyblish.api.ContextPlugin):
@@ -19,8 +22,18 @@ class ValidateSubsetUniqueness(pyblish.api.ContextPlugin):
if instance.data.get('publish'):
subset_names.append(instance.data.get('subset'))
- msg = (
- "Instance subset names are not unique. " +
- "Remove duplicates via SubsetManager."
- )
- assert len(subset_names) == len(set(subset_names)), msg
+ duplicates = [item
+ for item, count in
+ collections.Counter(subset_names).items()
+ if count > 1]
+
+ if duplicates:
+ duplicates_str = ",".join(duplicates)
+ formatting_data = {"duplicates_str": duplicates_str}
+ msg = (
+ "Instance subset names {} are not unique.".format(
+ duplicates_str) +
+ " Remove duplicates via SubsetManager."
+ )
+ raise PublishXmlValidationError(self, msg,
+ formatting_data=formatting_data)
diff --git a/openpype/pipeline/__init__.py b/openpype/pipeline/__init__.py
index e968df4011..79d6ce4d54 100644
--- a/openpype/pipeline/__init__.py
+++ b/openpype/pipeline/__init__.py
@@ -9,6 +9,7 @@ from .create import (
from .publish import (
PublishValidationError,
+ PublishXmlValidationError,
KnownPublishError,
OpenPypePyblishPluginMixin
)
@@ -23,6 +24,7 @@ __all__ = (
"CreatedInstance",
"PublishValidationError",
+ "PublishXmlValidationError",
"KnownPublishError",
"OpenPypePyblishPluginMixin"
)
diff --git a/openpype/pipeline/publish/__init__.py b/openpype/pipeline/publish/__init__.py
index ca958816fe..228c4d8dcb 100644
--- a/openpype/pipeline/publish/__init__.py
+++ b/openpype/pipeline/publish/__init__.py
@@ -1,20 +1,26 @@
from .publish_plugins import (
PublishValidationError,
+ PublishXmlValidationError,
KnownPublishError,
OpenPypePyblishPluginMixin
)
from .lib import (
DiscoverResult,
- publish_plugins_discover
+ publish_plugins_discover,
+ load_help_content_from_plugin,
+ load_help_content_from_filepath
)
__all__ = (
"PublishValidationError",
+ "PublishXmlValidationError",
"KnownPublishError",
"OpenPypePyblishPluginMixin",
"DiscoverResult",
- "publish_plugins_discover"
+ "publish_plugins_discover",
+ "load_help_content_from_plugin",
+ "load_help_content_from_filepath"
)
diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py
index 0fa712a301..f38e73afe2 100644
--- a/openpype/pipeline/publish/lib.py
+++ b/openpype/pipeline/publish/lib.py
@@ -1,6 +1,8 @@
import os
import sys
import types
+import inspect
+import xml.etree.ElementTree
import six
import pyblish.plugin
@@ -28,6 +30,61 @@ class DiscoverResult:
self.plugins[item] = value
+class HelpContent:
+ def __init__(self, title, description, detail=None):
+ self.title = title
+ self.description = description
+ self.detail = detail
+
+
+def load_help_content_from_filepath(filepath):
+ """Load help content from xml file.
+
+ Xml file may containt errors and warnings.
+ """
+ errors = {}
+ warnings = {}
+ output = {
+ "errors": errors,
+ "warnings": warnings
+ }
+ if not os.path.exists(filepath):
+ return output
+ tree = xml.etree.ElementTree.parse(filepath)
+ root = tree.getroot()
+ for child in root:
+ child_id = child.attrib.get("id")
+ if child_id is None:
+ continue
+
+ # Make sure ID is string
+ child_id = str(child_id)
+
+ title = child.find("title").text
+ description = child.find("description").text
+ detail_node = child.find("detail")
+ detail = None
+ if detail_node is not None:
+ detail = detail_node.text
+ if child.tag == "error":
+ errors[child_id] = HelpContent(title, description, detail)
+ elif child.tag == "warning":
+ warnings[child_id] = HelpContent(title, description, detail)
+ return output
+
+
+def load_help_content_from_plugin(plugin):
+ cls = plugin
+ if not inspect.isclass(plugin):
+ cls = plugin.__class__
+ plugin_filepath = inspect.getfile(cls)
+ plugin_dir = os.path.dirname(plugin_filepath)
+ basename = os.path.splitext(os.path.basename(plugin_filepath))[0]
+ filename = basename + ".xml"
+ filepath = os.path.join(plugin_dir, "help", filename)
+ return load_help_content_from_filepath(filepath)
+
+
def publish_plugins_discover(paths=None):
"""Find and return available pyblish plug-ins
diff --git a/openpype/pipeline/publish/publish_plugins.py b/openpype/pipeline/publish/publish_plugins.py
index b60b9f43a7..bce64ec709 100644
--- a/openpype/pipeline/publish/publish_plugins.py
+++ b/openpype/pipeline/publish/publish_plugins.py
@@ -1,3 +1,6 @@
+from .lib import load_help_content_from_plugin
+
+
class PublishValidationError(Exception):
"""Validation error happened during publishing.
@@ -12,13 +15,34 @@ class PublishValidationError(Exception):
description(str): Detailed description of an error. It is possible
to use Markdown syntax.
"""
- def __init__(self, message, title=None, description=None):
+ def __init__(self, message, title=None, description=None, detail=None):
self.message = message
self.title = title or "< Missing title >"
self.description = description or message
+ self.detail = detail
super(PublishValidationError, self).__init__(message)
+class PublishXmlValidationError(PublishValidationError):
+ def __init__(
+ self, plugin, message, key=None, formatting_data=None
+ ):
+ if key is None:
+ key = "main"
+
+ if not formatting_data:
+ formatting_data = {}
+ result = load_help_content_from_plugin(plugin)
+ content_obj = result["errors"][key]
+ description = content_obj.description.format(**formatting_data)
+ detail = content_obj.detail
+ if detail:
+ detail = detail.format(**formatting_data)
+ super(PublishXmlValidationError, self).__init__(
+ message, content_obj.title, description, detail
+ )
+
+
class KnownPublishError(Exception):
"""Publishing crashed because of known error.