Merge branch '2.x/develop' into 2.x/feature/harmony_flag_inventory

# Conflicts:
#	pype/hosts/harmony/__init__.py
This commit is contained in:
Toke Stuart Jepsen 2020-06-23 15:51:58 +01:00
commit d2efd2e791
39 changed files with 682 additions and 369 deletions

View file

@ -6,6 +6,12 @@ from pypeapp import (
execute
)
from pypeapp.lib.mongo import (
decompose_url,
compose_url,
get_default_components
)
from .plugin import (
Extractor,
@ -44,6 +50,9 @@ __all__ = [
"project_overrides_dir_path",
"config",
"execute",
"decompose_url",
"compose_url",
"get_default_components",
# plugin classes
"Extractor",

View file

@ -17,6 +17,7 @@ import six
import avalon.api
from .api import config
log = logging.getLogger(__name__)

View file

@ -16,6 +16,7 @@ import contextlib
from avalon import schema
from avalon.vendor import requests
from avalon.io import extract_port_from_url
# Third-party dependencies
import pymongo
@ -72,8 +73,17 @@ class DbConnector(object):
self.Session.update(self._from_environment())
timeout = int(self.Session["AVALON_TIMEOUT"])
self._mongo_client = pymongo.MongoClient(
self.Session["AVALON_MONGO"], serverSelectionTimeoutMS=timeout)
mongo_url = self.Session["AVALON_MONGO"]
kwargs = {
"host": mongo_url,
"serverSelectionTimeoutMS": timeout
}
port = extract_port_from_url(mongo_url)
if port is not None:
kwargs["port"] = int(port)
self._mongo_client = pymongo.MongoClient(**kwargs)
for retry in range(3):
try:
@ -381,6 +391,10 @@ class DbConnector(object):
if document is None:
break
if document.get("type") == "master_version":
_document = self.find_one({"_id": document["version_id"]})
document["data"] = _document["data"]
parents.append(document)
return parents

View file

@ -4,17 +4,14 @@ import json
import bson
import bson.json_util
from pype.modules.rest_api import RestApi, abort, CallbackResult
from pype.modules.ftrack.lib.custom_db_connector import DbConnector
from pype.modules.ftrack.lib.io_nonsingleton import DbConnector
class AvalonRestApi(RestApi):
dbcon = DbConnector(
os.environ["AVALON_MONGO"],
os.environ["AVALON_DB"]
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.dbcon = DbConnector()
self.dbcon.install()
@RestApi.route("/projects/<project_name>", url_prefix="/avalon", methods="GET")

View file

@ -13,10 +13,12 @@ import time
import uuid
import ftrack_api
import pymongo
from pype.modules.ftrack.lib import credentials
from pype.modules.ftrack.ftrack_server.lib import (
ftrack_events_mongo_settings, check_ftrack_url
check_ftrack_url, get_ftrack_event_mongo_info
)
import socket_thread
@ -30,22 +32,19 @@ class MongoPermissionsError(Exception):
def check_mongo_url(host, port, log_error=False):
"""Checks if mongo server is responding"""
sock = None
try:
sock = socket.create_connection(
(host, port),
timeout=1
)
return True
except socket.error as err:
client = pymongo.MongoClient(host=host, port=port)
# Force connection on a request as the connect=True parameter of
# MongoClient seems to be useless here
client.server_info()
except pymongo.errors.ServerSelectionTimeoutError as err:
if log_error:
print("Can't connect to MongoDB at {}:{} because: {}".format(
host, port, err
))
return False
finally:
if sock is not None:
sock.close()
return True
def validate_credentials(url, user, api):
@ -190,9 +189,10 @@ def main_loop(ftrack_url):
os.environ["FTRACK_EVENT_SUB_ID"] = str(uuid.uuid1())
# Get mongo hostname and port for testing mongo connection
mongo_list = ftrack_events_mongo_settings()
mongo_hostname = mongo_list[0]
mongo_port = mongo_list[1]
mongo_uri, mongo_port, database_name, collection_name = (
get_ftrack_event_mongo_info()
)
# Current file
file_path = os.path.dirname(os.path.realpath(__file__))
@ -270,13 +270,12 @@ def main_loop(ftrack_url):
ftrack_accessible = check_ftrack_url(ftrack_url)
if not mongo_accessible:
mongo_accessible = check_mongo_url(mongo_hostname, mongo_port)
mongo_accessible = check_mongo_url(mongo_uri, mongo_port)
# Run threads only if Ftrack is accessible
if not ftrack_accessible or not mongo_accessible:
if not mongo_accessible and not printed_mongo_error:
mongo_url = mongo_hostname + ":" + mongo_port
print("Can't access Mongo {}".format(mongo_url))
print("Can't access Mongo {}".format(mongo_uri))
if not ftrack_accessible and not printed_ftrack_error:
print("Can't access Ftrack {}".format(ftrack_url))

View file

@ -18,12 +18,13 @@ import ftrack_api.operation
import ftrack_api._centralized_storage_scenario
import ftrack_api.event
from ftrack_api.logging import LazyLogMessage as L
try:
from urllib.parse import urlparse, parse_qs
except ImportError:
from urlparse import urlparse, parse_qs
from pype.api import Logger
from pype.api import (
Logger,
get_default_components,
decompose_url,
compose_url
)
from pype.modules.ftrack.lib.custom_db_connector import DbConnector
@ -32,69 +33,29 @@ TOPIC_STATUS_SERVER = "pype.event.server.status"
TOPIC_STATUS_SERVER_RESULT = "pype.event.server.status.result"
def ftrack_events_mongo_settings():
host = None
port = None
username = None
password = None
collection = None
database = None
auth_db = ""
if os.environ.get('FTRACK_EVENTS_MONGO_URL'):
result = urlparse(os.environ['FTRACK_EVENTS_MONGO_URL'])
host = result.hostname
try:
port = result.port
except ValueError:
raise RuntimeError("invalid port specified")
username = result.username
password = result.password
try:
database = result.path.lstrip("/").split("/")[0]
collection = result.path.lstrip("/").split("/")[1]
except IndexError:
if not database:
raise RuntimeError("missing database name for logging")
try:
auth_db = parse_qs(result.query)['authSource'][0]
except KeyError:
# no auth db provided, mongo will use the one we are connecting to
pass
else:
host = os.environ.get('FTRACK_EVENTS_MONGO_HOST')
port = int(os.environ.get('FTRACK_EVENTS_MONGO_PORT', "0"))
database = os.environ.get('FTRACK_EVENTS_MONGO_DB')
username = os.environ.get('FTRACK_EVENTS_MONGO_USER')
password = os.environ.get('FTRACK_EVENTS_MONGO_PASSWORD')
collection = os.environ.get('FTRACK_EVENTS_MONGO_COL')
auth_db = os.environ.get('FTRACK_EVENTS_MONGO_AUTH_DB', 'avalon')
return host, port, database, username, password, collection, auth_db
def get_ftrack_event_mongo_info():
host, port, database, username, password, collection, auth_db = (
ftrack_events_mongo_settings()
database_name = (
os.environ.get("FTRACK_EVENTS_MONGO_DB") or "pype"
)
collection_name = (
os.environ.get("FTRACK_EVENTS_MONGO_COL") or "ftrack_events"
)
user_pass = ""
if username and password:
user_pass = "{}:{}@".format(username, password)
socket_path = "{}:{}".format(host, port)
mongo_url = os.environ.get("FTRACK_EVENTS_MONGO_URL")
if mongo_url is not None:
components = decompose_url(mongo_url)
_used_ftrack_url = True
else:
components = get_default_components()
_used_ftrack_url = False
dab = ""
if database:
dab = "/{}".format(database)
if not _used_ftrack_url or components["database"] is None:
components["database"] = database_name
components["collection"] = collection_name
auth = ""
if auth_db:
auth = "?authSource={}".format(auth_db)
uri = compose_url(components)
url = "mongodb://{}{}{}{}".format(user_pass, socket_path, dab, auth)
return url, database, collection
return uri, components["port"], database_name, collection_name
def check_ftrack_url(url, log_errors=True):
@ -198,16 +159,17 @@ class StorerEventHub(SocketBaseEventHub):
class ProcessEventHub(SocketBaseEventHub):
hearbeat_msg = b"processor"
url, database, table_name = get_ftrack_event_mongo_info()
uri, port, database, table_name = get_ftrack_event_mongo_info()
is_table_created = False
pypelog = Logger().get_logger("Session Processor")
def __init__(self, *args, **kwargs):
self.dbcon = DbConnector(
mongo_url=self.url,
database_name=self.database,
table_name=self.table_name
self.uri,
self.port,
self.database,
self.table_name
)
super(ProcessEventHub, self).__init__(*args, **kwargs)
@ -269,7 +231,7 @@ class ProcessEventHub(SocketBaseEventHub):
def load_events(self):
"""Load not processed events sorted by stored date"""
ago_date = datetime.datetime.now() - datetime.timedelta(days=3)
result = self.dbcon.delete_many({
self.dbcon.delete_many({
"pype_data.stored": {"$lte": ago_date},
"pype_data.is_processed": True
})

View file

@ -23,12 +23,8 @@ class SessionFactory:
session = None
url, database, table_name = get_ftrack_event_mongo_info()
dbcon = DbConnector(
mongo_url=url,
database_name=database,
table_name=table_name
)
uri, port, database, table_name = get_ftrack_event_mongo_info()
dbcon = DbConnector(uri, port, database, table_name)
# ignore_topics = ["ftrack.meta.connected"]
ignore_topics = []

View file

@ -12,6 +12,7 @@ import atexit
# Third-party dependencies
import pymongo
from pype.api import decompose_url
class NotActiveTable(Exception):
@ -63,13 +64,29 @@ class DbConnector:
log = logging.getLogger(__name__)
timeout = 1000
def __init__(self, mongo_url, database_name, table_name=None):
def __init__(
self, uri, port=None, database_name=None, table_name=None
):
self._mongo_client = None
self._sentry_client = None
self._sentry_logging_handler = None
self._database = None
self._is_installed = False
self._mongo_url = mongo_url
self._uri = uri
components = decompose_url(uri)
if port is None:
port = components.get("port")
if database_name is None:
database_name = components.get("database")
if database_name is None:
raise ValueError(
"Database is not defined for connection. {}".format(uri)
)
self._port = port
self._database_name = database_name
self.active_table = table_name
@ -95,10 +112,16 @@ class DbConnector:
atexit.register(self.uninstall)
logging.basicConfig()
self._mongo_client = pymongo.MongoClient(
self._mongo_url,
serverSelectionTimeoutMS=self.timeout
)
kwargs = {
"host": self._uri,
"serverSelectionTimeoutMS": self.timeout
}
if self._port is not None:
kwargs["port"] = self._port
self._mongo_client = pymongo.MongoClient(**kwargs)
if self._port is None:
self._port = self._mongo_client.PORT
for retry in range(3):
try:
@ -113,11 +136,11 @@ class DbConnector:
else:
raise IOError(
"ERROR: Couldn't connect to %s in "
"less than %.3f ms" % (self._mongo_url, self.timeout)
"less than %.3f ms" % (self._uri, self.timeout)
)
self.log.info("Connected to %s, delay %.3f s" % (
self._mongo_url, time.time() - t1
self._uri, time.time() - t1
))
self._database = self._mongo_client[self._database_name]

View file

@ -16,6 +16,7 @@ import contextlib
from avalon import schema
from avalon.vendor import requests
from avalon.io import extract_port_from_url
# Third-party dependencies
import pymongo
@ -72,8 +73,17 @@ class DbConnector(object):
self.Session.update(self._from_environment())
timeout = int(self.Session["AVALON_TIMEOUT"])
self._mongo_client = pymongo.MongoClient(
self.Session["AVALON_MONGO"], serverSelectionTimeoutMS=timeout)
mongo_url = self.Session["AVALON_MONGO"]
kwargs = {
"host": mongo_url,
"serverSelectionTimeoutMS": timeout
}
port = extract_port_from_url(mongo_url)
if port is not None:
kwargs["port"] = int(port)
self._mongo_client = pymongo.MongoClient(**kwargs)
for retry in range(3):
try:
@ -381,6 +391,10 @@ class DbConnector(object):
if document is None:
break
if document.get("type") == "master_version":
_document = self.find_one({"_id": document["version_id"]})
document["data"] = _document["data"]
parents.append(document)
return parents

View file

@ -1,8 +1,7 @@
import os
import collections
from Qt import QtCore
from pype.api import Logger
from pypeapp.lib.log import _bootstrap_mongo_log
from pypeapp.lib.log import _bootstrap_mongo_log, LOG_COLLECTION_NAME
log = Logger().get_logger("LogModel", "LoggingModule")
@ -41,11 +40,11 @@ class LogModel(QtCore.QAbstractItemModel):
super(LogModel, self).__init__(parent)
self._root_node = Node()
collection = os.environ.get('PYPE_LOG_MONGO_COL')
database = _bootstrap_mongo_log()
self.dbcon = None
if collection in database.list_collection_names():
self.dbcon = database[collection]
# Crash if connection is not possible to skip this module
database = _bootstrap_mongo_log()
if LOG_COLLECTION_NAME in database.list_collection_names():
self.dbcon = database[LOG_COLLECTION_NAME]
def add_log(self, log):
node = Node(log)

View file

@ -1,20 +1,23 @@
from Qt import QtWidgets
from pype.api import Logger
from ..gui.app import LogsWindow
log = Logger().get_logger("LoggingModule", "logging")
class LoggingModule:
def __init__(self, main_parent=None, parent=None):
self.parent = parent
self.log = Logger().get_logger(self.__class__.__name__, "logging")
self.window = LogsWindow()
try:
self.window = LogsWindow()
self.tray_menu = self._tray_menu
except Exception:
self.log.warning(
"Couldn't set Logging GUI due to error.", exc_info=True
)
# Definition of Tray menu
def tray_menu(self, parent_menu):
def _tray_menu(self, parent_menu):
# Menu for Tray App
menu = QtWidgets.QMenu('Logging', parent_menu)
# menu.setProperty('submenu', 'on')

View file

@ -48,8 +48,18 @@ class CollectAvalonEntities(pyblish.api.ContextPlugin):
data = asset_entity['data']
context.data["frameStart"] = data.get("frameStart")
context.data["frameEnd"] = data.get("frameEnd")
frame_start = data.get("frameStart")
if frame_start is None:
frame_start = 1
self.log.warning("Missing frame start. Defaulting to 1.")
frame_end = data.get("frameEnd")
if frame_end is None:
frame_end = 2
self.log.warning("Missing frame end. Defaulting to 2.")
context.data["frameStart"] = frame_start
context.data["frameEnd"] = frame_end
handles = data.get("handles") or 0
handle_start = data.get("handleStart")
@ -72,7 +82,7 @@ class CollectAvalonEntities(pyblish.api.ContextPlugin):
context.data["handleStart"] = int(handle_start)
context.data["handleEnd"] = int(handle_end)
frame_start_h = data.get("frameStart") - context.data["handleStart"]
frame_end_h = data.get("frameEnd") + context.data["handleEnd"]
frame_start_h = frame_start - context.data["handleStart"]
frame_end_h = frame_end + context.data["handleEnd"]
context.data["frameStartHandle"] = frame_start_h
context.data["frameEndHandle"] = frame_end_h

View file

@ -14,7 +14,10 @@ import pyblish.api
def _get_script():
"""Get path to the image sequence script."""
from pathlib import Path
try:
from pathlib import Path
except ImportError:
from pathlib2 import Path
try:
from pype.scripts import publish_filesequence
@ -26,6 +29,7 @@ def _get_script():
module_path = module_path[: -len(".pyc")] + ".py"
path = Path(os.path.normpath(module_path)).resolve(strict=True)
assert path is not None, ("Cannot determine path")
return str(path)
@ -840,7 +844,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
# add audio to metadata file if available
audio_file = context.data.get("audioFile")
if os.path.isfile(audio_file):
if audio_file and os.path.isfile(audio_file):
publish_job.update({"audio": audio_file})
# pass Ftrack credentials in case of Muster

View file

@ -8,12 +8,11 @@ except ImportError:
import errno
class ValidateFFmpegInstalled(pyblish.api.Validator):
class ValidateFFmpegInstalled(pyblish.api.ContextPlugin):
"""Validate availability of ffmpeg tool in PATH"""
order = pyblish.api.ValidatorOrder
label = 'Validate ffmpeg installation'
families = ['review']
optional = True
def is_tool(self, name):
@ -27,7 +26,7 @@ class ValidateFFmpegInstalled(pyblish.api.Validator):
return False
return True
def process(self, instance):
def process(self, context):
ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg")
self.log.info("ffmpeg path: `{}`".format(ffmpeg_path))
if self.is_tool(ffmpeg_path) is False:

View file

@ -0,0 +1,42 @@
from avalon import api, harmony
func = """
function getUniqueColumnName( column_prefix )
{
var suffix = 0;
// finds if unique name for a column
var column_name = column_prefix;
while(suffix < 2000)
{
if(!column.type(column_name))
break;
suffix = suffix + 1;
column_name = column_prefix + "_" + suffix;
}
return column_name;
}
function func(args)
{
var uniqueColumnName = getUniqueColumnName(args[0]);
column.add(uniqueColumnName , "SOUND");
column.importSound(uniqueColumnName, 1, args[1]);
}
func
"""
class ImportAudioLoader(api.Loader):
"""Import audio."""
families = ["shot"]
representations = ["wav"]
label = "Import Audio"
def load(self, context, name=None, namespace=None, data=None):
wav_file = api.get_representation_path(context["representation"])
harmony.send(
{"function": func, "args": [context["subset"]["name"], wav_file]}
)

View file

@ -14,18 +14,6 @@ class ImportTemplateLoader(api.Loader):
label = "Import Template"
def load(self, context, name=None, namespace=None, data=None):
# Make backdrops from metadata.
backdrops = context["representation"]["data"].get("backdrops", [])
func = """function func(args)
{
Backdrop.addBackdrop("Top", args[0]);
}
func
"""
for backdrop in backdrops:
harmony.send({"function": func, "args": [backdrop]})
# Import template.
temp_dir = tempfile.mkdtemp()
zip_file = api.get_representation_path(context["representation"])
@ -33,19 +21,6 @@ class ImportTemplateLoader(api.Loader):
with zipfile.ZipFile(zip_file, "r") as zip_ref:
zip_ref.extractall(template_path)
func = """function func(args)
{
var template_path = args[0];
var drag_object = copyPaste.copyFromTemplate(
template_path, 0, 0, copyPaste.getCurrentCreateOptions()
);
copyPaste.pasteNewNodes(
drag_object, "", copyPaste.getCurrentPasteOptions()
);
}
func
"""
func = """function func(args)
{
var template_path = args[0];

View file

@ -28,7 +28,8 @@ class ExtractRender(pyblish.api.InstancePlugin):
scene.currentScene(),
scene.getFrameRate(),
scene.getStartFrame(),
scene.getStopFrame()
scene.getStopFrame(),
sound.getSoundtrackAll().path()
]
}
func
@ -41,6 +42,7 @@ class ExtractRender(pyblish.api.InstancePlugin):
frame_rate = result[3]
frame_start = result[4]
frame_end = result[5]
audio_path = result[6]
# Set output path to temp folder.
path = tempfile.mkdtemp()
@ -111,6 +113,7 @@ class ExtractRender(pyblish.api.InstancePlugin):
mov_path = os.path.join(path, instance.data["name"] + ".mov")
args = [
"ffmpeg", "-y",
"-i", audio_path,
"-i",
os.path.join(path, collection.head + "%04d" + collection.tail),
mov_path

View file

@ -9,5 +9,5 @@ class ExtractSaveScene(pyblish.api.ContextPlugin):
order = pyblish.api.ExtractorOrder - 0.49
hosts = ["harmony"]
def process(self, instance):
def process(self, context):
harmony.save_scene()

View file

@ -2,7 +2,8 @@ import os
import shutil
import pype.api
from avalon import harmony
import avalon.harmony
import pype.hosts.harmony
class ExtractTemplate(pype.api.Extractor):
@ -14,6 +15,7 @@ class ExtractTemplate(pype.api.Extractor):
def process(self, instance):
staging_dir = self.staging_dir(instance)
filepath = os.path.join(staging_dir, "{}.tpl".format(instance.name))
self.log.info("Outputting template to {}".format(staging_dir))
@ -28,7 +30,7 @@ class ExtractTemplate(pype.api.Extractor):
unique_backdrops = [backdrops[x] for x in set(backdrops.keys())]
# Get non-connected nodes within backdrops.
all_nodes = harmony.send(
all_nodes = avalon.harmony.send(
{"function": "node.subNodes", "args": ["Top"]}
)["result"]
for node in [x for x in all_nodes if x not in dependencies]:
@ -43,48 +45,9 @@ class ExtractTemplate(pype.api.Extractor):
dependencies.remove(instance[0])
# Export template.
func = """function func(args)
{
// Add an extra node just so a new group can be created.
var temp_node = node.add("Top", "temp_note", "NOTE", 0, 0, 0);
var template_group = node.createGroup(temp_node, "temp_group");
node.deleteNode( template_group + "/temp_note" );
// This will make Node View to focus on the new group.
selection.clearSelection();
selection.addNodeToSelection(template_group);
Action.perform("onActionEnterGroup()", "Node View");
// Recreate backdrops in group.
for (var i = 0 ; i < args[0].length; i++)
{
Backdrop.addBackdrop(template_group, args[0][i]);
};
// Copy-paste the selected nodes into the new group.
var drag_object = copyPaste.copy(args[1], 1, frame.numberOf, "");
copyPaste.pasteNewNodes(drag_object, template_group, "");
// Select all nodes within group and export as template.
Action.perform( "selectAll()", "Node View" );
copyPaste.createTemplateFromSelection(args[2], args[3]);
// Unfocus the group in Node view, delete all nodes and backdrops
// created during the process.
Action.perform("onActionUpToParent()", "Node View");
node.deleteNode(template_group, true, true);
}
func
"""
harmony.send({
"function": func,
"args": [
unique_backdrops,
dependencies,
"{}.tpl".format(instance.name),
staging_dir
]
})
pype.hosts.harmony.export_template(
unique_backdrops, dependencies, filepath
)
# Prep representation.
os.chdir(staging_dir)
@ -131,7 +94,7 @@ class ExtractTemplate(pype.api.Extractor):
}
func
"""
return harmony.send(
return avalon.harmony.send(
{"function": func, "args": [node]}
)["result"]
@ -150,7 +113,7 @@ class ExtractTemplate(pype.api.Extractor):
func
"""
current_dependencies = harmony.send(
current_dependencies = avalon.harmony.send(
{"function": func, "args": [node]}
)["result"]

View file

@ -2,6 +2,8 @@ import os
import shutil
import pype.api
import avalon.harmony
import pype.hosts.harmony
class ExtractWorkfile(pype.api.Extractor):
@ -12,17 +14,25 @@ class ExtractWorkfile(pype.api.Extractor):
families = ["workfile"]
def process(self, instance):
file_path = instance.context.data["currentFile"]
# Export template.
backdrops = avalon.harmony.send(
{"function": "Backdrop.backdrops", "args": ["Top"]}
)["result"]
nodes = avalon.harmony.send(
{"function": "node.subNodes", "args": ["Top"]}
)["result"]
staging_dir = self.staging_dir(instance)
filepath = os.path.join(staging_dir, "{}.tpl".format(instance.name))
pype.hosts.harmony.export_template(backdrops, nodes, filepath)
# Prep representation.
os.chdir(staging_dir)
shutil.make_archive(
instance.name,
"{}".format(instance.name),
"zip",
os.path.dirname(file_path)
os.path.join(staging_dir, "{}.tpl".format(instance.name))
)
zip_path = os.path.join(staging_dir, instance.name + ".zip")
self.log.info(f"Output zip file: {zip_path}")
representation = {
"name": "tpl",

View file

@ -22,8 +22,7 @@ class IncrementWorkfile(pyblish.api.InstancePlugin):
errored_plugins = get_errored_plugins_from_data(instance.context)
if errored_plugins:
raise RuntimeError(
"Skipping incrementing current file because submission to"
" deadline failed."
"Skipping incrementing current file because publishing failed."
)
scene_dir = version_up(

View file

@ -31,6 +31,12 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin):
def process(self, instance):
expected_settings = pype.hosts.harmony.get_asset_settings()
# Harmony is expected to start at 1.
frame_start = expected_settings["frameStart"]
frame_end = expected_settings["frameEnd"]
expected_settings["frameEnd"] = frame_end - frame_start + 1
expected_settings["frameStart"] = 1
func = """function func()
{
return {

View file

@ -179,7 +179,7 @@ class CreateRender(avalon.maya.Creator):
self.data["framesPerTask"] = 1
self.data["whitelist"] = False
self.data["machineList"] = ""
self.data["useMayaBatch"] = True
self.data["useMayaBatch"] = False
self.data["vrayScene"] = False
# Disable for now as this feature is not working yet
# self.data["assScene"] = False

View file

@ -332,9 +332,9 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
options["extendFrames"] = extend_frames
options["overrideExistingFrame"] = override_frames
maya_render_plugin = "MayaBatch"
if not attributes.get("useMayaBatch", True):
maya_render_plugin = "MayaCmd"
maya_render_plugin = "MayaPype"
if attributes.get("useMayaBatch", True):
maya_render_plugin = "MayaBatch"
options["mayaRenderPlugin"] = maya_render_plugin

View file

@ -41,7 +41,7 @@ payload_skeleton = {
"BatchName": None, # Top-level group name
"Name": None, # Job name, as seen in Monitor
"UserName": None,
"Plugin": "MayaBatch",
"Plugin": "MayaPype",
"Frames": "{start}-{end}x{step}",
"Comment": None,
},
@ -274,7 +274,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin):
step=int(self._instance.data["byFrameStep"]))
payload_skeleton["JobInfo"]["Plugin"] = self._instance.data.get(
"mayaRenderPlugin", "MayaBatch")
"mayaRenderPlugin", "MayaPype")
payload_skeleton["JobInfo"]["BatchName"] = filename
# Job name, as seen in Monitor
@ -311,12 +311,14 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin):
"AVALON_TASK",
"PYPE_USERNAME",
"PYPE_DEV",
"PYPE_LOG_NO_COLORS"
"PYPE_LOG_NO_COLORS",
"PYPE_SETUP_PATH"
]
environment = dict({key: os.environ[key] for key in keys
if key in os.environ}, **api.Session)
environment["PYPE_LOG_NO_COLORS"] = "1"
environment["PYPE_MAYA_VERSION"] = cmds.about(v=True)
payload_skeleton["JobInfo"].update({
"EnvironmentKeyValue%d" % index: "{key}={value}".format(
key=key,
@ -428,7 +430,8 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin):
int(self._instance.data["frameStartHandle"]),
int(self._instance.data["frameEndHandle"])),
"Plugin": "MayaBatch",
"Plugin": self._instance.data.get(
"mayaRenderPlugin", "MayaPype"),
"FramesPerTask": self._instance.data.get("framesPerTask", 1)
}

View file

@ -1,12 +1,77 @@
from avalon import photoshop
from avalon import api, photoshop
from avalon.vendor import Qt
class CreateImage(photoshop.Creator):
class CreateImage(api.Creator):
"""Image folder for publish."""
name = "imageDefault"
label = "Image"
family = "image"
def __init__(self, *args, **kwargs):
super(CreateImage, self).__init__(*args, **kwargs)
def process(self):
groups = []
layers = []
create_group = False
group_constant = photoshop.get_com_objects().constants().psLayerSet
if (self.options or {}).get("useSelection"):
multiple_instances = False
selection = photoshop.get_selected_layers()
if len(selection) > 1:
# Ask user whether to create one image or image per selected
# item.
msg_box = Qt.QtWidgets.QMessageBox()
msg_box.setIcon(Qt.QtWidgets.QMessageBox.Warning)
msg_box.setText(
"Multiple layers selected."
"\nDo you want to make one image per layer?"
)
msg_box.setStandardButtons(
Qt.QtWidgets.QMessageBox.Yes |
Qt.QtWidgets.QMessageBox.No |
Qt.QtWidgets.QMessageBox.Cancel
)
ret = msg_box.exec_()
if ret == Qt.QtWidgets.QMessageBox.Yes:
multiple_instances = True
elif ret == Qt.QtWidgets.QMessageBox.Cancel:
return
if multiple_instances:
for item in selection:
if item.LayerType == group_constant:
groups.append(item)
else:
layers.append(item)
else:
group = photoshop.group_selected_layers()
group.Name = self.name
groups.append(group)
elif len(selection) == 1:
# One selected item. Use group if its a LayerSet (group), else
# create a new group.
if selection[0].LayerType == group_constant:
groups.append(selection[0])
else:
layers.append(selection[0])
elif len(selection) == 0:
# No selection creates an empty group.
create_group = True
else:
create_group = True
if create_group:
group = photoshop.app().ActiveDocument.LayerSets.Add()
group.Name = self.name
groups.append(group)
for layer in layers:
photoshop.select_layers([layer])
group = photoshop.group_selected_layers()
group.Name = layer.Name
groups.append(group)
for group in groups:
photoshop.imprint(group, self.data)

View file

@ -0,0 +1,29 @@
import pyblish.api
from pype.action import get_errored_plugins_from_data
from pype.lib import version_up
from avalon import photoshop
class IncrementWorkfile(pyblish.api.InstancePlugin):
"""Increment the current workfile.
Saves the current scene with an increased version number.
"""
label = "Increment Workfile"
order = pyblish.api.IntegratorOrder + 9.0
hosts = ["photoshop"]
families = ["workfile"]
optional = True
def process(self, instance):
errored_plugins = get_errored_plugins_from_data(instance.context)
if errored_plugins:
raise RuntimeError(
"Skipping incrementing current file because publishing failed."
)
scene_path = version_up(instance.context.data["currentFile"])
photoshop.app().ActiveDocument.SaveAs(scene_path)
self.log.info("Incremented workfile to: {}".format(scene_path))

View file

@ -0,0 +1,44 @@
import pyblish.api
import pype.api
class ValidateNamingRepair(pyblish.api.Action):
"""Repair the instance asset."""
label = "Repair"
icon = "wrench"
on = "failed"
def process(self, context, plugin):
# Get the errored instances
failed = []
for result in context.data["results"]:
if (result["error"] is not None and result["instance"] is not None
and result["instance"] not in failed):
failed.append(result["instance"])
# Apply pyblish.logic to get the instances for the plug-in
instances = pyblish.api.instances_by_plugin(failed, plugin)
for instance in instances:
instance[0].Name = instance.data["name"].replace(" ", "_")
return True
class ValidateNaming(pyblish.api.InstancePlugin):
"""Validate the instance name.
Spaces in names are not allowed. Will be replace with underscores.
"""
label = "Validate Naming"
hosts = ["photoshop"]
order = pype.api.ValidateContentsOrder
families = ["image"]
actions = [ValidateNamingRepair]
def process(self, instance):
msg = "Name \"{}\" is not allowed.".format(instance.data["name"])
assert " " not in instance.data["name"], msg

View file

@ -60,14 +60,8 @@ class CollectShots(pyblish.api.InstancePlugin):
# options to be more flexible.
asset_name = asset_name.split("_")[0]
shot_number = 10
instances = []
for track in tracks:
self.log.info(track)
if "audio" in track.name.lower():
continue
instances = []
for child in track.each_child():
# Transitions are ignored, because Clips have the full frame
@ -75,10 +69,13 @@ class CollectShots(pyblish.api.InstancePlugin):
if isinstance(child, otio.schema.transition.Transition):
continue
# Hardcoded to expect a shot name of "[name].[extension]"
child_name = os.path.splitext(child.name)[0].lower()
name = f"{asset_name}_{child_name}"
frame_start = child.range_in_parent().start_time.value
frame_end = child.range_in_parent().end_time_inclusive().value
name = f"{asset_name}_sh{shot_number:04}"
label = f"{name} (framerange: {frame_start}-{frame_end})"
instances.append(
instance.context.create_instance(**{
@ -96,8 +93,6 @@ class CollectShots(pyblish.api.InstancePlugin):
})
)
shot_number += 10
visual_hierarchy = [asset_entity]
while True:
visual_parent = io.find_one(

View file

@ -26,8 +26,9 @@ class ExtractShot(pype.api.Extractor):
os.path.dirname(editorial_path), basename + ".mov"
)
shot_mov = os.path.join(staging_dir, instance.data["name"] + ".mov")
ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg")
args = [
"ffmpeg",
ffmpeg_path,
"-ss", str(instance.data["frameStart"] / fps),
"-i", input_path,
"-t", str(
@ -58,7 +59,7 @@ class ExtractShot(pype.api.Extractor):
shot_jpegs = os.path.join(
staging_dir, instance.data["name"] + ".%04d.jpeg"
)
args = ["ffmpeg", "-i", shot_mov, shot_jpegs]
args = [ffmpeg_path, "-i", shot_mov, shot_jpegs]
self.log.info(f"Processing: {args}")
output = pype.lib._subprocess(args)
self.log.info(output)
@ -79,7 +80,7 @@ class ExtractShot(pype.api.Extractor):
# Generate wav file.
shot_wav = os.path.join(staging_dir, instance.data["name"] + ".wav")
args = ["ffmpeg", "-i", shot_mov, shot_wav]
args = [ffmpeg_path, "-i", shot_mov, shot_wav]
self.log.info(f"Processing: {args}")
output = pype.lib._subprocess(args)
self.log.info(output)

View file

@ -0,0 +1,23 @@
import pyblish.api
import pype.api
class ValidateShots(pyblish.api.ContextPlugin):
"""Validate there is a "mov" next to the editorial file."""
label = "Validate Shots"
hosts = ["standalonepublisher"]
order = pype.api.ValidateContentsOrder
def process(self, context):
shot_names = []
duplicate_names = []
for instance in context:
name = instance.data["name"]
if name in shot_names:
duplicate_names.append(name)
else:
shot_names.append(name)
msg = "There are duplicate shot names:\n{}".format(duplicate_names)
assert not duplicate_names, msg

View file

@ -491,3 +491,24 @@ QToolButton {
#TerminalFilerBtn[type="log_critical"]:checked {color: rgb(255, 79, 117);}
#TerminalFilerBtn[type="log_critical"] {color: rgba(255, 79, 117, 63);}
#SuspendLogsBtn {
background: #444;
border: none;
border-top-right-radius: 7px;
border-bottom-right-radius: 7px;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
font-family: "FontAwesome";
font-size: 11pt;
color: white;
padding: 0px;
}
#SuspendLogsBtn:hover {
background: #333;
}
#SuspendLogsBtn:disabled {
background: #4c4c4c;
}

View file

@ -8,6 +8,7 @@ an active window manager; such as via Travis-CI.
import os
import sys
import traceback
import inspect
from Qt import QtCore
@ -60,11 +61,15 @@ class Controller(QtCore.QObject):
# store OrderGroups - now it is a singleton
order_groups = util.OrderGroups
# When instance is toggled
instance_toggled = QtCore.Signal(object, object, object)
def __init__(self, parent=None):
super(Controller, self).__init__(parent)
self.context = None
self.plugins = {}
self.optional_default = {}
self.instance_toggled.connect(self._on_instance_toggled)
def reset_variables(self):
# Data internal to the GUI itself
@ -81,7 +86,6 @@ class Controller(QtCore.QObject):
# - passing collectors order disables plugin/instance toggle
self.collectors_order = None
self.collect_state = 0
self.collected = False
# - passing validators order disables validate button and gives ability
# to know when to stop on validate button press
@ -415,3 +419,19 @@ class Controller(QtCore.QObject):
for plugin in self.plugins:
del(plugin)
def _on_instance_toggled(self, instance, old_value, new_value):
callbacks = pyblish.api.registered_callbacks().get("instanceToggled")
if not callbacks:
return
for callback in callbacks:
try:
callback(instance, old_value, new_value)
except Exception:
print(
"Callback for `instanceToggled` crashed. {}".format(
os.path.abspath(inspect.getfile(callback))
)
)
traceback.print_exception(*sys.exc_info())

View file

@ -32,7 +32,6 @@ from .awesome import tags as awesome
import Qt
from Qt import QtCore, QtGui
from six import text_type
from six.moves import queue
from .vendor import qtawesome
from .constants import PluginStates, InstanceStates, GroupStates, Roles
@ -49,6 +48,7 @@ TerminalDetailType = QtGui.QStandardItem.UserType + 4
class QAwesomeTextIconFactory:
icons = {}
@classmethod
def icon(cls, icon_name):
if icon_name not in cls.icons:
@ -58,6 +58,7 @@ class QAwesomeTextIconFactory:
class QAwesomeIconFactory:
icons = {}
@classmethod
def icon(cls, icon_name, icon_color):
if icon_name not in cls.icons:
@ -489,12 +490,8 @@ class PluginModel(QtGui.QStandardItemModel):
new_records = result.get("records") or []
if not has_warning:
for record in new_records:
if not hasattr(record, "levelname"):
continue
if str(record.levelname).lower() in [
"warning", "critical", "error"
]:
level_no = record.get("levelno")
if level_no and level_no >= 30:
new_flag_states[PluginStates.HasWarning] = True
break
@ -788,12 +785,8 @@ class InstanceModel(QtGui.QStandardItemModel):
new_records = result.get("records") or []
if not has_warning:
for record in new_records:
if not hasattr(record, "levelname"):
continue
if str(record.levelname).lower() in [
"warning", "critical", "error"
]:
level_no = record.get("levelno")
if level_no and level_no >= 30:
new_flag_states[InstanceStates.HasWarning] = True
break
@ -1009,7 +1002,7 @@ class ArtistProxy(QtCore.QAbstractProxyModel):
return QtCore.QModelIndex()
class TerminalModel(QtGui.QStandardItemModel):
class TerminalDetailItem(QtGui.QStandardItem):
key_label_record_map = (
("instance", "Instance"),
("msg", "Message"),
@ -1022,6 +1015,57 @@ class TerminalModel(QtGui.QStandardItemModel):
("msecs", "Millis")
)
def __init__(self, record_item):
self.record_item = record_item
self.msg = None
msg = record_item.get("msg")
if msg is None:
msg = record_item["label"].split("\n")[0]
super(TerminalDetailItem, self).__init__(msg)
def data(self, role=QtCore.Qt.DisplayRole):
if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
if self.msg is None:
self.msg = self.compute_detail_text(self.record_item)
return self.msg
return super(TerminalDetailItem, self).data(role)
def compute_detail_text(self, item_data):
if item_data["type"] == "info":
return item_data["label"]
html_text = ""
for key, title in self.key_label_record_map:
if key not in item_data:
continue
value = item_data[key]
text = (
str(value)
.replace("<", "&#60;")
.replace(">", "&#62;")
.replace('\n', '<br/>')
.replace(' ', '&nbsp;')
)
title_tag = (
'<span style=\" font-size:8pt; font-weight:600;'
# ' background-color:#bbb; color:#333;\" >{}:</span> '
' color:#fff;\" >{}:</span> '
).format(title)
html_text += (
'<tr><td width="100%" align=left>{}</td></tr>'
'<tr><td width="100%">{}</td></tr>'
).format(title_tag, text)
html_text = '<table width="100%" cellspacing="3">{}</table>'.format(
html_text
)
return html_text
class TerminalModel(QtGui.QStandardItemModel):
item_icon_name = {
"info": "fa.info",
"record": "fa.circle",
@ -1053,38 +1097,38 @@ class TerminalModel(QtGui.QStandardItemModel):
self.reset()
def reset(self):
self.items_to_set_widget = queue.Queue()
self.clear()
def prepare_records(self, result):
def prepare_records(self, result, suspend_logs):
prepared_records = []
instance_name = None
instance = result["instance"]
if instance is not None:
instance_name = instance.data["name"]
for record in result.get("records") or []:
if isinstance(record, dict):
record_item = record
else:
record_item = {
"label": text_type(record.msg),
"type": "record",
"levelno": record.levelno,
"threadName": record.threadName,
"name": record.name,
"filename": record.filename,
"pathname": record.pathname,
"lineno": record.lineno,
"msg": text_type(record.msg),
"msecs": record.msecs,
"levelname": record.levelname
}
if not suspend_logs:
for record in result.get("records") or []:
if isinstance(record, dict):
record_item = record
else:
record_item = {
"label": text_type(record.msg),
"type": "record",
"levelno": record.levelno,
"threadName": record.threadName,
"name": record.name,
"filename": record.filename,
"pathname": record.pathname,
"lineno": record.lineno,
"msg": text_type(record.msg),
"msecs": record.msecs,
"levelname": record.levelname
}
if instance_name is not None:
record_item["instance"] = instance_name
if instance_name is not None:
record_item["instance"] = instance_name
prepared_records.append(record_item)
prepared_records.append(record_item)
error = result.get("error")
if error:
@ -1140,49 +1184,14 @@ class TerminalModel(QtGui.QStandardItemModel):
self.appendRow(top_item)
detail_text = self.prepare_detail_text(record_item)
detail_item = QtGui.QStandardItem(detail_text)
detail_item = TerminalDetailItem(record_item)
detail_item.setData(TerminalDetailType, Roles.TypeRole)
top_item.appendRow(detail_item)
self.items_to_set_widget.put(detail_item)
def update_with_result(self, result):
for record in result["records"]:
self.append(record)
def prepare_detail_text(self, item_data):
if item_data["type"] == "info":
return item_data["label"]
html_text = ""
for key, title in self.key_label_record_map:
if key not in item_data:
continue
value = item_data[key]
text = (
str(value)
.replace("<", "&#60;")
.replace(">", "&#62;")
.replace('\n', '<br/>')
.replace(' ', '&nbsp;')
)
title_tag = (
'<span style=\" font-size:8pt; font-weight:600;'
# ' background-color:#bbb; color:#333;\" >{}:</span> '
' color:#fff;\" >{}:</span> '
).format(title)
html_text += (
'<tr><td width="100%" align=left>{}</td></tr>'
'<tr><td width="100%">{}</td></tr>'
).format(title_tag, text)
html_text = '<table width="100%" cellspacing="3">{}</table>'.format(
html_text
)
return html_text
class TerminalProxy(QtCore.QSortFilterProxyModel):
filter_buttons_checks = {

View file

@ -1,6 +1,14 @@
from Qt import QtCore, QtWidgets
from . import model
from .constants import Roles
# Imported when used
widgets = None
def _import_widgets():
global widgets
if widgets is None:
from . import widgets
class ArtistView(QtWidgets.QListView):
@ -151,6 +159,8 @@ class TerminalView(QtWidgets.QTreeView):
self.clicked.connect(self.item_expand)
_import_widgets()
def event(self, event):
if not event.type() == QtCore.QEvent.KeyPress:
return super(TerminalView, self).event(event)
@ -190,6 +200,23 @@ class TerminalView(QtWidgets.QTreeView):
self.updateGeometry()
self.scrollToBottom()
def expand(self, index):
"""Wrapper to set widget for expanded index."""
model = index.model()
row_count = model.rowCount(index)
is_new = False
for child_idx in range(row_count):
child_index = model.index(child_idx, index.column(), index)
widget = self.indexWidget(child_index)
if widget is None:
is_new = True
msg = child_index.data(QtCore.Qt.DisplayRole)
widget = widgets.TerminalDetail(msg)
self.setIndexWidget(child_index, widget)
super(TerminalView, self).expand(index)
if is_new:
self.updateGeometries()
def resizeEvent(self, event):
super(self.__class__, self).resizeEvent(event)
self.model().layoutChanged.emit()

View file

@ -321,11 +321,6 @@ class PerspectiveWidget(QtWidgets.QWidget):
data = {"records": records}
self.terminal_model.reset()
self.terminal_model.update_with_result(data)
while not self.terminal_model.items_to_set_widget.empty():
item = self.terminal_model.items_to_set_widget.get()
widget = TerminalDetail(item.data(QtCore.Qt.DisplayRole))
index = self.terminal_proxy.mapFromSource(item.index())
self.terminal_view.setIndexWidget(index, widget)
self.records.button_toggle_text.setText(
"{} ({})".format(self.l_rec, len_records)

View file

@ -54,6 +54,7 @@ class Window(QtWidgets.QDialog):
def __init__(self, controller, parent=None):
super(Window, self).__init__(parent=parent)
self._suspend_logs = False
# Use plastique style for specific ocations
# TODO set style name via environment variable
low_keys = {
@ -95,6 +96,18 @@ class Window(QtWidgets.QDialog):
header_tab_terminal = QtWidgets.QRadioButton(header_tab_widget)
header_spacer = QtWidgets.QWidget(header_tab_widget)
button_suspend_logs_widget = QtWidgets.QWidget()
button_suspend_logs_widget_layout = QtWidgets.QHBoxLayout(
button_suspend_logs_widget
)
button_suspend_logs_widget_layout.setContentsMargins(0, 10, 0, 10)
button_suspend_logs = QtWidgets.QPushButton(header_widget)
button_suspend_logs.setFixedWidth(7)
button_suspend_logs.setSizePolicy(
QtWidgets.QSizePolicy.Preferred,
QtWidgets.QSizePolicy.Expanding
)
button_suspend_logs_widget_layout.addWidget(button_suspend_logs)
header_aditional_btns = QtWidgets.QWidget(header_tab_widget)
aditional_btns_layout = QtWidgets.QHBoxLayout(header_aditional_btns)
@ -109,9 +122,11 @@ class Window(QtWidgets.QDialog):
layout_tab.addWidget(header_tab_artist, 0)
layout_tab.addWidget(header_tab_overview, 0)
layout_tab.addWidget(header_tab_terminal, 0)
layout_tab.addWidget(button_suspend_logs_widget, 0)
# Compress items to the left
layout_tab.addWidget(header_spacer, 1)
layout_tab.addWidget(header_aditional_btns, 1)
layout_tab.addWidget(header_aditional_btns, 0)
layout = QtWidgets.QHBoxLayout(header_widget)
layout.setContentsMargins(0, 0, 0, 0)
@ -226,6 +241,10 @@ class Window(QtWidgets.QDialog):
footer_info = QtWidgets.QLabel(footer_widget)
footer_spacer = QtWidgets.QWidget(footer_widget)
footer_button_stop = QtWidgets.QPushButton(
awesome["stop"], footer_widget
)
footer_button_reset = QtWidgets.QPushButton(
awesome["refresh"], footer_widget
)
@ -235,14 +254,12 @@ class Window(QtWidgets.QDialog):
footer_button_play = QtWidgets.QPushButton(
awesome["play"], footer_widget
)
footer_button_stop = QtWidgets.QPushButton(
awesome["stop"], footer_widget
)
layout = QtWidgets.QHBoxLayout()
layout.setContentsMargins(5, 5, 5, 5)
layout.addWidget(footer_info, 0)
layout.addWidget(footer_spacer, 1)
layout.addWidget(footer_button_stop, 0)
layout.addWidget(footer_button_reset, 0)
layout.addWidget(footer_button_validate, 0)
@ -342,10 +359,11 @@ class Window(QtWidgets.QDialog):
"TerminalView": terminal_view,
# Buttons
"Play": footer_button_play,
"Validate": footer_button_validate,
"Reset": footer_button_reset,
"SuspendLogsBtn": button_suspend_logs,
"Stop": footer_button_stop,
"Reset": footer_button_reset,
"Validate": footer_button_validate,
"Play": footer_button_play,
# Misc
"HeaderSpacer": header_spacer,
@ -370,10 +388,11 @@ class Window(QtWidgets.QDialog):
overview_page,
terminal_page,
footer_widget,
footer_button_play,
footer_button_validate,
button_suspend_logs,
footer_button_stop,
footer_button_reset,
footer_button_validate,
footer_button_play,
footer_spacer,
closing_placeholder
):
@ -415,10 +434,11 @@ class Window(QtWidgets.QDialog):
QtCore.Qt.DirectConnection
)
artist_view.toggled.connect(self.on_item_toggled)
overview_instance_view.toggled.connect(self.on_item_toggled)
overview_plugin_view.toggled.connect(self.on_item_toggled)
artist_view.toggled.connect(self.on_instance_toggle)
overview_instance_view.toggled.connect(self.on_instance_toggle)
overview_plugin_view.toggled.connect(self.on_plugin_toggle)
button_suspend_logs.clicked.connect(self.on_suspend_clicked)
footer_button_stop.clicked.connect(self.on_stop_clicked)
footer_button_reset.clicked.connect(self.on_reset_clicked)
footer_button_validate.clicked.connect(self.on_validate_clicked)
@ -442,10 +462,11 @@ class Window(QtWidgets.QDialog):
self.terminal_filters_widget = terminal_filters_widget
self.footer_widget = footer_widget
self.button_suspend_logs = button_suspend_logs
self.footer_button_stop = footer_button_stop
self.footer_button_reset = footer_button_reset
self.footer_button_validate = footer_button_validate
self.footer_button_play = footer_button_play
self.footer_button_stop = footer_button_stop
self.overview_instance_view = overview_instance_view
self.overview_plugin_view = overview_plugin_view
@ -537,7 +558,29 @@ class Window(QtWidgets.QDialog):
):
instance_item.setData(enable_value, Roles.IsEnabledRole)
def on_item_toggled(self, index, state=None):
def on_instance_toggle(self, index, state=None):
"""An item is requesting to be toggled"""
if not index.data(Roles.IsOptionalRole):
return self.info("This item is mandatory")
if self.controller.collect_state != 1:
return self.info("Cannot toggle")
current_state = index.data(QtCore.Qt.CheckStateRole)
if state is None:
state = not current_state
instance_id = index.data(Roles.ObjectIdRole)
instance_item = self.instance_model.instance_items[instance_id]
instance_item.setData(state, QtCore.Qt.CheckStateRole)
self.controller.instance_toggled.emit(
instance_item.instance, current_state, state
)
self.update_compatibility()
def on_plugin_toggle(self, index, state=None):
"""An item is requesting to be toggled"""
if not index.data(Roles.IsOptionalRole):
return self.info("This item is mandatory")
@ -548,7 +591,10 @@ class Window(QtWidgets.QDialog):
if state is None:
state = not index.data(QtCore.Qt.CheckStateRole)
index.model().setData(index, state, QtCore.Qt.CheckStateRole)
plugin_id = index.data(Roles.ObjectIdRole)
plugin_item = self.plugin_model.plugin_items[plugin_id]
plugin_item.setData(state, QtCore.Qt.CheckStateRole)
self.update_compatibility()
def on_tab_changed(self, target):
@ -587,6 +633,13 @@ class Window(QtWidgets.QDialog):
self.footer_button_play.setEnabled(False)
self.footer_button_stop.setEnabled(False)
def on_suspend_clicked(self):
self._suspend_logs = not self._suspend_logs
if self.state["current_page"] == "terminal":
self.on_tab_changed("overview")
self.tabs["terminal"].setVisible(not self._suspend_logs)
def on_comment_entered(self):
"""The user has typed a comment."""
self.controller.context.data["comment"] = self.comment_box.text()
@ -701,14 +754,14 @@ class Window(QtWidgets.QDialog):
self.on_tab_changed(self.state["current_page"])
self.update_compatibility()
self.footer_button_validate.setEnabled(True)
self.footer_button_reset.setEnabled(True)
self.footer_button_stop.setEnabled(False)
self.footer_button_play.setEnabled(True)
self.footer_button_play.setFocus()
self.button_suspend_logs.setEnabled(False)
self.footer_button_validate.setEnabled(False)
self.footer_button_reset.setEnabled(False)
self.footer_button_stop.setEnabled(True)
self.footer_button_play.setEnabled(False)
def on_passed_group(self, order):
for group_item in self.instance_model.group_items.values():
if self.overview_instance_view.isExpanded(group_item.index()):
continue
@ -740,16 +793,28 @@ class Window(QtWidgets.QDialog):
def on_was_stopped(self):
errored = self.controller.errored
self.footer_button_play.setEnabled(not errored)
self.footer_button_validate.setEnabled(
not errored and not self.controller.validated
)
if self.controller.collect_state == 0:
self.footer_button_play.setEnabled(False)
self.footer_button_validate.setEnabled(False)
else:
self.footer_button_play.setEnabled(not errored)
self.footer_button_validate.setEnabled(
not errored and not self.controller.validated
)
self.footer_button_play.setFocus()
self.footer_button_reset.setEnabled(True)
self.footer_button_stop.setEnabled(False)
if errored:
self.footer_widget.setProperty("success", 0)
self.footer_widget.style().polish(self.footer_widget)
suspend_log_bool = (
self.controller.collect_state == 1
and not self.controller.stopped
)
self.button_suspend_logs.setEnabled(suspend_log_bool)
def on_was_skipped(self, plugin):
plugin_item = self.plugin_model.plugin_items[plugin.id]
plugin_item.setData(
@ -809,17 +874,15 @@ class Window(QtWidgets.QDialog):
if self.tabs["artist"].isChecked():
self.tabs["overview"].toggle()
result["records"] = self.terminal_model.prepare_records(result)
result["records"] = self.terminal_model.prepare_records(
result,
self._suspend_logs
)
plugin_item = self.plugin_model.update_with_result(result)
instance_item = self.instance_model.update_with_result(result)
self.terminal_model.update_with_result(result)
while not self.terminal_model.items_to_set_widget.empty():
item = self.terminal_model.items_to_set_widget.get()
widget = widgets.TerminalDetail(item.data(QtCore.Qt.DisplayRole))
index = self.terminal_proxy.mapFromSource(item.index())
self.terminal_view.setIndexWidget(index, widget)
self.update_compatibility()
@ -872,16 +935,19 @@ class Window(QtWidgets.QDialog):
self.footer_button_validate.setEnabled(False)
self.footer_button_play.setEnabled(False)
self.button_suspend_logs.setEnabled(False)
util.defer(5, self.controller.validate)
def publish(self):
self.info(self.tr("Preparing publish.."))
self.footer_button_stop.setEnabled(True)
self.footer_button_reset.setEnabled(False)
self.footer_button_validate.setEnabled(False)
self.footer_button_play.setEnabled(False)
self.button_suspend_logs.setEnabled(False)
util.defer(5, self.controller.publish)
def act(self, plugin_item, action):
@ -913,30 +979,24 @@ class Window(QtWidgets.QDialog):
plugin_item = self.plugin_model.plugin_items[result["plugin"].id]
action_state = plugin_item.data(Roles.PluginActionProgressRole)
action_state |= PluginActionStates.HasFinished
result["records"] = self.terminal_model.prepare_records(result)
result["records"] = self.terminal_model.prepare_records(
result,
self._suspend_logs
)
error = result.get("error")
if error:
records = result.get("records") or []
if result.get("error"):
action_state |= PluginActionStates.HasFailed
fname, line_no, func, exc = error.traceback
records.append({
"label": str(error),
"type": "error",
"filename": str(fname),
"lineno": str(line_no),
"func": str(func),
"traceback": error.formatted_traceback
})
result["records"] = records
plugin_item.setData(action_state, Roles.PluginActionProgressRole)
self.plugin_model.update_with_result(result)
self.instance_model.update_with_result(result)
self.terminal_model.update_with_result(result)
plugin_item = self.plugin_model.update_with_result(result)
instance_item = self.instance_model.update_with_result(result)
if self.perspective_widget.isVisible():
self.perspective_widget.update_context(
plugin_item, instance_item
)
def closeEvent(self, event):
"""Perform post-flight checks before closing

View file

@ -1 +1 @@
__version__ = "2.9.1"
__version__ = "2.10.0"

View file

@ -56,13 +56,6 @@
"pattern": "^\\w*$",
"example": "maya2016"
},
"AVALON_MONGO": {
"description": "Address to the asset database",
"type": "string",
"pattern": "^mongodb://[\\w/@:.]*$",
"example": "mongodb://localhost:27017",
"default": "mongodb://localhost:27017"
},
"AVALON_DB": {
"description": "Name of database",
"type": "string",