mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-01 16:34:53 +01:00
Merge pull request #5824 from ynput/enhancement/OP-7071_Validate-Attributes
This commit is contained in:
commit
20eaa0b83e
8 changed files with 222 additions and 3 deletions
131
openpype/hosts/max/plugins/publish/validate_attributes.py
Normal file
131
openpype/hosts/max/plugins/publish/validate_attributes.py
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Validator for Attributes."""
|
||||||
|
from pyblish.api import ContextPlugin, ValidatorOrder
|
||||||
|
from pymxs import runtime as rt
|
||||||
|
|
||||||
|
from openpype.pipeline.publish import (
|
||||||
|
OptionalPyblishPluginMixin,
|
||||||
|
PublishValidationError,
|
||||||
|
RepairContextAction
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def has_property(object_name, property_name):
|
||||||
|
"""Return whether an object has a property with given name"""
|
||||||
|
return rt.Execute(f'isProperty {object_name} "{property_name}"')
|
||||||
|
|
||||||
|
|
||||||
|
def is_matching_value(object_name, property_name, value):
|
||||||
|
"""Return whether an existing property matches value `value"""
|
||||||
|
property_value = rt.Execute(f"{object_name}.{property_name}")
|
||||||
|
|
||||||
|
# Wrap property value if value is a string valued attributes
|
||||||
|
# starting with a `#`
|
||||||
|
if (
|
||||||
|
isinstance(value, str) and
|
||||||
|
value.startswith("#") and
|
||||||
|
not value.endswith(")")
|
||||||
|
):
|
||||||
|
# prefix value with `#`
|
||||||
|
# not applicable for #() array value type
|
||||||
|
# and only applicable for enum i.e. #bob, #sally
|
||||||
|
property_value = f"#{property_value}"
|
||||||
|
|
||||||
|
return property_value == value
|
||||||
|
|
||||||
|
|
||||||
|
class ValidateAttributes(OptionalPyblishPluginMixin,
|
||||||
|
ContextPlugin):
|
||||||
|
"""Validates attributes in the project setting are consistent
|
||||||
|
with the nodes from MaxWrapper Class in 3ds max.
|
||||||
|
E.g. "renderers.current.separateAovFiles",
|
||||||
|
"renderers.production.PrimaryGIEngine"
|
||||||
|
Admin(s) need to put the dict below and enable this validator for a check:
|
||||||
|
{
|
||||||
|
"renderers.current":{
|
||||||
|
"separateAovFiles" : True
|
||||||
|
},
|
||||||
|
"renderers.production":{
|
||||||
|
"PrimaryGIEngine": "#RS_GIENGINE_BRUTE_FORCE"
|
||||||
|
}
|
||||||
|
....
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
order = ValidatorOrder
|
||||||
|
hosts = ["max"]
|
||||||
|
label = "Attributes"
|
||||||
|
actions = [RepairContextAction]
|
||||||
|
optional = True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_invalid(cls, context):
|
||||||
|
attributes = (
|
||||||
|
context.data["project_settings"]["max"]["publish"]
|
||||||
|
["ValidateAttributes"]["attributes"]
|
||||||
|
)
|
||||||
|
if not attributes:
|
||||||
|
return
|
||||||
|
invalid = []
|
||||||
|
for object_name, required_properties in attributes.items():
|
||||||
|
if not rt.Execute(f"isValidValue {object_name}"):
|
||||||
|
# Skip checking if the node does not
|
||||||
|
# exist in MaxWrapper Class
|
||||||
|
cls.log.debug(f"Unable to find '{object_name}'."
|
||||||
|
" Skipping validation of attributes.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
for property_name, value in required_properties.items():
|
||||||
|
if not has_property(object_name, property_name):
|
||||||
|
cls.log.error(
|
||||||
|
"Non-existing property: "
|
||||||
|
f"{object_name}.{property_name}")
|
||||||
|
invalid.append((object_name, property_name))
|
||||||
|
|
||||||
|
if not is_matching_value(object_name, property_name, value):
|
||||||
|
cls.log.error(
|
||||||
|
f"Invalid value for: {object_name}.{property_name}"
|
||||||
|
f" should be: {value}")
|
||||||
|
invalid.append((object_name, property_name))
|
||||||
|
|
||||||
|
return invalid
|
||||||
|
|
||||||
|
def process(self, context):
|
||||||
|
if not self.is_active(context.data):
|
||||||
|
self.log.debug("Skipping Validate Attributes...")
|
||||||
|
return
|
||||||
|
invalid_attributes = self.get_invalid(context)
|
||||||
|
if invalid_attributes:
|
||||||
|
bullet_point_invalid_statement = "\n".join(
|
||||||
|
"- {}".format(invalid) for invalid
|
||||||
|
in invalid_attributes
|
||||||
|
)
|
||||||
|
report = (
|
||||||
|
"Required Attribute(s) have invalid value(s).\n\n"
|
||||||
|
f"{bullet_point_invalid_statement}\n\n"
|
||||||
|
"You can use repair action to fix them if they are not\n"
|
||||||
|
"unknown property value(s)."
|
||||||
|
)
|
||||||
|
raise PublishValidationError(
|
||||||
|
report, title="Invalid Value(s) for Required Attribute(s)")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def repair(cls, context):
|
||||||
|
attributes = (
|
||||||
|
context.data["project_settings"]["max"]["publish"]
|
||||||
|
["ValidateAttributes"]["attributes"]
|
||||||
|
)
|
||||||
|
invalid_attributes = cls.get_invalid(context)
|
||||||
|
for attrs in invalid_attributes:
|
||||||
|
prop, attr = attrs
|
||||||
|
value = attributes[prop][attr]
|
||||||
|
if isinstance(value, str) and not value.startswith("#"):
|
||||||
|
attribute_fix = '{}.{}="{}"'.format(
|
||||||
|
prop, attr, value
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
attribute_fix = "{}.{}={}".format(
|
||||||
|
prop, attr, value
|
||||||
|
)
|
||||||
|
rt.Execute(attribute_fix)
|
||||||
|
|
@ -639,6 +639,15 @@ def _convert_3dsmax_project_settings(ayon_settings, output):
|
||||||
for item in point_cloud_attribute
|
for item in point_cloud_attribute
|
||||||
}
|
}
|
||||||
ayon_max["PointCloud"]["attribute"] = new_point_cloud_attribute
|
ayon_max["PointCloud"]["attribute"] = new_point_cloud_attribute
|
||||||
|
# --- Publish (START) ---
|
||||||
|
ayon_publish = ayon_max["publish"]
|
||||||
|
try:
|
||||||
|
attributes = json.loads(
|
||||||
|
ayon_publish["ValidateAttributes"]["attributes"]
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
attributes = {}
|
||||||
|
ayon_publish["ValidateAttributes"]["attributes"] = attributes
|
||||||
|
|
||||||
output["max"] = ayon_max
|
output["max"] = ayon_max
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,10 @@
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"active": true
|
"active": true
|
||||||
|
},
|
||||||
|
"ValidateAttributes": {
|
||||||
|
"enabled": false,
|
||||||
|
"attributes": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,25 @@
|
||||||
"label": "Active"
|
"label": "Active"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "dict",
|
||||||
|
"collapsible": true,
|
||||||
|
"key": "ValidateAttributes",
|
||||||
|
"label": "ValidateAttributes",
|
||||||
|
"checkbox_key": "enabled",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"key": "enabled",
|
||||||
|
"label": "Enabled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "raw-json",
|
||||||
|
"key": "attributes",
|
||||||
|
"label": "Attributes"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,30 @@
|
||||||
from pydantic import Field
|
import json
|
||||||
|
from pydantic import Field, validator
|
||||||
|
|
||||||
from ayon_server.settings import BaseSettingsModel
|
from ayon_server.settings import BaseSettingsModel
|
||||||
|
from ayon_server.exceptions import BadRequestException
|
||||||
|
|
||||||
|
|
||||||
|
class ValidateAttributesModel(BaseSettingsModel):
|
||||||
|
enabled: bool = Field(title="ValidateAttributes")
|
||||||
|
attributes: str = Field(
|
||||||
|
"{}", title="Attributes", widget="textarea")
|
||||||
|
|
||||||
|
@validator("attributes")
|
||||||
|
def validate_json(cls, value):
|
||||||
|
if not value.strip():
|
||||||
|
return "{}"
|
||||||
|
try:
|
||||||
|
converted_value = json.loads(value)
|
||||||
|
success = isinstance(converted_value, dict)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
success = False
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
raise BadRequestException(
|
||||||
|
"The attibutes can't be parsed as json object"
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
class BasicValidateModel(BaseSettingsModel):
|
class BasicValidateModel(BaseSettingsModel):
|
||||||
|
|
@ -15,6 +39,10 @@ class PublishersModel(BaseSettingsModel):
|
||||||
title="Validate Frame Range",
|
title="Validate Frame Range",
|
||||||
section="Validators"
|
section="Validators"
|
||||||
)
|
)
|
||||||
|
ValidateAttributes: ValidateAttributesModel = Field(
|
||||||
|
default_factory=ValidateAttributesModel,
|
||||||
|
title="Validate Attributes"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_PUBLISH_SETTINGS = {
|
DEFAULT_PUBLISH_SETTINGS = {
|
||||||
|
|
@ -22,5 +50,9 @@ DEFAULT_PUBLISH_SETTINGS = {
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"optional": True,
|
"optional": True,
|
||||||
"active": True
|
"active": True
|
||||||
}
|
},
|
||||||
|
"ValidateAttributes": {
|
||||||
|
"enabled": False,
|
||||||
|
"attributes": "{}"
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
__version__ = "0.1.0"
|
__version__ = "0.1.1"
|
||||||
|
|
|
||||||
|
|
@ -118,4 +118,28 @@ Current OpenPype integration (ver 3.15.0) supports only ```PointCache```, ```Ca
|
||||||
This part of documentation is still work in progress.
|
This part of documentation is still work in progress.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
## Validators
|
||||||
|
|
||||||
|
Current Openpype integration supports different validators such as Frame Range and Attributes.
|
||||||
|
Some validators are mandatory while some are optional and user can choose to enable them in the setting.
|
||||||
|
|
||||||
|
**Validate Frame Range**: Optional Validator for checking Frame Range
|
||||||
|
|
||||||
|
**Validate Attributes**: Optional Validator for checking if object properties' attributes are valid
|
||||||
|
in MaxWrapper Class.
|
||||||
|
:::note
|
||||||
|
Users can write the properties' attributes they want to check in dict format in the setting
|
||||||
|
before validation. The attributes are then to be converted into Maxscript and do a check.
|
||||||
|
E.g. ```renderers.current.separateAovFiles``` and ```renderers.current.PrimaryGIEngine```
|
||||||
|
User can put the attributes in the dict format below
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"renderer.current":{
|
||||||
|
"separateAovFiles" : True
|
||||||
|
"PrimaryGIEngine": "#RS_GIENGINE_BRUTE_FORCE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|

|
||||||
|
:::
|
||||||
## ...to be added
|
## ...to be added
|
||||||
|
|
|
||||||
BIN
website/docs/assets/3dsmax_validate_attributes.png
Normal file
BIN
website/docs/assets/3dsmax_validate_attributes.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
Loading…
Add table
Add a link
Reference in a new issue