Skip to content
This repository has been archived by the owner on Sep 20, 2024. It is now read-only.

Max: Implement Validator for Properties/Attributes Value Check #5824

Merged
merged 16 commits into from
Nov 6, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions openpype/hosts/max/plugins/publish/validate_attributes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# -*- 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 json below and enable this validator for a check:
{
"renderers.current":{
"separateAovFiles" : True
}
"renderers.production":{
"PrimaryGIEngine": "#RS_GIENGINE_BRUTE_FORCE",
moonyuet marked this conversation as resolved.
Show resolved Hide resolved
}
....
}

"""

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
continue
moonyuet marked this conversation as resolved.
Show resolved Hide resolved

for property_name, value in required_properties.items():
if not has_property(object_name, property_name):
cls.log.error(f"Non-existing property: {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}. 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)"
moonyuet marked this conversation as resolved.
Show resolved Hide resolved
)
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)
9 changes: 9 additions & 0 deletions openpype/settings/ayon_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions openpype/settings/defaults/project_settings/max.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@
"enabled": true,
"optional": true,
"active": true
},
"ValidateAttributes": {
"enabled": false,
"attributes": {}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
]
}
36 changes: 34 additions & 2 deletions server_addon/max/server/settings/publishers.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -15,12 +39,20 @@ class PublishersModel(BaseSettingsModel):
title="Validate Frame Range",
section="Validators"
)
ValidateAttributes: ValidateAttributesModel = Field(
default_factory=ValidateAttributesModel,
title="Validate Attributes"
)


DEFAULT_PUBLISH_SETTINGS = {
"ValidateFrameRange": {
"enabled": True,
"optional": True,
"active": True
}
},
"ValidateAttributes": {
"enabled": False,
"attributes": "{}"
},
}