Merge branch 'develop' into bugfix/YN-0273_big_resolution_thumbnail_ftrack

This commit is contained in:
Petr Kalis 2025-12-11 18:32:39 +01:00 committed by GitHub
commit ec5766f656
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 611 additions and 127 deletions

View file

@ -1234,17 +1234,11 @@ def oiio_color_convert(
if source_view and source_display:
color_convert_args = None
ocio_display_args = None
oiio_cmd.extend([
"--ociodisplay:inverse=1:subimages=0",
source_display,
source_view,
])
if target_colorspace:
# This is a two-step conversion process since there's no direct
# display/view to colorspace command
# This could be a config parameter or determined from OCIO config
# Use temporarty role space 'scene_linear'
# Use temporary role space 'scene_linear'
color_convert_args = ("scene_linear", target_colorspace)
elif source_display != target_display or source_view != target_view:
# Complete display/view pair conversion
@ -1256,6 +1250,15 @@ def oiio_color_convert(
" No color conversion needed."
)
if color_convert_args or ocio_display_args:
# Invert source display/view so that we can go from there to the
# target colorspace or display/view
oiio_cmd.extend([
"--ociodisplay:inverse=1:subimages=0",
source_display,
source_view,
])
if color_convert_args:
# Use colorconvert for colorspace target
oiio_cmd.extend([

View file

@ -544,11 +544,6 @@ class BaseCreator(ABC):
if host_name is None:
host_name = self.create_context.host_name
task_name = task_type = None
if task_entity:
task_name = task_entity["name"]
task_type = task_entity["taskType"]
dynamic_data = self.get_dynamic_data(
project_name,
folder_entity,
@ -564,16 +559,15 @@ class BaseCreator(ABC):
return get_product_name(
project_name,
task_name,
task_type,
host_name,
self.product_type,
variant,
folder_entity=folder_entity,
task_entity=task_entity,
product_base_type=self.product_base_type,
product_type=self.product_type,
host_name=host_name,
variant=variant,
dynamic_data=dynamic_data,
project_settings=self.project_settings,
project_entity=project_entity,
# until we make product_base_type mandatory
product_base_type=self.product_base_type
)
def get_instance_attr_defs(self):

View file

@ -1,7 +1,9 @@
"""Functions for handling product names."""
from __future__ import annotations
from typing import Any, Optional, Union
import warnings
from functools import wraps
from typing import Any, Optional, Union, overload
from warnings import warn
import ayon_api
@ -9,12 +11,17 @@ from ayon_core.lib import (
StringTemplate,
filter_profiles,
prepare_template_data,
Logger,
is_func_signature_supported,
)
from ayon_core.lib.path_templates import TemplateResult
from ayon_core.settings import get_project_settings
from .constants import DEFAULT_PRODUCT_TEMPLATE
from .exceptions import TaskNotSetError, TemplateFillError
log = Logger.get_logger(__name__)
def get_product_name_template(
project_name: str,
@ -82,7 +89,7 @@ def get_product_name_template(
return template
def get_product_name(
def _get_product_name_old(
project_name: str,
task_name: Optional[str],
task_type: Optional[str],
@ -94,61 +101,16 @@ def get_product_name(
project_settings: Optional[dict[str, Any]] = None,
product_type_filter: Optional[str] = None,
project_entity: Optional[dict[str, Any]] = None,
product_base_type: Optional[str] = None
):
"""Calculate product name based on passed context and AYON settings.
Subst name templates are defined in `project_settings/global/tools/creator
/product_name_profiles` where are profiles with host name, product type,
task name and task type filters. If context does not match any profile
then `DEFAULT_PRODUCT_TEMPLATE` is used as default template.
That's main reason why so many arguments are required to calculate product
name.
Deprecation:
The `product_base_type` argument is optional now, but it will be
mandatory in future versions. It is recommended to pass it now to
avoid issues in the future. If it is not passed, a warning will be
raised to inform about this change.
Todos:
Find better filtering options to avoid requirement of
argument 'family_filter'.
Args:
project_name (str): Project name.
task_name (Optional[str]): Task name.
task_type (Optional[str]): Task type.
host_name (str): Host name.
product_type (str): Product type.
variant (str): In most of the cases it is user input during creation.
default_template (Optional[str]): Default template if any profile does
not match passed context. Constant 'DEFAULT_PRODUCT_TEMPLATE'
is used if is not passed.
dynamic_data (Optional[Dict[str, Any]]): Dynamic data specific for
a creator which creates instance.
project_settings (Optional[Union[Dict[str, Any]]]): Prepared settings
for project. Settings are queried if not passed.
product_type_filter (Optional[str]): Use different product type for
product template filtering. Value of `product_type` is used when
not passed.
project_entity (Optional[Dict[str, Any]]): Project entity used when
task short name is required by template.
product_base_type (Optional[str]): Base type of product.
This will be mandatory in future versions.
Returns:
str: Product name.
Raises:
TaskNotSetError: If template requires task which is not provided.
TemplateFillError: If filled template contains placeholder key which
is not collected.
"""
product_base_type: Optional[str] = None,
) -> TemplateResult:
warnings.warn(
"Used deprecated 'task_name' and 'task_type' arguments."
" Please use new signature with 'folder_entity' and 'task_entity'.",
DeprecationWarning,
stacklevel=2
)
if not product_type:
return ""
return StringTemplate("").format({})
template = get_product_name_template(
project_name=project_name,
@ -160,19 +122,26 @@ def get_product_name(
project_settings=project_settings,
product_base_type=product_base_type,
)
# Simple check of task name existence for template with {task} in
# - missing task should be possible only in Standalone publisher
if not task_name and "{task" in template.lower():
template_low = template.lower()
# Simple check of task name existence for template with {task[name]} in
if not task_name and "{task" in template_low:
raise TaskNotSetError()
task_value = {
"name": task_name,
"type": task_type,
}
if "{task}" in template.lower():
if "{task}" in template_low:
task_value = task_name
# NOTE this is message for TDs and Admins -> not really for users
# TODO validate this in settings and not allow it
log.warning(
"Found deprecated task key '{task}' in product name template."
" Please use '{task[name]}' instead."
)
elif "{task[short]}" in template.lower():
elif "{task[short]}" in template_low:
if project_entity is None:
project_entity = ayon_api.get_project(project_name)
task_types_by_name = {
@ -217,3 +186,305 @@ def get_product_name(
f" Available values are {fill_pairs}"
)
raise TemplateFillError(msg) from exp
def _backwards_compatibility_product_name(func):
"""Helper to decide which variant of 'get_product_name' to use.
The old version expected 'task_name' and 'task_type' arguments. The new
version expects 'folder_entity' and 'task_entity' arguments instead.
The function is also marked with an attribute 'version' so other addons
can check if the function is using the new signature or is using
the old signature. That should allow addons to adapt to new signature.
>>> if getattr(get_product_name, "use_entities", None):
>>> # New signature is used
>>> path = get_product_name(project_name, folder_entity, ...)
>>> else:
>>> # Old signature is used
>>> path = get_product_name(project_name, taks_name, ...)
"""
# Add attribute to function to identify it as the new function
# so other addons can easily identify it.
# >>> geattr(get_product_name, "use_entities", False)
setattr(func, "use_entities", True)
@wraps(func)
def inner(*args, **kwargs):
# ---
# Decide which variant of the function is used based on
# passed arguments.
# ---
# Entities in key-word arguments mean that the new function is used
if "folder_entity" in kwargs or "task_entity" in kwargs:
return func(*args, **kwargs)
# Using more than 7 positional arguments is not allowed
# in the new function
if len(args) > 7:
return _get_product_name_old(*args, **kwargs)
if len(args) > 1:
arg_2 = args[1]
# The second argument is a string -> task name
if isinstance(arg_2, str):
return _get_product_name_old(*args, **kwargs)
if is_func_signature_supported(func, *args, **kwargs):
return func(*args, **kwargs)
return _get_product_name_old(*args, **kwargs)
return inner
@overload
def get_product_name(
project_name: str,
folder_entity: dict[str, Any],
task_entity: Optional[dict[str, Any]],
product_base_type: str,
product_type: str,
host_name: str,
variant: str,
*,
dynamic_data: Optional[dict[str, Any]] = None,
project_settings: Optional[dict[str, Any]] = None,
project_entity: Optional[dict[str, Any]] = None,
default_template: Optional[str] = None,
product_base_type_filter: Optional[str] = None,
) -> TemplateResult:
"""Calculate product name based on passed context and AYON settings.
Subst name templates are defined in `project_settings/global/tools/creator
/product_name_profiles` where are profiles with host name, product type,
task name and task type filters. If context does not match any profile
then `DEFAULT_PRODUCT_TEMPLATE` is used as default template.
That's main reason why so many arguments are required to calculate product
name.
Args:
project_name (str): Project name.
folder_entity (Optional[dict[str, Any]]): Folder entity.
task_entity (Optional[dict[str, Any]]): Task entity.
host_name (str): Host name.
product_base_type (str): Product base type.
product_type (str): Product type.
variant (str): In most of the cases it is user input during creation.
dynamic_data (Optional[dict[str, Any]]): Dynamic data specific for
a creator which creates instance.
project_settings (Optional[dict[str, Any]]): Prepared settings
for project. Settings are queried if not passed.
project_entity (Optional[dict[str, Any]]): Project entity used when
task short name is required by template.
default_template (Optional[str]): Default template if any profile does
not match passed context. Constant 'DEFAULT_PRODUCT_TEMPLATE'
is used if is not passed.
product_base_type_filter (Optional[str]): Use different product base
type for product template filtering. Value of
`product_base_type_filter` is used when not passed.
Returns:
TemplateResult: Product name.
Raises:
TaskNotSetError: If template requires task which is not provided.
TemplateFillError: If filled template contains placeholder key which
is not collected.
"""
@overload
def get_product_name(
project_name,
task_name,
task_type,
host_name,
product_type,
variant,
default_template=None,
dynamic_data=None,
project_settings=None,
product_type_filter=None,
project_entity=None,
) -> TemplateResult:
"""Calculate product name based on passed context and AYON settings.
Product name templates are defined in `project_settings/global/tools
/creator/product_name_profiles` where are profiles with host name,
product type, task name and task type filters. If context does not match
any profile then `DEFAULT_PRODUCT_TEMPLATE` is used as default template.
That's main reason why so many arguments are required to calculate product
name.
Deprecated:
This function is using deprecated signature that does not support
folder entity data to be used.
Args:
project_name (str): Project name.
task_name (Optional[str]): Task name.
task_type (Optional[str]): Task type.
host_name (str): Host name.
product_type (str): Product type.
variant (str): In most of the cases it is user input during creation.
default_template (Optional[str]): Default template if any profile does
not match passed context. Constant 'DEFAULT_PRODUCT_TEMPLATE'
is used if is not passed.
dynamic_data (Optional[Dict[str, Any]]): Dynamic data specific for
a creator which creates instance.
project_settings (Optional[Union[Dict[str, Any]]]): Prepared settings
for project. Settings are queried if not passed.
product_type_filter (Optional[str]): Use different product type for
product template filtering. Value of `product_type` is used when
not passed.
project_entity (Optional[Dict[str, Any]]): Project entity used when
task short name is required by template.
Returns:
TemplateResult: Product name.
"""
pass
@_backwards_compatibility_product_name
def get_product_name(
project_name: str,
folder_entity: dict[str, Any],
task_entity: Optional[dict[str, Any]],
product_base_type: str,
product_type: str,
host_name: str,
variant: str,
*,
dynamic_data: Optional[dict[str, Any]] = None,
project_settings: Optional[dict[str, Any]] = None,
project_entity: Optional[dict[str, Any]] = None,
default_template: Optional[str] = None,
product_base_type_filter: Optional[str] = None,
) -> TemplateResult:
"""Calculate product name based on passed context and AYON settings.
Product name templates are defined in `project_settings/global/tools
/creator/product_name_profiles` where are profiles with host name,
product base type, product type, task name and task type filters.
If context does not match any profile then `DEFAULT_PRODUCT_TEMPLATE`
is used as default template.
That's main reason why so many arguments are required to calculate product
name.
Args:
project_name (str): Project name.
folder_entity (Optional[dict[str, Any]]): Folder entity.
task_entity (Optional[dict[str, Any]]): Task entity.
host_name (str): Host name.
product_base_type (str): Product base type.
product_type (str): Product type.
variant (str): In most of the cases it is user input during creation.
dynamic_data (Optional[dict[str, Any]]): Dynamic data specific for
a creator which creates instance.
project_settings (Optional[dict[str, Any]]): Prepared settings
for project. Settings are queried if not passed.
project_entity (Optional[dict[str, Any]]): Project entity used when
task short name is required by template.
default_template (Optional[str]): Default template if any profile does
not match passed context. Constant 'DEFAULT_PRODUCT_TEMPLATE'
is used if is not passed.
product_base_type_filter (Optional[str]): Use different product base
type for product template filtering. Value of
`product_base_type_filter` is used when not passed.
Returns:
TemplateResult: Product name.
Raises:
TaskNotSetError: If template requires task which is not provided.
TemplateFillError: If filled template contains placeholder key which
is not collected.
"""
if not product_type:
return StringTemplate("").format({})
task_name = task_type = None
if task_entity:
task_name = task_entity["name"]
task_type = task_entity["taskType"]
template = get_product_name_template(
project_name=project_name,
product_base_type=product_base_type_filter or product_base_type,
product_type=product_type,
task_name=task_name,
task_type=task_type,
host_name=host_name,
default_template=default_template,
project_settings=project_settings,
)
template_low = template.lower()
# Simple check of task name existence for template with {task[name]} in
if not task_name and "{task" in template_low:
raise TaskNotSetError()
task_value = {
"name": task_name,
"type": task_type,
}
if "{task}" in template_low:
task_value = task_name
# NOTE this is message for TDs and Admins -> not really for users
# TODO validate this in settings and not allow it
log.warning(
"Found deprecated task key '{task}' in product name template."
" Please use '{task[name]}' instead."
)
elif "{task[short]}" in template_low:
if project_entity is None:
project_entity = ayon_api.get_project(project_name)
task_types_by_name = {
task["name"]: task
for task in project_entity["taskTypes"]
}
task_short = task_types_by_name.get(task_type, {}).get("shortName")
task_value["short"] = task_short
fill_pairs = {
"variant": variant,
# TODO We should stop support 'family' key.
"family": product_type,
"task": task_value,
"product": {
"type": product_type,
"basetype": product_base_type,
}
}
if folder_entity:
fill_pairs["folder"] = {
"name": folder_entity["name"],
"type": folder_entity["folderType"],
}
if dynamic_data:
# Dynamic data may override default values
for key, value in dynamic_data.items():
fill_pairs[key] = value
try:
return StringTemplate.format_strict_template(
template=template,
data=prepare_template_data(fill_pairs)
)
except KeyError as exp:
msg = (
f"Value for {exp} key is missing in template '{template}'."
f" Available values are {fill_pairs}"
)
raise TemplateFillError(msg)

View file

@ -192,7 +192,9 @@ class HelpContent:
self.detail = detail
def load_help_content_from_filepath(filepath):
def load_help_content_from_filepath(
filepath: str
) -> dict[str, dict[str, HelpContent]]:
"""Load help content from xml file.
Xml file may contain errors and warnings.
"""
@ -227,15 +229,20 @@ def load_help_content_from_filepath(filepath):
return output
def load_help_content_from_plugin(plugin):
def load_help_content_from_plugin(
plugin: pyblish.api.Plugin,
help_filename: Optional[str] = None,
) -> dict[str, dict[str, HelpContent]]:
cls = plugin
if not inspect.isclass(plugin):
cls = plugin.__class__
plugin_filepath = inspect.getfile(cls)
plugin_dir = os.path.dirname(plugin_filepath)
basename = os.path.splitext(os.path.basename(plugin_filepath))[0]
filename = basename + ".xml"
filepath = os.path.join(plugin_dir, "help", filename)
if help_filename is None:
basename = os.path.splitext(os.path.basename(plugin_filepath))[0]
help_filename = basename + ".xml"
filepath = os.path.join(plugin_dir, "help", help_filename)
return load_help_content_from_filepath(filepath)

View file

@ -1,7 +1,7 @@
import inspect
from abc import ABCMeta
import typing
from typing import Optional
from typing import Optional, Any
import pyblish.api
import pyblish.logic
@ -82,22 +82,51 @@ class PublishValidationError(PublishError):
class PublishXmlValidationError(PublishValidationError):
"""Raise an error from a dedicated xml file.
Can be useful to have one xml file with different possible messages that
helps to avoid flood code with dedicated artist messages.
XML files should live relative to the plugin file location:
'{plugin dir}/help/some_plugin.xml'.
Args:
plugin (pyblish.api.Plugin): Plugin that raised an error. Is used
to get path to xml file.
message (str): Exception message, can be technical, is used for
console output.
key (Optional[str]): XML file can contain multiple error messages, key
is used to get one of them. By default is used 'main'.
formatting_data (Optional[dict[str, Any]): Error message can have
variables to fill.
help_filename (Optional[str]): Name of xml file with messages. By
default, is used filename where plugin lives with .xml extension.
"""
def __init__(
self, plugin, message, key=None, formatting_data=None
):
self,
plugin: pyblish.api.Plugin,
message: str,
key: Optional[str] = None,
formatting_data: Optional[dict[str, Any]] = None,
help_filename: Optional[str] = None,
) -> None:
if key is None:
key = "main"
if not formatting_data:
formatting_data = {}
result = load_help_content_from_plugin(plugin)
result = load_help_content_from_plugin(plugin, help_filename)
content_obj = result["errors"][key]
description = content_obj.description.format(**formatting_data)
detail = content_obj.detail
if detail:
detail = detail.format(**formatting_data)
super(PublishXmlValidationError, self).__init__(
message, content_obj.title, description, detail
super().__init__(
message,
content_obj.title,
description,
detail
)

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<root>
<error id="main">
<title>{upload_type} upload timed out</title>
<description>
## {upload_type} upload failed after retries
The connection to the AYON server timed out while uploading a file.
### How to resolve?
1. Try publishing again. Intermittent network hiccups often resolve on retry.
2. Ensure your network/VPN is stable and large uploads are allowed.
3. If it keeps failing, try again later or contact your admin.
<pre>File: {file}
Error: {error}</pre>
</description>
</error>
</root>

View file

@ -1,11 +1,17 @@
import os
import time
import pyblish.api
import ayon_api
from ayon_api import TransferProgress
from ayon_api.server_api import RequestTypes
import pyblish.api
from ayon_core.lib import get_media_mime_type
from ayon_core.pipeline.publish import get_publish_repre_path
from ayon_core.lib import get_media_mime_type, format_file_size
from ayon_core.pipeline.publish import (
PublishXmlValidationError,
get_publish_repre_path,
)
import requests.exceptions
class IntegrateAYONReview(pyblish.api.InstancePlugin):
@ -44,7 +50,7 @@ class IntegrateAYONReview(pyblish.api.InstancePlugin):
if "webreview" not in repre_tags:
continue
# exclude representations with are going to be published on farm
# exclude representations going to be published on farm
if "publish_on_farm" in repre_tags:
continue
@ -75,18 +81,13 @@ class IntegrateAYONReview(pyblish.api.InstancePlugin):
f"/projects/{project_name}"
f"/versions/{version_id}/reviewables{query}"
)
filename = os.path.basename(repre_path)
# Upload the reviewable
self.log.info(f"Uploading reviewable '{label or filename}' ...")
headers = ayon_con.get_headers(content_type)
headers["x-file-name"] = filename
self.log.info(f"Uploading reviewable {repre_path}")
ayon_con.upload_file(
# Upload with retries and clear help if it keeps failing
self._upload_with_retries(
ayon_con,
endpoint,
repre_path,
headers=headers,
request_type=RequestTypes.post,
content_type,
)
def _get_review_label(self, repre, uploaded_labels):
@ -100,3 +101,74 @@ class IntegrateAYONReview(pyblish.api.InstancePlugin):
idx += 1
label = f"{orig_label}_{idx}"
return label
def _upload_with_retries(
self,
ayon_con: ayon_api.ServerAPI,
endpoint: str,
repre_path: str,
content_type: str,
):
"""Upload file with simple retries."""
filename = os.path.basename(repre_path)
headers = ayon_con.get_headers(content_type)
headers["x-file-name"] = filename
max_retries = ayon_con.get_default_max_retries()
# Retries are already implemented in 'ayon_api.upload_file'
# - added in ayon api 1.2.7
if hasattr(TransferProgress, "get_attempt"):
max_retries = 1
size = os.path.getsize(repre_path)
self.log.info(
f"Uploading '{repre_path}' (size: {format_file_size(size)})"
)
# How long to sleep before next attempt
wait_time = 1
last_error = None
for attempt in range(max_retries):
attempt += 1
start = time.time()
try:
output = ayon_con.upload_file(
endpoint,
repre_path,
headers=headers,
request_type=RequestTypes.post,
)
self.log.debug(f"Uploaded in {time.time() - start}s.")
return output
except (
requests.exceptions.Timeout,
requests.exceptions.ConnectionError
) as exc:
# Log and retry with backoff if attempts remain
if attempt >= max_retries:
last_error = exc
break
self.log.warning(
f"Review upload failed ({attempt}/{max_retries})"
f" after {time.time() - start}s."
f" Retrying in {wait_time}s...",
exc_info=True,
)
time.sleep(wait_time)
# Exhausted retries - raise a user-friendly validation error with help
raise PublishXmlValidationError(
self,
(
"Upload of reviewable timed out or failed after multiple"
" attempts. Please try publishing again."
),
formatting_data={
"upload_type": "Review",
"file": repre_path,
"error": str(last_error),
},
help_filename="upload_file.xml",
)

View file

@ -24,11 +24,16 @@
import os
import collections
import time
import pyblish.api
import ayon_api
from ayon_api import RequestTypes
from ayon_api import RequestTypes, TransferProgress
from ayon_api.operations import OperationsSession
import pyblish.api
import requests
from ayon_core.lib import get_media_mime_type, format_file_size
from ayon_core.pipeline.publish import PublishXmlValidationError
InstanceFilterResult = collections.namedtuple(
@ -164,25 +169,17 @@ class IntegrateThumbnailsAYON(pyblish.api.ContextPlugin):
return os.path.normpath(filled_path)
def _create_thumbnail(self, project_name: str, src_filepath: str) -> str:
"""Upload thumbnail to AYON and return its id.
This is temporary fix of 'create_thumbnail' function in ayon_api to
fix jpeg mime type.
"""
mime_type = None
with open(src_filepath, "rb") as stream:
if b"\xff\xd8\xff" == stream.read(3):
mime_type = "image/jpeg"
"""Upload thumbnail to AYON and return its id."""
mime_type = get_media_mime_type(src_filepath)
if mime_type is None:
return ayon_api.create_thumbnail(project_name, src_filepath)
return ayon_api.create_thumbnail(
project_name, src_filepath
)
response = ayon_api.upload_file(
response = self._upload_with_retries(
f"projects/{project_name}/thumbnails",
src_filepath,
request_type=RequestTypes.post,
headers={"Content-Type": mime_type},
mime_type,
)
response.raise_for_status()
return response.json()["id"]
@ -248,3 +245,71 @@ class IntegrateThumbnailsAYON(pyblish.api.ContextPlugin):
or instance.data.get("name")
or "N/A"
)
def _upload_with_retries(
self,
endpoint: str,
repre_path: str,
content_type: str,
):
"""Upload file with simple retries."""
ayon_con = ayon_api.get_server_api_connection()
headers = ayon_con.get_headers(content_type)
max_retries = ayon_con.get_default_max_retries()
# Retries are already implemented in 'ayon_api.upload_file'
# - added in ayon api 1.2.7
if hasattr(TransferProgress, "get_attempt"):
max_retries = 1
size = os.path.getsize(repre_path)
self.log.info(
f"Uploading '{repre_path}' (size: {format_file_size(size)})"
)
# How long to sleep before next attempt
wait_time = 1
last_error = None
for attempt in range(max_retries):
attempt += 1
start = time.time()
try:
output = ayon_con.upload_file(
endpoint,
repre_path,
headers=headers,
request_type=RequestTypes.post,
)
self.log.debug(f"Uploaded in {time.time() - start}s.")
return output
except (
requests.exceptions.Timeout,
requests.exceptions.ConnectionError
) as exc:
# Log and retry with backoff if attempts remain
if attempt >= max_retries:
last_error = exc
break
self.log.warning(
f"Review upload failed ({attempt}/{max_retries})"
f" after {time.time() - start}s."
f" Retrying in {wait_time}s...",
exc_info=True,
)
time.sleep(wait_time)
# Exhausted retries - raise a user-friendly validation error with help
raise PublishXmlValidationError(
self,
(
"Upload of thumbnail timed out or failed after multiple"
" attempts. Please try publishing again."
),
formatting_data={
"upload_type": "Thumbnail",
"file": repre_path,
"error": str(last_error),
},
help_filename="upload_file.xml",
)

View file

@ -112,6 +112,7 @@ class HierarchyPage(QtWidgets.QWidget):
self._is_visible = False
self._controller = controller
self._filters_widget = filters_widget
self._btn_back = btn_back
self._projects_combobox = projects_combobox
self._folders_widget = folders_widget
@ -136,6 +137,10 @@ class HierarchyPage(QtWidgets.QWidget):
self._folders_widget.refresh()
self._tasks_widget.refresh()
self._workfiles_page.refresh()
# Update my tasks
self._on_my_tasks_checkbox_state_changed(
self._filters_widget.is_my_tasks_checked()
)
def _on_back_clicked(self):
self._controller.set_selected_project(None)
@ -155,6 +160,7 @@ class HierarchyPage(QtWidgets.QWidget):
)
folder_ids = entity_ids["folder_ids"]
task_ids = entity_ids["task_ids"]
self._folders_widget.set_folder_ids_filter(folder_ids)
self._tasks_widget.set_task_ids_filter(task_ids)

View file

@ -527,6 +527,10 @@ class LoaderWindow(QtWidgets.QWidget):
if not self._refresh_handler.project_refreshed:
self._projects_combobox.refresh()
self._update_filters()
# Update my tasks
self._on_my_tasks_checkbox_state_changed(
self._filters_widget.is_my_tasks_checked()
)
def _on_load_finished(self, event):
error_info = event["error_info"]

View file

@ -221,6 +221,7 @@ class CreateContextWidget(QtWidgets.QWidget):
filters_widget.text_changed.connect(self._on_folder_filter_change)
filters_widget.my_tasks_changed.connect(self._on_my_tasks_change)
self._filters_widget = filters_widget
self._current_context_btn = current_context_btn
self._folders_widget = folders_widget
self._tasks_widget = tasks_widget
@ -290,6 +291,10 @@ class CreateContextWidget(QtWidgets.QWidget):
self._hierarchy_controller.set_expected_selection(
self._last_project_name, folder_id, task_name
)
# Update my tasks
self._on_my_tasks_change(
self._filters_widget.is_my_tasks_checked()
)
def _clear_selection(self):
self._folders_widget.set_selected_folder(None)

View file

@ -310,9 +310,6 @@ class CreateWidget(QtWidgets.QWidget):
folder_path = None
if self._context_change_is_enabled():
folder_path = self._context_widget.get_selected_folder_path()
if folder_path is None:
folder_path = self.get_current_folder_path()
return folder_path or None
def _get_folder_id(self):
@ -328,9 +325,6 @@ class CreateWidget(QtWidgets.QWidget):
folder_path = self._context_widget.get_selected_folder_path()
if folder_path:
task_name = self._context_widget.get_selected_task_name()
if not task_name:
task_name = self.get_current_task_name()
return task_name
def _set_context_enabled(self, enabled):

View file

@ -113,6 +113,7 @@ class FoldersDialog(QtWidgets.QDialog):
self._soft_reset_enabled = False
self._folders_widget.set_project_name(self._project_name)
self._on_my_tasks_change(self._filters_widget.is_my_tasks_checked())
def get_selected_folder_path(self):
"""Get selected folder path."""

View file

@ -834,6 +834,12 @@ class FoldersFiltersWidget(QtWidgets.QWidget):
self._folders_filter_input = folders_filter_input
self._my_tasks_checkbox = my_tasks_checkbox
def is_my_tasks_checked(self) -> bool:
return self._my_tasks_checkbox.isChecked()
def text(self) -> str:
return self._folders_filter_input.text()
def set_text(self, text: str) -> None:
self._folders_filter_input.setText(text)

View file

@ -205,6 +205,8 @@ class WorkfilesToolWindow(QtWidgets.QWidget):
self._folders_widget = folder_widget
self._filters_widget = filters_widget
return col_widget
def _create_col_3_widget(self, controller, parent):
@ -343,6 +345,10 @@ class WorkfilesToolWindow(QtWidgets.QWidget):
self._project_name = self._controller.get_current_project_name()
self._folders_widget.set_project_name(self._project_name)
# Update my tasks
self._on_my_tasks_checkbox_state_changed(
self._filters_widget.is_my_tasks_checked()
)
def _on_save_as_finished(self, event):
if event["failed"]: