Merge branch 'develop' into bugfix/1375-nuke-fix-set-colorspace-with-new-settings

This commit is contained in:
Jakub Jezek 2021-04-22 15:36:32 +02:00
commit 3eff75b142
No known key found for this signature in database
GPG key ID: D8548FBF690B100A
51 changed files with 1685 additions and 850 deletions

View file

@ -5,7 +5,7 @@ import logging
from avalon import io
from avalon import api as avalon
from avalon.vendor import Qt
from openpype import lib
from openpype import lib, api
import pyblish.api as pyblish
import openpype.hosts.aftereffects
@ -81,3 +81,69 @@ def uninstall():
def on_pyblish_instance_toggled(instance, old_value, new_value):
"""Toggle layer visibility on instance toggles."""
instance[0].Visible = new_value
def get_asset_settings():
"""Get settings on current asset from database.
Returns:
dict: Scene data.
"""
asset_data = lib.get_asset()["data"]
fps = asset_data.get("fps")
frame_start = asset_data.get("frameStart")
frame_end = asset_data.get("frameEnd")
handle_start = asset_data.get("handleStart")
handle_end = asset_data.get("handleEnd")
resolution_width = asset_data.get("resolutionWidth")
resolution_height = asset_data.get("resolutionHeight")
duration = frame_end + handle_end - max(frame_start - handle_start, 0)
entity_type = asset_data.get("entityType")
scene_data = {
"fps": fps,
"frameStart": frame_start,
"frameEnd": frame_end,
"handleStart": handle_start,
"handleEnd": handle_end,
"resolutionWidth": resolution_width,
"resolutionHeight": resolution_height,
"duration": duration
}
try:
# temporary, in pype3 replace with api.get_current_project_settings
skip_resolution_check = (
api.get_current_project_settings()
["plugins"]
["aftereffects"]
["publish"]
["ValidateSceneSettings"]
["skip_resolution_check"]
)
skip_timelines_check = (
api.get_current_project_settings()
["plugins"]
["aftereffects"]
["publish"]
["ValidateSceneSettings"]
["skip_timelines_check"]
)
except KeyError:
skip_resolution_check = ['*']
skip_timelines_check = ['*']
if os.getenv('AVALON_TASK') in skip_resolution_check or \
'*' in skip_timelines_check:
scene_data.pop("resolutionWidth")
scene_data.pop("resolutionHeight")
if entity_type in skip_timelines_check or '*' in skip_timelines_check:
scene_data.pop('fps', None)
scene_data.pop('frameStart', None)
scene_data.pop('frameEnd', None)
scene_data.pop('handleStart', None)
scene_data.pop('handleEnd', None)
return scene_data

View file

@ -12,6 +12,7 @@ class AERenderInstance(RenderInstance):
# extend generic, composition name is needed
comp_name = attr.ib(default=None)
comp_id = attr.ib(default=None)
fps = attr.ib(default=None)
class CollectAERender(abstract_collect_render.AbstractCollectRender):
@ -45,6 +46,7 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender):
raise ValueError("Couldn't find id, unable to publish. " +
"Please recreate instance.")
item_id = inst["members"][0]
work_area_info = self.stub.get_work_area(int(item_id))
if not work_area_info:
@ -57,6 +59,8 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender):
frameEnd = round(work_area_info.workAreaStart +
float(work_area_info.workAreaDuration) *
float(work_area_info.frameRate)) - 1
fps = work_area_info.frameRate
# TODO add resolution when supported by extension
if inst["family"] == "render" and inst["active"]:
instance = AERenderInstance(
@ -86,7 +90,8 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender):
frameStart=frameStart,
frameEnd=frameEnd,
frameStep=1,
toBeRenderedOn='deadline'
toBeRenderedOn='deadline',
fps=fps
)
comp = compositions_by_id.get(int(item_id))
@ -102,7 +107,6 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender):
instances.append(instance)
self.log.debug("instances::{}".format(instances))
return instances
def get_expected_files(self, render_instance):

View file

@ -0,0 +1,110 @@
# -*- coding: utf-8 -*-
"""Validate scene settings."""
import os
import pyblish.api
from avalon import aftereffects
import openpype.hosts.aftereffects.api as api
stub = aftereffects.stub()
class ValidateSceneSettings(pyblish.api.InstancePlugin):
"""
Ensures that Composition Settings (right mouse on comp) are same as
in FTrack on task.
By default checks only duration - how many frames should be rendered.
Compares:
Frame start - Frame end + 1 from FTrack
against
Duration in Composition Settings.
If this complains:
Check error message where is discrepancy.
Check FTrack task 'pype' section of task attributes for expected
values.
Check/modify rendered Composition Settings.
If you know what you are doing run publishing again, uncheck this
validation before Validation phase.
"""
"""
Dev docu:
Could be configured by 'presets/plugins/aftereffects/publish'
skip_timelines_check - fill task name for which skip validation of
frameStart
frameEnd
fps
handleStart
handleEnd
skip_resolution_check - fill entity type ('asset') to skip validation
resolutionWidth
resolutionHeight
TODO support in extension is missing for now
By defaults validates duration (how many frames should be published)
"""
order = pyblish.api.ValidatorOrder
label = "Validate Scene Settings"
families = ["render.farm"]
hosts = ["aftereffects"]
optional = True
skip_timelines_check = ["*"] # * >> skip for all
skip_resolution_check = ["*"]
def process(self, instance):
"""Plugin entry point."""
expected_settings = api.get_asset_settings()
self.log.info("expected_settings::{}".format(expected_settings))
# handle case where ftrack uses only two decimal places
# 23.976023976023978 vs. 23.98
fps = instance.data.get("fps")
if fps:
if isinstance(fps, float):
fps = float(
"{:.2f}".format(fps))
expected_settings["fps"] = fps
duration = instance.data.get("frameEndHandle") - \
instance.data.get("frameStartHandle") + 1
current_settings = {
"fps": fps,
"frameStartHandle": instance.data.get("frameStartHandle"),
"frameEndHandle": instance.data.get("frameEndHandle"),
"resolutionWidth": instance.data.get("resolutionWidth"),
"resolutionHeight": instance.data.get("resolutionHeight"),
"duration": duration
}
self.log.info("current_settings:: {}".format(current_settings))
invalid_settings = []
for key, value in expected_settings.items():
if value != current_settings[key]:
invalid_settings.append(
"{} expected: {} found: {}".format(key, value,
current_settings[key])
)
if ((expected_settings.get("handleStart")
or expected_settings.get("handleEnd"))
and invalid_settings):
msg = "Handles included in calculation. Remove handles in DB " +\
"or extend frame range in Composition Setting."
invalid_settings[-1]["reason"] = msg
msg = "Found invalid settings:\n{}".format(
"\n".join(invalid_settings)
)
assert not invalid_settings, msg
assert os.path.exists(instance.data.get("source")), (
"Scene file not found (saved under wrong name)"
)

View file

@ -9,7 +9,7 @@ from avalon import api
import avalon.blender
from openpype.api import PypeCreatorMixin
VALID_EXTENSIONS = [".blend", ".json"]
VALID_EXTENSIONS = [".blend", ".json", ".abc"]
def asset_name(

View file

@ -0,0 +1,35 @@
"""Create a pointcache asset."""
import bpy
from avalon import api
from avalon.blender import lib
import openpype.hosts.blender.api.plugin
class CreatePointcache(openpype.hosts.blender.api.plugin.Creator):
"""Polygonal static geometry"""
name = "pointcacheMain"
label = "Point Cache"
family = "pointcache"
icon = "gears"
def process(self):
asset = self.data["asset"]
subset = self.data["subset"]
name = openpype.hosts.blender.api.plugin.asset_name(asset, subset)
collection = bpy.data.collections.new(name=name)
bpy.context.scene.collection.children.link(collection)
self.data['task'] = api.Session.get('AVALON_TASK')
lib.imprint(collection, self.data)
if (self.options or {}).get("useSelection"):
objects = lib.get_selection()
for obj in objects:
collection.objects.link(obj)
if obj.type == 'EMPTY':
objects.extend(obj.children)
return collection

View file

@ -0,0 +1,246 @@
"""Load an asset in Blender from an Alembic file."""
from pathlib import Path
from pprint import pformat
from typing import Dict, List, Optional
from avalon import api, blender
import bpy
import openpype.hosts.blender.api.plugin as plugin
class CacheModelLoader(plugin.AssetLoader):
"""Load cache models.
Stores the imported asset in a collection named after the asset.
Note:
At least for now it only supports Alembic files.
"""
families = ["model", "pointcache"]
representations = ["abc"]
label = "Link Alembic"
icon = "code-fork"
color = "orange"
def _remove(self, objects, container):
for obj in list(objects):
if obj.type == 'MESH':
bpy.data.meshes.remove(obj.data)
elif obj.type == 'EMPTY':
bpy.data.objects.remove(obj)
bpy.data.collections.remove(container)
def _process(self, libpath, container_name, parent_collection):
bpy.ops.object.select_all(action='DESELECT')
view_layer = bpy.context.view_layer
view_layer_collection = view_layer.active_layer_collection.collection
relative = bpy.context.preferences.filepaths.use_relative_paths
bpy.ops.wm.alembic_import(
filepath=libpath,
relative_path=relative
)
parent = parent_collection
if parent is None:
parent = bpy.context.scene.collection
model_container = bpy.data.collections.new(container_name)
parent.children.link(model_container)
for obj in bpy.context.selected_objects:
model_container.objects.link(obj)
view_layer_collection.objects.unlink(obj)
name = obj.name
obj.name = f"{name}:{container_name}"
# Groups are imported as Empty objects in Blender
if obj.type == 'MESH':
data_name = obj.data.name
obj.data.name = f"{data_name}:{container_name}"
if not obj.get(blender.pipeline.AVALON_PROPERTY):
obj[blender.pipeline.AVALON_PROPERTY] = dict()
avalon_info = obj[blender.pipeline.AVALON_PROPERTY]
avalon_info.update({"container_name": container_name})
bpy.ops.object.select_all(action='DESELECT')
return model_container
def process_asset(
self, context: dict, name: str, namespace: Optional[str] = None,
options: Optional[Dict] = None
) -> Optional[List]:
"""
Arguments:
name: Use pre-defined name
namespace: Use pre-defined namespace
context: Full parenthood of representation to load
options: Additional settings dictionary
"""
libpath = self.fname
asset = context["asset"]["name"]
subset = context["subset"]["name"]
lib_container = plugin.asset_name(
asset, subset
)
unique_number = plugin.get_unique_number(
asset, subset
)
namespace = namespace or f"{asset}_{unique_number}"
container_name = plugin.asset_name(
asset, subset, unique_number
)
container = bpy.data.collections.new(lib_container)
container.name = container_name
blender.pipeline.containerise_existing(
container,
name,
namespace,
context,
self.__class__.__name__,
)
container_metadata = container.get(
blender.pipeline.AVALON_PROPERTY)
container_metadata["libpath"] = libpath
container_metadata["lib_container"] = lib_container
obj_container = self._process(
libpath, container_name, None)
container_metadata["obj_container"] = obj_container
# Save the list of objects in the metadata container
container_metadata["objects"] = obj_container.all_objects
nodes = list(container.objects)
nodes.append(container)
self[:] = nodes
return nodes
def update(self, container: Dict, representation: Dict):
"""Update the loaded asset.
This will remove all objects of the current collection, load the new
ones and add them to the collection.
If the objects of the collection are used in another collection they
will not be removed, only unlinked. Normally this should not be the
case though.
Warning:
No nested collections are supported at the moment!
"""
collection = bpy.data.collections.get(
container["objectName"]
)
libpath = Path(api.get_representation_path(representation))
extension = libpath.suffix.lower()
self.log.info(
"Container: %s\nRepresentation: %s",
pformat(container, indent=2),
pformat(representation, indent=2),
)
assert collection, (
f"The asset is not loaded: {container['objectName']}"
)
assert not (collection.children), (
"Nested collections are not supported."
)
assert libpath, (
"No existing library file found for {container['objectName']}"
)
assert libpath.is_file(), (
f"The file doesn't exist: {libpath}"
)
assert extension in plugin.VALID_EXTENSIONS, (
f"Unsupported file: {libpath}"
)
collection_metadata = collection.get(
blender.pipeline.AVALON_PROPERTY)
collection_libpath = collection_metadata["libpath"]
obj_container = plugin.get_local_collection_with_name(
collection_metadata["obj_container"].name
)
objects = obj_container.all_objects
container_name = obj_container.name
normalized_collection_libpath = (
str(Path(bpy.path.abspath(collection_libpath)).resolve())
)
normalized_libpath = (
str(Path(bpy.path.abspath(str(libpath))).resolve())
)
self.log.debug(
"normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s",
normalized_collection_libpath,
normalized_libpath,
)
if normalized_collection_libpath == normalized_libpath:
self.log.info("Library already loaded, not updating...")
return
parent = plugin.get_parent_collection(obj_container)
self._remove(objects, obj_container)
obj_container = self._process(
str(libpath), container_name, parent)
collection_metadata["obj_container"] = obj_container
collection_metadata["objects"] = obj_container.all_objects
collection_metadata["libpath"] = str(libpath)
collection_metadata["representation"] = str(representation["_id"])
def remove(self, container: Dict) -> bool:
"""Remove an existing container from a Blender scene.
Arguments:
container (openpype:container-1.0): Container to remove,
from `host.ls()`.
Returns:
bool: Whether the container was deleted.
Warning:
No nested collections are supported at the moment!
"""
collection = bpy.data.collections.get(
container["objectName"]
)
if not collection:
return False
assert not (collection.children), (
"Nested collections are not supported."
)
collection_metadata = collection.get(
blender.pipeline.AVALON_PROPERTY)
obj_container = plugin.get_local_collection_with_name(
collection_metadata["obj_container"].name
)
objects = obj_container.all_objects
self._remove(objects, obj_container)
bpy.data.collections.remove(collection)
return True

View file

@ -242,65 +242,3 @@ class BlendModelLoader(plugin.AssetLoader):
bpy.data.collections.remove(collection)
return True
class CacheModelLoader(plugin.AssetLoader):
"""Load cache models.
Stores the imported asset in a collection named after the asset.
Note:
At least for now it only supports Alembic files.
"""
families = ["model"]
representations = ["abc"]
label = "Link Model"
icon = "code-fork"
color = "orange"
def process_asset(
self, context: dict, name: str, namespace: Optional[str] = None,
options: Optional[Dict] = None
) -> Optional[List]:
"""
Arguments:
name: Use pre-defined name
namespace: Use pre-defined namespace
context: Full parenthood of representation to load
options: Additional settings dictionary
"""
raise NotImplementedError(
"Loading of Alembic files is not yet implemented.")
# TODO (jasper): implement Alembic import.
libpath = self.fname
asset = context["asset"]["name"]
subset = context["subset"]["name"]
# TODO (jasper): evaluate use of namespace which is 'alien' to Blender.
lib_container = container_name = (
plugin.asset_name(asset, subset, namespace)
)
relative = bpy.context.preferences.filepaths.use_relative_paths
with bpy.data.libraries.load(
libpath, link=True, relative=relative
) as (data_from, data_to):
data_to.collections = [lib_container]
scene = bpy.context.scene
instance_empty = bpy.data.objects.new(
container_name, None
)
scene.collection.objects.link(instance_empty)
instance_empty.instance_type = 'COLLECTION'
collection = bpy.data.collections[lib_container]
collection.name = container_name
instance_empty.instance_collection = collection
nodes = list(collection.objects)
nodes.append(collection)
nodes.append(instance_empty)
self[:] = nodes
return nodes

View file

@ -11,14 +11,14 @@ class ExtractABC(openpype.api.Extractor):
label = "Extract ABC"
hosts = ["blender"]
families = ["model"]
families = ["model", "pointcache"]
optional = True
def process(self, instance):
# Define extract output file path
stagingdir = self.staging_dir(instance)
filename = f"{instance.name}.fbx"
filename = f"{instance.name}.abc"
filepath = os.path.join(stagingdir, filename)
context = bpy.context
@ -52,6 +52,8 @@ class ExtractABC(openpype.api.Extractor):
old_scale = scene.unit_settings.scale_length
bpy.ops.object.select_all(action='DESELECT')
selected = list()
for obj in instance:
@ -67,14 +69,11 @@ class ExtractABC(openpype.api.Extractor):
# We set the scale of the scene for the export
scene.unit_settings.scale_length = 0.01
self.log.info(new_context)
# We export the abc
bpy.ops.wm.alembic_export(
new_context,
filepath=filepath,
start=1,
end=1
selected=True
)
view_layer.active_layer_collection = old_active_layer_collection

View file

@ -74,6 +74,8 @@ class ExtractRedshiftProxy(openpype.api.Extractor):
'files': repr_files,
"stagingDir": staging_dir,
}
if anim_on:
representation["frameStart"] = instance.data["proxyFrameStart"]
instance.data["representations"].append(representation)
self.log.info("Extracted instance '%s' to: %s"

View file

@ -106,7 +106,7 @@ def on_pyblish_instance_toggled(instance, old_value, new_value):
log.info("instance toggle: {}, old_value: {}, new_value:{} ".format(
instance, old_value, new_value))
from avalon.api.nuke import (
from avalon.nuke import (
viewer_update_and_undo_stop,
add_publish_knob
)

View file

@ -26,9 +26,9 @@ def install():
menu.addCommand(
name,
workfiles.show,
index=(rm_item[0])
index=2
)
menu.addSeparator(index=3)
# replace reset resolution from avalon core to pype's
name = "Reset Resolution"
new_name = "Set Resolution"
@ -63,16 +63,7 @@ def install():
# add colorspace menu item
name = "Set Colorspace"
menu.addCommand(
name, lambda: WorkfileSettings().set_colorspace(),
index=(rm_item[0] + 2)
)
log.debug("Adding menu item: {}".format(name))
# add workfile builder menu item
name = "Build Workfile"
menu.addCommand(
name, lambda: BuildWorkfile().process(),
index=(rm_item[0] + 7)
name, lambda: WorkfileSettings().set_colorspace()
)
log.debug("Adding menu item: {}".format(name))
@ -80,11 +71,20 @@ def install():
name = "Apply All Settings"
menu.addCommand(
name,
lambda: WorkfileSettings().set_context_settings(),
index=(rm_item[0] + 3)
lambda: WorkfileSettings().set_context_settings()
)
log.debug("Adding menu item: {}".format(name))
menu.addSeparator()
# add workfile builder menu item
name = "Build Workfile"
menu.addCommand(
name, lambda: BuildWorkfile().process()
)
log.debug("Adding menu item: {}".format(name))
# adding shortcuts
add_shortcuts_from_presets()

View file

@ -1,30 +0,0 @@
import os
import sys
from avalon import api, pipeline
PACKAGE_DIR = os.path.dirname(__file__)
PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins", "launcher")
ACTIONS_DIR = os.path.join(PLUGINS_DIR, "actions")
def register_launcher_actions():
"""Register specific actions which should be accessible in the launcher"""
actions = []
ext = ".py"
sys.path.append(ACTIONS_DIR)
for f in os.listdir(ACTIONS_DIR):
file, extention = os.path.splitext(f)
if ext in extention:
module = __import__(file)
klass = getattr(module, file)
actions.append(klass)
if actions is []:
return
for action in actions:
print("Using launcher action from config @ '{}'".format(action.name))
pipeline.register_plugin(api.Action, action)

View file

@ -79,6 +79,16 @@ from .avalon_context import (
change_timer_to_current_context
)
from .local_settings import (
IniSettingRegistry,
JSONSettingRegistry,
OpenPypeSecureRegistry,
OpenPypeSettingsRegistry,
get_local_site_id,
change_openpype_mongo_url,
get_openpype_username
)
from .applications import (
ApplicationLaunchFailed,
ApplictionExecutableNotFound,
@ -112,15 +122,6 @@ from .plugin_tools import (
should_decompress
)
from .local_settings import (
IniSettingRegistry,
JSONSettingRegistry,
OpenPypeSecureRegistry,
OpenPypeSettingsRegistry,
get_local_site_id,
change_openpype_mongo_url
)
from .path_tools import (
version_up,
get_version_from_path,
@ -179,6 +180,14 @@ __all__ = [
"change_timer_to_current_context",
"IniSettingRegistry",
"JSONSettingRegistry",
"OpenPypeSecureRegistry",
"OpenPypeSettingsRegistry",
"get_local_site_id",
"change_openpype_mongo_url",
"get_openpype_username",
"ApplicationLaunchFailed",
"ApplictionExecutableNotFound",
"ApplicationNotFound",
@ -224,13 +233,6 @@ __all__ = [
"validate_mongo_connection",
"OpenPypeMongoConnection",
"IniSettingRegistry",
"JSONSettingRegistry",
"OpenPypeSecureRegistry",
"OpenPypeSettingsRegistry",
"get_local_site_id",
"change_openpype_mongo_url",
"timeit",
"is_overlapping_otio_ranges",

View file

@ -25,6 +25,7 @@ from . import (
PypeLogger,
Anatomy
)
from .local_settings import get_openpype_username
from .avalon_context import (
get_workdir_data,
get_workdir_with_workdir_data
@ -262,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):
@ -279,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():
@ -1225,7 +1247,7 @@ def _prepare_last_workfile(data, workdir):
file_template = anatomy.templates["work"]["file"]
workdir_data.update({
"version": 1,
"user": os.environ.get("OPENPYPE_USERNAME") or getpass.getuser(),
"user": get_openpype_username(),
"ext": extensions[0]
})

View file

@ -1,9 +1,11 @@
# -*- coding: utf-8 -*-
"""Package to deal with saving and retrieving user specific settings."""
import os
import json
import getpass
import platform
from datetime import datetime
from abc import ABCMeta, abstractmethod
import json
# TODO Use pype igniter logic instead of using duplicated code
# disable lru cache in Python 2
@ -24,11 +26,11 @@ try:
except ImportError:
import ConfigParser as configparser
import platform
import six
import appdirs
from openpype.settings import get_local_settings
from .import validate_mongo_connection
_PLACEHOLDER = object()
@ -538,3 +540,25 @@ def change_openpype_mongo_url(new_mongo_url):
if existing_value is not None:
registry.delete_item(key)
registry.set_item(key, new_mongo_url)
def get_openpype_username():
"""OpenPype username used for templates and publishing.
May be different than machine's username.
Always returns "OPENPYPE_USERNAME" environment if is set then tries local
settings and last option is to use `getpass.getuser()` which returns
machine username.
"""
username = os.environ.get("OPENPYPE_USERNAME")
if not username:
local_settings = get_local_settings()
username = (
local_settings
.get("general", {})
.get("username")
)
if not username:
username = getpass.getuser()
return username

View file

@ -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 * "=",

View file

@ -18,10 +18,6 @@ from .webserver import (
WebServerModule,
IWebServerRoutes
)
from .user import (
UserModule,
IUserModule
)
from .idle_manager import (
IdleManager,
IIdleManager
@ -60,9 +56,6 @@ __all__ = (
"WebServerModule",
"IWebServerRoutes",
"UserModule",
"IUserModule",
"IdleManager",
"IIdleManager",

View file

@ -64,7 +64,6 @@ class AfterEffectsSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline
"AVALON_ASSET",
"AVALON_TASK",
"AVALON_APP_NAME",
"OPENPYPE_USERNAME",
"OPENPYPE_DEV",
"OPENPYPE_LOG_NO_COLORS"
]

View file

@ -273,7 +273,6 @@ class HarmonySubmitDeadline(
"AVALON_ASSET",
"AVALON_TASK",
"AVALON_APP_NAME",
"OPENPYPE_USERNAME",
"OPENPYPE_DEV",
"OPENPYPE_LOG_NO_COLORS"
]

View file

@ -441,7 +441,6 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin):
"AVALON_ASSET",
"AVALON_TASK",
"AVALON_APP_NAME",
"OPENPYPE_USERNAME",
"OPENPYPE_DEV",
"OPENPYPE_LOG_NO_COLORS"
]

View file

@ -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"]

View file

@ -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

View file

@ -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"]

View file

@ -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,

View file

@ -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"]

View file

@ -1,4 +1,5 @@
import os
import json
import collections
from abc import ABCMeta, abstractmethod
import six
@ -8,10 +9,10 @@ from openpype.modules import (
ITrayModule,
IPluginPaths,
ITimersManager,
IUserModule,
ILaunchHookPaths,
ISettingsChangeListener
)
from openpype.settings import SaveWarningExc
FTRACK_MODULE_DIR = os.path.dirname(os.path.abspath(__file__))
@ -32,7 +33,6 @@ class FtrackModule(
ITrayModule,
IPluginPaths,
ITimersManager,
IUserModule,
ILaunchHookPaths,
ISettingsChangeListener
):
@ -123,15 +123,86 @@ class FtrackModule(
if self.tray_module:
self.tray_module.stop_timer_manager()
def on_pype_user_change(self, username):
"""Implementation of IUserModule interface."""
if self.tray_module:
self.tray_module.changed_user()
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."""
@ -139,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:
@ -150,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()
@ -189,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

View file

@ -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",

View file

@ -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"]:

View file

@ -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"

View file

@ -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

View file

@ -22,7 +22,6 @@ class LauncherAction(PypeModule, ITrayAction):
# Register actions
if self.tray_initialized:
from openpype.tools.launcher import actions
# actions.register_default_actions()
actions.register_config_actions()
actions_paths = self.manager.collect_plugin_paths()["actions"]
actions.register_actions_from_paths(actions_paths)

View file

@ -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

View file

@ -1,4 +1,7 @@
from Qt import QtCore
import attr
import abc
import six
from openpype.lib import PypeLogger
@ -20,8 +23,111 @@ ProviderRole = QtCore.Qt.UserRole + 2
ProgressRole = QtCore.Qt.UserRole + 4
DateRole = QtCore.Qt.UserRole + 6
FailedRole = QtCore.Qt.UserRole + 8
HeaderNameRole = QtCore.Qt.UserRole + 10
@six.add_metaclass(abc.ABCMeta)
class AbstractColumnFilter:
def __init__(self, column_name, dbcon=None):
self.column_name = column_name
self.dbcon = dbcon
self._search_variants = []
def search_variants(self):
"""
Returns all flavors of search available for this column,
"""
return self._search_variants
@abc.abstractmethod
def values(self):
"""
Returns dict of available values for filter {'label':'value'}
"""
pass
@abc.abstractmethod
def prepare_match_part(self, values):
"""
Prepares format valid for $match part from 'values
Args:
values (dict): {'label': 'value'}
Returns:
(dict): {'COLUMN_NAME': {'$in': ['val1', 'val2']}}
"""
pass
class PredefinedSetFilter(AbstractColumnFilter):
def __init__(self, column_name, values):
super().__init__(column_name)
self._search_variants = ['checkbox']
self._values = values
if self._values and \
list(self._values.keys())[0] == list(self._values.values())[0]:
self._search_variants.append('text')
def values(self):
return {k: v for k, v in self._values.items()}
def prepare_match_part(self, values):
return {'$in': list(values.keys())}
class RegexTextFilter(AbstractColumnFilter):
def __init__(self, column_name):
super().__init__(column_name)
self._search_variants = ['text']
def values(self):
return {}
def prepare_match_part(self, values):
""" values = {'text1 text2': 'text1 text2'} """
if not values:
return {}
regex_strs = set()
text = list(values.keys())[0] # only single key always expected
for word in text.split():
regex_strs.add('.*{}.*'.format(word))
return {"$regex": "|".join(regex_strs),
"$options": 'i'}
class MultiSelectFilter(AbstractColumnFilter):
def __init__(self, column_name, values=None, dbcon=None):
super().__init__(column_name)
self._values = values
self.dbcon = dbcon
self._search_variants = ['checkbox']
def values(self):
if self._values:
return {k: v for k, v in self._values.items()}
recs = self.dbcon.find({'type': self.column_name}, {"name": 1,
"_id": -1})
values = {}
for item in recs:
values[item["name"]] = item["name"]
return dict(sorted(values.items(), key=lambda it: it[1]))
def prepare_match_part(self, values):
return {'$in': list(values.keys())}
@attr.s
class FilterDefinition:
type = attr.ib()
values = attr.ib(factory=list)
def pretty_size(value, suffix='B'):
for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']:
if abs(value) < 1024.0:

View file

@ -56,17 +56,31 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel):
"""Returns project"""
return self._project
@property
def column_filtering(self):
return self._column_filtering
def rowCount(self, _index):
return len(self._data)
def columnCount(self, _index):
def columnCount(self, _index=None):
return len(self._header)
def headerData(self, section, orientation, role):
def headerData(self, section, orientation, role=Qt.DisplayRole):
if section >= len(self.COLUMN_LABELS):
return
if role == Qt.DisplayRole:
if orientation == Qt.Horizontal:
return self.COLUMN_LABELS[section][1]
if role == lib.HeaderNameRole:
if orientation == Qt.Horizontal:
return self.COLUMN_LABELS[section][0] # return name
def get_column(self, index):
return self.COLUMN_LABELS[index]
def get_header_index(self, value):
"""
Returns index of 'value' in headers
@ -103,7 +117,7 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel):
self._rec_loaded = 0
if not representations:
self.query = self.get_default_query(load_records)
self.query = self.get_query(load_records)
representations = self.dbcon.aggregate(self.query)
self.add_page_records(self.local_site, self.remote_site,
@ -138,7 +152,7 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel):
log.debug("fetchMore")
items_to_fetch = min(self._total_records - self._rec_loaded,
self.PAGE_SIZE)
self.query = self.get_default_query(self._rec_loaded)
self.query = self.get_query(self._rec_loaded)
representations = self.dbcon.aggregate(self.query)
self.beginInsertRows(index,
self._rec_loaded,
@ -171,7 +185,7 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel):
order = -1
self.sort = {self.SORT_BY_COLUMN[index]: order, '_id': 1}
self.query = self.get_default_query()
self.query = self.get_query()
# import json
# log.debug(json.dumps(self.query, indent=4).\
# replace('False', 'false').\
@ -180,16 +194,86 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel):
representations = self.dbcon.aggregate(self.query)
self.refresh(representations)
def set_filter(self, word_filter):
def set_word_filter(self, word_filter):
"""
Adds text value filtering
Args:
word_filter (str): string inputted by user
"""
self.word_filter = word_filter
self._word_filter = word_filter
self.refresh()
def get_filters(self):
"""
Returns all available filter editors per column_name keys.
"""
filters = {}
for column_name, _ in self.COLUMN_LABELS:
filter_rec = self.COLUMN_FILTERS.get(column_name)
if filter_rec:
filter_rec.dbcon = self.dbcon
filters[column_name] = filter_rec
return filters
def get_column_filter(self, index):
"""
Returns filter object for column 'index
Args:
index(int): index of column in header
Returns:
(AbstractColumnFilter)
"""
column_name = self._header[index]
filter_rec = self.COLUMN_FILTERS.get(column_name)
if filter_rec:
filter_rec.dbcon = self.dbcon # up-to-date db connection
return filter_rec
def set_column_filtering(self, checked_values):
"""
Sets dictionary used in '$match' part of MongoDB aggregate
Args:
checked_values(dict): key:values ({'status':{1:"Foo",3:"Bar"}}
Modifies:
self._column_filtering : {'status': {'$in': [1, 2, 3]}}
"""
filtering = {}
for column_name, dict_value in checked_values.items():
column_f = self.COLUMN_FILTERS.get(column_name)
if not column_f:
continue
column_f.dbcon = self.dbcon
filtering[column_name] = column_f.prepare_match_part(dict_value)
self._column_filtering = filtering
def get_column_filter_values(self, index):
"""
Returns list of available values for filtering in the column
Args:
index(int): index of column in header
Returns:
(dict) of value: label shown in filtering menu
'value' is used in MongoDB query, 'label' is human readable for
menu
for some columns ('subset') might be 'value' and 'label' same
"""
filter_rec = self.get_column_filter(index)
if not filter_rec:
return {}
return filter_rec.values()
def set_project(self, project):
"""
Changes project, called after project selection is changed
@ -251,7 +335,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
("files_count", "Files"),
("files_size", "Size"),
("priority", "Priority"),
("state", "Status")
("status", "Status")
]
DEFAULT_SORT = {
@ -259,18 +343,25 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
"_id": 1
}
SORT_BY_COLUMN = [
"context.asset", # asset
"context.subset", # subset
"context.version", # version
"context.representation", # representation
"asset", # asset
"subset", # subset
"version", # version
"representation", # representation
"updated_dt_local", # local created_dt
"updated_dt_remote", # remote created_dt
"files_count", # count of files
"files_size", # file size of all files
"context.asset", # priority TODO
"status" # state
"status" # status
]
COLUMN_FILTERS = {
'status': lib.PredefinedSetFilter('status', lib.STATUS),
'subset': lib.RegexTextFilter('subset'),
'asset': lib.RegexTextFilter('asset'),
'representation': lib.MultiSelectFilter('representation')
}
refresh_started = QtCore.Signal()
refresh_finished = QtCore.Signal()
@ -297,7 +388,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
files_count = attr.ib(default=None)
files_size = attr.ib(default=None)
priority = attr.ib(default=None)
state = attr.ib(default=None)
status = attr.ib(default=None)
path = attr.ib(default=None)
def __init__(self, sync_server, header, project=None):
@ -307,7 +398,10 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
self._project = project
self._rec_loaded = 0
self._total_records = 0 # how many documents query actually found
self.word_filter = None
self._word_filter = None
self._column_filtering = {}
self._word_filter = None
self._initialized = False
if not self._project or self._project == lib.DUMMY_PROJECT:
@ -319,12 +413,10 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
self.local_site = self.sync_server.get_active_site(self.project)
self.remote_site = self.sync_server.get_remote_site(self.project)
self.projection = self.get_default_projection()
self.sort = self.DEFAULT_SORT
self.query = self.get_default_query()
self.default_query = list(self.get_default_query())
self.query = self.get_query()
self.default_query = list(self.get_query())
representations = self.dbcon.aggregate(self.query)
self.refresh(representations)
@ -359,9 +451,11 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
if role == lib.FailedRole:
if header_value == 'local_site':
return item.state == lib.STATUS[2] and item.local_progress < 1
return item.status == lib.STATUS[2] and \
item.local_progress < 1
if header_value == 'remote_site':
return item.state == lib.STATUS[2] and item.remote_progress < 1
return item.status == lib.STATUS[2] and \
item.remote_progress < 1
if role == Qt.DisplayRole:
# because of ImageDelegate
@ -397,7 +491,6 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
remote_site)
for repre in result.get("paginatedResults"):
context = repre.get("context").pop()
files = repre.get("files", [])
if isinstance(files, dict): # aggregate returns dictionary
files = [files]
@ -420,17 +513,17 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
avg_progress_local = lib.convert_progress(
repre.get('avg_progress_local', '0'))
if context.get("version"):
version = "v{:0>3d}".format(context.get("version"))
if repre.get("version"):
version = "v{:0>3d}".format(repre.get("version"))
else:
version = "master"
item = self.SyncRepresentation(
repre.get("_id"),
context.get("asset"),
context.get("subset"),
repre.get("asset"),
repre.get("subset"),
version,
context.get("representation"),
repre.get("representation"),
local_updated,
remote_updated,
local_site,
@ -449,7 +542,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
self._data.append(item)
self._rec_loaded += 1
def get_default_query(self, limit=0):
def get_query(self, limit=0):
"""
Returns basic aggregate query for main table.
@ -461,7 +554,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
'sync_dt' - same for remote side
'local_site' - progress of repr on local side, 1 = finished
'remote_site' - progress on remote side, calculates from files
'state' -
'status' -
0 - in progress
1 - failed
2 - queued
@ -481,7 +574,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
if limit == 0:
limit = SyncRepresentationSummaryModel.PAGE_SIZE
return [
aggr = [
{"$match": self.get_match_part()},
{'$unwind': '$files'},
# merge potentially unwinded records back to single per repre
@ -584,16 +677,26 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
'paused_local': {'$sum': '$paused_local'},
'updated_dt_local': {'$max': "$updated_dt_local"}
}},
{"$project": self.projection},
{"$sort": self.sort},
{
{"$project": self.projection}
]
if self.column_filtering:
aggr.append(
{"$match": self.column_filtering}
)
aggr.extend(
[{"$sort": self.sort},
{
'$facet': {
'paginatedResults': [{'$skip': self._rec_loaded},
{'$limit': limit}],
'totalCount': [{'$count': 'count'}]
}
}
]
}]
)
return aggr
def get_match_part(self):
"""
@ -614,22 +717,23 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
'files.sites.name': {'$all': [self.local_site,
self.remote_site]}
}
if not self.word_filter:
if not self._word_filter:
return base_match
else:
regex_str = '.*{}.*'.format(self.word_filter)
regex_str = '.*{}.*'.format(self._word_filter)
base_match['$or'] = [
{'context.subset': {'$regex': regex_str, '$options': 'i'}},
{'context.asset': {'$regex': regex_str, '$options': 'i'}},
{'context.representation': {'$regex': regex_str,
'$options': 'i'}}]
if ObjectId.is_valid(self.word_filter):
base_match['$or'] = [{'_id': ObjectId(self.word_filter)}]
if ObjectId.is_valid(self._word_filter):
base_match['$or'] = [{'_id': ObjectId(self._word_filter)}]
return base_match
def get_default_projection(self):
@property
def projection(self):
"""
Projection part for aggregate query.
@ -639,10 +743,10 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
(dict)
"""
return {
"context.subset": 1,
"context.asset": 1,
"context.version": 1,
"context.representation": 1,
"subset": {"$first": "$context.subset"},
"asset": {"$first": "$context.asset"},
"version": {"$first": "$context.version"},
"representation": {"$first": "$context.representation"},
"data.path": 1,
"files": 1,
'files_count': 1,
@ -721,7 +825,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
("remote_site", "Remote site"),
("files_size", "Size"),
("priority", "Priority"),
("state", "Status")
("status", "Status")
]
PAGE_SIZE = 30
@ -733,10 +837,15 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
"updated_dt_local", # local created_dt
"updated_dt_remote", # remote created_dt
"size", # remote progress
"context.asset", # priority TODO
"status" # state
"size", # priority TODO
"status" # status
]
COLUMN_FILTERS = {
'status': lib.PredefinedSetFilter('status', lib.STATUS),
'file': lib.RegexTextFilter('file'),
}
refresh_started = QtCore.Signal()
refresh_finished = QtCore.Signal()
@ -759,7 +868,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
remote_progress = attr.ib(default=None)
size = attr.ib(default=None)
priority = attr.ib(default=None)
state = attr.ib(default=None)
status = attr.ib(default=None)
tries = attr.ib(default=None)
error = attr.ib(default=None)
path = attr.ib(default=None)
@ -772,9 +881,10 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
self._project = project
self._rec_loaded = 0
self._total_records = 0 # how many documents query actually found
self.word_filter = None
self._word_filter = None
self._id = _id
self._initialized = False
self._column_filtering = {}
self.sync_server = sync_server
# TODO think about admin mode
@ -784,10 +894,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
self.sort = self.DEFAULT_SORT
# in case we would like to hide/show some columns
self.projection = self.get_default_projection()
self.query = self.get_default_query()
self.query = self.get_query()
representations = self.dbcon.aggregate(self.query)
self.refresh(representations)
@ -821,9 +928,11 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
if role == lib.FailedRole:
if header_value == 'local_site':
return item.state == lib.STATUS[2] and item.local_progress < 1
return item.status == lib.STATUS[2] and \
item.local_progress < 1
if header_value == 'remote_site':
return item.state == lib.STATUS[2] and item.remote_progress < 1
return item.status == lib.STATUS[2] and \
item.remote_progress < 1
if role == Qt.DisplayRole:
# because of ImageDelegate
@ -909,7 +1018,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
self._data.append(item)
self._rec_loaded += 1
def get_default_query(self, limit=0):
def get_query(self, limit=0):
"""
Gets query that gets used when no extra sorting, filtering or
projecting is needed.
@ -923,7 +1032,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
if limit == 0:
limit = SyncRepresentationSummaryModel.PAGE_SIZE
return [
aggr = [
{"$match": self.get_match_part()},
{"$unwind": "$files"},
{'$addFields': {
@ -1019,7 +1128,16 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
]}
]}}
}},
{"$project": self.projection},
{"$project": self.projection}
]
if self.column_filtering:
aggr.append(
{"$match": self.column_filtering}
)
print(self.column_filtering)
aggr.extend([
{"$sort": self.sort},
{
'$facet': {
@ -1028,7 +1146,9 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
'totalCount': [{'$count': 'count'}]
}
}
]
])
return aggr
def get_match_part(self):
"""
@ -1038,20 +1158,21 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
Returns:
(dict)
"""
if not self.word_filter:
if not self._word_filter:
return {
"type": "representation",
"_id": self._id
}
else:
regex_str = '.*{}.*'.format(self.word_filter)
regex_str = '.*{}.*'.format(self._word_filter)
return {
"type": "representation",
"_id": self._id,
'$or': [{'files.path': {'$regex': regex_str, '$options': 'i'}}]
}
def get_default_projection(self):
@property
def projection(self):
"""
Projection part for aggregate query.

View file

@ -1,6 +1,7 @@
import os
import subprocess
import sys
from functools import partial
from Qt import QtWidgets, QtCore, QtGui
from Qt.QtCore import Qt
@ -14,6 +15,7 @@ from openpype.api import get_local_site_id
from openpype.lib import PypeLogger
from avalon.tools.delegates import pretty_timestamp
from avalon.vendor import qtawesome
from openpype.modules.sync_server.tray.models import (
SyncRepresentationSummaryModel,
@ -40,6 +42,8 @@ class SyncProjectListWidget(ProjectListWidget):
self.local_site = None
self.icons = {}
self.layout().setContentsMargins(0, 0, 0, 0)
def validate_context_change(self):
return True
@ -91,7 +95,6 @@ class SyncProjectListWidget(ProjectListWidget):
self.project_name = point_index.data(QtCore.Qt.DisplayRole)
menu = QtWidgets.QMenu()
menu.setStyleSheet(style.load_stylesheet())
actions_mapping = {}
if self.sync_server.is_project_paused(self.project_name):
@ -141,16 +144,16 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
message_generated = QtCore.Signal(str)
default_widths = (
("asset", 220),
("subset", 190),
("version", 55),
("representation", 95),
("local_site", 170),
("remote_site", 170),
("asset", 190),
("subset", 170),
("version", 60),
("representation", 145),
("local_site", 160),
("remote_site", 160),
("files_count", 50),
("files_size", 60),
("priority", 50),
("state", 110)
("priority", 70),
("status", 110)
)
def __init__(self, sync_server, project=None, parent=None):
@ -162,13 +165,16 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
self.representation_id = None
self.site_name = None # to pause/unpause representation
self.filter = QtWidgets.QLineEdit()
self.filter.setPlaceholderText("Filter representations..")
self.txt_filter = QtWidgets.QLineEdit()
self.txt_filter.setPlaceholderText("Quick filter representations..")
self.txt_filter.setClearButtonEnabled(True)
self.txt_filter.addAction(qtawesome.icon("fa.filter", color="gray"),
QtWidgets.QLineEdit.LeadingPosition)
self._scrollbar_pos = None
top_bar_layout = QtWidgets.QHBoxLayout()
top_bar_layout.addWidget(self.filter)
top_bar_layout.addWidget(self.txt_filter)
self.table_view = QtWidgets.QTableView()
headers = [item[0] for item in self.default_widths]
@ -182,8 +188,6 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
QtWidgets.QAbstractItemView.SelectRows)
self.table_view.horizontalHeader().setSortIndicator(
-1, Qt.AscendingOrder)
self.table_view.setSortingEnabled(True)
self.table_view.horizontalHeader().setSortIndicatorShown(True)
self.table_view.setAlternatingRowColors(True)
self.table_view.verticalHeader().hide()
@ -195,32 +199,39 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
delegate = ImageDelegate(self)
self.table_view.setItemDelegateForColumn(column, delegate)
for column_name, width in self.default_widths:
idx = model.get_header_index(column_name)
self.table_view.setColumnWidth(idx, width)
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addLayout(top_bar_layout)
layout.addWidget(self.table_view)
self.table_view.doubleClicked.connect(self._double_clicked)
self.filter.textChanged.connect(lambda: model.set_filter(
self.filter.text()))
self.txt_filter.textChanged.connect(lambda: model.set_word_filter(
self.txt_filter.text()))
self.table_view.customContextMenuRequested.connect(
self._on_context_menu)
model.refresh_started.connect(self._save_scrollbar)
model.refresh_finished.connect(self._set_scrollbar)
self.table_view.model().modelReset.connect(self._set_selection)
model.modelReset.connect(self._set_selection)
self.model = model
self.selection_model = self.table_view.selectionModel()
self.selection_model.selectionChanged.connect(self._selection_changed)
horizontal_header = HorizontalHeader(self)
self.table_view.setHorizontalHeader(horizontal_header)
self.table_view.setSortingEnabled(True)
for column_name, width in self.default_widths:
idx = model.get_header_index(column_name)
self.table_view.setColumnWidth(idx, width)
def _selection_changed(self, _new_selection):
index = self.selection_model.currentIndex()
self._selected_id = \
self.table_view.model().data(index, Qt.UserRole)
self.model.data(index, Qt.UserRole)
def _set_selection(self):
"""
@ -229,7 +240,7 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
Keep selection during model refresh.
"""
if self._selected_id:
index = self.table_view.model().get_index(self._selected_id)
index = self.model.get_index(self._selected_id)
if index and index.isValid():
mode = QtCore.QItemSelectionModel.Select | \
QtCore.QItemSelectionModel.Rows
@ -241,9 +252,9 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
"""
Opens representation dialog with all files after doubleclick
"""
_id = self.table_view.model().data(index, Qt.UserRole)
_id = self.model.data(index, Qt.UserRole)
detail_window = SyncServerDetailWindow(
self.sync_server, _id, self.table_view.model().project)
self.sync_server, _id, self.model.project)
detail_window.exec()
def _on_context_menu(self, point):
@ -254,13 +265,12 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
if not point_index.isValid():
return
self.item = self.table_view.model()._data[point_index.row()]
self.item = self.model._data[point_index.row()]
self.representation_id = self.item._id
log.debug("menu representation _id:: {}".
format(self.representation_id))
menu = QtWidgets.QMenu()
menu.setStyleSheet(style.load_stylesheet())
actions_mapping = {}
actions_kwargs_mapping = {}
@ -271,7 +281,7 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
for site, progress in {local_site: local_progress,
remote_site: remote_progress}.items():
project = self.table_view.model().project
project = self.model.project
provider = self.sync_server.get_provider_for_site(project,
site)
if provider == 'local_drive':
@ -291,17 +301,17 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
else:
self.site_name = remote_site
if self.item.state in [lib.STATUS[0], lib.STATUS[1]]:
if self.item.status in [lib.STATUS[0], lib.STATUS[1]]:
action = QtWidgets.QAction("Pause")
actions_mapping[action] = self._pause
menu.addAction(action)
if self.item.state == lib.STATUS[3]:
if self.item.status == lib.STATUS[3]:
action = QtWidgets.QAction("Unpause")
actions_mapping[action] = self._unpause
menu.addAction(action)
# if self.item.state == lib.STATUS[1]:
# if self.item.status == lib.STATUS[1]:
# action = QtWidgets.QAction("Open error detail")
# actions_mapping[action] = self._show_detail
# menu.addAction(action)
@ -337,10 +347,10 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
if to_run:
to_run(**to_run_kwargs)
self.table_view.model().refresh()
self.model.refresh()
def _pause(self):
self.sync_server.pause_representation(self.table_view.model().project,
self.sync_server.pause_representation(self.model.project,
self.representation_id,
self.site_name)
self.site_name = None
@ -348,7 +358,7 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
def _unpause(self):
self.sync_server.unpause_representation(
self.table_view.model().project,
self.model.project,
self.representation_id,
self.site_name)
self.site_name = None
@ -358,7 +368,7 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
# temporary here for testing, will be removed TODO
def _add_site(self):
log.info(self.representation_id)
project_name = self.table_view.model().project
project_name = self.model.project
local_site_name = get_local_site_id()
try:
self.sync_server.add_site(
@ -386,15 +396,15 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
try:
local_site = get_local_site_id()
self.sync_server.remove_site(
self.table_view.model().project,
self.model.project,
self.representation_id,
local_site,
True)
self.message_generated.emit("Site {} removed".format(local_site))
except ValueError as exp:
self.message_generated.emit("Error {}".format(str(exp)))
self.table_view.model().refresh(
load_records=self.table_view.model()._rec_loaded)
self.model.refresh(
load_records=self.model._rec_loaded)
def _reset_local_site(self):
"""
@ -402,11 +412,11 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
redo of upload/download
"""
self.sync_server.reset_provider_for_file(
self.table_view.model().project,
self.model.project,
self.representation_id,
'local')
self.table_view.model().refresh(
load_records=self.table_view.model()._rec_loaded)
self.model.refresh(
load_records=self.model._rec_loaded)
def _reset_remote_site(self):
"""
@ -414,18 +424,18 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
redo of upload/download
"""
self.sync_server.reset_provider_for_file(
self.table_view.model().project,
self.model.project,
self.representation_id,
'remote')
self.table_view.model().refresh(
load_records=self.table_view.model()._rec_loaded)
self.model.refresh(
load_records=self.model._rec_loaded)
def _open_in_explorer(self, site):
if not self.item:
return
fpath = self.item.path
project = self.table_view.model().project
project = self.model.project
fpath = self.sync_server.get_local_file_path(project,
site,
fpath)
@ -466,8 +476,8 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget):
("local_site", 185),
("remote_site", 185),
("size", 60),
("priority", 25),
("state", 110)
("priority", 60),
("status", 110)
)
def __init__(self, sync_server, _id=None, project=None, parent=None):
@ -482,64 +492,73 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget):
self._selected_id = None
self.filter = QtWidgets.QLineEdit()
self.filter.setPlaceholderText("Filter representation..")
self.txt_filter = QtWidgets.QLineEdit()
self.txt_filter.setPlaceholderText("Quick filter representation..")
self.txt_filter.setClearButtonEnabled(True)
self.txt_filter.addAction(qtawesome.icon("fa.filter", color="gray"),
QtWidgets.QLineEdit.LeadingPosition)
self._scrollbar_pos = None
top_bar_layout = QtWidgets.QHBoxLayout()
top_bar_layout.addWidget(self.filter)
top_bar_layout.addWidget(self.txt_filter)
self.table_view = QtWidgets.QTableView()
table_view = QtWidgets.QTableView()
headers = [item[0] for item in self.default_widths]
model = SyncRepresentationDetailModel(sync_server, headers, _id,
project)
self.table_view.setModel(model)
self.table_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.table_view.setSelectionMode(
table_view.setModel(model)
table_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
table_view.setSelectionMode(
QtWidgets.QAbstractItemView.SingleSelection)
self.table_view.setSelectionBehavior(
table_view.setSelectionBehavior(
QtWidgets.QTableView.SelectRows)
self.table_view.horizontalHeader().setSortIndicator(-1,
Qt.AscendingOrder)
self.table_view.setSortingEnabled(True)
self.table_view.horizontalHeader().setSortIndicatorShown(True)
self.table_view.setAlternatingRowColors(True)
self.table_view.verticalHeader().hide()
table_view.horizontalHeader().setSortIndicator(-1, Qt.AscendingOrder)
table_view.horizontalHeader().setSortIndicatorShown(True)
table_view.setAlternatingRowColors(True)
table_view.verticalHeader().hide()
column = self.table_view.model().get_header_index("local_site")
column = model.get_header_index("local_site")
delegate = ImageDelegate(self)
self.table_view.setItemDelegateForColumn(column, delegate)
table_view.setItemDelegateForColumn(column, delegate)
column = self.table_view.model().get_header_index("remote_site")
column = model.get_header_index("remote_site")
delegate = ImageDelegate(self)
self.table_view.setItemDelegateForColumn(column, delegate)
for column_name, width in self.default_widths:
idx = model.get_header_index(column_name)
self.table_view.setColumnWidth(idx, width)
table_view.setItemDelegateForColumn(column, delegate)
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addLayout(top_bar_layout)
layout.addWidget(self.table_view)
layout.addWidget(table_view)
self.filter.textChanged.connect(lambda: model.set_filter(
self.filter.text()))
self.table_view.customContextMenuRequested.connect(
self._on_context_menu)
self.model = model
self.selection_model = table_view.selectionModel()
self.selection_model.selectionChanged.connect(self._selection_changed)
horizontal_header = HorizontalHeader(self)
table_view.setHorizontalHeader(horizontal_header)
table_view.setSortingEnabled(True)
for column_name, width in self.default_widths:
idx = model.get_header_index(column_name)
table_view.setColumnWidth(idx, width)
self.table_view = table_view
self.txt_filter.textChanged.connect(lambda: model.set_word_filter(
self.txt_filter.text()))
table_view.customContextMenuRequested.connect(self._on_context_menu)
model.refresh_started.connect(self._save_scrollbar)
model.refresh_finished.connect(self._set_scrollbar)
self.table_view.model().modelReset.connect(self._set_selection)
self.selection_model = self.table_view.selectionModel()
self.selection_model.selectionChanged.connect(self._selection_changed)
model.modelReset.connect(self._set_selection)
def _selection_changed(self):
index = self.selection_model.currentIndex()
self._selected_id = self.table_view.model().data(index, Qt.UserRole)
self._selected_id = self.model.data(index, Qt.UserRole)
def _set_selection(self):
"""
@ -548,7 +567,7 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget):
Keep selection during model refresh.
"""
if self._selected_id:
index = self.table_view.model().get_index(self._selected_id)
index = self.model.get_index(self._selected_id)
if index and index.isValid():
mode = QtCore.QItemSelectionModel.Select | \
QtCore.QItemSelectionModel.Rows
@ -576,10 +595,9 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget):
if not point_index.isValid():
return
self.item = self.table_view.model()._data[point_index.row()]
self.item = self.model._data[point_index.row()]
menu = QtWidgets.QMenu()
menu.setStyleSheet(style.load_stylesheet())
actions_mapping = {}
actions_kwargs_mapping = {}
@ -590,7 +608,7 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget):
for site, progress in {local_site: local_progress,
remote_site: remote_progress}.items():
project = self.table_view.model().project
project = self.model.project
provider = self.sync_server.get_provider_for_site(project,
site)
if provider == 'local_drive':
@ -604,7 +622,7 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget):
actions_kwargs_mapping[action] = {'site': site}
menu.addAction(action)
if self.item.state == lib.STATUS[2]:
if self.item.status == lib.STATUS[2]:
action = QtWidgets.QAction("Open error detail")
actions_mapping[action] = self._show_detail
menu.addAction(action)
@ -637,12 +655,12 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget):
redo of upload/download
"""
self.sync_server.reset_provider_for_file(
self.table_view.model().project,
self.model.project,
self.representation_id,
'local',
self.item._id)
self.table_view.model().refresh(
load_records=self.table_view.model()._rec_loaded)
self.model.refresh(
load_records=self.model._rec_loaded)
def _reset_remote_site(self):
"""
@ -650,12 +668,12 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget):
redo of upload/download
"""
self.sync_server.reset_provider_for_file(
self.table_view.model().project,
self.model.project,
self.representation_id,
'remote',
self.item._id)
self.table_view.model().refresh(
load_records=self.table_view.model()._rec_loaded)
self.model.refresh(
load_records=self.model._rec_loaded)
def _open_in_explorer(self, site):
if not self.item:
@ -818,3 +836,274 @@ class SyncRepresentationErrorWindow(QtWidgets.QDialog):
self.setLayout(body_layout)
self.setWindowTitle("Sync Representation Error Detail")
class TransparentWidget(QtWidgets.QWidget):
clicked = QtCore.Signal(str)
def __init__(self, column_name, *args, **kwargs):
super(TransparentWidget, self).__init__(*args, **kwargs)
self.column_name = column_name
# self.setStyleSheet("background: red;")
def mouseReleaseEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
self.clicked.emit(self.column_name)
super(TransparentWidget, self).mouseReleaseEvent(event)
class HorizontalHeader(QtWidgets.QHeaderView):
def __init__(self, parent=None):
super(HorizontalHeader, self).__init__(QtCore.Qt.Horizontal, parent)
self._parent = parent
self.checked_values = {}
self.setModel(self._parent.model)
self.setSectionsClickable(True)
self.menu_items_dict = {}
self.menu = None
self.header_cells = []
self.filter_buttons = {}
self.filter_icon = qtawesome.icon("fa.filter", color="gray")
self.filter_set_icon = qtawesome.icon("fa.filter", color="white")
self.init_layout()
self._resetting = False
@property
def model(self):
"""Keep model synchronized with parent widget"""
return self._parent.model
def init_layout(self):
for column_idx in range(self.model.columnCount()):
column_name, column_label = self.model.get_column(column_idx)
filter_rec = self.model.get_filters().get(column_name)
if not filter_rec:
continue
icon = self.filter_icon
button = QtWidgets.QPushButton(icon, "", self)
button.setFixedSize(24, 24)
button.setStyleSheet(
"QPushButton::menu-indicator{width:0px;}"
"QPushButton{border: none;background: transparent;}")
button.clicked.connect(partial(self._get_menu,
column_name, column_idx))
button.setFlat(True)
self.filter_buttons[column_name] = button
def showEvent(self, event):
super(HorizontalHeader, self).showEvent(event)
for i in range(len(self.header_cells)):
cell_content = self.header_cells[i]
cell_content.setGeometry(self.sectionViewportPosition(i), 0,
self.sectionSize(i) - 1, self.height())
cell_content.show()
def _set_filter_icon(self, column_name):
button = self.filter_buttons.get(column_name)
if button:
if self.checked_values.get(column_name):
button.setIcon(self.filter_set_icon)
else:
button.setIcon(self.filter_icon)
def _reset_filter(self, column_name):
"""
Remove whole column from filter >> not in $match at all (faster)
"""
self._resetting = True # mark changes to consume them
if self.checked_values.get(column_name) is not None:
self.checked_values.pop(column_name)
self._set_filter_icon(column_name)
self._filter_and_refresh_model_and_menu(column_name, True, True)
self._resetting = False
def _apply_filter(self, column_name, values, state):
"""
Sets 'values' to specific 'state' (checked/unchecked),
sends to model.
"""
if self._resetting: # event triggered by _resetting, skip it
return
self._update_checked_values(column_name, values, state)
self._set_filter_icon(column_name)
self._filter_and_refresh_model_and_menu(column_name, True, False)
def _apply_text_filter(self, column_name, items, line_edit):
"""
Resets all checkboxes, prefers inserted text.
"""
le_text = line_edit.text()
self._update_checked_values(column_name, items, 0) # reset other
if self.checked_values.get(column_name) is not None or \
le_text == '':
self.checked_values.pop(column_name) # reset during typing
if le_text:
self._update_checked_values(column_name, {le_text: le_text}, 2)
self._set_filter_icon(column_name)
self._filter_and_refresh_model_and_menu(column_name, True, True)
def _filter_and_refresh_model_and_menu(self, column_name,
model=True, menu=True):
"""
Refresh model and its content and possibly menu for big changes.
"""
if model:
self.model.set_column_filtering(self.checked_values)
self.model.refresh()
if menu:
self._menu_refresh(column_name)
def _get_menu(self, column_name, index):
"""Prepares content of menu for 'column_name'"""
menu = QtWidgets.QMenu(self)
filter_rec = self.model.get_filters()[column_name]
self.menu_items_dict[column_name] = filter_rec.values()
# text filtering only if labels same as values, not if codes are used
if 'text' in filter_rec.search_variants():
line_edit = QtWidgets.QLineEdit(menu)
line_edit.setClearButtonEnabled(True)
line_edit.addAction(self.filter_icon,
QtWidgets.QLineEdit.LeadingPosition)
line_edit.setFixedHeight(line_edit.height())
txt = ""
if self.checked_values.get(column_name):
txt = list(self.checked_values.get(column_name).keys())[0]
line_edit.setText(txt)
action_le = QtWidgets.QWidgetAction(menu)
action_le.setDefaultWidget(line_edit)
line_edit.textChanged.connect(
partial(self._apply_text_filter, column_name,
filter_rec.values(), line_edit))
menu.addAction(action_le)
menu.addSeparator()
if 'checkbox' in filter_rec.search_variants():
action_all = QtWidgets.QAction("All", self)
action_all.triggered.connect(partial(self._reset_filter,
column_name))
menu.addAction(action_all)
action_none = QtWidgets.QAction("Unselect all", self)
state_unchecked = 0
action_none.triggered.connect(partial(self._apply_filter,
column_name,
filter_rec.values(),
state_unchecked))
menu.addAction(action_none)
menu.addSeparator()
# nothing explicitly >> ALL implicitly >> first time
if self.checked_values.get(column_name) is None:
checked_keys = self.menu_items_dict[column_name].keys()
else:
checked_keys = self.checked_values[column_name]
for value, label in self.menu_items_dict[column_name].items():
checkbox = QtWidgets.QCheckBox(str(label), menu)
# temp
checkbox.setStyleSheet("QCheckBox{spacing: 5px;"
"padding:5px 5px 5px 5px;}")
if value in checked_keys:
checkbox.setChecked(True)
action = QtWidgets.QWidgetAction(menu)
action.setDefaultWidget(checkbox)
checkbox.stateChanged.connect(partial(self._apply_filter,
column_name, {value: label}))
menu.addAction(action)
self.menu = menu
self._show_menu(index, menu)
def _show_menu(self, index, menu):
"""Shows 'menu' under header column of 'index'"""
global_pos_point = self.mapToGlobal(
QtCore.QPoint(self.sectionViewportPosition(index), 0))
menu.setMinimumWidth(self.sectionSize(index))
menu.setMinimumHeight(self.height())
menu.exec_(QtCore.QPoint(global_pos_point.x(),
global_pos_point.y() + self.height()))
def _menu_refresh(self, column_name):
"""
Reset boxes after big change - word filtering or reset
"""
for action in self.menu.actions():
if not isinstance(action, QtWidgets.QWidgetAction):
continue
widget = action.defaultWidget()
if not isinstance(widget, QtWidgets.QCheckBox):
continue
if not self.checked_values.get(column_name) or \
widget.text() in self.checked_values[column_name].values():
widget.setChecked(True)
else:
widget.setChecked(False)
def _update_checked_values(self, column_name, values, state):
"""
Modify dictionary of set values in columns for filtering.
Modifies 'self.checked_values'
"""
copy_menu_items = dict(self.menu_items_dict[column_name])
checked = self.checked_values.get(column_name, copy_menu_items)
set_items = dict(values.items()) # prevent dict change during loop
for value, label in set_items.items():
if state == 2 and label: # checked
checked[value] = label
elif state == 0 and checked.get(value):
checked.pop(value)
self.checked_values[column_name] = checked
def paintEvent(self, event):
self._fix_size()
super(HorizontalHeader, self).paintEvent(event)
def _fix_size(self):
for column_idx in range(self.model.columnCount()):
vis_index = self.visualIndex(column_idx)
index = self.logicalIndex(vis_index)
section_width = self.sectionSize(index)
column_name = self.model.headerData(column_idx,
QtCore.Qt.Horizontal,
lib.HeaderNameRole)
button = self.filter_buttons.get(column_name)
if not button:
continue
pos_x = self.sectionViewportPosition(
index) + section_width - self.height()
pos_y = 0
if button.height() < self.height():
pos_y = int((self.height() - button.height()) / 2)
button.setGeometry(
pos_x,
pos_y,
self.height(),
self.height())

View file

@ -1,10 +0,0 @@
from .user_module import (
UserModule,
IUserModule
)
__all__ = (
"UserModule",
"IUserModule"
)

View file

@ -1,35 +0,0 @@
import json
from aiohttp.web_response import Response
class UserModuleRestApi:
def __init__(self, user_module, server_manager):
self.module = user_module
self.server_manager = server_manager
self.prefix = "/user"
self.register()
def register(self):
self.server_manager.add_route(
"GET",
self.prefix + "/username",
self.get_username
)
self.server_manager.add_route(
"GET",
self.prefix + "/show_widget",
self.show_user_widget
)
async def get_username(self, request):
return Response(
status=200,
body=json.dumps(self.module.cred, indent=4),
content_type="application/json"
)
async def show_user_widget(self, request):
self.module.action_show_widget.trigger()
return Response(status=200)

View file

@ -1,169 +0,0 @@
import os
import json
import getpass
from abc import ABCMeta, abstractmethod
import six
import appdirs
from .. import (
PypeModule,
ITrayModule,
IWebServerRoutes
)
@six.add_metaclass(ABCMeta)
class IUserModule:
"""Interface for other modules to use user change callbacks."""
@abstractmethod
def on_pype_user_change(self, username):
"""What should happen on Pype user change."""
pass
class UserModule(PypeModule, ITrayModule, IWebServerRoutes):
cred_folder_path = os.path.normpath(
appdirs.user_data_dir('pype-app', 'pype')
)
cred_filename = 'user_info.json'
env_name = "OPENPYPE_USERNAME"
name = "user"
def initialize(self, modules_settings):
user_settings = modules_settings[self.name]
self.enabled = user_settings["enabled"]
self.callbacks_on_user_change = []
self.cred = {}
self.cred_path = os.path.normpath(os.path.join(
self.cred_folder_path, self.cred_filename
))
# Tray attributes
self.widget_login = None
self.action_show_widget = None
self.rest_api_obj = None
def tray_init(self):
from .widget_user import UserWidget
self.widget_login = UserWidget(self)
self.load_credentials()
def register_callback_on_user_change(self, callback):
self.callbacks_on_user_change.append(callback)
def tray_start(self):
"""Store credentials to env and preset them to widget"""
username = ""
if self.cred:
username = self.cred.get("username") or ""
os.environ[self.env_name] = username
self.widget_login.set_user(username)
def tray_exit(self):
"""Nothing special for User."""
return
def get_user(self):
return self.cred.get("username") or getpass.getuser()
def webserver_initialization(self, server_manager):
"""Implementation of IWebServerRoutes interface."""
from .rest_api import UserModuleRestApi
self.rest_api_obj = UserModuleRestApi(self, server_manager)
def connect_with_modules(self, enabled_modules):
for module in enabled_modules:
if isinstance(module, IUserModule):
self.callbacks_on_user_change.append(
module.on_pype_user_change
)
# Definition of Tray menu
def tray_menu(self, parent_menu):
from Qt import QtWidgets
"""Add menu or action to Tray(or parent)'s menu"""
action = QtWidgets.QAction("Username", parent_menu)
action.triggered.connect(self.show_widget)
parent_menu.addAction(action)
parent_menu.addSeparator()
self.action_show_widget = action
def load_credentials(self):
"""Get credentials from JSON file """
credentials = {}
try:
file = open(self.cred_path, "r")
credentials = json.load(file)
file.close()
self.cred = credentials
username = credentials.get("username")
if username:
self.log.debug("Loaded Username \"{}\"".format(username))
else:
self.log.debug("Pype Username is not set")
return credentials
except FileNotFoundError:
return self.save_credentials(getpass.getuser())
except json.decoder.JSONDecodeError:
self.log.warning((
"File where users credentials should be stored"
" has invalid json format. Loading system username."
))
return self.save_credentials(getpass.getuser())
def change_credentials(self, username):
self.save_credentials(username)
for callback in self.callbacks_on_user_change:
try:
callback(username)
except Exception:
self.log.warning(
"Failed to execute callback \"{}\".".format(
str(callback)
),
exc_info=True
)
def save_credentials(self, username):
"""Save credentials to JSON file, env and widget"""
if username is None:
username = ""
username = str(username).strip()
self.cred = {"username": username}
os.environ[self.env_name] = username
if self.widget_login:
self.widget_login.set_user(username)
try:
file = open(self.cred_path, "w")
file.write(json.dumps(self.cred))
file.close()
self.log.debug("Username \"{}\" stored".format(username))
except Exception:
self.log.error(
"Could not store username to file \"{}\"".format(
self.cred_path
),
exc_info=True
)
return self.cred
def show_widget(self):
"""Show dialog to enter credentials"""
self.widget_login.show()

View file

@ -1,88 +0,0 @@
from Qt import QtCore, QtGui, QtWidgets
from avalon import style
from openpype import resources
class UserWidget(QtWidgets.QWidget):
MIN_WIDTH = 300
def __init__(self, module):
super(UserWidget, self).__init__()
self.module = module
# Style
icon = QtGui.QIcon(resources.pype_icon_filepath())
self.setWindowIcon(icon)
self.setWindowTitle("Username Settings")
self.setMinimumWidth(self.MIN_WIDTH)
self.setStyleSheet(style.load_stylesheet())
self.setWindowFlags(
QtCore.Qt.WindowCloseButtonHint |
QtCore.Qt.WindowMinimizeButtonHint
)
self.setLayout(self._main())
def show(self, *args, **kwargs):
super().show(*args, **kwargs)
# Move widget to center of active screen on show
screen = QtWidgets.QApplication.desktop().screen()
screen_center = lambda self: (
screen.rect().center() - self.rect().center()
)
self.move(screen_center(self))
def _main(self):
main_layout = QtWidgets.QVBoxLayout()
form_layout = QtWidgets.QFormLayout()
form_layout.setContentsMargins(10, 15, 10, 5)
label_username = QtWidgets.QLabel("Username:")
label_username.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor))
label_username.setTextFormat(QtCore.Qt.RichText)
input_username = QtWidgets.QLineEdit()
input_username.setPlaceholderText(
QtCore.QCoreApplication.translate("main", "e.g. John Smith")
)
form_layout.addRow(label_username, input_username)
btn_save = QtWidgets.QPushButton("Save")
btn_save.clicked.connect(self.click_save)
btn_cancel = QtWidgets.QPushButton("Cancel")
btn_cancel.clicked.connect(self.close)
btn_group = QtWidgets.QHBoxLayout()
btn_group.addStretch(1)
btn_group.addWidget(btn_save)
btn_group.addWidget(btn_cancel)
main_layout.addLayout(form_layout)
main_layout.addLayout(btn_group)
self.input_username = input_username
return main_layout
def set_user(self, username):
self.input_username.setText(username)
def click_save(self):
# all what should happen - validations and saving into appsdir
username = self.input_username.text()
self.module.change_credentials(username)
self._close_widget()
def closeEvent(self, event):
event.ignore()
self._close_widget()
def _close_widget(self):
self.hide()

View file

@ -1,6 +1,7 @@
import os
import getpass
import pyblish.api
from openpype.lib import get_openpype_username
class CollectCurrentUserPype(pyblish.api.ContextPlugin):
@ -11,9 +12,6 @@ class CollectCurrentUserPype(pyblish.api.ContextPlugin):
label = "Collect Pype User"
def process(self, context):
user = os.getenv("OPENPYPE_USERNAME", "").strip()
if not user:
user = context.data.get("user", getpass.getuser())
user = get_openpype_username()
context.data["user"] = user
self.log.debug("Colected user \"{}\"".format(user))

View file

@ -1,9 +1,13 @@
from .exceptions import (
SaveWarningExc
)
from .lib import (
get_system_settings,
get_project_settings,
get_current_project_settings,
get_anatomy_settings,
get_environments
get_environments,
get_local_settings
)
from .entities import (
SystemSettings,
@ -12,11 +16,14 @@ from .entities import (
__all__ = (
"SaveWarningExc",
"get_system_settings",
"get_project_settings",
"get_current_project_settings",
"get_anatomy_settings",
"get_environments",
"get_local_settings",
"SystemSettings",
"ProjectSettings"

View file

@ -1165,6 +1165,7 @@
},
"variants": {
"4-26": {
"use_python_2": false,
"executables": {
"windows": [],
"darwin": [],

View file

@ -161,9 +161,6 @@
"log_viewer": {
"enabled": true
},
"user": {
"enabled": true
},
"standalonepublish_tool": {
"enabled": true
}

View file

@ -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."""

View file

@ -154,20 +154,6 @@
}
]
},
{
"type": "dict",
"key": "user",
"label": "User setting",
"collapsible": true,
"checkbox_key": "enabled",
"children": [
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
}
]
},
{
"type": "dict",
"key": "standalonepublish_tool",

View file

@ -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)

View file

@ -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

View file

@ -1,7 +1,7 @@
import os
import importlib
from avalon import api, lib, style
from avalon import api, style
from openpype import PLUGINS_DIR
from openpype.api import Logger, resources
from openpype.lib import (
ApplictionExecutableNotFound,
@ -10,81 +10,6 @@ from openpype.lib import (
from Qt import QtWidgets, QtGui
class ProjectManagerAction(api.Action):
name = "projectmanager"
label = "Project Manager"
icon = "gear"
order = 999 # at the end
def is_compatible(self, session):
return "AVALON_PROJECT" in session
def process(self, session, **kwargs):
return lib.launch(
executable="python",
args=[
"-u", "-m", "avalon.tools.projectmanager",
session['AVALON_PROJECT']
]
)
class LoaderAction(api.Action):
name = "loader"
label = "Loader"
icon = "cloud-download"
order = 998
def is_compatible(self, session):
return "AVALON_PROJECT" in session
def process(self, session, **kwargs):
return lib.launch(
executable="python",
args=[
"-u", "-m", "avalon.tools.loader", session['AVALON_PROJECT']
]
)
class LoaderLibrary(api.Action):
name = "loader_os"
label = "Library Loader"
icon = "book"
order = 997 # at the end
def is_compatible(self, session):
return True
def process(self, session, **kwargs):
return lib.launch(
executable="python",
args=["-u", "-m", "avalon.tools.libraryloader"]
)
def register_default_actions():
"""Register default actions for Launcher"""
api.register_plugin(api.Action, ProjectManagerAction)
api.register_plugin(api.Action, LoaderAction)
api.register_plugin(api.Action, LoaderLibrary)
def register_config_actions():
"""Register actions from the configuration for Launcher"""
module_name = os.environ["AVALON_CONFIG"]
config = importlib.import_module(module_name)
if not hasattr(config, "register_launcher_actions"):
print(
"Current configuration `%s` has no 'register_launcher_actions'"
% config.__name__
)
return
config.register_launcher_actions()
def register_actions_from_paths(paths):
if not paths:
return
@ -106,6 +31,13 @@ def register_actions_from_paths(paths):
api.register_plugin_path(api.Action, path)
def register_config_actions():
"""Register actions from the configuration for Launcher"""
actions_dir = os.path.join(PLUGINS_DIR, "actions")
register_actions_from_paths([actions_dir])
def register_environment_actions():
"""Register actions from AVALON_ACTIONS for Launcher."""

View file

@ -1,3 +1,5 @@
import getpass
from Qt import QtWidgets
@ -5,16 +7,29 @@ class LocalGeneralWidgets(QtWidgets.QWidget):
def __init__(self, parent):
super(LocalGeneralWidgets, self).__init__(parent)
username_input = QtWidgets.QLineEdit(self)
username_input.setPlaceholderText(getpass.getuser())
layout = QtWidgets.QFormLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addRow("OpenPype Username", username_input)
self.username_input = username_input
def update_local_settings(self, value):
return
# RETURNING EARLY TO HIDE WIDGET WITHOUT CONTENT
username = ""
if value:
username = value.get("username", username)
self.username_input.setText(username)
def settings_value(self):
# Add changed
# If these have changed then
output = {}
# TEMPORARILY EMPTY AS THERE IS NOTHING TO PUT HERE
username = self.username_input.text()
if username:
output["username"] = username
# Do not return output yet since we don't have mechanism to save or
# load these data through api calls
return output

View file

@ -80,7 +80,6 @@ class LocalSettingsWidget(QtWidgets.QWidget):
general_widget = LocalGeneralWidgets(general_content)
general_layout.addWidget(general_widget)
general_expand_widget.hide()
self.main_layout.addWidget(general_expand_widget)
@ -127,9 +126,9 @@ class LocalSettingsWidget(QtWidgets.QWidget):
self.system_settings.reset()
self.project_settings.reset()
# self.general_widget.update_local_settings(
# value.get(LOCAL_GENERAL_KEY)
# )
self.general_widget.update_local_settings(
value.get(LOCAL_GENERAL_KEY)
)
self.app_widget.update_local_settings(
value.get(LOCAL_APPS_KEY)
)
@ -139,9 +138,9 @@ class LocalSettingsWidget(QtWidgets.QWidget):
def settings_value(self):
output = {}
# general_value = self.general_widget.settings_value()
# if general_value:
# output[LOCAL_GENERAL_KEY] = general_value
general_value = self.general_widget.settings_value()
if general_value:
output[LOCAL_GENERAL_KEY] = general_value
app_value = self.app_widget.settings_value()
if app_value:

View file

@ -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 = [
"<b>Settings were saved but few issues happened.</b>"
]
for item in exc.warnings:
warnings.append(item.replace("\n", "<br>"))
msg = "<br><br>".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)