diff --git a/client/ayon_core/hosts/nuke/api/lib.py b/client/ayon_core/hosts/nuke/api/lib.py index 0a4755c166..500a0f9601 100644 --- a/client/ayon_core/hosts/nuke/api/lib.py +++ b/client/ayon_core/hosts/nuke/api/lib.py @@ -1024,6 +1024,18 @@ def script_name(): return nuke.root().knob("name").value() +def add_button_render_on_farm(node): + name = "renderOnFarm" + label = "Render On Farm" + value = ( + "from ayon_core.hosts.nuke.api.utils import submit_render_on_farm;" + "submit_render_on_farm(nuke.thisNode())" + ) + knob = nuke.PyScript_Knob(name, label, value) + knob.clearFlag(nuke.STARTLINE) + node.addKnob(knob) + + def add_button_write_to_read(node): name = "createReadNode" label = "Read From Rendered" @@ -1146,6 +1158,17 @@ def create_write_node( Return: node (obj): group node with avalon data as Knobs ''' + # Ensure name does not contain any invalid characters. + special_chars = re.escape("!@#$%^&*()=[]{}|\\;',.<>/?~+-") + special_chars_regex = re.compile(f"[{special_chars}]") + found_special_characters = list(special_chars_regex.findall(name)) + + msg = ( + f"Special characters found in name \"{name}\": " + f"{' '.join(found_special_characters)}" + ) + assert not found_special_characters, msg + prenodes = prenodes or [] # filtering variables @@ -1270,6 +1293,10 @@ def create_write_node( link.setFlag(0x1000) GN.addKnob(link) + # Adding render farm submission button. + if data.get("render_on_farm", False): + add_button_render_on_farm(GN) + # adding write to read button add_button_write_to_read(GN) diff --git a/client/ayon_core/hosts/nuke/api/utils.py b/client/ayon_core/hosts/nuke/api/utils.py index 1bfc1919fa..646bb0ece1 100644 --- a/client/ayon_core/hosts/nuke/api/utils.py +++ b/client/ayon_core/hosts/nuke/api/utils.py @@ -3,9 +3,15 @@ import re import nuke -from ayon_core import resources +import pyblish.util +import pyblish.api from qtpy import QtWidgets +from ayon_core import resources +from ayon_core.pipeline import registered_host +from ayon_core.tools.utils import show_message_dialog +from ayon_core.pipeline.create import CreateContext + def set_context_favorites(favorites=None): """ Adding favorite folders to nuke's browser @@ -142,3 +148,77 @@ def is_headless(): bool: headless """ return QtWidgets.QApplication.instance() is None + + +def submit_render_on_farm(node): + # Ensure code is executed in root context. + if nuke.root() == nuke.thisNode(): + _submit_render_on_farm(node) + else: + # If not in root context, move to the root context and then execute the + # code. + with nuke.root(): + _submit_render_on_farm(node) + + +def _submit_render_on_farm(node): + """Render on farm submission + + This function prepares the context for farm submission, validates it, + extracts relevant data, copies the current workfile to a timestamped copy, + and submits the job to the farm. + + Args: + node (Node): The node for which the farm submission is being made. + """ + + host = registered_host() + create_context = CreateContext(host) + + # Ensure CreateInstance is enabled. + for instance in create_context.instances: + if node.name() != instance.transient_data["node"].name(): + continue + + instance.data["active"] = True + + context = pyblish.api.Context() + context.data["create_context"] = create_context + # Used in pyblish plugin to determine which instance to publish. + context.data["node_name"] = node.name() + # Used in pyblish plugins to determine whether to run or not. + context.data["render_on_farm"] = True + + # Since we need to bypass version validation and incrementing, we need to + # remove the plugins from the list that are responsible for these tasks. + plugins = pyblish.api.discover() + blacklist = ["IncrementScriptVersion", "ValidateVersion"] + plugins = [ + plugin + for plugin in plugins + if plugin.__name__ not in blacklist + ] + + context = pyblish.util.publish(context, plugins=plugins) + + error_message = "" + success = True + for result in context.data["results"]: + if result["success"]: + continue + + success = False + + err = result["error"] + error_message += "\n" + error_message += err.formatted_traceback + + if not success: + show_message_dialog( + "Publish Errors", error_message, level="critical" + ) + return + + show_message_dialog( + "Submission Successful", "Submission to the farm was successful." + ) diff --git a/client/ayon_core/hosts/nuke/plugins/create/create_write_image.py b/client/ayon_core/hosts/nuke/plugins/create/create_write_image.py index 770726e34f..fc2538f23d 100644 --- a/client/ayon_core/hosts/nuke/plugins/create/create_write_image.py +++ b/client/ayon_core/hosts/nuke/plugins/create/create_write_image.py @@ -65,12 +65,16 @@ class CreateWriteImage(napi.NukeWriteCreator): ) def create_instance_node(self, product_name, instance_data): + settings = self.project_settings["nuke"]["create"]["CreateWriteImage"] # add fpath_template write_data = { "creator": self.__class__.__name__, "productName": product_name, - "fpath_template": self.temp_rendering_path_template + "fpath_template": self.temp_rendering_path_template, + "render_on_farm": ( + "render_on_farm" in settings["instance_attributes"] + ) } write_data.update(instance_data) diff --git a/client/ayon_core/hosts/nuke/plugins/create/create_write_prerender.py b/client/ayon_core/hosts/nuke/plugins/create/create_write_prerender.py index 96ac2fac9c..47796d159c 100644 --- a/client/ayon_core/hosts/nuke/plugins/create/create_write_prerender.py +++ b/client/ayon_core/hosts/nuke/plugins/create/create_write_prerender.py @@ -46,11 +46,17 @@ class CreateWritePrerender(napi.NukeWriteCreator): return attr_defs def create_instance_node(self, product_name, instance_data): + settings = self.project_settings["nuke"]["create"] + settings = settings["CreateWritePrerender"] + # add fpath_template write_data = { "creator": self.__class__.__name__, "productName": product_name, - "fpath_template": self.temp_rendering_path_template + "fpath_template": self.temp_rendering_path_template, + "render_on_farm": ( + "render_on_farm" in settings["instance_attributes"] + ) } write_data.update(instance_data) diff --git a/client/ayon_core/hosts/nuke/plugins/create/create_write_render.py b/client/ayon_core/hosts/nuke/plugins/create/create_write_render.py index 24bddb3d26..4cb5ccdfa2 100644 --- a/client/ayon_core/hosts/nuke/plugins/create/create_write_render.py +++ b/client/ayon_core/hosts/nuke/plugins/create/create_write_render.py @@ -40,11 +40,16 @@ class CreateWriteRender(napi.NukeWriteCreator): return attr_defs def create_instance_node(self, product_name, instance_data): + settings = self.project_settings["nuke"]["create"]["CreateWriteRender"] + # add fpath_template write_data = { "creator": self.__class__.__name__, "productName": product_name, - "fpath_template": self.temp_rendering_path_template + "fpath_template": self.temp_rendering_path_template, + "render_on_farm": ( + "render_on_farm" in settings["instance_attributes"] + ) } write_data.update(instance_data) diff --git a/client/ayon_core/hosts/nuke/plugins/publish/collect_headless_farm.py b/client/ayon_core/hosts/nuke/plugins/publish/collect_headless_farm.py new file mode 100644 index 0000000000..3f49a2bf01 --- /dev/null +++ b/client/ayon_core/hosts/nuke/plugins/publish/collect_headless_farm.py @@ -0,0 +1,56 @@ +import pyblish.api + +from ayon_core.pipeline.publish import ( + AYONPyblishPluginMixin +) + + +class CollectRenderOnFarm(pyblish.api.ContextPlugin): + """Setup instances for render on farm submission.""" + + # Needs to be after CollectFromCreateContext + order = pyblish.api.CollectorOrder - 0.49 + label = "Collect Render On Farm" + hosts = ["nuke"] + + def process(self, context): + if not context.data.get("render_on_farm", False): + return + + for instance in context: + if instance.data["family"] == "workfile": + instance.data["active"] = False + continue + + # Filter out all other instances. + node = instance.data["transientData"]["node"] + if node.name() != instance.context.data["node_name"]: + instance.data["active"] = False + continue + + instance.data["families"].append("render_on_farm") + + # Enable for farm publishing. + instance.data["farm"] = True + + # Skip workfile version incremental save. + instance.context.data["increment_script_version"] = False + + +class SetupRenderOnFarm(pyblish.api.InstancePlugin, AYONPyblishPluginMixin): + """Setup instance for render on farm submission.""" + + order = pyblish.api.CollectorOrder + 0.4999 + label = "Setup Render On Farm" + hosts = ["nuke"] + families = ["render_on_farm"] + + def process(self, instance): + # Clear the families as we only want the main family, ei. no review + # etc. + instance.data["families"] = ["render_on_farm"] + + # Use the workfile instead of published. + publish_attributes = instance.data["publish_attributes"] + plugin_attributes = publish_attributes["NukeSubmitDeadline"] + plugin_attributes["use_published_workfile"] = False diff --git a/client/ayon_core/hosts/nuke/plugins/publish/extract_headless_farm.py b/client/ayon_core/hosts/nuke/plugins/publish/extract_headless_farm.py new file mode 100644 index 0000000000..4ba55f8c46 --- /dev/null +++ b/client/ayon_core/hosts/nuke/plugins/publish/extract_headless_farm.py @@ -0,0 +1,36 @@ +import os +from datetime import datetime +import shutil + +import pyblish.api + +from ayon_core.pipeline import registered_host + + +class ExtractRenderOnFarm(pyblish.api.InstancePlugin): + """Copy the workfile to a timestamped copy.""" + + order = pyblish.api.ExtractorOrder + 0.499 + label = "Extract Render On Farm" + hosts = ["nuke"] + families = ["render_on_farm"] + + def process(self, instance): + if not instance.context.data.get("render_on_farm", False): + return + + host = registered_host() + current_datetime = datetime.now() + formatted_timestamp = current_datetime.strftime("%Y%m%d%H%M%S") + base, ext = os.path.splitext(host.current_file()) + + directory = os.path.join(os.path.dirname(base), "farm_submissions") + if not os.path.exists(directory): + os.makedirs(directory) + + filename = "{}_{}{}".format( + os.path.basename(base), formatted_timestamp, ext + ) + path = os.path.join(directory, filename).replace("\\", "/") + instance.context.data["currentFile"] = path + shutil.copy(host.current_file(), path) diff --git a/client/ayon_core/hosts/nuke/plugins/publish/increment_script_version.py b/client/ayon_core/hosts/nuke/plugins/publish/increment_script_version.py index 6b0be42ba1..70fd04a985 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/increment_script_version.py +++ b/client/ayon_core/hosts/nuke/plugins/publish/increment_script_version.py @@ -13,6 +13,8 @@ class IncrementScriptVersion(pyblish.api.ContextPlugin): hosts = ['nuke'] def process(self, context): + if not context.data.get("increment_script_version", True): + return assert all(result["success"] for result in context.data["results"]), ( "Publishing not successful so version is not increased.") diff --git a/client/ayon_core/modules/deadline/plugins/publish/collect_pools.py b/client/ayon_core/modules/deadline/plugins/publish/collect_pools.py index 6923c2b16b..2592d358e5 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/collect_pools.py +++ b/client/ayon_core/modules/deadline/plugins/publish/collect_pools.py @@ -26,27 +26,32 @@ class CollectDeadlinePools(pyblish.api.InstancePlugin, order = pyblish.api.CollectorOrder + 0.420 label = "Collect Deadline Pools" - hosts = ["aftereffects", - "fusion", - "harmony" - "nuke", - "maya", - "max", - "houdini"] + hosts = [ + "aftereffects", + "fusion", + "harmony", + "maya", + "max", + "houdini", + "nuke", + ] - families = ["render", - "rendering", - "render.farm", - "renderFarm", - "renderlayer", - "maxrender", - "usdrender", - "redshift_rop", - "arnold_rop", - "mantra_rop", - "karma_rop", - "vray_rop", - "publish.hou"] + families = [ + "render", + "prerender", + "rendering", + "render.farm", + "renderFarm", + "renderlayer", + "maxrender", + "usdrender", + "redshift_rop", + "arnold_rop", + "mantra_rop", + "karma_rop", + "vray_rop", + "publish.hou", + ] primary_pool = None secondary_pool = None diff --git a/client/ayon_core/plugins/publish/validate_version.py b/client/ayon_core/plugins/publish/validate_version.py index 9031194e8c..25a5757330 100644 --- a/client/ayon_core/plugins/publish/validate_version.py +++ b/client/ayon_core/plugins/publish/validate_version.py @@ -1,8 +1,10 @@ import pyblish.api -from ayon_core.pipeline.publish import PublishValidationError +from ayon_core.pipeline.publish import ( + PublishValidationError, OptionalPyblishPluginMixin +) -class ValidateVersion(pyblish.api.InstancePlugin): +class ValidateVersion(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate instance version. AYON does not allow overwriting previously published versions. @@ -18,6 +20,9 @@ class ValidateVersion(pyblish.api.InstancePlugin): active = True def process(self, instance): + if not self.is_active(instance.data): + return + version = instance.data.get("version") latest_version = instance.data.get("latestVersion") diff --git a/client/ayon_core/tools/common_models/projects.py b/client/ayon_core/tools/common_models/projects.py index 19a38bee21..89dd881a10 100644 --- a/client/ayon_core/tools/common_models/projects.py +++ b/client/ayon_core/tools/common_models/projects.py @@ -5,7 +5,7 @@ import ayon_api import six from ayon_core.style import get_default_entity_icon_color -from ayon_core.lib import CacheItem +from ayon_core.lib import CacheItem, NestedCacheItem PROJECTS_MODEL_SENDER = "projects.model" @@ -17,6 +17,49 @@ class AbstractHierarchyController: pass +class StatusItem: + """Item representing status of project. + + Args: + name (str): Status name ("Not ready"). + color (str): Status color in hex ("#434a56"). + short (str): Short status name ("NRD"). + icon (str): Icon name in MaterialIcons ("fiber_new"). + state (Literal["not_started", "in_progress", "done", "blocked"]): + Status state. + + """ + def __init__(self, name, color, short, icon, state): + self.name = name + self.color = color + self.short = short + self.icon = icon + self.state = state + + def to_data(self): + return { + "name": self.name, + "color": self.color, + "short": self.short, + "icon": self.icon, + "state": self.state, + } + + @classmethod + def from_data(cls, data): + return cls(**data) + + @classmethod + def from_project_item(cls, status_data): + return cls( + name=status_data["name"], + color=status_data["color"], + short=status_data["shortName"], + icon=status_data["icon"], + state=status_data["state"], + ) + + class ProjectItem: """Item representing folder entity on a server. @@ -40,6 +83,23 @@ class ProjectItem: } self.icon = icon + @classmethod + def from_entity(cls, project_entity): + """Creates folder item from entity. + + Args: + project_entity (dict[str, Any]): Project entity. + + Returns: + ProjectItem: Project item. + + """ + return cls( + project_entity["name"], + project_entity["active"], + project_entity["library"], + ) + def to_data(self): """Converts folder item to data. @@ -79,7 +139,7 @@ def _get_project_items_from_entitiy(projects): """ return [ - ProjectItem(project["name"], project["active"], project["library"]) + ProjectItem.from_entity(project) for project in projects ] @@ -87,18 +147,29 @@ def _get_project_items_from_entitiy(projects): class ProjectsModel(object): def __init__(self, controller): self._projects_cache = CacheItem(default_factory=list) - self._project_items_by_name = {} - self._projects_by_name = {} + self._project_statuses_cache = NestedCacheItem( + levels=1, default_factory=list + ) + self._projects_by_name = NestedCacheItem( + levels=1, default_factory=list + ) self._is_refreshing = False self._controller = controller def reset(self): self._projects_cache.reset() - self._project_items_by_name = {} - self._projects_by_name = {} + self._project_statuses_cache.reset() + self._projects_by_name.reset() def refresh(self): + """Refresh project items. + + This method will requery list of ProjectItem returned by + 'get_project_items'. + + To reset all cached items use 'reset' method. + """ self._refresh_projects_cache() def get_project_items(self, sender): @@ -117,12 +188,51 @@ class ProjectsModel(object): return self._projects_cache.get_data() def get_project_entity(self, project_name): - if project_name not in self._projects_by_name: + """Get project entity. + + Args: + project_name (str): Project name. + + Returns: + Union[dict[str, Any], None]: Project entity or None if project + was not found by name. + + """ + project_cache = self._projects_by_name[project_name] + if not project_cache.is_valid: entity = None if project_name: entity = ayon_api.get_project(project_name) - self._projects_by_name[project_name] = entity - return self._projects_by_name[project_name] + project_cache.update_data(entity) + return project_cache.get_data() + + def get_project_status_items(self, project_name, sender): + """Get project status items. + + Args: + project_name (str): Project name. + sender (Union[str, None]): Name of sender who asked for items. + + Returns: + list[StatusItem]: Status items for project. + + """ + statuses_cache = self._project_statuses_cache[project_name] + if not statuses_cache.is_valid: + with self._project_statuses_refresh_event_manager( + sender, project_name + ): + project_entity = None + if project_name: + project_entity = self.get_project_entity(project_name) + statuses = [] + if project_entity: + statuses = [ + StatusItem.from_project_item(status) + for status in project_entity["statuses"] + ] + statuses_cache.update_data(statuses) + return statuses_cache.get_data() @contextlib.contextmanager def _project_refresh_event_manager(self, sender): @@ -143,6 +253,23 @@ class ProjectsModel(object): ) self._is_refreshing = False + @contextlib.contextmanager + def _project_statuses_refresh_event_manager(self, sender, project_name): + self._controller.emit_event( + "projects.statuses.refresh.started", + {"sender": sender, "project_name": project_name}, + PROJECTS_MODEL_SENDER + ) + try: + yield + + finally: + self._controller.emit_event( + "projects.statuses.refresh.finished", + {"sender": sender, "project_name": project_name}, + PROJECTS_MODEL_SENDER + ) + def _refresh_projects_cache(self, sender=None): if self._is_refreshing: return None diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index 7a7d335092..509db4d037 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -114,6 +114,7 @@ class VersionItem: thumbnail_id (Union[str, None]): Thumbnail id. published_time (Union[str, None]): Published time in format '%Y%m%dT%H%M%SZ'. + status (Union[str, None]): Status name. author (Union[str, None]): Author. frame_range (Union[str, None]): Frame range. duration (Union[int, None]): Duration. @@ -132,6 +133,7 @@ class VersionItem: thumbnail_id, published_time, author, + status, frame_range, duration, handles, @@ -146,6 +148,7 @@ class VersionItem: self.is_hero = is_hero self.published_time = published_time self.author = author + self.status = status self.frame_range = frame_range self.duration = duration self.handles = handles @@ -185,6 +188,7 @@ class VersionItem: "is_hero": self.is_hero, "published_time": self.published_time, "author": self.author, + "status": self.status, "frame_range": self.frame_range, "duration": self.duration, "handles": self.handles, @@ -488,6 +492,27 @@ class FrontendLoaderController(_BaseLoaderController): pass + @abstractmethod + def get_project_status_items(self, project_name, sender=None): + """Items for all projects available on server. + + Triggers event topics "projects.statuses.refresh.started" and + "projects.statuses.refresh.finished" with data: + { + "sender": sender, + "project_name": project_name + } + + Args: + project_name (Union[str, None]): Project name. + sender (Optional[str]): Sender who requested the items. + + Returns: + list[StatusItem]: List of status items. + """ + + pass + @abstractmethod def get_product_items(self, project_name, folder_ids, sender=None): """Product items for folder ids. diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index 0c9bb369c7..35188369c2 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -180,6 +180,11 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): def get_project_items(self, sender=None): return self._projects_model.get_project_items(sender) + def get_project_status_items(self, project_name, sender=None): + return self._projects_model.get_project_status_items( + project_name, sender + ) + def get_folder_items(self, project_name, sender=None): return self._hierarchy_model.get_folder_items(project_name, sender) diff --git a/client/ayon_core/tools/loader/models/products.py b/client/ayon_core/tools/loader/models/products.py index a3bbc30a09..c9325c4480 100644 --- a/client/ayon_core/tools/loader/models/products.py +++ b/client/ayon_core/tools/loader/models/products.py @@ -58,6 +58,7 @@ def version_item_from_entity(version): thumbnail_id=version["thumbnailId"], published_time=published_time, author=author, + status=version["status"], frame_range=frame_range, duration=duration, handles=handles, @@ -526,8 +527,11 @@ class ProductsModel: products = list(ayon_api.get_products(project_name, **kwargs)) product_ids = {product["id"] for product in products} + # Add 'status' to fields -> fixed in ayon-python-api 1.0.4 + fields = ayon_api.get_default_fields_for_type("version") + fields.add("status") versions = ayon_api.get_versions( - project_name, product_ids=product_ids + project_name, product_ids=product_ids, fields=fields ) return self._create_product_items( diff --git a/client/ayon_core/tools/loader/ui/products_delegates.py b/client/ayon_core/tools/loader/ui/products_delegates.py index 12ed1165ae..6bcb78ec66 100644 --- a/client/ayon_core/tools/loader/ui/products_delegates.py +++ b/client/ayon_core/tools/loader/ui/products_delegates.py @@ -6,6 +6,9 @@ from ayon_core.tools.utils.lib import format_version from .products_model import ( PRODUCT_ID_ROLE, VERSION_NAME_EDIT_ROLE, + VERSION_STATUS_NAME_ROLE, + VERSION_STATUS_SHORT_ROLE, + VERSION_STATUS_COLOR_ROLE, VERSION_ID_ROLE, PRODUCT_IN_SCENE_ROLE, ACTIVE_SITE_ICON_ROLE, @@ -194,6 +197,49 @@ class LoadedInSceneDelegate(QtWidgets.QStyledItemDelegate): option.palette.setBrush(QtGui.QPalette.Text, color) +class StatusDelegate(QtWidgets.QStyledItemDelegate): + """Delegate showing status name and short name.""" + + def paint(self, painter, option, index): + if option.widget: + style = option.widget.style() + else: + style = QtWidgets.QApplication.style() + + style.drawControl( + style.CE_ItemViewItem, option, painter, option.widget + ) + + painter.save() + + text_rect = style.subElementRect(style.SE_ItemViewItemText, option) + text_margin = style.proxy().pixelMetric( + style.PM_FocusFrameHMargin, option, option.widget + ) + 1 + padded_text_rect = text_rect.adjusted( + text_margin, 0, - text_margin, 0 + ) + + fm = QtGui.QFontMetrics(option.font) + text = index.data(VERSION_STATUS_NAME_ROLE) + if padded_text_rect.width() < fm.width(text): + text = index.data(VERSION_STATUS_SHORT_ROLE) + + status_color = index.data(VERSION_STATUS_COLOR_ROLE) + fg_color = QtGui.QColor(status_color) + pen = painter.pen() + pen.setColor(fg_color) + painter.setPen(pen) + + painter.drawText( + padded_text_rect, + option.displayAlignment, + text + ) + + painter.restore() + + class SiteSyncDelegate(QtWidgets.QStyledItemDelegate): """Paints icons and downloaded representation ration for both sites.""" diff --git a/client/ayon_core/tools/loader/ui/products_model.py b/client/ayon_core/tools/loader/ui/products_model.py index b465679c3b..f309473d10 100644 --- a/client/ayon_core/tools/loader/ui/products_model.py +++ b/client/ayon_core/tools/loader/ui/products_model.py @@ -22,18 +22,21 @@ VERSION_HERO_ROLE = QtCore.Qt.UserRole + 11 VERSION_NAME_ROLE = QtCore.Qt.UserRole + 12 VERSION_NAME_EDIT_ROLE = QtCore.Qt.UserRole + 13 VERSION_PUBLISH_TIME_ROLE = QtCore.Qt.UserRole + 14 -VERSION_AUTHOR_ROLE = QtCore.Qt.UserRole + 15 -VERSION_FRAME_RANGE_ROLE = QtCore.Qt.UserRole + 16 -VERSION_DURATION_ROLE = QtCore.Qt.UserRole + 17 -VERSION_HANDLES_ROLE = QtCore.Qt.UserRole + 18 -VERSION_STEP_ROLE = QtCore.Qt.UserRole + 19 -VERSION_AVAILABLE_ROLE = QtCore.Qt.UserRole + 20 -VERSION_THUMBNAIL_ID_ROLE = QtCore.Qt.UserRole + 21 -ACTIVE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 22 -REMOTE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 23 -REPRESENTATIONS_COUNT_ROLE = QtCore.Qt.UserRole + 24 -SYNC_ACTIVE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 25 -SYNC_REMOTE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 26 +VERSION_STATUS_NAME_ROLE = QtCore.Qt.UserRole + 15 +VERSION_STATUS_SHORT_ROLE = QtCore.Qt.UserRole + 16 +VERSION_STATUS_COLOR_ROLE = QtCore.Qt.UserRole + 17 +VERSION_AUTHOR_ROLE = QtCore.Qt.UserRole + 18 +VERSION_FRAME_RANGE_ROLE = QtCore.Qt.UserRole + 19 +VERSION_DURATION_ROLE = QtCore.Qt.UserRole + 20 +VERSION_HANDLES_ROLE = QtCore.Qt.UserRole + 21 +VERSION_STEP_ROLE = QtCore.Qt.UserRole + 22 +VERSION_AVAILABLE_ROLE = QtCore.Qt.UserRole + 23 +VERSION_THUMBNAIL_ID_ROLE = QtCore.Qt.UserRole + 24 +ACTIVE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 25 +REMOTE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 26 +REPRESENTATIONS_COUNT_ROLE = QtCore.Qt.UserRole + 27 +SYNC_ACTIVE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 28 +SYNC_REMOTE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 29 class ProductsModel(QtGui.QStandardItemModel): @@ -44,6 +47,7 @@ class ProductsModel(QtGui.QStandardItemModel): "Product type", "Folder", "Version", + "Status", "Time", "Author", "Frames", @@ -69,11 +73,35 @@ class ProductsModel(QtGui.QStandardItemModel): ] ] - version_col = column_labels.index("Version") - published_time_col = column_labels.index("Time") + product_name_col = column_labels.index("Product name") + product_type_col = column_labels.index("Product type") folders_label_col = column_labels.index("Folder") + version_col = column_labels.index("Version") + status_col = column_labels.index("Status") + published_time_col = column_labels.index("Time") + author_col = column_labels.index("Author") + frame_range_col = column_labels.index("Frames") + duration_col = column_labels.index("Duration") + handles_col = column_labels.index("Handles") + step_col = column_labels.index("Step") in_scene_col = column_labels.index("In scene") sitesync_avail_col = column_labels.index("Availability") + _display_role_mapping = { + product_name_col: QtCore.Qt.DisplayRole, + product_type_col: PRODUCT_TYPE_ROLE, + folders_label_col: FOLDER_LABEL_ROLE, + version_col: VERSION_NAME_ROLE, + status_col: VERSION_STATUS_NAME_ROLE, + published_time_col: VERSION_PUBLISH_TIME_ROLE, + author_col: VERSION_AUTHOR_ROLE, + frame_range_col: VERSION_FRAME_RANGE_ROLE, + duration_col: VERSION_DURATION_ROLE, + handles_col: VERSION_HANDLES_ROLE, + step_col: VERSION_STEP_ROLE, + in_scene_col: PRODUCT_IN_SCENE_ROLE, + sitesync_avail_col: VERSION_AVAILABLE_ROLE, + + } def __init__(self, controller): super(ProductsModel, self).__init__() @@ -96,6 +124,7 @@ class ProductsModel(QtGui.QStandardItemModel): self._last_project_name = None self._last_folder_ids = [] + self._last_project_statuses = {} def get_product_item_indexes(self): return [ @@ -141,6 +170,15 @@ class ProductsModel(QtGui.QStandardItemModel): if not index.isValid(): return None + if role in (VERSION_STATUS_SHORT_ROLE, VERSION_STATUS_COLOR_ROLE): + status_name = self.data(index, VERSION_STATUS_NAME_ROLE) + status_item = self._last_project_statuses.get(status_name) + if status_item is None: + return "" + if role == VERSION_STATUS_SHORT_ROLE: + return status_item.short + return status_item.color + col = index.column() if col == 0: return super(ProductsModel, self).data(index, role) @@ -168,29 +206,8 @@ class ProductsModel(QtGui.QStandardItemModel): if role == QtCore.Qt.DisplayRole: if not index.data(PRODUCT_ID_ROLE): return None - if col == self.version_col: - role = VERSION_NAME_ROLE - elif col == 1: - role = PRODUCT_TYPE_ROLE - elif col == 2: - role = FOLDER_LABEL_ROLE - elif col == 4: - role = VERSION_PUBLISH_TIME_ROLE - elif col == 5: - role = VERSION_AUTHOR_ROLE - elif col == 6: - role = VERSION_FRAME_RANGE_ROLE - elif col == 7: - role = VERSION_DURATION_ROLE - elif col == 8: - role = VERSION_HANDLES_ROLE - elif col == 9: - role = VERSION_STEP_ROLE - elif col == 10: - role = PRODUCT_IN_SCENE_ROLE - elif col == 11: - role = VERSION_AVAILABLE_ROLE - else: + role = self._display_role_mapping.get(col) + if role is None: return None index = self.index(index.row(), 0, index.parent()) @@ -312,6 +329,7 @@ class ProductsModel(QtGui.QStandardItemModel): version_item.published_time, VERSION_PUBLISH_TIME_ROLE ) model_item.setData(version_item.author, VERSION_AUTHOR_ROLE) + model_item.setData(version_item.status, VERSION_STATUS_NAME_ROLE) model_item.setData(version_item.frame_range, VERSION_FRAME_RANGE_ROLE) model_item.setData(version_item.duration, VERSION_DURATION_ROLE) model_item.setData(version_item.handles, VERSION_HANDLES_ROLE) @@ -393,6 +411,11 @@ class ProductsModel(QtGui.QStandardItemModel): self._last_project_name = project_name self._last_folder_ids = folder_ids + status_items = self._controller.get_project_status_items(project_name) + self._last_project_statuses = { + status_item.name: status_item + for status_item in status_items + } active_site_icon_def = self._controller.get_active_site_icon_def( project_name diff --git a/client/ayon_core/tools/loader/ui/products_widget.py b/client/ayon_core/tools/loader/ui/products_widget.py index d9f027153e..3a30d83d52 100644 --- a/client/ayon_core/tools/loader/ui/products_widget.py +++ b/client/ayon_core/tools/loader/ui/products_widget.py @@ -22,7 +22,8 @@ from .products_model import ( from .products_delegates import ( VersionDelegate, LoadedInSceneDelegate, - SiteSyncDelegate + StatusDelegate, + SiteSyncDelegate, ) from .actions_utils import show_actions_menu @@ -89,6 +90,7 @@ class ProductsWidget(QtWidgets.QWidget): 90, # Product type 130, # Folder label 60, # Version + 100, # Status 125, # Time 75, # Author 75, # Frames @@ -128,20 +130,19 @@ class ProductsWidget(QtWidgets.QWidget): products_view.setColumnWidth(idx, width) version_delegate = VersionDelegate() - products_view.setItemDelegateForColumn( - products_model.version_col, version_delegate) - time_delegate = PrettyTimeDelegate() - products_view.setItemDelegateForColumn( - products_model.published_time_col, time_delegate) - + status_delegate = StatusDelegate() in_scene_delegate = LoadedInSceneDelegate() - products_view.setItemDelegateForColumn( - products_model.in_scene_col, in_scene_delegate) - sitesync_delegate = SiteSyncDelegate() - products_view.setItemDelegateForColumn( - products_model.sitesync_avail_col, sitesync_delegate) + + for col, delegate in ( + (products_model.version_col, version_delegate), + (products_model.published_time_col, time_delegate), + (products_model.status_col, status_delegate), + (products_model.in_scene_col, in_scene_delegate), + (products_model.sitesync_avail_col, sitesync_delegate), + ): + products_view.setItemDelegateForColumn(col, delegate) main_layout = QtWidgets.QHBoxLayout(self) main_layout.setContentsMargins(0, 0, 0, 0) @@ -175,6 +176,7 @@ class ProductsWidget(QtWidgets.QWidget): self._version_delegate = version_delegate self._time_delegate = time_delegate + self._status_delegate = status_delegate self._in_scene_delegate = in_scene_delegate self._sitesync_delegate = sitesync_delegate diff --git a/server_addon/nuke/package.py b/server_addon/nuke/package.py index bc166bd14e..d8decef208 100644 --- a/server_addon/nuke/package.py +++ b/server_addon/nuke/package.py @@ -1,3 +1,3 @@ name = "nuke" title = "Nuke" -version = "0.1.13" +version = "0.1.14" diff --git a/server_addon/nuke/server/settings/create_plugins.py b/server_addon/nuke/server/settings/create_plugins.py index 6bdc5ee5ad..e4a0f9c938 100644 --- a/server_addon/nuke/server/settings/create_plugins.py +++ b/server_addon/nuke/server/settings/create_plugins.py @@ -12,7 +12,11 @@ def instance_attributes_enum(): return [ {"value": "reviewable", "label": "Reviewable"}, {"value": "farm_rendering", "label": "Farm rendering"}, - {"value": "use_range_limit", "label": "Use range limit"} + {"value": "use_range_limit", "label": "Use range limit"}, + { + "value": "render_on_farm", + "label": "Render On Farm" + } ]