Merge branch 'develop' of github.com:pypeclub/pype into feature/1784_helper_classes_for_automatic_testing

This commit is contained in:
Petr Kalis 2021-09-01 11:25:39 +02:00
commit ee9aef01e1
24 changed files with 557 additions and 100 deletions

View file

@ -47,7 +47,7 @@ jobs:
enhancementLabel: '**🚀 Enhancements**'
bugsLabel: '**🐛 Bug fixes**'
deprecatedLabel: '**⚠️ Deprecations**'
addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]}}'
addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"### 🆕 New features","labels":["feature"]},}'
issues: false
issuesWoLabels: false
sinceTag: "3.0.0"

2
.gitmodules vendored
View file

@ -6,7 +6,7 @@
url = https://github.com/pypeclub/avalon-unreal-integration.git
[submodule "openpype/modules/default_modules/ftrack/python2_vendor/arrow"]
path = openpype/modules/default_modules/ftrack/python2_vendor/arrow
url = git@github.com:arrow-py/arrow.git
url = https://github.com/arrow-py/arrow.git
[submodule "openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api"]
path = openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api
url = https://bitbucket.org/ftrack/ftrack-python-api.git

View file

@ -1,15 +1,29 @@
# Changelog
## [3.4.0-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD)
## [3.4.0-nightly.4](https://github.com/pypeclub/OpenPype/tree/HEAD)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.3.1...HEAD)
**Merged pull requests:**
- Ftrack: Fix hosts attribute in collect ftrack username [\#1972](https://github.com/pypeclub/OpenPype/pull/1972)
- Removed deprecated submodules [\#1967](https://github.com/pypeclub/OpenPype/pull/1967)
- Launcher: Fix crashes on action click [\#1964](https://github.com/pypeclub/OpenPype/pull/1964)
- Settings: Minor fixes in UI and missing default values [\#1963](https://github.com/pypeclub/OpenPype/pull/1963)
- Blender: Toggle system console works on windows [\#1962](https://github.com/pypeclub/OpenPype/pull/1962)
- Resolve path when adding to zip [\#1960](https://github.com/pypeclub/OpenPype/pull/1960)
- Bump url-parse from 1.5.1 to 1.5.3 in /website [\#1958](https://github.com/pypeclub/OpenPype/pull/1958)
- Global: Avalon Host name collector [\#1949](https://github.com/pypeclub/OpenPype/pull/1949)
- Global: Define hosts in CollectSceneVersion [\#1948](https://github.com/pypeclub/OpenPype/pull/1948)
- Maya: Add Xgen family support [\#1947](https://github.com/pypeclub/OpenPype/pull/1947)
- Add face sets to exported alembics [\#1942](https://github.com/pypeclub/OpenPype/pull/1942)
- Bump path-parse from 1.0.6 to 1.0.7 in /website [\#1933](https://github.com/pypeclub/OpenPype/pull/1933)
- \#1894 - adds host to template\_name\_profiles for filtering [\#1915](https://github.com/pypeclub/OpenPype/pull/1915)
- Environments: Tool environments in alphabetical order [\#1910](https://github.com/pypeclub/OpenPype/pull/1910)
- Disregard publishing time. [\#1888](https://github.com/pypeclub/OpenPype/pull/1888)
- Feature/webpublisher backend [\#1876](https://github.com/pypeclub/OpenPype/pull/1876)
- Dynamic modules [\#1872](https://github.com/pypeclub/OpenPype/pull/1872)
- Houdini: add Camera, Point Cache, Composite, Redshift ROP and VDB Cache support [\#1821](https://github.com/pypeclub/OpenPype/pull/1821)
## [3.3.1](https://github.com/pypeclub/OpenPype/tree/3.3.1) (2021-08-20)
@ -72,21 +86,11 @@
- Maya: support for configurable `dirmap` 🗺️ [\#1859](https://github.com/pypeclub/OpenPype/pull/1859)
- Maya: don't add reference members as connections to the container set 📦 [\#1855](https://github.com/pypeclub/OpenPype/pull/1855)
- Settings list can use template or schema as object type [\#1815](https://github.com/pypeclub/OpenPype/pull/1815)
- Maya: expected files -\> render products ⚙️ overhaul [\#1812](https://github.com/pypeclub/OpenPype/pull/1812)
- Settings error dialog on show [\#1798](https://github.com/pypeclub/OpenPype/pull/1798)
## [3.2.0](https://github.com/pypeclub/OpenPype/tree/3.2.0) (2021-07-13)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.2.0-nightly.7...3.2.0)
**Merged pull requests:**
- Build: don't add Poetry to `PATH` [\#1808](https://github.com/pypeclub/OpenPype/pull/1808)
- Nuke: ftrack family plugin settings preset [\#1805](https://github.com/pypeclub/OpenPype/pull/1805)
- nuke: fixing wrong name of family folder when `used existing frames` [\#1803](https://github.com/pypeclub/OpenPype/pull/1803)
- Collect ftrack family bugs [\#1801](https://github.com/pypeclub/OpenPype/pull/1801)
- Standalone publisher last project [\#1799](https://github.com/pypeclub/OpenPype/pull/1799)
## [2.18.4](https://github.com/pypeclub/OpenPype/tree/2.18.4) (2021-06-24)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.3...2.18.4)

View file

@ -71,6 +71,8 @@ from .avalon_context import (
get_linked_assets,
get_latest_version,
get_workfile_template_key,
get_workfile_template_key_from_context,
get_workdir_data,
get_workdir,
get_workdir_with_workdir_data,
@ -189,6 +191,8 @@ __all__ = [
"get_linked_assets",
"get_latest_version",
"get_workfile_template_key",
"get_workfile_template_key_from_context",
"get_workdir_data",
"get_workdir",
"get_workdir_with_workdir_data",

View file

@ -28,7 +28,8 @@ from . import (
from .local_settings import get_openpype_username
from .avalon_context import (
get_workdir_data,
get_workdir_with_workdir_data
get_workdir_with_workdir_data,
get_workfile_template_key_from_context
)
from .python_module_tools import (
@ -1236,8 +1237,18 @@ def prepare_context_environments(data):
anatomy = data["anatomy"]
template_key = get_workfile_template_key_from_context(
asset_doc["name"],
task_name,
app.host_name,
project_name=project_name,
dbcon=data["dbcon"]
)
try:
workdir = get_workdir_with_workdir_data(workdir_data, anatomy)
workdir = get_workdir_with_workdir_data(
workdir_data, anatomy, template_key=template_key
)
except Exception as exc:
raise ApplicationLaunchFailed(

View file

@ -344,6 +344,127 @@ def get_latest_version(asset_name, subset_name, dbcon=None, project_name=None):
return version_doc
def get_workfile_template_key_from_context(
asset_name, task_name, host_name, project_name=None,
dbcon=None, project_settings=None
):
"""Helper function to get template key for workfile template.
Do the same as `get_workfile_template_key` but returns value for "session
context".
It is required to pass one of 'dbcon' with already set project name or
'project_name' arguments.
Args:
asset_name(str): Name of asset document.
task_name(str): Task name for which is template key retrieved.
Must be available on asset document under `data.tasks`.
host_name(str): Name of host implementation for which is workfile
used.
project_name(str): Project name where asset and task is. Not required
when 'dbcon' is passed.
dbcon(AvalonMongoDB): Connection to mongo with already set project
under `AVALON_PROJECT`. Not required when 'project_name' is passed.
project_settings(dict): Project settings for passed 'project_name'.
Not required at all but makes function faster.
Raises:
ValueError: When both 'dbcon' and 'project_name' were not
passed.
"""
if not dbcon:
if not project_name:
raise ValueError((
"`get_workfile_template_key_from_context` requires to pass"
" one of 'dbcon' or 'project_name' arguments."
))
from avalon.api import AvalonMongoDB
dbcon = AvalonMongoDB()
dbcon.Session["AVALON_PROJECT"] = project_name
elif not project_name:
project_name = dbcon.Session["AVALON_PROJECT"]
asset_doc = dbcon.find_one(
{
"type": "asset",
"name": asset_name
},
{
"data.tasks": 1
}
)
asset_tasks = asset_doc.get("data", {}).get("tasks") or {}
task_info = asset_tasks.get(task_name) or {}
task_type = task_info.get("type")
return get_workfile_template_key(
task_type, host_name, project_name, project_settings
)
def get_workfile_template_key(
task_type, host_name, project_name=None, project_settings=None
):
"""Workfile template key which should be used to get workfile template.
Function is using profiles from project settings to return right template
for passet task type and host name.
One of 'project_name' or 'project_settings' must be passed it is preffered
to pass settings if are already available.
Args:
task_type(str): Name of task type.
host_name(str): Name of host implementation (e.g. "maya", "nuke", ...)
project_name(str): Name of project in which context should look for
settings. Not required if `project_settings` are passed.
project_settings(dict): Prepare project settings for project name.
Not needed if `project_name` is passed.
Raises:
ValueError: When both 'project_name' and 'project_settings' were not
passed.
"""
default = "work"
if not task_type or not host_name:
return default
if not project_settings:
if not project_name:
raise ValueError((
"`get_workfile_template_key` requires to pass"
" one of 'project_name' or 'project_settings' arguments."
))
project_settings = get_project_settings(project_name)
try:
profiles = (
project_settings
["global"]
["tools"]
["Workfiles"]
["workfile_template_profiles"]
)
except Exception:
profiles = []
if not profiles:
return default
from .profiles_filtering import filter_profiles
profile_filter = {
"task_types": task_type,
"hosts": host_name
}
profile = filter_profiles(profiles, profile_filter)
if profile:
return profile["workfile_template"] or default
return default
def get_workdir_data(project_doc, asset_doc, task_name, host_name):
"""Prepare data for workdir template filling from entered information.
@ -373,7 +494,8 @@ def get_workdir_data(project_doc, asset_doc, task_name, host_name):
def get_workdir_with_workdir_data(
workdir_data, anatomy=None, project_name=None, template_key=None
workdir_data, anatomy=None, project_name=None,
template_key=None, dbcon=None
):
"""Fill workdir path from entered data and project's anatomy.
@ -387,8 +509,10 @@ def get_workdir_with_workdir_data(
`project_name` is entered.
project_name (str): Project's name. Optional if `anatomy` is entered
otherwise Anatomy object is created with using the project name.
template_key (str): Key of work templates in anatomy templates. By
default is seto to `"work"`.
template_key (str): Key of work templates in anatomy templates. If not
passed `get_workfile_template_key_from_context` is used to get it.
dbcon(AvalonMongoDB): Mongo connection. Required only if 'template_key'
and 'project_name' are not passed.
Returns:
TemplateResult: Workdir path.
@ -406,7 +530,13 @@ def get_workdir_with_workdir_data(
anatomy = Anatomy(project_name)
if not template_key:
template_key = "work"
template_key = get_workfile_template_key_from_context(
workdir_data["asset"],
workdir_data["task"],
workdir_data["app"],
project_name=workdir_data["project"]["name"],
dbcon=dbcon
)
anatomy_filled = anatomy.format(workdir_data)
# Output is TemplateResult object which contain usefull data
@ -447,7 +577,9 @@ def get_workdir(
project_doc, asset_doc, task_name, host_name
)
# Output is TemplateResult object which contain usefull data
return get_workdir_with_workdir_data(workdir_data, anatomy, template_key)
return get_workdir_with_workdir_data(
workdir_data, anatomy, template_key=template_key
)
@with_avalon
@ -516,7 +648,9 @@ def create_workfile_doc(asset_doc, task_name, filename, workdir, dbcon=None):
# Prepare anatomy
anatomy = Anatomy(project_doc["name"])
# Get workdir path (result is anatomy.TemplateResult)
template_workdir = get_workdir_with_workdir_data(workdir_data, anatomy)
template_workdir = get_workdir_with_workdir_data(
workdir_data, anatomy, dbcon=dbcon
)
template_workdir_path = str(template_workdir).replace("\\", "/")
# Replace slashses in workdir path where workfile is located

View file

@ -26,7 +26,7 @@ class CollectUsername(pyblish.api.ContextPlugin):
"""
order = pyblish.api.CollectorOrder - 0.488
label = "Collect ftrack username"
host = ["webpublisher"]
hosts = ["webpublisher"]
_context = None

View file

@ -95,7 +95,7 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin):
# use same input args like with mov
jpeg_items.extend(ffmpeg_args.get("input") or [])
# input file
jpeg_items.append("-i {}".format(full_input_path))
jpeg_items.append("-i \"{}\"".format(full_input_path))
# output arguments from presets
jpeg_items.extend(ffmpeg_args.get("output") or [])
@ -104,7 +104,7 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin):
jpeg_items.append("-vframes 1")
# output file
jpeg_items.append(full_output_path)
jpeg_items.append("\"{}\"".format(full_output_path))
subprocess_jpeg = " ".join(jpeg_items)

View file

@ -251,6 +251,13 @@
]
},
"Workfiles": {
"workfile_template_profiles": [
{
"task_types": [],
"hosts": [],
"workfile_template": "work"
}
],
"last_workfile_on_startup": [
{
"hosts": [],

View file

@ -106,7 +106,8 @@ from .enum_entity import (
ToolsEnumEntity,
TaskTypeEnumEntity,
ProvidersEnum,
DeadlineUrlEnumEntity
DeadlineUrlEnumEntity,
AnatomyTemplatesEnumEntity
)
from .list_entity import ListEntity
@ -162,6 +163,7 @@ __all__ = (
"TaskTypeEnumEntity",
"ProvidersEnum",
"DeadlineUrlEnumEntity",
"AnatomyTemplatesEnumEntity",
"ListEntity",

View file

@ -494,3 +494,69 @@ class DeadlineUrlEnumEntity(BaseEnumEntity):
elif self._current_value not in self.valid_keys:
self._current_value = tuple(self.valid_keys)[0]
class AnatomyTemplatesEnumEntity(BaseEnumEntity):
schema_types = ["anatomy-templates-enum"]
def _item_initalization(self):
self.multiselection = False
self.enum_items = []
self.valid_keys = set()
enum_default = self.schema_data.get("default") or "work"
self.value_on_not_set = enum_default
self.valid_value_types = (STRING_TYPE,)
# GUI attribute
self.placeholder = self.schema_data.get("placeholder")
def _get_enum_values(self):
templates_entity = self.get_entity_from_path(
"project_anatomy/templates"
)
valid_keys = set()
enum_items_list = []
others_entity = None
for key, entity in templates_entity.items():
# Skip defaults key
if key == "defaults":
continue
if key == "others":
others_entity = entity
continue
label = key
if hasattr(entity, "label"):
label = entity.label or label
enum_items_list.append({key: label})
valid_keys.add(key)
if others_entity is not None:
get_child_label_func = getattr(
others_entity, "get_child_label", None
)
for key, child_entity in others_entity.items():
label = key
if callable(get_child_label_func):
label = get_child_label_func(child_entity) or label
enum_items_list.append({key: label})
valid_keys.add(key)
return enum_items_list, valid_keys
def set_override_state(self, *args, **kwargs):
super(AnatomyTemplatesEnumEntity, self).set_override_state(
*args, **kwargs
)
self.enum_items, self.valid_keys = self._get_enum_values()
if self._current_value not in self.valid_keys:
self._current_value = self.value_on_not_set

View file

@ -380,6 +380,20 @@ How output of the schema could look like on save:
}
```
### anatomy-templates-enum
- enumeration of all available anatomy template keys
- have only single selection mode
- it is possible to define default value `default`
- `"work"` is used if default value is not specified
```
{
"key": "host",
"label": "Host name",
"type": "anatomy-templates-enum",
"default": "publish"
}
```
### hosts-enum
- enumeration of available hosts
- multiselection can be allowed with setting key `"multiselection"` to `True` (Default: `False`)

View file

@ -65,6 +65,37 @@
"key": "Workfiles",
"label": "Workfiles",
"children": [
{
"type": "list",
"key": "workfile_template_profiles",
"label": "Workfile template profiles",
"use_label_wrap": true,
"object_type": {
"type": "dict",
"children": [
{
"key": "task_types",
"label": "Task types",
"type": "task-types-enum"
},
{
"type": "hosts-enum",
"key": "hosts",
"label": "Hosts",
"multiselection": true
},
{
"type": "splitter"
},
{
"key": "workfile_template",
"label": "Workfile template",
"type": "anatomy-templates-enum",
"multiselection": false
}
]
}
},
{
"type": "list",
"key": "last_workfile_on_startup",

View file

@ -12,10 +12,15 @@ from avalon import style, io, api, pipeline
from avalon.tools import lib as tools_lib
from avalon.tools.widgets import AssetWidget
from avalon.tools.models import TasksModel
from avalon.tools.delegates import PrettyTimeDelegate
from .model import FilesModel
from .model import (
TASK_NAME_ROLE,
TASK_TYPE_ROLE,
FilesModel,
TasksModel,
TasksProxyModel
)
from .view import FilesView
from openpype.lib import (
@ -23,7 +28,8 @@ from openpype.lib import (
get_workdir,
get_workfile_doc,
create_workfile_doc,
save_workfile_data_to_doc
save_workfile_data_to_doc,
get_workfile_template_key
)
log = logging.getLogger(__name__)
@ -55,9 +61,13 @@ class NameWindow(QtWidgets.QDialog):
# Set work file data for template formatting
asset_name = session["AVALON_ASSET"]
project_doc = io.find_one({
"type": "project"
})
project_doc = io.find_one(
{"type": "project"},
{
"name": True,
"data.code": True
}
)
self.data = {
"project": {
"name": project_doc["name"],
@ -126,10 +136,14 @@ class NameWindow(QtWidgets.QDialog):
# for "{version".
if "{version" in self.template:
inputs_layout.addRow("Version:", version_widget)
else:
version_widget.setVisible(False)
# Add subversion only if template containt `{comment}`
if "{comment}" in self.template:
inputs_layout.addRow("Subversion:", subversion_input)
else:
subversion_input.setVisible(False)
inputs_layout.addRow("Extension:", ext_combo)
inputs_layout.addRow("Preview:", preview_label)
@ -305,48 +319,46 @@ class TasksWidget(QtWidgets.QWidget):
task_changed = QtCore.Signal()
def __init__(self, parent=None):
def __init__(self, dbcon=None, parent=None):
super(TasksWidget, self).__init__(parent)
self.setContentsMargins(0, 0, 0, 0)
view = QtWidgets.QTreeView()
view.setIndentation(0)
model = TasksModel(io)
view.setModel(model)
tasks_view = QtWidgets.QTreeView(self)
tasks_view.setIndentation(0)
tasks_view.setSortingEnabled(True)
if dbcon is None:
dbcon = io
tasks_model = TasksModel(dbcon)
tasks_proxy = TasksProxyModel()
tasks_proxy.setSourceModel(tasks_model)
tasks_view.setModel(tasks_proxy)
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(view)
layout.addWidget(tasks_view)
# Hide the default tasks "count" as we don't need that data here.
view.setColumnHidden(1, True)
selection_model = tasks_view.selectionModel()
selection_model.currentChanged.connect(self.task_changed)
selection = view.selectionModel()
selection.currentChanged.connect(self.task_changed)
self.models = {
"tasks": model
}
self.widgets = {
"view": view,
}
self._tasks_model = tasks_model
self._tasks_proxy = tasks_proxy
self._tasks_view = tasks_view
self._last_selected_task = None
def set_asset(self, asset):
if asset is None:
# Asset deselected
def set_asset(self, asset_doc):
# Asset deselected
if asset_doc is None:
return
# Try and preserve the last selected task and reselect it
# after switching assets. If there's no currently selected
# asset keep whatever the "last selected" was prior to it.
current = self.get_current_task()
current = self.get_current_task_name()
if current:
self._last_selected_task = current
self.models["tasks"].set_assets(asset_docs=[asset])
self._tasks_model.set_asset(asset_doc)
if self._last_selected_task:
self.select_task(self._last_selected_task)
@ -354,7 +366,7 @@ class TasksWidget(QtWidgets.QWidget):
# Force a task changed emit.
self.task_changed.emit()
def select_task(self, task):
def select_task(self, task_name):
"""Select a task by name.
If the task does not exist in the current model then selection is only
@ -366,39 +378,40 @@ class TasksWidget(QtWidgets.QWidget):
"""
# Clear selection
view = self.widgets["view"]
model = view.model()
selection_model = view.selectionModel()
selection_model = self._tasks_view.selectionModel()
selection_model.clearSelection()
# Select the task
mode = selection_model.Select | selection_model.Rows
for row in range(model.rowCount(QtCore.QModelIndex())):
index = model.index(row, 0, QtCore.QModelIndex())
name = index.data(QtCore.Qt.DisplayRole)
if name == task:
for row in range(self._tasks_model.rowCount()):
index = self._tasks_model.index(row, 0)
name = index.data(TASK_NAME_ROLE)
if name == task_name:
selection_model.select(index, mode)
# Set the currently active index
view.setCurrentIndex(index)
self._tasks_view.setCurrentIndex(index)
break
def get_current_task(self):
def get_current_task_name(self):
"""Return name of task at current index (selected)
Returns:
str: Name of the current task.
"""
view = self.widgets["view"]
index = view.currentIndex()
index = index.sibling(index.row(), 0) # ensure column zero for name
index = self._tasks_view.currentIndex()
selection_model = self._tasks_view.selectionModel()
if index.isValid() and selection_model.isSelected(index):
return index.data(TASK_NAME_ROLE)
return None
selection = view.selectionModel()
if selection.isSelected(index):
# Ignore when the current task is not selected as the "No task"
# placeholder might be the current index even though it's
# disallowed to be selected. So we only return if it is selected.
return index.data(QtCore.Qt.DisplayRole)
def get_current_task_type(self):
index = self._tasks_view.currentIndex()
selection_model = self._tasks_view.selectionModel()
if index.isValid() and selection_model.isSelected(index):
return index.data(TASK_TYPE_ROLE)
return None
class FilesWidget(QtWidgets.QWidget):
@ -411,7 +424,8 @@ class FilesWidget(QtWidgets.QWidget):
# Setup
self._asset = None
self._task = None
self._task_name = None
self._task_type = None
# Pype's anatomy object for current project
self.anatomy = Anatomy(io.Session["AVALON_PROJECT"])
@ -506,14 +520,15 @@ class FilesWidget(QtWidgets.QWidget):
self.btn_browse = btn_browse
self.btn_save = btn_save
def set_asset_task(self, asset, task):
def set_asset_task(self, asset, task_name, task_type):
self._asset = asset
self._task = task
self._task_name = task_name
self._task_type = task_type
# Define a custom session so we can query the work root
# for a "Work area" that is not our current Session.
# This way we can browse it even before we enter it.
if self._asset and self._task:
if self._asset and self._task_name and self._task_type:
session = self._get_session()
self.root = self.host.work_root(session)
self.files_model.set_root(self.root)
@ -533,10 +548,16 @@ class FilesWidget(QtWidgets.QWidget):
"""Return a modified session for the current asset and task"""
session = api.Session.copy()
self.template_key = get_workfile_template_key(
self._task_type,
session["AVALON_APP"],
project_name=session["AVALON_PROJECT"]
)
changes = pipeline.compute_session_changes(
session,
asset=self._asset,
task=self._task
task=self._task_name,
template_key=self.template_key
)
session.update(changes)
@ -549,14 +570,19 @@ class FilesWidget(QtWidgets.QWidget):
changes = pipeline.compute_session_changes(
session,
asset=self._asset,
task=self._task
task=self._task_name,
template_key=self.template_key
)
if not changes:
# Return early if we're already in the right Session context
# to avoid any unwanted Task Changed callbacks to be triggered.
return
api.update_current_task(asset=self._asset, task=self._task)
api.update_current_task(
asset=self._asset,
task=self._task_name,
template_key=self.template_key
)
def open_file(self, filepath):
host = self.host
@ -606,7 +632,7 @@ class FilesWidget(QtWidgets.QWidget):
result = messagebox.exec_()
if result == messagebox.Yes:
return True
elif result == messagebox.No:
if result == messagebox.No:
return False
return None
@ -700,7 +726,7 @@ class FilesWidget(QtWidgets.QWidget):
self._enter_session() # Make sure we are in the right session
self.host.save_file(file_path)
self.set_asset_task(self._asset, self._task)
self.set_asset_task(self._asset, self._task_name, self._task_type)
pipeline.emit("after.workfile.save", [file_path])
@ -727,7 +753,8 @@ class FilesWidget(QtWidgets.QWidget):
changes = pipeline.compute_session_changes(
session,
asset=self._asset,
task=self._task
task=self._task_name,
template_key=self.template_key
)
session.update(changes)
@ -750,7 +777,7 @@ class FilesWidget(QtWidgets.QWidget):
# Force a full to the asset as opposed to just self.refresh() so
# that it will actually check again whether the Work directory exists
self.set_asset_task(self._asset, self._task)
self.set_asset_task(self._asset, self._task_name, self._task_type)
def refresh(self):
"""Refresh listed files for current selection in the interface"""
@ -927,7 +954,7 @@ class Window(QtWidgets.QMainWindow):
assets_widget = AssetWidget(io, parent=home_body_widget)
assets_widget.set_current_asset_btn_visibility(True)
tasks_widget = TasksWidget(home_body_widget)
tasks_widget = TasksWidget(io, home_body_widget)
files_widget = FilesWidget(home_body_widget)
side_panel = SidePanelWidget(home_body_widget)
@ -999,7 +1026,7 @@ class Window(QtWidgets.QMainWindow):
if asset_docs:
asset_doc = asset_docs[0]
task_name = self.tasks_widget.get_current_task()
task_name = self.tasks_widget.get_current_task_name()
workfile_doc = None
if asset_doc and task_name and filepath:
@ -1026,7 +1053,7 @@ class Window(QtWidgets.QMainWindow):
def _get_current_workfile_doc(self, filepath=None):
if filepath is None:
filepath = self.files_widget._get_selected_filepath()
task_name = self.tasks_widget.get_current_task()
task_name = self.tasks_widget.get_current_task_name()
asset_docs = self.assets_widget.get_selected_assets()
if not task_name or not asset_docs or not filepath:
return
@ -1046,7 +1073,7 @@ class Window(QtWidgets.QMainWindow):
workdir, filename = os.path.split(filepath)
asset_docs = self.assets_widget.get_selected_assets()
asset_doc = asset_docs[0]
task_name = self.tasks_widget.get_current_task()
task_name = self.tasks_widget.get_current_task_name()
create_workfile_doc(asset_doc, task_name, filename, workdir, io)
def set_context(self, context):
@ -1065,7 +1092,6 @@ class Window(QtWidgets.QMainWindow):
# Select the asset
self.assets_widget.select_assets([asset], expand=True)
# Force a refresh on Tasks?
self.tasks_widget.set_asset(asset_document)
if "task" in context:
@ -1095,12 +1121,13 @@ class Window(QtWidgets.QMainWindow):
asset = self.assets_widget.get_selected_assets() or None
if asset is not None:
asset = asset[0]
task = self.tasks_widget.get_current_task()
task_name = self.tasks_widget.get_current_task_name()
task_type = self.tasks_widget.get_current_task_type()
self.tasks_widget.setEnabled(bool(asset))
self.files_widget.setEnabled(all([bool(task), bool(asset)]))
self.files_widget.set_asset_task(asset, task)
self.files_widget.setEnabled(all([bool(task_name), bool(asset)]))
self.files_widget.set_asset_task(asset, task_name, task_type)
self.files_widget.refresh()

View file

@ -1,7 +1,7 @@
import os
import logging
from Qt import QtCore
from Qt import QtCore, QtGui
from avalon import style
from avalon.vendor import qtawesome
@ -9,6 +9,10 @@ from avalon.tools.models import TreeModel, Item
log = logging.getLogger(__name__)
TASK_NAME_ROLE = QtCore.Qt.UserRole + 1
TASK_TYPE_ROLE = QtCore.Qt.UserRole + 2
TASK_ORDER_ROLE = QtCore.Qt.UserRole + 3
class FilesModel(TreeModel):
"""Model listing files with specified extensions in a root folder"""
@ -151,3 +155,142 @@ class FilesModel(TreeModel):
return "Date modified"
return super(FilesModel, self).headerData(section, orientation, role)
class TasksProxyModel(QtCore.QSortFilterProxyModel):
def lessThan(self, x_index, y_index):
x_order = x_index.data(TASK_ORDER_ROLE)
y_order = y_index.data(TASK_ORDER_ROLE)
if x_order is not None and y_order is not None:
if x_order < y_order:
return True
if x_order > y_order:
return False
elif x_order is None and y_order is not None:
return True
elif y_order is None and x_order is not None:
return False
x_name = x_index.data(QtCore.Qt.DisplayRole)
y_name = y_index.data(QtCore.Qt.DisplayRole)
if x_name == y_name:
return True
if x_name == tuple(sorted((x_name, y_name)))[0]:
return False
return True
class TasksModel(QtGui.QStandardItemModel):
"""A model listing the tasks combined for a list of assets"""
def __init__(self, dbcon, parent=None):
super(TasksModel, self).__init__(parent=parent)
self.dbcon = dbcon
self._default_icon = qtawesome.icon(
"fa.male",
color=style.colors.default
)
self._no_tasks_icon = qtawesome.icon(
"fa.exclamation-circle",
color=style.colors.mid
)
self._cached_icons = {}
self._project_task_types = {}
self._refresh_task_types()
def _refresh_task_types(self):
# Get the project configured icons from database
project = self.dbcon.find_one(
{"type": "project"},
{"config.tasks"}
)
tasks = project["config"].get("tasks") or {}
self._project_task_types = tasks
def _try_get_awesome_icon(self, icon_name):
icon = None
if icon_name:
try:
icon = qtawesome.icon(
"fa.{}".format(icon_name),
color=style.colors.default
)
except Exception:
pass
return icon
def headerData(self, section, orientation, role):
# Show nice labels in the header
if (
role == QtCore.Qt.DisplayRole
and orientation == QtCore.Qt.Horizontal
):
if section == 0:
return "Tasks"
return super(TasksModel, self).headerData(section, orientation, role)
def _get_icon(self, task_icon, task_type_icon):
if task_icon in self._cached_icons:
return self._cached_icons[task_icon]
icon = self._try_get_awesome_icon(task_icon)
if icon is not None:
self._cached_icons[task_icon] = icon
return icon
if task_type_icon in self._cached_icons:
icon = self._cached_icons[task_type_icon]
self._cached_icons[task_icon] = icon
return icon
icon = self._try_get_awesome_icon(task_type_icon)
if icon is None:
icon = self._default_icon
self._cached_icons[task_icon] = icon
self._cached_icons[task_type_icon] = icon
return icon
def set_asset(self, asset_doc):
"""Set assets to track by their database id
Arguments:
asset_doc (dict): Asset document from MongoDB.
"""
self.clear()
if not asset_doc:
return
asset_tasks = asset_doc.get("data", {}).get("tasks") or {}
items = []
for task_name, task_info in asset_tasks.items():
task_icon = task_info.get("icon")
task_type = task_info.get("type")
task_order = task_info.get("order")
task_type_info = self._project_task_types.get(task_type) or {}
task_type_icon = task_type_info.get("icon")
icon = self._get_icon(task_icon, task_type_icon)
label = "{} ({})".format(task_name, task_type or "type N/A")
item = QtGui.QStandardItem(label)
item.setData(task_name, TASK_NAME_ROLE)
item.setData(task_type, TASK_TYPE_ROLE)
item.setData(task_order, TASK_ORDER_ROLE)
item.setData(icon, QtCore.Qt.DecorationRole)
item.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable)
items.append(item)
if not items:
item = QtGui.QStandardItem("No task")
item.setData(self._no_tasks_icon, QtCore.Qt.DecorationRole)
item.setFlags(QtCore.Qt.NoItemFlags)
items.append(item)
self.invisibleRootItem().appendRows(items)

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring Pype version."""
__version__ = "3.4.0-nightly.2"
__version__ = "3.4.0-nightly.4"

@ -1 +1 @@
Subproject commit 52e24a9993e5223b0a719786e77a4b87e936e556
Subproject commit f48fce09c0986c1fd7f6731de33907be46b436c5

View file

@ -135,6 +135,16 @@ progress_bar.close()
# iterate over frozen libs and create list to delete
libs_dir = build_dir / "lib"
# On Windows "python3.dll" is needed for PyQt5 from the build.
if platform.system().lower() == "windows":
src = Path(libs_dir / "PyQt5" / "python3.dll")
dst = Path(deps_dir / "PyQt5" / "python3.dll")
if src.exists():
shutil.copyfile(src, dst)
else:
_print("Could not find {}".format(src), 1)
sys.exit(1)
to_delete = []
# _print("Finding duplicates ...")
deps_items = list(deps_dir.iterdir())

View file

@ -36,7 +36,7 @@ def get_log_since_tag(version):
def release_type(log):
regex_minor = ["feature/", "(feat)"]
regex_patch = ["bugfix/", "fix/", "(fix)"]
regex_patch = ["bugfix/", "fix/", "(fix)", "enhancement/"]
for reg in regex_minor:
if re.search(reg, log):
return "minor"

View file

@ -40,14 +40,13 @@ Deploy OP build distribution (Openpype Igniter) on an OS of your choice.
```sh
#!/usr/bin/env bash
export OPENPYPE_DEBUG=3
export WEBSERVER_HOST_IP=localhost
export FTRACK_BOT_API_USER=YOUR_API_USER
export FTRACK_BOT_API_KEY=YOUR_API_KEY
export PYTHONDONTWRITEBYTECODE=1
export OPENPYPE_MONGO=YOUR_MONGODB_CONNECTION
pushd /opt/openpype
./openpype_console webpublisherwebserver --upload_dir YOUR_SHARED_FOLDER_ON_HOST --executable /opt/openpype/openpype_console > /tmp/openpype.log 2>&1
./openpype_console webpublisherwebserver --upload_dir YOUR_SHARED_FOLDER_ON_HOST --executable /opt/openpype/openpype_console --host YOUR_HOST_IP --port YOUR_HOST_PORT > /tmp/openpype.log 2>&1
```
1. create service file `sudo vi /etc/systemd/system/openpye-webserver.service`

View file

@ -120,7 +120,12 @@ const studios = [
title: "Bad Clay",
image: "/img/badClay_logo.png",
infoLink: "https://www.bad-clay.com/",
}
},
{
title: "Moonrock Animation Studio",
image: "/img/moonrock_logo.png",
infoLink: "https://www.moonrock.eu/",
}
];
function Service({imageUrl, title, description}) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB