diff --git a/avalon/api.py b/avalon/api.py index 33e4a3bed..d73a478eb 100644 --- a/avalon/api.py +++ b/avalon/api.py @@ -42,6 +42,8 @@ get_representation_context, update_current_task, + get_session_template_data, + get_publish_path, get_representation_path, loaders_from_representation, @@ -96,6 +98,8 @@ "get_representation_context", "update_current_task", + "get_session_template_data", + "get_publish_path", "get_representation_path", "loaders_from_representation", diff --git a/avalon/inventory.py b/avalon/inventory.py index 4f54054e0..f667566e7 100644 --- a/avalon/inventory.py +++ b/avalon/inventory.py @@ -107,7 +107,7 @@ def create_project(name): }).inserted_id -def create_asset(name, silo, data, parent): +def create_asset(name, silo, data, parent, episode, sequence, asset_type): assert isinstance(parent, io.ObjectId) if io.find_one({"type": "asset", "name": name}): raise RuntimeError("%s already exists" % name) @@ -116,6 +116,9 @@ def create_asset(name, silo, data, parent): "schema": "avalon-core:asset-2.0", "name": name, "silo": silo, + "episode": episode, + "sequence": sequence, + "asset_type": asset_type, "parent": parent, "type": "asset", "data": data @@ -313,7 +316,10 @@ def _save_inventory_1_0(project_name, data): name=data.pop("name"), silo=data.pop("silo"), data=data, - parent=document["_id"] + parent=document["_id"], + episode=data.pop("episode"), + sequence=data.pop("sequence"), + asset_type=data.pop("asset_type") ) else: diff --git a/avalon/io.py b/avalon/io.py index b8a17a74e..0f634c047 100644 --- a/avalon/io.py +++ b/avalon/io.py @@ -120,6 +120,15 @@ def _from_environment(): # Name of current Project ("AVALON_PROJECT", None), + # Name of current Episode + ("AVALON_EPISODE", None), + + # Name of current Sequence + ("AVALON_SEQUENCE", None), + + # Name of current Asset type + ("AVALON_ASSET_TYPE", None), + # Name of current Asset ("AVALON_ASSET", None), diff --git a/avalon/maya/pipeline.py b/avalon/maya/pipeline.py index 74bc11c30..9596fbd1a 100644 --- a/avalon/maya/pipeline.py +++ b/avalon/maya/pipeline.py @@ -137,6 +137,9 @@ def deferred(): enable=False ) + cmds.menuItem("Switch Task", + command=lambda *args: workfiles.show_switch_task(parent=self._parent)) + cmds.setParent("..", menu=True) cmds.menuItem(divider=True) diff --git a/avalon/nuke/pipeline.py b/avalon/nuke/pipeline.py index 18d9b56f8..206959383 100644 --- a/avalon/nuke/pipeline.py +++ b/avalon/nuke/pipeline.py @@ -414,6 +414,9 @@ def _install_menu(): # Create menu menubar = nuke.menu("Nuke") menu = menubar.addMenu(api.Session["AVALON_LABEL"]) + menu.addCommand("Switch Task", + lambda: workfiles.show_switch_task(parent=get_main_window()) + ) _add_contextmanager_menu(menu) diff --git a/avalon/nuke/workio.py b/avalon/nuke/workio.py index b36f91a37..067e835a6 100644 --- a/avalon/nuke/workio.py +++ b/avalon/nuke/workio.py @@ -1,5 +1,6 @@ """Host API required Work Files tool""" import os +import sys import nuke @@ -75,9 +76,31 @@ def open_file(filepath): nuke.connectViewer(0, viewer) nuke.callbacks.onScriptLoad() + # Set colorManagement + _set_color_management() + return True +def _set_color_management(): + if os.getenv("OCIO"): + # log.info("Setup project color management to OCIO") + # nuke.knob("root.colorManagement", "OCIO") + + # region === Switch colorSpace === + project_name = os.environ.get("AVALON_PROJECT") + show_config_root = os.environ.get("MS_SHOW_CONFIG_ROOT") + if project_name and show_config_root: + project_root = r'{}/ms_show_config/show/s_{}'.format( + show_config_root, project_name) + if os.path.isfile(r'{}/switch_lut.py'.format(project_root)): + sys.path.insert(0, project_root) + import switch_lut + if hasattr(switch_lut, 'switch_viewPort_lut'): + switch_lut.switch_viewPort_lut() + # endregion + + def clear_root_user_knobs(tabs=[]): root_knobs = nuke.Root().allKnobs() diff --git a/avalon/pipeline.py b/avalon/pipeline.py index 315256ccb..c83b7191e 100644 --- a/avalon/pipeline.py +++ b/avalon/pipeline.py @@ -1069,11 +1069,14 @@ def compute_session_changes(session, task=None, asset=None, app=None): # Detect any changes compared session mapping = { "AVALON_ASSET": asset, + "AVALON_EPISODE": asset_document.get("episode", ""), + "AVALON_SEQUENCE": asset_document.get("sequence", ""), + "AVALON_ASSET_TYPE": asset_document.get("asset_type", ""), "AVALON_TASK": task, "AVALON_APP": app, } changes = {key: value for key, value in mapping.items() - if value and value != session.get(key)} + if value is not None and value != session.get(key)} if not changes: return changes @@ -1155,7 +1158,14 @@ def _format_work_template(template, session=None): if session is None: session = Session - return template.format(**{ + if session.get("AVALON_SILO") == "Assets": + template = template.replace("{category}", "{asset_type}") + elif session.get("AVALON_SILO") == "Shots": + template = template.replace("{category}", "{episode}/{sequence}") + else: + template = template.replace("{category}", "") + + path = template.format(**{ "root": registered_root(), "project": session["AVALON_PROJECT"], "asset": session["AVALON_ASSET"], @@ -1164,9 +1174,13 @@ def _format_work_template(template, session=None): # Optional "silo": session.get("AVALON_SILO"), + "episode": session.get("AVALON_EPISODE", ""), + "sequence": session.get("AVALON_SEQUENCE", ""), + "asset_type": session.get("AVALON_ASSET_TYPE", ""), "user": session.get("AVALON_USER", getpass.getuser()), "hierarchy": session.get("AVALON_HIERARCHY"), }) + return os.path.normpath(path).replace('\\', '/') def _make_backwards_compatible_loader(Loader): @@ -1359,6 +1373,164 @@ def switch(container, representation): return new_loader.switch_form_loader(container, new_representation, loader) +def get_session_template_data(): + project = io.find_one({"name": Session["AVALON_PROJECT"], "type": "project"}) + project_root = project["data"].get("root") + root = project_root or registered_root() + silo = Session.get("AVALON_SILO", "") + + category = "" + if silo == "Assets": + category = Session.get("AVALON_ASSET_TYPE", "") + elif silo == "Shots": + category = "%s/%s" % (Session.get("AVALON_EPISODE", ""), Session.get("AVALON_SEQUENCE", "")) + + data = { + "root": root, + "project": Session["AVALON_PROJECT"], + "silo": silo, + "episode": Session.get("AVALON_EPISODE", ""), + "sequence": Session.get("AVALON_SEQUENCE", ""), + "asset_type": Session.get("AVALON_ASSET_TYPE", ""), + "category": category, + "asset": Session.get("AVALON_ASSET", ""), + "task": Session.get("AVALON_TASK", ""), + "subset": "", + "version": "", + "representation": "", + "user": Session.get("AVALON_USER", getpass.getuser()), + "app": Session.get("AVALON_APP", ""), + "hierarchy": Session.get("AVALON_HIERARCHY", ""), + } + return data + + +def get_publish_path(representation_name, version, subset, asset, + root=None, project=None): + project = project or io.find_one({"name": Session["AVALON_PROJECT"], "type": "project"}) + try: + template = project["config"]["template"]["publish"] + except KeyError: + log.debug( + "No template in project %s, " + "likely a bug" % project["name"] + ) + return None + + project_root = project["data"].get("root") + root = root or project_root or registered_root() + + if asset.get("silo") == "Assets": + template = template.replace("{category}", "{asset_type}") + elif asset.get("silo") == "Shots": + template = template.replace("{category}", "{episode}/{sequence}") + else: + template = template.replace("{category}", "") + + data = { + "root": root, + "project": project.get("name"), + "asset": asset.get("name"), + "silo": asset.get("silo"), + "episode": asset.get("episode", ""), + "sequence": asset.get("sequence", ""), + "asset_type": asset.get("asset_type", ""), + "subset": subset.get("name"), + "version": version.get("name"), + "representation": representation_name, + "user": Session.get("AVALON_USER", getpass.getuser()), + "app": Session.get("AVALON_APP", ""), + "task": version.get("data", {}).get("task") or Session.get("AVALON_TASK") + } + + try: + path = template.format(**data) + except KeyError as e: + log.debug("Template references unavailable data: %s" % e) + return None + + return os.path.normpath(path) + + +def get_session_template_data(): + project = io.find_one({"name": Session["AVALON_PROJECT"], "type": "project"}) + project_root = project["data"].get("root") + root = project_root or registered_root() + silo = Session.get("AVALON_SILO", "") + + category = "" + if silo == "Assets": + category = Session.get("AVALON_ASSET_TYPE", "") + elif silo == "Shots": + category = "%s/%s" % (Session.get("AVALON_EPISODE", ""), Session.get("AVALON_SEQUENCE", "")) + + data = { + "root": root, + "project": Session["AVALON_PROJECT"], + "silo": silo, + "episode": Session.get("AVALON_EPISODE", ""), + "sequence": Session.get("AVALON_SEQUENCE", ""), + "asset_type": Session.get("AVALON_ASSET_TYPE", ""), + "category": category, + "asset": Session.get("AVALON_ASSET", ""), + "task": Session.get("AVALON_TASK", ""), + "subset": "", + "version": "", + "representation": "", + "user": Session.get("AVALON_USER", getpass.getuser()), + "app": Session.get("AVALON_APP", ""), + "hierarchy": Session.get("AVALON_HIERARCHY", ""), + } + return data + + +def get_publish_path(representation_name, version, subset, asset, + root=None, project=None): + project = project or io.find_one({"name": Session["AVALON_PROJECT"], "type": "project"}) + try: + template = project["config"]["template"]["publish"] + except KeyError: + log.debug( + "No template in project %s, " + "likely a bug" % project["name"] + ) + return None + + project_root = project["data"].get("root") + root = root or project_root or registered_root() + + if asset.get("silo") == "Assets": + template = template.replace("{category}", "{asset_type}") + elif asset.get("silo") == "Shots": + template = template.replace("{category}", "{episode}/{sequence}") + else: + template = template.replace("{category}", "") + + data = { + "root": root, + "project": project.get("name"), + "asset": asset.get("name"), + "silo": asset.get("silo"), + "episode": asset.get("episode", ""), + "sequence": asset.get("sequence", ""), + "asset_type": asset.get("asset_type", ""), + "subset": subset.get("name"), + "version": version.get("name"), + "representation": representation_name, + "user": Session.get("AVALON_USER", getpass.getuser()), + "app": Session.get("AVALON_APP", ""), + "task": version.get("data", {}).get("task") or Session.get("AVALON_TASK") + } + + try: + path = template.format(**data) + except KeyError as e: + log.debug("Template references unavailable data: %s" % e) + return None + + return os.path.normpath(path) + + def get_representation_path(representation): """Get filename from representation document @@ -1406,37 +1578,9 @@ def path_from_config(): ) return None - try: - template = project["config"]["template"]["publish"] - except KeyError: - log.debug( - "No template in project %s, " - "likely a bug" % project["name"] - ) - return None - - # Cannot fail, required members only - data = { - "root": registered_root(), - "project": project["name"], - "asset": asset["name"], - "silo": asset.get("silo"), - "subset": subset["name"], - "version": version_["name"], - "representation": representation["name"], - "user": Session.get("AVALON_USER", getpass.getuser()), - "app": Session.get("AVALON_APP", ""), - "task": Session.get("AVALON_TASK", "") - } + repr_root = representation["data"].get("reprRoot") - try: - path = template.format(**data) - except KeyError as e: - log.debug("Template references unavailable data: %s" % e) - return None - - if os.path.exists(path): - return os.path.normpath(path) + return get_publish_path(representation["name"], version_, subset, asset, root=repr_root, project=project) def path_from_data(): if "path" not in representation["data"]: diff --git a/avalon/schema/asset-2.0.json b/avalon/schema/asset-2.0.json index 8ea33feec..38d69b193 100644 --- a/avalon/schema/asset-2.0.json +++ b/avalon/schema/asset-2.0.json @@ -33,6 +33,18 @@ "description": "Unique identifier to parent document", "example": "592c33475f8c1b064c4d1696" }, + "episode": { + "description": "Unique identifier to episode document", + "example": "592c33475f8c1b064c4d1696" + }, + "sequence": { + "description": "Unique identifier to sequence document", + "example": "592c33475f8c1b064c4d1696" + }, + "asset_type": { + "description": "Unique identifier to asset_type document", + "example": "592c33475f8c1b064c4d1696" + }, "name": { "description": "Name of asset", "type": "string", diff --git a/avalon/schema/asset-3.0.json b/avalon/schema/asset-3.0.json index c021601ea..88eff32c2 100644 --- a/avalon/schema/asset-3.0.json +++ b/avalon/schema/asset-3.0.json @@ -32,6 +32,18 @@ "description": "Unique identifier to parent document", "example": "592c33475f8c1b064c4d1696" }, + "episode": { + "description": "Unique identifier to episode document", + "example": "592c33475f8c1b064c4d1696" + }, + "sequence": { + "description": "Unique identifier to sequence document", + "example": "592c33475f8c1b064c4d1696" + }, + "asset_type": { + "description": "Unique identifier to asset_type document", + "example": "592c33475f8c1b064c4d1696" + }, "name": { "description": "Name of asset", "type": "string", diff --git a/avalon/schema/asset_type-1.0.json b/avalon/schema/asset_type-1.0.json new file mode 100644 index 000000000..35491b89a --- /dev/null +++ b/avalon/schema/asset_type-1.0.json @@ -0,0 +1,55 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "avalon-core:asset_type-1.0", + "description": "A unit of data", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "schema", + "type", + "name", + "silo", + "data" + ], + + "properties": { + "schema": { + "description": "Schema identifier for payload", + "type": "string", + "enum": ["avalon-core:asset_type-1.0"], + "example": "avalon-core:asset_type-1.0" + }, + "type": { + "description": "The type of document", + "type": "string", + "enum": ["asset_type"], + "example": "asset_type" + }, + "parent": { + "description": "Unique identifier to parent document", + "example": "592c33475f8c1b064c4d1696" + }, + "name": { + "description": "Name of asset type", + "type": "string", + "pattern": "^[a-zA-Z0-9_.]*$", + "example": "Bruce" + }, + "silo": { + "description": "Group or container of asset type", + "type": "string", + "example": "Assets" + }, + "data": { + "description": "Document metadata", + "type": "object", + "example": {"key": "value"} + } + }, + + "definitions": {} +} \ No newline at end of file diff --git a/avalon/schema/asset_type-2.0.json b/avalon/schema/asset_type-2.0.json new file mode 100644 index 000000000..594624a00 --- /dev/null +++ b/avalon/schema/asset_type-2.0.json @@ -0,0 +1,53 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "avalon-core:asset_type-2.0", + "description": "A unit of data", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "schema", + "type", + "name", + "data" + ], + + "properties": { + "schema": { + "description": "Schema identifier for payload", + "type": "string", + "enum": ["avalon-core:asset_type-2.0"], + "example": "avalon-core:asset_type-2.0" + }, + "type": { + "description": "The type of document", + "type": "string", + "enum": ["asset_type"], + "example": "asset_type" + }, + "parent": { + "description": "Unique identifier to parent document", + "example": "592c33475f8c1b064c4d1696" + }, + "name": { + "description": "Name of asset type", + "type": "string", + "pattern": "^[a-zA-Z0-9_.]*$", + "example": "Bruce" + }, + "silo": { + "description": "(Deprecated) Group or container of asset type", + "example": "Assets" + }, + "data": { + "description": "Document metadata", + "type": "object", + "example": {"key": "value"} + } + }, + + "definitions": {} +} diff --git a/avalon/schema/episode-1.0.json b/avalon/schema/episode-1.0.json new file mode 100644 index 000000000..62637e05b --- /dev/null +++ b/avalon/schema/episode-1.0.json @@ -0,0 +1,55 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "avalon-core:episode-1.0", + "description": "A unit of data", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "schema", + "type", + "name", + "silo", + "data" + ], + + "properties": { + "schema": { + "description": "Schema identifier for payload", + "type": "string", + "enum": ["avalon-core:episode-1.0"], + "example": "avalon-core:episode-1.0" + }, + "type": { + "description": "The type of document", + "type": "string", + "enum": ["episode"], + "example": "episode" + }, + "parent": { + "description": "Unique identifier to parent document", + "example": "592c33475f8c1b064c4d1696" + }, + "name": { + "description": "Name of episode", + "type": "string", + "pattern": "^[a-zA-Z0-9_.]*$", + "example": "Bruce" + }, + "silo": { + "description": "Group or container of episode", + "type": "string", + "example": "episodes" + }, + "data": { + "description": "Document metadata", + "type": "object", + "example": {"key": "value"} + } + }, + + "definitions": {} +} \ No newline at end of file diff --git a/avalon/schema/episode-2.0.json b/avalon/schema/episode-2.0.json new file mode 100644 index 000000000..d0c1122b8 --- /dev/null +++ b/avalon/schema/episode-2.0.json @@ -0,0 +1,53 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "avalon-core:episode-2.0", + "description": "A unit of data", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "schema", + "type", + "name", + "data" + ], + + "properties": { + "schema": { + "description": "Schema identifier for payload", + "type": "string", + "enum": ["avalon-core:episode-2.0"], + "example": "avalon-core:episode-2.0" + }, + "type": { + "description": "The type of document", + "type": "string", + "enum": ["episode"], + "example": "episode" + }, + "parent": { + "description": "Unique identifier to parent document", + "example": "592c33475f8c1b064c4d1696" + }, + "name": { + "description": "Name of episode", + "type": "string", + "pattern": "^[a-zA-Z0-9_.]*$", + "example": "Bruce" + }, + "silo": { + "description": "(Deprecated) Group or container of episode", + "example": "episodes" + }, + "data": { + "description": "Document metadata", + "type": "object", + "example": {"key": "value"} + } + }, + + "definitions": {} +} diff --git a/avalon/schema/sequence-1.0.json b/avalon/schema/sequence-1.0.json new file mode 100644 index 000000000..c08c1eab5 --- /dev/null +++ b/avalon/schema/sequence-1.0.json @@ -0,0 +1,59 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "avalon-core:sequence-1.0", + "description": "A unit of data", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "schema", + "type", + "name", + "silo", + "data" + ], + + "properties": { + "schema": { + "description": "Schema identifier for payload", + "type": "string", + "enum": ["avalon-core:sequence-1.0"], + "example": "avalon-core:sequence-1.0" + }, + "type": { + "description": "The type of document", + "type": "string", + "enum": ["sequence"], + "example": "sequence" + }, + "parent": { + "description": "Unique identifier to parent document", + "example": "592c33475f8c1b064c4d1696" + }, + "episode": { + "description": "Unique identifier to episode document", + "example": "592c33475f8c1b064c4d1696" + }, + "name": { + "description": "Name of sequence", + "type": "string", + "pattern": "^[a-zA-Z0-9_.]*$", + "example": "Bruce" + }, + "silo": { + "description": "Group or container of sequence", + "type": "string", + "example": "sequences" + }, + "data": { + "description": "Document metadata", + "type": "object", + "example": {"key": "value"} + } + }, + + "definitions": {} +} \ No newline at end of file diff --git a/avalon/schema/sequence-2.0.json b/avalon/schema/sequence-2.0.json new file mode 100644 index 000000000..6ed078961 --- /dev/null +++ b/avalon/schema/sequence-2.0.json @@ -0,0 +1,57 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "avalon-core:sequence-2.0", + "description": "A unit of data", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "schema", + "type", + "name", + "data" + ], + + "properties": { + "schema": { + "description": "Schema identifier for payload", + "type": "string", + "enum": ["avalon-core:sequence-2.0"], + "example": "avalon-core:sequence-2.0" + }, + "type": { + "description": "The type of document", + "type": "string", + "enum": ["sequence"], + "example": "sequence" + }, + "parent": { + "description": "Unique identifier to parent document", + "example": "592c33475f8c1b064c4d1696" + }, + "episode": { + "description": "Unique identifier to episode document", + "example": "592c33475f8c1b064c4d1696" + }, + "name": { + "description": "Name of sequence", + "type": "string", + "pattern": "^[a-zA-Z0-9_.]*$", + "example": "Bruce" + }, + "silo": { + "description": "(Deprecated) Group or container of sequence", + "example": "sequences" + }, + "data": { + "description": "Document metadata", + "type": "object", + "example": {"key": "value"} + } + }, + + "definitions": {} +} diff --git a/avalon/schema/session-2.0.json b/avalon/schema/session-2.0.json index bcbad60ea..6f75f6020 100644 --- a/avalon/schema/session-2.0.json +++ b/avalon/schema/session-2.0.json @@ -27,6 +27,24 @@ "pattern": "^\\w*$", "example": "Hulk" }, + "AVALON_EPISODE": { + "description": "Name of episode", + "type": "string", + "pattern": "^\\w*$", + "example": "EP1" + }, + "AVALON_SEQUENCE": { + "description": "Name of sequence", + "type": "string", + "pattern": "^\\w*$", + "example": "seq0010" + }, + "AVALON_ASSET_TYPE": { + "description": "Name of asset type", + "type": "string", + "pattern": "^\\w*$", + "example": "Char" + }, "AVALON_ASSET": { "description": "Name of asset", "type": "string", diff --git a/avalon/tools/delegates.py b/avalon/tools/delegates.py index 6f191e615..0bcf32c3d 100644 --- a/avalon/tools/delegates.py +++ b/avalon/tools/delegates.py @@ -92,6 +92,7 @@ def paint(self, painter, option, index): else: QtWidgets.QStyledItemDelegate.paint(self, painter, option, index) + def pretty_date(t, now=None, strftime="%b %d %Y %H:%M"): """Parse datetime to readable timestamp @@ -201,3 +202,65 @@ def displayText(self, value, locale): return return pretty_timestamp(value) + + +class ButtonDelegate(QtWidgets.QStyledItemDelegate): + clicked = QtCore.Signal(QtCore.QModelIndex) + + def __init__(self, parent=None): + super(ButtonDelegate, self).__init__(parent) + self.pressed = {} + + def paint(self, painter, option, index): + text = index.data(QtCore.Qt.DisplayRole) + icon = index.data(QtCore.Qt.DecorationRole) + if not text and not icon: + return + + bg_color_role = QtGui.QPalette.Window + text_color_role = QtGui.QPalette.Mid + button_color_role = QtGui.QPalette.Button + + if option.state & QtWidgets.QStyle.State_Enabled: + color_group = QtGui.QPalette.Normal + if option.state & QtWidgets.QStyle.State_Selected: + bg_color_role = QtGui.QPalette.Highlight + text_color_role = QtGui.QPalette.Midlight + elif option.state & QtWidgets.QStyle.State_MouseOver: + text_color_role = QtGui.QPalette.Midlight + if self.pressed.get(index): + button_color_role = QtGui.QPalette.Dark + else: + color_group = QtGui.QPalette.Disabled + + text_color = option.palette.color(color_group, text_color_role) + bg_color = option.palette.color(color_group, bg_color_role) + button_color = option.palette.color(color_group, button_color_role) + + pen = QtGui.QPen(text_color) + pen.setWidth(1) + painter.setPen(pen) + + rect = option.rect.adjusted(5, 5, -5, -5) + path = QtGui.QPainterPath() + path.addRoundedRect(rect, 2, 2) + + painter.fillRect(option.rect, bg_color) + painter.fillPath(path, button_color) + painter.drawPath(path) + + if text: + painter.drawText(rect, QtCore.Qt.AlignHCenter, text) + if icon: + icon.paint(painter, rect) + + def editorEvent(self, event, editor, model, index): + if index.flags() & QtCore.Qt.ItemIsEnabled: + if event.type() == QtCore.QEvent.MouseButtonPress: + self.pressed[index] = True + return True + if event.type() == QtCore.QEvent.MouseButtonRelease: + self.pressed[index] = False + self.clicked.emit(index) + return True + return super(ButtonDelegate, self).editorEvent(event, editor, model, index) diff --git a/avalon/tools/lib.py b/avalon/tools/lib.py index 7ddddcb6f..fd8f4609a 100644 --- a/avalon/tools/lib.py +++ b/avalon/tools/lib.py @@ -302,6 +302,9 @@ def get_family_cached_config(name): config = FAMILY_CONFIG_CACHE if 'reveries.shotgun.' in name: return config.get(name, config.get("shotgun_icon", None)) + if '.usd' in name: + return config.get(name, config.get("usd_icon", None)) + return config.get(name, config.get("__default__", None)) @@ -346,10 +349,13 @@ def refresh_family_config_cache(): # Replace icons with a Qt icon we can use in the user interfaces default_icon = qtawesome.icon("fa.folder", color=FAMILY_ICON_COLOR) shotgun_icon = QtGui.QIcon(os.path.join(ICON_DIR, 'shotgun.png')) + usd_icon = QtGui.QIcon(os.path.join(ICON_DIR, 'usd.png')) for name, family in families.items(): # Set family icon icon = family.get("icon", None) - if icon: + if icon == 'usd': + family["icon"] = usd_icon + elif icon: _icon_name = "fa.{}".format(icon) if len(icon.split(".")) > 1: _icon_name = icon @@ -367,6 +373,7 @@ def refresh_family_config_cache(): # Default configuration families["__default__"] = {"icon": default_icon} families["shotgun_icon"] = {"icon": shotgun_icon} + families["usd_icon"] = {"icon": usd_icon} FAMILY_CONFIG_CACHE.clear() FAMILY_CONFIG_CACHE.update(families) @@ -544,3 +551,25 @@ def to_trash(item, **kwargs): def create_silo(data): from reveries.common.publish.create_silo import create return create(data) + + +def get_latest_grab_source_version(grab_log): + import pymongo + + grab_from = grab_log.get("grab_from") + source_id = grab_log.get("source_id") + + if not grab_from or not source_id: + return + + timeout = int(io.Session["AVALON_TIMEOUT"]) + mongo_client = pymongo.MongoClient( + io.Session["AVALON_MONGO"], serverSelectionTimeoutMS=timeout) + database = mongo_client[io.Session["AVALON_DB"]] + collection = database[grab_from] + + from_version = collection.find_one({"type": "version", "_id": source_id}) + if from_version: + latest_version = collection.find_one({ + "type": "version", "parent": from_version["parent"]}, sort=[("name", -1)]) + return latest_version diff --git a/avalon/tools/loader/app.py b/avalon/tools/loader/app.py index db1d65fa7..374a293ce 100644 --- a/avalon/tools/loader/app.py +++ b/avalon/tools/loader/app.py @@ -1,15 +1,24 @@ import sys import time +import getpass +import copy from ...vendor.Qt import QtWidgets, QtCore from ... import api, io, style -from ..models import AssetModel +from ..models import AssetModel, TreeModel from ..widgets import AssetWidget from .. import lib from .widgets import SubsetWidget, VersionWidget, FamilyListWidget, CommentWidget +from shot_manager.tools.inventory_publish.utils import ( + SupportFamilies, + update_st_data, + update_pc_data +) +from shot_manager.tools.inventory_publish.publish import InventoryPublisher + module = sys.modules[__name__] module.window = None @@ -120,6 +129,7 @@ def __init__(self, parent=None): subsets.active_changed.connect(self.on_subsetschanged) subsets.version_changed.connect(self.on_versionschanged) + subsets.shot_inventory_clicked.connect(self.on_add_to_shot_inventory) # Defaults self.resize(1330, 700) @@ -144,6 +154,129 @@ def on_versionschanged(self, *args): self.echo("Fetching version..") lib.schedule(self._versionschanged, 150, channel="mongo") + def on_add_to_shot_inventory(self, index): + subsets_widget = self.data["model"]["subsets"] + subsets_model = subsets_widget.model + inventory_data = copy.deepcopy(subsets_model.inventory_data) + item = index.data(TreeModel.ItemRole) + subset_name = item["name"] + _id = str(item["_id"]) + current_inside = _id in inventory_data + + accept = QtWidgets.QMessageBox.Ok + buttons = accept | QtWidgets.QMessageBox.Cancel + + if current_inside: + title = "Remove from Shot Inventory" + message = ('Are you sure you want to remove ' + '"{}" from shot inventory?'.format(subset_name)) + else: + title = "Add to Shot Inventory" + message = ('Are you sure you want to add ' + '"{}" to shot inventory?'.format(subset_name)) + state = QtWidgets.QMessageBox.question(self, title, + message, + buttons=buttons, + defaultButton=accept) + + if state != accept: + return + + if current_inside: + del inventory_data[_id] + comment = "Publish from loader. " \ + "Remove {} by {}.".format(subset_name, getpass.getuser()) + else: + comment = "Publish from loader. " \ + "Add {} by {}.".format(subset_name, getpass.getuser()) + + family = item["family"] + inventory_data.update({ + _id: { + 'subset': subset_name, + 'type': SupportFamilies[family]['type'], + 'icon': lib.get_family_cached_config( + family).get('icon_name', 'fa.folder') + } + }) + # Update lookdev data + if family in ['reveries.pointcache']: + inventory_data = update_pc_data(inventory_data, _id) + if 'look' not in inventory_data[_id].keys(): + title = "No look for this pointcache" + message = ('No look for this pointcache. ' + 'Continue add to shot inventory?') + state = QtWidgets.QMessageBox.question(self, title, + message, + buttons=buttons, + defaultButton=accept) + if state != accept: + return + + comment += "\nNo look for this pointcache." + + if family in ['reveries.setdress']: + inventory_data = update_st_data(inventory_data, _id) + + if 'look' not in inventory_data[_id].keys(): + title = "No look for this setdress" + message = ('No look for this setdress. ' + 'Continue add to shot inventory?') + state = QtWidgets.QMessageBox.question(self, title, + message, + buttons=buttons, + defaultButton=accept) + if state != accept: + return + + comment += "\nNo look for this setdress." + + pub_data = {} + for _id in inventory_data: + _data = inventory_data[_id] + _tmp_data = { + 'type': _data['type'], + 'count': 1, + 'info': _data.get('info', '') + } + + if 'look' in list(_data.keys()): + if _data['type'] == 'pointcache': + _tmp_data['look'] = {} + for _label in _data['look']: + _tmp_data['look'].update({ + _label: { + 'id': str(_data['look'][_label]['id']), + 'changed': _data['look'][_label].get('changed', False), + 'info': _data['look'][_label].get('info', '') + } + }) + if _data['type'] == 'setdress': + _tmp_data['look'] = {} + for ns, _st_look_data in _data['look'].items(): + look_data = _st_look_data + look_data['namespace'] = ns + _tmp_data['look'].update({ + _st_look_data['container_id']: look_data + }) + + pub_data[_id] = copy.deepcopy(_tmp_data) + + asset_name = self.data["state"]["context"]["asset"] + publisher = InventoryPublisher( + shot_name=asset_name, + shot_data=pub_data, + comment=comment + ) + if publisher.do_publish(): + message = 'Shot inventory published success.\n\n' + else: + message = 'Shot inventory published failed.\n\n' + + QtWidgets.QMessageBox.information(self, title, + message, QtWidgets.QMessageBox.Ok) + lib.schedule(self._refresh, 50, channel="mongo") + def set_context(self, context, refresh=True): self.echo("Setting context: {}".format(context)) lib.schedule(lambda: self._set_context(context, refresh=refresh), diff --git a/avalon/tools/loader/model.py b/avalon/tools/loader/model.py index 006c29185..0d3afaf75 100644 --- a/avalon/tools/loader/model.py +++ b/avalon/tools/loader/model.py @@ -1,5 +1,6 @@ import logging +import traceback from ... import io, style, pipeline from ...vendor.Qt import QtCore @@ -8,6 +9,10 @@ from ..models import TreeModel, Item from .. import lib +from shot_manager.tools.inventory_publish.utils import ( + SupportFamilies, + get_inventory_data, +) log = logging.getLogger(__name__) @@ -43,7 +48,8 @@ class SubsetsModel(TreeModel): "frames", "duration", "handles", - "step" + "step", + "shot inventory" ] SortAscendingRole = QtCore.Qt.UserRole + 2 @@ -71,10 +77,15 @@ def __init__(self, grouping=True, parent=None): "review.status.rendering": qtawesome.icon("fa.thermometer-2", color="#f9a825"), "review.status.pending": qtawesome.icon("fa.thumbs-up", color="#464646"), "review.status.approved": qtawesome.icon("fa.thumbs-up", color="#7cb342"), + + "inventory.inside": qtawesome.icon("fa.shopping-basket", color="#d77681"), + "inventory.outside": qtawesome.icon("fa.shopping-basket", color="#464646"), + "inventory.failed": qtawesome.icon("fa.question", color="#e53935"), } self._doc_fetching_thread = None self._doc_fetching_stop = False self._doc_payload = list() + self.inventory_data = dict() self.doc_fetched.connect(self.on_doc_fetched) @@ -121,6 +132,8 @@ def set_version(self, index, version): version_data = version.get("data", dict()) # Compute frame ranges (if data is present) + render_frames = version_data.get("renderFrames", None) + render_frame_list = version_data.get("renderFrameList", None) frame_start = version_data.get( "frameStart", # backwards compatibility @@ -139,7 +152,10 @@ def set_version(self, index, version): else: handles = version_data.get("handles", None) - if frame_start is not None and frame_end is not None: + if render_frames is not None: + frames = render_frames + duration = len(render_frame_list) + elif frame_start is not None and frame_end is not None: # Remove superfluous zeros from numbers (3.0 -> 3) to improve # readability for most frame ranges start_clean = ("%f" % frame_start).rstrip("0").rstrip(".") @@ -254,6 +270,15 @@ def _fetch(): self._doc_payload[:] = [(subset, versions.get(subset["_id"])) for subset in _subsets] + + shot_name = io.find_one({'_id': self._asset_id})["name"] + try: + self.inventory_data = get_inventory_data(shot_name) + except Exception: + print("Error: Get inventory data failed!") + print(traceback.format_exc()) + self.inventory_data = "Failed" + self.doc_fetched.emit() self._doc_payload[:] = [] @@ -353,6 +378,21 @@ def data(self, index, role): else: return "render progress: %d%%" % item["progress_percent"] + if index.column() == self.columns_index["shot inventory"]: + item = index.internalPointer() + _id = item.get("_id") + family = item.get("family", None) + if not _id: + return None + elif family not in list(SupportFamilies.keys()): + return "Shot inventory isn\'t support this family." + elif self.inventory_data == "Failed": + return "Get inventory data failed! See log..." + elif str(_id) in self.inventory_data: + return "Click to remove subset from shot inventory." + else: + return "Click to add subset into shot inventory." + if role == QtCore.Qt.DecorationRole: # Add icon to subset column @@ -382,6 +422,22 @@ def data(self, index, role): else: return self._icons["version.%s" % item["progress"]] + # Add icon to shot inventory column + if index.column() == self.columns_index["shot inventory"]: + item = index.internalPointer() + _id = item.get("_id") + family = item.get("family", None) + if not _id: + return None + elif family not in list(SupportFamilies.keys()): + return None + elif self.inventory_data == "Failed": + return self._icons["inventory.failed"] + elif str(_id) in self.inventory_data: + return self._icons["inventory.inside"] + else: + return self._icons["inventory.outside"] + if role == self.SortDescendingRole: item = index.internalPointer() if item.get("isGroup"): @@ -413,6 +469,13 @@ def data(self, index, role): def flags(self, index): flags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + if index.column() == self.columns_index["shot inventory"]: + item = index.internalPointer() + family = item.get("family", None) + if family not in list(SupportFamilies.keys()) or \ + self.inventory_data == "Failed": + flags = QtCore.Qt.ItemIsSelectable + # Make the version column editable if index.column() == self.columns_index["version"]: flags |= QtCore.Qt.ItemIsEditable diff --git a/avalon/tools/loader/widgets.py b/avalon/tools/loader/widgets.py index 70feacbfc..de2c8b693 100644 --- a/avalon/tools/loader/widgets.py +++ b/avalon/tools/loader/widgets.py @@ -12,7 +12,7 @@ from ... import style from .. import lib as tools_lib -from ..delegates import VersionDelegate +from ..delegates import VersionDelegate, ButtonDelegate from ..widgets import OptionalMenu, OptionalAction, OptionDialog from .model import ( @@ -20,7 +20,7 @@ SubsetFilterProxyModel, FamiliesFilterProxyModel, ) -from ..delegates import PrettyTimeDelegate +from ..delegates import PrettyTimeDelegate, pretty_timestamp local_encoding = locale.getlocale()[1] @@ -30,6 +30,7 @@ class SubsetWidget(QtWidgets.QWidget): active_changed = QtCore.Signal() # active index changed version_changed = QtCore.Signal() # version state changed for a subset + shot_inventory_clicked = QtCore.Signal(QtCore.QModelIndex) def __init__(self, enable_grouping=True, parent=None): super(SubsetWidget, self).__init__(parent=parent) @@ -58,6 +59,7 @@ def __init__(self, enable_grouping=True, parent=None): } """) view.setAllColumnsShowFocus(True) + view.setMouseTracking(True) # Set view delegates version_delegate = VersionDelegate() @@ -68,6 +70,10 @@ def __init__(self, enable_grouping=True, parent=None): column = model.Columns.index("time") view.setItemDelegateForColumn(column, time_delegate) + shot_inventory_delegate = ButtonDelegate() + column = model.Columns.index("shot inventory") + view.setItemDelegateForColumn(column, shot_inventory_delegate) + layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.addLayout(top_bar_layout) @@ -82,7 +88,8 @@ def __init__(self, enable_grouping=True, parent=None): self.data = { "delegates": { "version": version_delegate, - "time": time_delegate + "time": time_delegate, + "shot inventory": shot_inventory_delegate }, "state": { "groupable": groupable @@ -105,18 +112,20 @@ def __init__(self, enable_grouping=True, parent=None): view.setColumnWidth(0, 240) # subset view.setColumnWidth(1, 120) # family - view.setColumnWidth(2, 100) # version + view.setColumnWidth(2, 80) # version view.setColumnWidth(3, 120) # time - view.setColumnWidth(4, 100) # author - view.setColumnWidth(5, 80) # frames + view.setColumnWidth(4, 50) # author + view.setColumnWidth(5, 70) # frames view.setColumnWidth(6, 60) # duration view.setColumnWidth(7, 50) # handles - view.setColumnWidth(8, 50) # step + view.setColumnWidth(8, 40) # step + view.setColumnWidth(9, 90) # shot inventory selection = view.selectionModel() selection.selectionChanged.connect(self.active_changed) version_delegate.version_changed.connect(self.version_changed) + shot_inventory_delegate.clicked.connect(self.shot_inventory_clicked) groupable.stateChanged.connect(self.set_grouping) @@ -549,6 +558,11 @@ def set_version(self, version_id): source = version["data"].get("source", None) source_label = source if source else "No source" + grab_log = version["data"].get("grab_log", {}) + grab_from = grab_log.get("grab_from", "") + grab_user = grab_log.get("grab_user", "") + grab_time = grab_log.get("grab_time", "") + # Store source and raw data self.data["source"] = source self.data["raw"] = version @@ -562,13 +576,22 @@ def set_version(self, version_id): } _version_info = u""" -

{subset} v{version:03d}

-Created
-{created}
-
-Source
-{source}
-
""".format(**data) +

{subset} v{version:03d}

+
Created
+ {created}
+
""".format(**data) + + latest_grab_source_version = tools_lib.get_latest_grab_source_version(grab_log) + if latest_grab_source_version and int(version["name"]) < int(latest_grab_source_version["name"]): + _not_from_latest_info = u""" + !!! Newer Version Found !!!
+ v{:03d} (from {})
+
""".format(int(latest_grab_source_version["name"]), grab_from) + _version_info += _not_from_latest_info + + _version_info += u"""Source
+ {source}
+
""".format(**data) # Setting rig review data review_data = version.get("data", {}).get("review", {}) @@ -580,6 +603,16 @@ def set_version(self, version_id): """.format(_ppl) _version_info += _ppl_info + # grab log + if grab_log: + _grab_info = u""" + Grab Info
+ Grab from: {}
+ Grab by: {}
+ Grab time: {}

+ """.format(grab_from, grab_user, pretty_timestamp(grab_time)) + _version_info += _grab_info + self.setHtml(_version_info) def contextMenuEvent(self, event): diff --git a/avalon/tools/models.py b/avalon/tools/models.py index ceb42b1d3..40e5698c9 100644 --- a/avalon/tools/models.py +++ b/avalon/tools/models.py @@ -416,12 +416,18 @@ def refresh(self): self.add_child(self.trash_item, parent=None) # Get all assets sorted by name + db_episodes = io.find({"type": "episode"}).sort("name", 1) + db_sequences = io.find({"type": "sequence"}).sort("name", 1) + db_asset_types = io.find({"type": "asset_type"}).sort("name", 1) db_assets = io.find({"type": "asset"}).sort("name", 1) project_doc = io.find_one({"type": "project"}) silos = [] if lib.project_use_silo(project_doc): - for _silo in db_assets.distinct("silo"): + for _silo in sorted(list(set(db_episodes.distinct("silo") + + db_sequences.distinct("silo") + + db_asset_types.distinct("silo") + + db_assets.distinct("silo")))): _filter = {"type": "silo", "name": _silo} silo_data = io.find_one(_filter) if not silo_data: @@ -431,6 +437,21 @@ def refresh(self): # Group the assets by their visual parent's id assets_by_parent = collections.defaultdict(list) + for episode in db_episodes: + parent_id = episode.get("data", {}).get("visualParent") + if parent_id is None and silos is not None: + parent_id = episode.get("silo") + assets_by_parent[parent_id].append(episode) + for sequence in db_sequences: + parent_id = sequence.get("data", {}).get("visualParent") + if parent_id is None and silos is not None: + parent_id = sequence.get("silo") + assets_by_parent[parent_id].append(sequence) + for asset_type in db_asset_types: + parent_id = asset_type.get("data", {}).get("visualParent") + if parent_id is None and silos is not None: + parent_id = asset_type.get("silo") + assets_by_parent[parent_id].append(asset_type) for asset in db_assets: parent_id = asset.get("data", {}).get("visualParent") if parent_id is None and silos is not None: diff --git a/avalon/tools/projectmanager/app.py b/avalon/tools/projectmanager/app.py index 500557055..ef94f39a1 100644 --- a/avalon/tools/projectmanager/app.py +++ b/avalon/tools/projectmanager/app.py @@ -1,5 +1,6 @@ import sys from functools import partial +import getpass from ...vendor.Qt import QtWidgets, QtCore, QtGui from ...vendor import qargparse, qtawesome @@ -8,7 +9,7 @@ from .. import lib as tools_lib from ..widgets import AssetWidget, TaskWidget -from .dialogs import TasksCreateDialog, AssetCreateDialog +from .dialogs import TasksCreateDialog, AssetCreateDialog, EpisodeCreateDialog, SequenceCreateDialog from project_api.utils import shotgun_io from project_api.shot.create import ShotCreator @@ -171,9 +172,26 @@ def __init__(self, is_silo_project=None, parent=None): assets_layout = QtWidgets.QVBoxLayout(assets_widgets) assets = AssetWidget() assets.view.setSelectionMode(assets.view.ExtendedSelection) + add_episode = QtWidgets.QPushButton("Add episode") + add_sequence = QtWidgets.QPushButton("Add sequence") add_asset = QtWidgets.QPushButton("Add asset") + add_layout = QtWidgets.QHBoxLayout() + add_layout.addWidget(add_episode) + add_layout.addWidget(add_sequence) + add_layout.addWidget(add_asset) assets_layout.addWidget(assets) - assets_layout.addWidget(add_asset) + assets_layout.addLayout(add_layout) + + role_admin = project_doc["data"].get("role", {}).get("admin", []) + role_developer = project_doc["data"].get("role", {}).get("developer", []) + username = getpass.getuser() + if not (username in role_admin+role_developer): + add_episode.setEnabled(False) + add_sequence.setEnabled(False) + add_asset.setEnabled(False) + permission_warn_label = QtWidgets.QLabel("** You don't have permission to add asset ! **") + permission_warn_label.setStyleSheet("color: red") + assets_layout.addWidget(permission_warn_label) # region Shotgrid snyc shotgrid_icon = QtGui.QIcon( @@ -278,6 +296,8 @@ def __init__(self, is_silo_project=None, parent=None): } # signals + add_episode.clicked.connect(self.on_add_episode) + add_sequence.clicked.connect(self.on_add_sequence) add_asset.clicked.connect(self.on_add_asset) add_task.clicked.connect(self.on_add_task) options_accept.clicked.connect(self.on_task_options_accepted) @@ -384,6 +404,141 @@ def sync_from_shotgrid(self, project_name, func): # endregion print('Sync done.') + def on_add_episode(self): + """Show add episode dialog""" + + # Get parent asset (active index in selection) + model = self.data["model"]["assets"] + parent = model.get_active_asset() + + if parent and parent["type"] == "silo": + silo = parent["name"] + else: + silo = parent.get("_document", {}).get("silo") if parent else None + + if not self.shotgrid_io: + self.shotgrid_io = shotgun_io.ShotgunIO() + shorgrid_data = self.shotgrid_io.sg.find_one( + "Project", [['tank_name', 'is', self.data["project"]["name"]]], ["id"]) + + dialog = EpisodeCreateDialog( + is_silo_required=self.is_silo_project, parent=self, + shotgrid_io=self.shotgrid_io, shotgrid_project_id=shorgrid_data["id"] + ) + + def _on_episode_created(data): + """Callback whenever episode gets created""" + self.echo("Added episode: {name}".format(**data)) + model.refresh() + + # Preserve focus on the dialog label field + # This is to allow quick continuing of typing a new episode name + # whenever the user created one; this way we can press the "ENTER" + # key to add an episode and continue typing for the next. + dialog.data["label"]["label"].setFocus() + + def _on_current_asset_changed(): + """Callback on current asset changed in item widget. + + Whenever the current index changes in the item widget we want to + update under which asset we're creating *to be created* asset. + + """ + + parent = model.get_active_asset() + if parent and parent["type"] == "silo": + _silo = parent["name"] + dialog.set_silo(_silo) + + # Set initial values + dialog.set_silo(silo) + + # Signals + model.current_changed.connect(_on_current_asset_changed) + dialog.episode_created.connect(_on_episode_created) + + dialog.show() + + def on_add_sequence(self): + """Show add sequence dialog""" + + # Get parent asset (active index in selection) + model = self.data["model"]["assets"] + parent = model.get_active_asset() + + if parent: + if parent["type"] == "silo": + silo = parent["name"] + else: + silo = parent.get("_document", {}).get("silo") + if parent["type"] == "episode": + episode_id = parent["_id"] + else: + episode_id = None + else: + episode_id = None + silo = None + + if not self.shotgrid_io: + self.shotgrid_io = shotgun_io.ShotgunIO() + shorgrid_data = self.shotgrid_io.sg.find_one( + "Project", [['tank_name', 'is', self.data["project"]["name"]]], ["id"]) + + dialog = SequenceCreateDialog( + is_silo_required=self.is_silo_project, parent=self, + shotgrid_io=self.shotgrid_io, shotgrid_project_id=shorgrid_data["id"] + ) + + def _on_sequence_created(data): + """Callback whenever sequence gets created""" + self.echo("Added sequence: {name}".format(**data)) + model.refresh() + + # Preserve focus on the dialog label field + # This is to allow quick continuing of typing a new sequence name + # whenever the user created one; this way we can press the "ENTER" + # key to add an sequence and continue typing for the next. + dialog.data["label"]["label"].setFocus() + + def _on_current_asset_changed(): + """Callback on current asset changed in item widget. + + Whenever the current index changes in the item widget we want to + update under which asset we're creating *to be created* asset. + + """ + + parent = model.get_active_asset() + if parent: + if parent["type"] == "silo": + _episode_id = None + _silo = parent["name"] + elif parent["type"] == "episode": + _episode_id = parent["_id"] + _silo = parent.get("_document", {}).get("silo") + elif parent["type"] == "trash_bin": + _episode_id = None + _silo = None + else: + _episode_id = parent.get("_document", {}).get("data", {}).get("episode_id", None) + _silo = parent.get("_document", {}).get("silo") + else: + _episode_id = None + _silo = None + + dialog.set_episode(_episode_id) + dialog.set_silo(_silo) + + # Set initial values + dialog.set_episode(episode_id) + dialog.set_silo(silo) + + # Signals + model.current_changed.connect(_on_current_asset_changed) + dialog.sequence_created.connect(_on_sequence_created) + + dialog.show() + def on_add_asset(self): """Show add asset dialog""" @@ -398,8 +553,14 @@ def on_add_asset(self): parent_id = parent["_id"] if parent else None silo = parent.get("_document", {}).get("silo") if parent else None + if not self.shotgrid_io: + self.shotgrid_io = shotgun_io.ShotgunIO() + shorgrid_data = self.shotgrid_io.sg.find_one( + "Project", [['tank_name', 'is', self.data["project"]["name"]]], ["id"]) + dialog = AssetCreateDialog( - is_silo_required=self.is_silo_project, parent=self + is_silo_required=self.is_silo_project, parent=self, + shotgrid_io=self.shotgrid_io, shotgrid_project_id=shorgrid_data["id"] ) if self.is_silo_project: dialog.set_silo_input_enable( @@ -426,14 +587,19 @@ def _on_current_asset_changed(): """ parent = model.get_active_asset() - if parent and parent["type"] == "silo": - _parent_id = None - _silo = parent["name"] + if parent: + if parent["type"] == "silo": + _parent_id = None + _silo = parent["name"] + elif parent["type"] == "trash_bin": + _parent_id = None + _silo = None + else: + _parent_id = parent["_id"] + _silo = parent.get("_document", {}).get("silo") else: - _parent_id = parent["_id"] if parent else None + _parent_id = None _silo = None - if parent: - _silo = parent.get("_document", {}).get("silo") dialog.set_parent(_parent_id) dialog.set_silo(_silo) diff --git a/avalon/tools/projectmanager/dialogs.py b/avalon/tools/projectmanager/dialogs.py index febc4227c..fa98f36d2 100644 --- a/avalon/tools/projectmanager/dialogs.py +++ b/avalon/tools/projectmanager/dialogs.py @@ -58,10 +58,12 @@ class AssetCreateDialog(QtWidgets.QDialog): asset_created = QtCore.Signal(dict) - def __init__(self, is_silo_required=True, parent=None): + def __init__(self, is_silo_required=True, parent=None, shotgrid_io=None, shotgrid_project_id=None): super(AssetCreateDialog, self).__init__(parent=parent) self.setWindowTitle("Add asset") self.is_silo_required = is_silo_required + self.shotgrid_io = shotgrid_io + self.shotgrid_project_id = shotgrid_project_id self.parent_doc = None @@ -137,7 +139,7 @@ def set_parent(self, parent_id): # Get the parent asset (if any provided) if parent_id: - parent_doc = io.find_one({"_id": parent_id, "type": "asset"}) + parent_doc = io.find_one({"_id": parent_id}) assert parent_doc, "Parent asset does not exist." parent_name = parent_doc["name"] @@ -170,11 +172,35 @@ def on_add_asset(self): if self.parent_doc: parent_id = self.parent_doc["_id"] + episode = self.parent_doc.get("episode", "") + episode_id = self.parent_doc.get("data", {}).get("episode_id") + sequence = self.parent_doc["name"] if self.parent_doc["type"] == "sequence"\ + else self.parent_doc.get("sequence", "") + sequence_id = parent_id if self.parent_doc["type"] == "sequence"\ + else self.parent_doc.get("data", {}).get("sequence_id") else: parent_id = None + episode = "" + episode_id = None + sequence = "" + sequence_id = None name = self.data["label"]["name"].text() label = self.data["label"]["label"].text() + silo = self.data["label"]["silo"].text() + + # Make sure there is no asset/shot in Shotgrid have the same name + if self.shotgrid_io and self.shotgrid_project_id: + exists_names = [] + all_shots = self.shotgrid_io.get_all_shots(self.shotgrid_project_id) + exists_names += [shot["code"] for shot in all_shots] + all_assets = self.shotgrid_io.get_all_assets(self.shotgrid_project_id) + exists_names += [asset["code"] for asset in all_assets] + + if name in exists_names or label in exists_names: + QtWidgets.QMessageBox.warning(self, "Label invalid", + "Could not create! There is a asset/shot in Shotgrid have the same name.") + return if not label: QtWidgets.QMessageBox.warning(self, "Missing required label", @@ -182,20 +208,38 @@ def on_add_asset(self): return if self.is_silo_required: - silo_field = self.data["label"]["silo"] - silo = silo_field.text() if not silo: QtWidgets.QMessageBox.critical( self, "Missing silo", "Please enter a silo." ) return + # Check if exists other asset have same name with the asset name + same_name_asset = io.find_one({"name": name, "type": "asset"}) + if same_name_asset: + QtWidgets.QMessageBox.critical( + self, "Name invalid", "Asset name already used by other asset." + ) + return + + # Check if exists other asset have same name with the silo name + same_name_asset = io.find_one({"name": silo, "type": "asset"}) + if same_name_asset: + QtWidgets.QMessageBox.critical( + self, "Name invalid", "Silo name already used by other asset." + ) + return + # Name is based on label, so if label passes then name should too assert name, "This is a bug" data = { "name": name, "label": label, + "episode": episode, + "episode_id": episode_id, + "sequence": sequence, + "sequence_id": sequence_id, "visualParent": parent_id } @@ -229,3 +273,302 @@ def on_add_asset(self): return self.asset_created.emit(data) + + +class EpisodeCreateDialog(QtWidgets.QDialog): + """A Dialog to create a new episode.""" + + episode_created = QtCore.Signal(dict) + + def __init__(self, is_silo_required=True, parent=None, shotgrid_io=None, shotgrid_project_id=None): + super(EpisodeCreateDialog, self).__init__(parent=parent) + self.setWindowTitle("Add episode") + self.is_silo_required = is_silo_required + self.shotgrid_io = shotgrid_io + self.shotgrid_project_id = shotgrid_project_id + + # Label + label_label = QtWidgets.QLabel("Label:") + label = widgets.NameValidEdit() + label.setPlaceholderText("