mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +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
|
||||
}
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,10 @@
|
|||
"enabled": true,
|
||||
"optional": true,
|
||||
"active": true
|
||||
},
|
||||
"ValidateAttributes": {
|
||||
"enabled": false,
|
||||
"attributes": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,25 @@
|
|||
"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.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):
|
||||
|
|
@ -15,6 +39,10 @@ class PublishersModel(BaseSettingsModel):
|
|||
title="Validate Frame Range",
|
||||
section="Validators"
|
||||
)
|
||||
ValidateAttributes: ValidateAttributesModel = Field(
|
||||
default_factory=ValidateAttributesModel,
|
||||
title="Validate Attributes"
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_PUBLISH_SETTINGS = {
|
||||
|
|
@ -22,5 +50,9 @@ DEFAULT_PUBLISH_SETTINGS = {
|
|||
"enabled": True,
|
||||
"optional": 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.
|
||||
:::
|
||||
|
||||
## 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
|
||||
|
|
|
|||
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