diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py
index 225c4dd2d5..c5c192f51b 100644
--- a/openpype/lib/applications.py
+++ b/openpype/lib/applications.py
@@ -263,14 +263,32 @@ class Application:
class ApplicationManager:
- def __init__(self):
- self.log = PypeLogger().get_logger(self.__class__.__name__)
+ """Load applications and tools and store them by their full name.
+
+ Args:
+ system_settings (dict): Preloaded system settings. When passed manager
+ will always use these values. Gives ability to create manager
+ using different settings.
+ """
+ def __init__(self, system_settings=None):
+ self.log = PypeLogger.get_logger(self.__class__.__name__)
self.app_groups = {}
self.applications = {}
self.tool_groups = {}
self.tools = {}
+ self._system_settings = system_settings
+
+ self.refresh()
+
+ def set_system_settings(self, system_settings):
+ """Ability to change init system settings.
+
+ This will trigger refresh of manager.
+ """
+ self._system_settings = system_settings
+
self.refresh()
def refresh(self):
@@ -280,9 +298,12 @@ class ApplicationManager:
self.tool_groups.clear()
self.tools.clear()
- settings = get_system_settings(
- clear_metadata=False, exclude_locals=False
- )
+ if self._system_settings is not None:
+ settings = copy.deepcopy(self._system_settings)
+ else:
+ settings = get_system_settings(
+ clear_metadata=False, exclude_locals=False
+ )
app_defs = settings["applications"]
for group_name, variant_defs in app_defs.items():
diff --git a/openpype/lib/log.py b/openpype/lib/log.py
index 9745279e28..39b6c67080 100644
--- a/openpype/lib/log.py
+++ b/openpype/lib/log.py
@@ -123,6 +123,8 @@ class PypeFormatter(logging.Formatter):
if record.exc_info is not None:
line_len = len(str(record.exc_info[1]))
+ if line_len > 30:
+ line_len = 30
out = "{}\n{}\n{}\n{}\n{}".format(
out,
line_len * "=",
diff --git a/openpype/modules/ftrack/event_handlers_server/action_prepare_project.py b/openpype/modules/ftrack/event_handlers_server/action_prepare_project.py
index 8248bf532e..12d687bbf2 100644
--- a/openpype/modules/ftrack/event_handlers_server/action_prepare_project.py
+++ b/openpype/modules/ftrack/event_handlers_server/action_prepare_project.py
@@ -2,9 +2,9 @@ import json
from openpype.api import ProjectSettings
-from openpype.modules.ftrack.lib import ServerAction
-from openpype.modules.ftrack.lib.avalon_sync import (
- get_pype_attr,
+from openpype.modules.ftrack.lib import (
+ ServerAction,
+ get_openpype_attr,
CUST_ATTR_AUTO_SYNC
)
@@ -159,7 +159,7 @@ class PrepareProjectServer(ServerAction):
for key, entity in project_anatom_settings["attributes"].items():
attribute_values_by_key[key] = entity.value
- cust_attrs, hier_cust_attrs = get_pype_attr(self.session, True)
+ cust_attrs, hier_cust_attrs = get_openpype_attr(self.session, True)
for attr in hier_cust_attrs:
key = attr["key"]
diff --git a/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py b/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py
index 347b227dd3..3bb01798e4 100644
--- a/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py
+++ b/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py
@@ -18,12 +18,15 @@ from avalon import schema
from avalon.api import AvalonMongoDB
from openpype.modules.ftrack.lib import (
+ get_openpype_attr,
+ CUST_ATTR_ID_KEY,
+ CUST_ATTR_AUTO_SYNC,
+
avalon_sync,
+
BaseEvent
)
from openpype.modules.ftrack.lib.avalon_sync import (
- CUST_ATTR_ID_KEY,
- CUST_ATTR_AUTO_SYNC,
EntitySchemas
)
@@ -125,7 +128,7 @@ class SyncToAvalonEvent(BaseEvent):
@property
def avalon_cust_attrs(self):
if self._avalon_cust_attrs is None:
- self._avalon_cust_attrs = avalon_sync.get_pype_attr(
+ self._avalon_cust_attrs = get_openpype_attr(
self.process_session, query_keys=self.cust_attr_query_keys
)
return self._avalon_cust_attrs
diff --git a/openpype/modules/ftrack/event_handlers_user/action_clean_hierarchical_attributes.py b/openpype/modules/ftrack/event_handlers_user/action_clean_hierarchical_attributes.py
index c326c56a7c..45cc9adf55 100644
--- a/openpype/modules/ftrack/event_handlers_user/action_clean_hierarchical_attributes.py
+++ b/openpype/modules/ftrack/event_handlers_user/action_clean_hierarchical_attributes.py
@@ -1,7 +1,10 @@
import collections
import ftrack_api
-from openpype.modules.ftrack.lib import BaseAction, statics_icon
-from openpype.modules.ftrack.lib.avalon_sync import get_pype_attr
+from openpype.modules.ftrack.lib import (
+ BaseAction,
+ statics_icon,
+ get_openpype_attr
+)
class CleanHierarchicalAttrsAction(BaseAction):
@@ -52,7 +55,7 @@ class CleanHierarchicalAttrsAction(BaseAction):
)
entity_ids_joined = ", ".join(all_entities_ids)
- attrs, hier_attrs = get_pype_attr(session)
+ attrs, hier_attrs = get_openpype_attr(session)
for attr in hier_attrs:
configuration_key = attr["key"]
diff --git a/openpype/modules/ftrack/event_handlers_user/action_create_cust_attrs.py b/openpype/modules/ftrack/event_handlers_user/action_create_cust_attrs.py
index 63025d35b3..63605eda5e 100644
--- a/openpype/modules/ftrack/event_handlers_user/action_create_cust_attrs.py
+++ b/openpype/modules/ftrack/event_handlers_user/action_create_cust_attrs.py
@@ -2,10 +2,20 @@ import collections
import json
import arrow
import ftrack_api
-from openpype.modules.ftrack.lib import BaseAction, statics_icon
-from openpype.modules.ftrack.lib.avalon_sync import (
- CUST_ATTR_ID_KEY, CUST_ATTR_GROUP, default_custom_attributes_definition
+from openpype.modules.ftrack.lib import (
+ BaseAction,
+ statics_icon,
+
+ CUST_ATTR_ID_KEY,
+ CUST_ATTR_GROUP,
+ CUST_ATTR_TOOLS,
+ CUST_ATTR_APPLICATIONS,
+
+ default_custom_attributes_definition,
+ app_definitions_from_app_manager,
+ tool_definitions_from_app_manager
)
+
from openpype.api import get_system_settings
from openpype.lib import ApplicationManager
@@ -370,24 +380,12 @@ class CustomAttributes(BaseAction):
exc_info=True
)
- def app_defs_from_app_manager(self):
- app_definitions = []
- for app_name, app in self.app_manager.applications.items():
- if app.enabled and app.is_host:
- app_definitions.append({
- app_name: app.full_label
- })
-
- if not app_definitions:
- app_definitions.append({"empty": "< Empty >"})
- return app_definitions
-
def applications_attribute(self, event):
- apps_data = self.app_defs_from_app_manager()
+ apps_data = app_definitions_from_app_manager(self.app_manager)
applications_custom_attr_data = {
"label": "Applications",
- "key": "applications",
+ "key": CUST_ATTR_APPLICATIONS,
"type": "enumerator",
"entity_type": "show",
"group": CUST_ATTR_GROUP,
@@ -399,19 +397,11 @@ class CustomAttributes(BaseAction):
self.process_attr_data(applications_custom_attr_data, event)
def tools_attribute(self, event):
- tools_data = []
- for tool_name, tool in self.app_manager.tools.items():
- tools_data.append({
- tool_name: tool.label
- })
-
- # Make sure there is at least one item
- if not tools_data:
- tools_data.append({"empty": "< Empty >"})
+ tools_data = tool_definitions_from_app_manager(self.app_manager)
tools_custom_attr_data = {
"label": "Tools",
- "key": "tools_env",
+ "key": CUST_ATTR_TOOLS,
"type": "enumerator",
"is_hierarchical": True,
"group": CUST_ATTR_GROUP,
diff --git a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py
index bd25f995fe..5298c06371 100644
--- a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py
+++ b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py
@@ -4,10 +4,8 @@ from openpype.api import ProjectSettings
from openpype.modules.ftrack.lib import (
BaseAction,
- statics_icon
-)
-from openpype.modules.ftrack.lib.avalon_sync import (
- get_pype_attr,
+ statics_icon,
+ get_openpype_attr,
CUST_ATTR_AUTO_SYNC
)
@@ -162,7 +160,7 @@ class PrepareProjectLocal(BaseAction):
for key, entity in project_anatom_settings["attributes"].items():
attribute_values_by_key[key] = entity.value
- cust_attrs, hier_cust_attrs = get_pype_attr(self.session, True)
+ cust_attrs, hier_cust_attrs = get_openpype_attr(self.session, True)
for attr in hier_cust_attrs:
key = attr["key"]
diff --git a/openpype/modules/ftrack/ftrack_module.py b/openpype/modules/ftrack/ftrack_module.py
index e639e1a634..af578de86b 100644
--- a/openpype/modules/ftrack/ftrack_module.py
+++ b/openpype/modules/ftrack/ftrack_module.py
@@ -1,4 +1,5 @@
import os
+import json
import collections
from abc import ABCMeta, abstractmethod
import six
@@ -11,6 +12,7 @@ from openpype.modules import (
ILaunchHookPaths,
ISettingsChangeListener
)
+from openpype.settings import SaveWarningExc
FTRACK_MODULE_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -121,10 +123,86 @@ class FtrackModule(
if self.tray_module:
self.tray_module.stop_timer_manager()
- def on_system_settings_save(self, *_args, **_kwargs):
+ def on_system_settings_save(
+ self, old_value, new_value, changes, new_value_metadata
+ ):
"""Implementation of ISettingsChangeListener interface."""
- # Ignore
- return
+ try:
+ session = self.create_ftrack_session()
+ except Exception:
+ self.log.warning("Couldn't create ftrack session.", exc_info=True)
+ raise SaveWarningExc((
+ "Saving of attributes to ftrack wasn't successful,"
+ " try running Create/Update Avalon Attributes in ftrack."
+ ))
+
+ from .lib import (
+ get_openpype_attr,
+ CUST_ATTR_APPLICATIONS,
+ CUST_ATTR_TOOLS,
+ app_definitions_from_app_manager,
+ tool_definitions_from_app_manager
+ )
+ from openpype.api import ApplicationManager
+ query_keys = [
+ "id",
+ "key",
+ "config"
+ ]
+ custom_attributes = get_openpype_attr(
+ session,
+ split_hierarchical=False,
+ query_keys=query_keys
+ )
+ app_attribute = None
+ tool_attribute = None
+ for custom_attribute in custom_attributes:
+ key = custom_attribute["key"]
+ if key == CUST_ATTR_APPLICATIONS:
+ app_attribute = custom_attribute
+ elif key == CUST_ATTR_TOOLS:
+ tool_attribute = custom_attribute
+
+ app_manager = ApplicationManager(new_value_metadata)
+ missing_attributes = []
+ if not app_attribute:
+ missing_attributes.append(CUST_ATTR_APPLICATIONS)
+ else:
+ config = json.loads(app_attribute["config"])
+ new_data = app_definitions_from_app_manager(app_manager)
+ prepared_data = []
+ for item in new_data:
+ for key, label in item.items():
+ prepared_data.append({
+ "menu": label,
+ "value": key
+ })
+
+ config["data"] = json.dumps(prepared_data)
+ app_attribute["config"] = json.dumps(config)
+
+ if not tool_attribute:
+ missing_attributes.append(CUST_ATTR_TOOLS)
+ else:
+ config = json.loads(tool_attribute["config"])
+ new_data = tool_definitions_from_app_manager(app_manager)
+ prepared_data = []
+ for item in new_data:
+ for key, label in item.items():
+ prepared_data.append({
+ "menu": label,
+ "value": key
+ })
+ config["data"] = json.dumps(prepared_data)
+ tool_attribute["config"] = json.dumps(config)
+
+ session.commit()
+
+ if missing_attributes:
+ raise SaveWarningExc((
+ "Couldn't find custom attribute/s ({}) to update."
+ " Try running Create/Update Avalon Attributes in ftrack."
+ ).format(", ".join(missing_attributes)))
def on_project_settings_save(self, *_args, **_kwargs):
"""Implementation of ISettingsChangeListener interface."""
@@ -132,7 +210,7 @@ class FtrackModule(
return
def on_project_anatomy_save(
- self, old_value, new_value, changes, project_name
+ self, old_value, new_value, changes, project_name, new_value_metadata
):
"""Implementation of ISettingsChangeListener interface."""
if not project_name:
@@ -143,32 +221,49 @@ class FtrackModule(
return
import ftrack_api
- from openpype.modules.ftrack.lib import avalon_sync
+ from openpype.modules.ftrack.lib import get_openpype_attr
+
+ try:
+ session = self.create_ftrack_session()
+ except Exception:
+ self.log.warning("Couldn't create ftrack session.", exc_info=True)
+ raise SaveWarningExc((
+ "Saving of attributes to ftrack wasn't successful,"
+ " try running Create/Update Avalon Attributes in ftrack."
+ ))
- session = self.create_ftrack_session()
project_entity = session.query(
"Project where full_name is \"{}\"".format(project_name)
).first()
if not project_entity:
- self.log.warning((
- "Ftrack project with names \"{}\" was not found."
- " Skipping settings attributes change callback."
- ))
- return
+ msg = (
+ "Ftrack project with name \"{}\" was not found in Ftrack."
+ " Can't push attribute changes."
+ ).format(project_name)
+ self.log.warning(msg)
+ raise SaveWarningExc(msg)
project_id = project_entity["id"]
- cust_attr, hier_attr = avalon_sync.get_pype_attr(session)
+ cust_attr, hier_attr = get_openpype_attr(session)
cust_attr_by_key = {attr["key"]: attr for attr in cust_attr}
hier_attrs_by_key = {attr["key"]: attr for attr in hier_attr}
+
+ failed = {}
+ missing = {}
for key, value in attributes_changes.items():
configuration = hier_attrs_by_key.get(key)
if not configuration:
configuration = cust_attr_by_key.get(key)
if not configuration:
+ self.log.warning(
+ "Custom attribute \"{}\" was not found.".format(key)
+ )
+ missing[key] = value
continue
+ # TODO add add permissions check
# TODO add value validations
# - value type and list items
entity_key = collections.OrderedDict()
@@ -182,10 +277,45 @@ class FtrackModule(
"value",
ftrack_api.symbol.NOT_SET,
value
-
)
)
- session.commit()
+ try:
+ session.commit()
+ self.log.debug(
+ "Changed project custom attribute \"{}\" to \"{}\"".format(
+ key, value
+ )
+ )
+ except Exception:
+ self.log.warning(
+ "Failed to set \"{}\" to \"{}\"".format(key, value),
+ exc_info=True
+ )
+ session.rollback()
+ failed[key] = value
+
+ if not failed and not missing:
+ return
+
+ error_msg = (
+ "Values were not updated on Ftrack which may cause issues."
+ " try running Create/Update Avalon Attributes in ftrack "
+ " and resave project settings."
+ )
+ if missing:
+ error_msg += "\nMissing Custom attributes on Ftrack: {}.".format(
+ ", ".join([
+ '"{}"'.format(key)
+ for key in missing.keys()
+ ])
+ )
+ if failed:
+ joined_failed = ", ".join([
+ '"{}": "{}"'.format(key, value)
+ for key, value in failed.items()
+ ])
+ error_msg += "\nFailed to set: {}".format(joined_failed)
+ raise SaveWarningExc(error_msg)
def create_ftrack_session(self, **session_kwargs):
import ftrack_api
diff --git a/openpype/modules/ftrack/lib/__init__.py b/openpype/modules/ftrack/lib/__init__.py
index 82b6875590..ce6d5284b6 100644
--- a/openpype/modules/ftrack/lib/__init__.py
+++ b/openpype/modules/ftrack/lib/__init__.py
@@ -1,7 +1,21 @@
+from .constants import (
+ CUST_ATTR_ID_KEY,
+ CUST_ATTR_AUTO_SYNC,
+ CUST_ATTR_GROUP,
+ CUST_ATTR_TOOLS,
+ CUST_ATTR_APPLICATIONS
+)
from . settings import (
get_ftrack_url_from_settings,
get_ftrack_event_mongo_info
)
+from .custom_attributes import (
+ default_custom_attributes_definition,
+ app_definitions_from_app_manager,
+ tool_definitions_from_app_manager,
+ get_openpype_attr
+)
+
from . import avalon_sync
from . import credentials
from .ftrack_base_handler import BaseHandler
@@ -10,9 +24,20 @@ from .ftrack_action_handler import BaseAction, ServerAction, statics_icon
__all__ = (
+ "CUST_ATTR_ID_KEY",
+ "CUST_ATTR_AUTO_SYNC",
+ "CUST_ATTR_GROUP",
+ "CUST_ATTR_TOOLS",
+ "CUST_ATTR_APPLICATIONS",
+
"get_ftrack_url_from_settings",
"get_ftrack_event_mongo_info",
+ "default_custom_attributes_definition",
+ "app_definitions_from_app_manager",
+ "tool_definitions_from_app_manager",
+ "get_openpype_attr",
+
"avalon_sync",
"credentials",
diff --git a/openpype/modules/ftrack/lib/avalon_sync.py b/openpype/modules/ftrack/lib/avalon_sync.py
index 79e1366a0d..f58e858a5a 100644
--- a/openpype/modules/ftrack/lib/avalon_sync.py
+++ b/openpype/modules/ftrack/lib/avalon_sync.py
@@ -14,17 +14,21 @@ else:
from avalon.api import AvalonMongoDB
import avalon
+
from openpype.api import (
Logger,
Anatomy,
get_anatomy_settings
)
+from openpype.lib import ApplicationManager
+
+from .constants import CUST_ATTR_ID_KEY
+from .custom_attributes import get_openpype_attr
from bson.objectid import ObjectId
from bson.errors import InvalidId
from pymongo import UpdateOne
import ftrack_api
-from openpype.lib import ApplicationManager
log = Logger.get_logger(__name__)
@@ -36,23 +40,6 @@ EntitySchemas = {
"config": "openpype:config-2.0"
}
-# Group name of custom attributes
-CUST_ATTR_GROUP = "openpype"
-
-# name of Custom attribute that stores mongo_id from avalon db
-CUST_ATTR_ID_KEY = "avalon_mongo_id"
-CUST_ATTR_AUTO_SYNC = "avalon_auto_sync"
-
-
-def default_custom_attributes_definition():
- json_file_path = os.path.join(
- os.path.dirname(os.path.abspath(__file__)),
- "custom_attributes.json"
- )
- with open(json_file_path, "r") as json_stream:
- data = json.load(json_stream)
- return data
-
def check_regex(name, entity_type, in_schema=None, schema_patterns=None):
schema_name = "asset-3.0"
@@ -91,39 +78,6 @@ def join_query_keys(keys):
return ",".join(["\"{}\"".format(key) for key in keys])
-def get_pype_attr(session, split_hierarchical=True, query_keys=None):
- custom_attributes = []
- hier_custom_attributes = []
- if not query_keys:
- query_keys = [
- "id",
- "entity_type",
- "object_type_id",
- "is_hierarchical",
- "default"
- ]
- # TODO remove deprecated "pype" group from query
- cust_attrs_query = (
- "select {}"
- " from CustomAttributeConfiguration"
- # Kept `pype` for Backwards Compatiblity
- " where group.name in (\"pype\", \"{}\")"
- ).format(", ".join(query_keys), CUST_ATTR_GROUP)
- all_avalon_attr = session.query(cust_attrs_query).all()
- for cust_attr in all_avalon_attr:
- if split_hierarchical and cust_attr["is_hierarchical"]:
- hier_custom_attributes.append(cust_attr)
- continue
-
- custom_attributes.append(cust_attr)
-
- if split_hierarchical:
- # return tuple
- return custom_attributes, hier_custom_attributes
-
- return custom_attributes
-
-
def get_python_type_for_custom_attribute(cust_attr, cust_attr_type_name=None):
"""Python type that should value of custom attribute have.
@@ -921,7 +875,7 @@ class SyncEntitiesFactory:
def set_cutom_attributes(self):
self.log.debug("* Preparing custom attributes")
# Get custom attributes and values
- custom_attrs, hier_attrs = get_pype_attr(
+ custom_attrs, hier_attrs = get_openpype_attr(
self.session, query_keys=self.cust_attr_query_keys
)
ent_types = self.session.query("select id, name from ObjectType").all()
@@ -2508,7 +2462,7 @@ class SyncEntitiesFactory:
if new_entity_id not in p_chilren:
self.entities_dict[parent_id]["children"].append(new_entity_id)
- cust_attr, _ = get_pype_attr(self.session)
+ cust_attr, _ = get_openpype_attr(self.session)
for _attr in cust_attr:
key = _attr["key"]
if key not in av_entity["data"]:
diff --git a/openpype/modules/ftrack/lib/constants.py b/openpype/modules/ftrack/lib/constants.py
new file mode 100644
index 0000000000..73d5112e6d
--- /dev/null
+++ b/openpype/modules/ftrack/lib/constants.py
@@ -0,0 +1,12 @@
+# Group name of custom attributes
+CUST_ATTR_GROUP = "openpype"
+
+# name of Custom attribute that stores mongo_id from avalon db
+CUST_ATTR_ID_KEY = "avalon_mongo_id"
+# Auto sync of project
+CUST_ATTR_AUTO_SYNC = "avalon_auto_sync"
+
+# Applications custom attribute name
+CUST_ATTR_APPLICATIONS = "applications"
+# Environment tools custom attribute
+CUST_ATTR_TOOLS = "tools_env"
diff --git a/openpype/modules/ftrack/lib/custom_attributes.py b/openpype/modules/ftrack/lib/custom_attributes.py
new file mode 100644
index 0000000000..33eea32baa
--- /dev/null
+++ b/openpype/modules/ftrack/lib/custom_attributes.py
@@ -0,0 +1,73 @@
+import os
+import json
+
+from .constants import CUST_ATTR_GROUP
+
+
+def default_custom_attributes_definition():
+ json_file_path = os.path.join(
+ os.path.dirname(os.path.abspath(__file__)),
+ "custom_attributes.json"
+ )
+ with open(json_file_path, "r") as json_stream:
+ data = json.load(json_stream)
+ return data
+
+
+def app_definitions_from_app_manager(app_manager):
+ app_definitions = []
+ for app_name, app in app_manager.applications.items():
+ if app.enabled and app.is_host:
+ app_definitions.append({
+ app_name: app.full_label
+ })
+
+ if not app_definitions:
+ app_definitions.append({"empty": "< Empty >"})
+ return app_definitions
+
+
+def tool_definitions_from_app_manager(app_manager):
+ tools_data = []
+ for tool_name, tool in app_manager.tools.items():
+ tools_data.append({
+ tool_name: tool.label
+ })
+
+ # Make sure there is at least one item
+ if not tools_data:
+ tools_data.append({"empty": "< Empty >"})
+ return tools_data
+
+
+def get_openpype_attr(session, split_hierarchical=True, query_keys=None):
+ custom_attributes = []
+ hier_custom_attributes = []
+ if not query_keys:
+ query_keys = [
+ "id",
+ "entity_type",
+ "object_type_id",
+ "is_hierarchical",
+ "default"
+ ]
+ # TODO remove deprecated "pype" group from query
+ cust_attrs_query = (
+ "select {}"
+ " from CustomAttributeConfiguration"
+ # Kept `pype` for Backwards Compatiblity
+ " where group.name in (\"pype\", \"{}\")"
+ ).format(", ".join(query_keys), CUST_ATTR_GROUP)
+ all_avalon_attr = session.query(cust_attrs_query).all()
+ for cust_attr in all_avalon_attr:
+ if split_hierarchical and cust_attr["is_hierarchical"]:
+ hier_custom_attributes.append(cust_attr)
+ continue
+
+ custom_attributes.append(cust_attr)
+
+ if split_hierarchical:
+ # return tuple
+ return custom_attributes, hier_custom_attributes
+
+ return custom_attributes
diff --git a/openpype/modules/settings_action.py b/openpype/modules/settings_action.py
index 371e190c12..3f7cb8c3ba 100644
--- a/openpype/modules/settings_action.py
+++ b/openpype/modules/settings_action.py
@@ -16,18 +16,20 @@ class ISettingsChangeListener:
}
"""
@abstractmethod
- def on_system_settings_save(self, old_value, new_value, changes):
+ def on_system_settings_save(
+ self, old_value, new_value, changes, new_value_metadata
+ ):
pass
@abstractmethod
def on_project_settings_save(
- self, old_value, new_value, changes, project_name
+ self, old_value, new_value, changes, project_name, new_value_metadata
):
pass
@abstractmethod
def on_project_anatomy_save(
- self, old_value, new_value, changes, project_name
+ self, old_value, new_value, changes, project_name, new_value_metadata
):
pass
diff --git a/openpype/settings/__init__.py b/openpype/settings/__init__.py
index c8dd64a41c..b5810deef4 100644
--- a/openpype/settings/__init__.py
+++ b/openpype/settings/__init__.py
@@ -1,3 +1,6 @@
+from .exceptions import (
+ SaveWarningExc
+)
from .lib import (
get_system_settings,
get_project_settings,
@@ -13,6 +16,8 @@ from .entities import (
__all__ = (
+ "SaveWarningExc",
+
"get_system_settings",
"get_project_settings",
"get_current_project_settings",
diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py
index eed3d47f46..b89473d9fb 100644
--- a/openpype/settings/entities/root_entities.py
+++ b/openpype/settings/entities/root_entities.py
@@ -23,6 +23,7 @@ from openpype.settings.constants import (
PROJECT_ANATOMY_KEY,
KEY_REGEX
)
+from openpype.settings.exceptions import SaveWarningExc
from openpype.settings.lib import (
DEFAULTS_DIR,
@@ -724,8 +725,19 @@ class ProjectSettings(RootEntity):
project_settings = settings_value.get(PROJECT_SETTINGS_KEY) or {}
project_anatomy = settings_value.get(PROJECT_ANATOMY_KEY) or {}
- save_project_settings(self.project_name, project_settings)
- save_project_anatomy(self.project_name, project_anatomy)
+ warnings = []
+ try:
+ save_project_settings(self.project_name, project_settings)
+ except SaveWarningExc as exc:
+ warnings.extend(exc.warnings)
+
+ try:
+ save_project_anatomy(self.project_name, project_anatomy)
+ except SaveWarningExc as exc:
+ warnings.extend(exc.warnings)
+
+ if warnings:
+ raise SaveWarningExc(warnings)
def _validate_defaults_to_save(self, value):
"""Valiations of default values before save."""
diff --git a/openpype/settings/exceptions.py b/openpype/settings/exceptions.py
new file mode 100644
index 0000000000..a06138eeaf
--- /dev/null
+++ b/openpype/settings/exceptions.py
@@ -0,0 +1,11 @@
+class SaveSettingsValidation(Exception):
+ pass
+
+
+class SaveWarningExc(SaveSettingsValidation):
+ def __init__(self, warnings):
+ if isinstance(warnings, str):
+ warnings = [warnings]
+ self.warnings = warnings
+ msg = " | ".join(warnings)
+ super(SaveWarningExc, self).__init__(msg)
diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py
index 3bf2141808..f61166fa69 100644
--- a/openpype/settings/lib.py
+++ b/openpype/settings/lib.py
@@ -4,6 +4,9 @@ import functools
import logging
import platform
import copy
+from .exceptions import (
+ SaveWarningExc
+)
from .constants import (
M_OVERRIDEN_KEY,
M_ENVIRONMENT_KEY,
@@ -101,8 +104,14 @@ def save_studio_settings(data):
For saving of data cares registered Settings handler.
+ Warning messages are not logged as module raising them should log it within
+ it's logger.
+
Args:
data(dict): Overrides data with metadata defying studio overrides.
+
+ Raises:
+ SaveWarningExc: If any module raises the exception.
"""
# Notify Pype modules
from openpype.modules import ModulesManager, ISettingsChangeListener
@@ -110,15 +119,25 @@ def save_studio_settings(data):
old_data = get_system_settings()
default_values = get_default_settings()[SYSTEM_SETTINGS_KEY]
new_data = apply_overrides(default_values, copy.deepcopy(data))
+ new_data_with_metadata = copy.deepcopy(new_data)
clear_metadata_from_settings(new_data)
changes = calculate_changes(old_data, new_data)
modules_manager = ModulesManager(_system_settings=new_data)
+
+ warnings = []
for module in modules_manager.get_enabled_modules():
if isinstance(module, ISettingsChangeListener):
- module.on_system_settings_save(old_data, new_data, changes)
+ try:
+ module.on_system_settings_save(
+ old_data, new_data, changes, new_data_with_metadata
+ )
+ except SaveWarningExc as exc:
+ warnings.extend(exc.warnings)
- return _SETTINGS_HANDLER.save_studio_settings(data)
+ _SETTINGS_HANDLER.save_studio_settings(data)
+ if warnings:
+ raise SaveWarningExc(warnings)
@require_handler
@@ -130,10 +149,16 @@ def save_project_settings(project_name, overrides):
For saving of data cares registered Settings handler.
+ Warning messages are not logged as module raising them should log it within
+ it's logger.
+
Args:
project_name (str): Project name for which overrides are passed.
Default project's value is None.
overrides(dict): Overrides data with metadata defying studio overrides.
+
+ Raises:
+ SaveWarningExc: If any module raises the exception.
"""
# Notify Pype modules
from openpype.modules import ModulesManager, ISettingsChangeListener
@@ -151,17 +176,29 @@ def save_project_settings(project_name, overrides):
old_data = get_default_project_settings(exclude_locals=True)
new_data = apply_overrides(default_values, copy.deepcopy(overrides))
+ new_data_with_metadata = copy.deepcopy(new_data)
clear_metadata_from_settings(new_data)
changes = calculate_changes(old_data, new_data)
modules_manager = ModulesManager()
+ warnings = []
for module in modules_manager.get_enabled_modules():
if isinstance(module, ISettingsChangeListener):
- module.on_project_settings_save(
- old_data, new_data, project_name, changes
- )
+ try:
+ module.on_project_settings_save(
+ old_data,
+ new_data,
+ project_name,
+ changes,
+ new_data_with_metadata
+ )
+ except SaveWarningExc as exc:
+ warnings.extend(exc.warnings)
- return _SETTINGS_HANDLER.save_project_settings(project_name, overrides)
+ _SETTINGS_HANDLER.save_project_settings(project_name, overrides)
+
+ if warnings:
+ raise SaveWarningExc(warnings)
@require_handler
@@ -173,10 +210,16 @@ def save_project_anatomy(project_name, anatomy_data):
For saving of data cares registered Settings handler.
+ Warning messages are not logged as module raising them should log it within
+ it's logger.
+
Args:
project_name (str): Project name for which overrides are passed.
Default project's value is None.
overrides(dict): Overrides data with metadata defying studio overrides.
+
+ Raises:
+ SaveWarningExc: If any module raises the exception.
"""
# Notify Pype modules
from openpype.modules import ModulesManager, ISettingsChangeListener
@@ -194,17 +237,29 @@ def save_project_anatomy(project_name, anatomy_data):
old_data = get_default_anatomy_settings(exclude_locals=True)
new_data = apply_overrides(default_values, copy.deepcopy(anatomy_data))
+ new_data_with_metadata = copy.deepcopy(new_data)
clear_metadata_from_settings(new_data)
changes = calculate_changes(old_data, new_data)
modules_manager = ModulesManager()
+ warnings = []
for module in modules_manager.get_enabled_modules():
if isinstance(module, ISettingsChangeListener):
- module.on_project_anatomy_save(
- old_data, new_data, changes, project_name
- )
+ try:
+ module.on_project_anatomy_save(
+ old_data,
+ new_data,
+ changes,
+ project_name,
+ new_data_with_metadata
+ )
+ except SaveWarningExc as exc:
+ warnings.extend(exc.warnings)
- return _SETTINGS_HANDLER.save_project_anatomy(project_name, anatomy_data)
+ _SETTINGS_HANDLER.save_project_anatomy(project_name, anatomy_data)
+
+ if warnings:
+ raise SaveWarningExc(warnings)
@require_handler
diff --git a/openpype/tools/settings/settings/widgets/categories.py b/openpype/tools/settings/settings/widgets/categories.py
index 9d286485a3..e4832c989a 100644
--- a/openpype/tools/settings/settings/widgets/categories.py
+++ b/openpype/tools/settings/settings/widgets/categories.py
@@ -27,7 +27,7 @@ from openpype.settings.entities import (
SchemaError
)
-from openpype.settings.lib import get_system_settings
+from openpype.settings import SaveWarningExc
from .widgets import ProjectListWidget
from . import lib
@@ -272,6 +272,22 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
# not required.
self.reset()
+ except SaveWarningExc as exc:
+ warnings = [
+ "Settings were saved but few issues happened."
+ ]
+ for item in exc.warnings:
+ warnings.append(item.replace("\n", "
"))
+
+ msg = "
".join(warnings)
+
+ dialog = QtWidgets.QMessageBox(self)
+ dialog.setText(msg)
+ dialog.setIcon(QtWidgets.QMessageBox.Warning)
+ dialog.exec_()
+
+ self.reset()
+
except Exception as exc:
formatted_traceback = traceback.format_exception(*sys.exc_info())
dialog = QtWidgets.QMessageBox(self)