mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge pull request #6092 from ynput/enhancement/publisher-report-viewer-sorting
Publish report viewer: Report items sorting
This commit is contained in:
commit
77bfe7e3bb
3 changed files with 187 additions and 52 deletions
|
|
@ -10,6 +10,7 @@ import inspect
|
|||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
import six
|
||||
import arrow
|
||||
import pyblish.api
|
||||
|
||||
from openpype import AYON_SERVER_ENABLED
|
||||
|
|
@ -285,6 +286,8 @@ class PublishReportMaker:
|
|||
|
||||
def get_report(self, publish_plugins=None):
|
||||
"""Report data with all details of current state."""
|
||||
|
||||
now = arrow.utcnow().to("local")
|
||||
instances_details = {}
|
||||
for instance in self._all_instances_by_id.values():
|
||||
instances_details[instance.id] = self._extract_instance_data(
|
||||
|
|
@ -334,7 +337,8 @@ class PublishReportMaker:
|
|||
"context": self._extract_context_data(self._current_context),
|
||||
"crashed_file_paths": crashed_file_paths,
|
||||
"id": uuid.uuid4().hex,
|
||||
"report_version": "1.0.0"
|
||||
"created_at": now.isoformat(),
|
||||
"report_version": "1.0.1",
|
||||
}
|
||||
|
||||
def _extract_context_data(self, context):
|
||||
|
|
|
|||
|
|
@ -26,14 +26,14 @@ class InstancesModel(QtGui.QStandardItemModel):
|
|||
return self._items_by_id
|
||||
|
||||
def set_report(self, report_item):
|
||||
self.clear()
|
||||
root_item = self.invisibleRootItem()
|
||||
if root_item.rowCount() > 0:
|
||||
root_item.removeRows(0, root_item.rowCount())
|
||||
self._items_by_id.clear()
|
||||
self._plugin_items_by_id.clear()
|
||||
if not report_item:
|
||||
return
|
||||
|
||||
root_item = self.invisibleRootItem()
|
||||
|
||||
families = set(report_item.instance_items_by_family.keys())
|
||||
families.remove(None)
|
||||
all_families = list(sorted(families))
|
||||
|
|
@ -125,14 +125,14 @@ class PluginsModel(QtGui.QStandardItemModel):
|
|||
return self._items_by_id
|
||||
|
||||
def set_report(self, report_item):
|
||||
self.clear()
|
||||
root_item = self.invisibleRootItem()
|
||||
if root_item.rowCount() > 0:
|
||||
root_item.removeRows(0, root_item.rowCount())
|
||||
self._items_by_id.clear()
|
||||
self._plugin_items_by_id.clear()
|
||||
if not report_item:
|
||||
return
|
||||
|
||||
root_item = self.invisibleRootItem()
|
||||
|
||||
labels_iter = iter(self.order_label_mapping)
|
||||
cur_order, cur_label = next(labels_iter)
|
||||
cur_plugin_items = []
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import six
|
|||
import uuid
|
||||
|
||||
import appdirs
|
||||
import arrow
|
||||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
|
||||
from openpype import style
|
||||
|
|
@ -25,6 +26,7 @@ else:
|
|||
|
||||
|
||||
ITEM_ID_ROLE = QtCore.Qt.UserRole + 1
|
||||
ITEM_CREATED_AT_ROLE = QtCore.Qt.UserRole + 2
|
||||
|
||||
|
||||
def get_reports_dir():
|
||||
|
|
@ -47,47 +49,77 @@ class PublishReportItem:
|
|||
"""Report item representing one file in report directory."""
|
||||
|
||||
def __init__(self, content):
|
||||
item_id = content.get("id")
|
||||
changed = False
|
||||
if not item_id:
|
||||
item_id = str(uuid.uuid4())
|
||||
changed = True
|
||||
content["id"] = item_id
|
||||
changed = self._fix_content(content)
|
||||
|
||||
if not content.get("report_version"):
|
||||
changed = True
|
||||
content["report_version"] = "0.0.1"
|
||||
|
||||
report_path = os.path.join(get_reports_dir(), item_id)
|
||||
report_path = os.path.join(get_reports_dir(), content["id"])
|
||||
file_modified = None
|
||||
if os.path.exists(report_path):
|
||||
file_modified = os.path.getmtime(report_path)
|
||||
|
||||
created_at_obj = arrow.get(content["created_at"]).to("local")
|
||||
created_at = created_at_obj.float_timestamp
|
||||
|
||||
self.content = content
|
||||
self.report_path = report_path
|
||||
self.file_modified = file_modified
|
||||
self.created_at = float(created_at)
|
||||
self._loaded_label = content.get("label")
|
||||
self._changed = changed
|
||||
self.publish_report = PublishReport(content)
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
"""Publish report version.
|
||||
|
||||
Returns:
|
||||
str: Publish report version.
|
||||
"""
|
||||
return self.content["report_version"]
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
"""Publish report id.
|
||||
|
||||
Returns:
|
||||
str: Publish report id.
|
||||
"""
|
||||
|
||||
return self.content["id"]
|
||||
|
||||
def get_label(self):
|
||||
"""Publish report label.
|
||||
|
||||
Returns:
|
||||
str: Publish report label showed in UI.
|
||||
"""
|
||||
|
||||
return self.content.get("label") or "Unfilled label"
|
||||
|
||||
def set_label(self, label):
|
||||
"""Set publish report label.
|
||||
|
||||
Args:
|
||||
label (str): New publish report label.
|
||||
"""
|
||||
|
||||
if not label:
|
||||
self.content.pop("label", None)
|
||||
self.content["label"] = label
|
||||
|
||||
label = property(get_label, set_label)
|
||||
|
||||
@property
|
||||
def loaded_label(self):
|
||||
return self._loaded_label
|
||||
|
||||
def mark_as_changed(self):
|
||||
"""Mark report as changed."""
|
||||
|
||||
self._changed = True
|
||||
|
||||
def save(self):
|
||||
"""Save publish report to file."""
|
||||
|
||||
save = False
|
||||
if (
|
||||
self._changed
|
||||
|
|
@ -109,6 +141,15 @@ class PublishReportItem:
|
|||
|
||||
@classmethod
|
||||
def from_filepath(cls, filepath):
|
||||
"""Create report item from file.
|
||||
|
||||
Args:
|
||||
filepath (str): Path to report file. Content must be json.
|
||||
|
||||
Returns:
|
||||
PublishReportItem: Report item.
|
||||
"""
|
||||
|
||||
if not os.path.exists(filepath):
|
||||
return None
|
||||
|
||||
|
|
@ -116,15 +157,25 @@ class PublishReportItem:
|
|||
with open(filepath, "r") as stream:
|
||||
content = json.load(stream)
|
||||
|
||||
return cls(content)
|
||||
file_modified = os.path.getmtime(filepath)
|
||||
changed = cls._fix_content(content, file_modified=file_modified)
|
||||
obj = cls(content)
|
||||
if changed:
|
||||
obj.mark_as_changed()
|
||||
return obj
|
||||
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def remove_file(self):
|
||||
"""Remove report file."""
|
||||
|
||||
if os.path.exists(self.report_path):
|
||||
os.remove(self.report_path)
|
||||
|
||||
def update_file_content(self):
|
||||
"""Update report content in file."""
|
||||
|
||||
if not os.path.exists(self.report_path):
|
||||
return
|
||||
|
||||
|
|
@ -148,9 +199,57 @@ class PublishReportItem:
|
|||
self.content = content
|
||||
self.file_modified = file_modified
|
||||
|
||||
@classmethod
|
||||
def _fix_content(cls, content, file_modified=None):
|
||||
"""Fix content for backward compatibility of older report items.
|
||||
|
||||
Args:
|
||||
content (dict[str, Any]): Report content.
|
||||
file_modified (Optional[float]): File modification time.
|
||||
|
||||
Returns:
|
||||
bool: True if content was changed, False otherwise.
|
||||
"""
|
||||
|
||||
# Fix created_at key
|
||||
changed = cls._fix_created_at(content, file_modified)
|
||||
|
||||
# NOTE backward compatibility for 'id' and 'report_version' is from
|
||||
# 28.10.2022 https://github.com/ynput/OpenPype/pull/4040
|
||||
# We can probably safely remove it
|
||||
|
||||
# Fix missing 'id'
|
||||
item_id = content.get("id")
|
||||
if not item_id:
|
||||
item_id = str(uuid.uuid4())
|
||||
changed = True
|
||||
content["id"] = item_id
|
||||
|
||||
# Fix missing 'report_version'
|
||||
if not content.get("report_version"):
|
||||
changed = True
|
||||
content["report_version"] = "0.0.1"
|
||||
return changed
|
||||
|
||||
@classmethod
|
||||
def _fix_created_at(cls, content, file_modified):
|
||||
# Key 'create_at' was added in report version 1.0.1
|
||||
created_at = content.get("created_at")
|
||||
if created_at:
|
||||
return False
|
||||
|
||||
# Auto fix 'created_at', use file modification time if it is not set
|
||||
# or current time if modification could not be received.
|
||||
if file_modified is not None:
|
||||
created_at_obj = arrow.Arrow.fromtimestamp(file_modified)
|
||||
else:
|
||||
created_at_obj = arrow.utcnow()
|
||||
content["created_at"] = created_at_obj.to("local").isoformat()
|
||||
return True
|
||||
|
||||
|
||||
class PublisherReportHandler:
|
||||
"""Class handling storing publish report tool."""
|
||||
"""Class handling storing publish report items."""
|
||||
|
||||
def __init__(self):
|
||||
self._reports = None
|
||||
|
|
@ -173,14 +272,23 @@ class PublisherReportHandler:
|
|||
continue
|
||||
filepath = os.path.join(report_dir, filename)
|
||||
item = PublishReportItem.from_filepath(filepath)
|
||||
reports.append(item)
|
||||
reports_by_id[item.id] = item
|
||||
if item is not None:
|
||||
reports.append(item)
|
||||
reports_by_id[item.id] = item
|
||||
|
||||
self._reports = reports
|
||||
self._reports_by_id = reports_by_id
|
||||
return reports
|
||||
|
||||
def remove_report_items(self, item_id):
|
||||
def remove_report_item(self, item_id):
|
||||
"""Remove report item by id.
|
||||
|
||||
Remove from cache and also remove the file with the content.
|
||||
|
||||
Args:
|
||||
item_id (str): Report item id.
|
||||
"""
|
||||
|
||||
item = self._reports_by_id.get(item_id)
|
||||
if item:
|
||||
try:
|
||||
|
|
@ -191,9 +299,16 @@ class PublisherReportHandler:
|
|||
|
||||
|
||||
class LoadedFilesModel(QtGui.QStandardItemModel):
|
||||
header_labels = ("Reports", "Created")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LoadedFilesModel, self).__init__(*args, **kwargs)
|
||||
|
||||
# Column count must be set before setting header data
|
||||
self.setColumnCount(len(self.header_labels))
|
||||
for col, label in enumerate(self.header_labels):
|
||||
self.setHeaderData(col, QtCore.Qt.Horizontal, label)
|
||||
|
||||
self._items_by_id = {}
|
||||
self._report_items_by_id = {}
|
||||
|
||||
|
|
@ -202,10 +317,14 @@ class LoadedFilesModel(QtGui.QStandardItemModel):
|
|||
self._loading_registry = False
|
||||
|
||||
def refresh(self):
|
||||
self._handler.reset()
|
||||
root_item = self.invisibleRootItem()
|
||||
if root_item.rowCount() > 0:
|
||||
root_item.removeRows(0, root_item.rowCount())
|
||||
self._items_by_id = {}
|
||||
self._report_items_by_id = {}
|
||||
|
||||
self._handler.reset()
|
||||
|
||||
new_items = []
|
||||
for report_item in self._handler.list_reports():
|
||||
item = self._create_item(report_item)
|
||||
|
|
@ -217,26 +336,26 @@ class LoadedFilesModel(QtGui.QStandardItemModel):
|
|||
root_item = self.invisibleRootItem()
|
||||
root_item.appendRows(new_items)
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
|
||||
if section == 0:
|
||||
return "Exports"
|
||||
if section == 1:
|
||||
return "Modified"
|
||||
return ""
|
||||
super(LoadedFilesModel, self).headerData(section, orientation, role)
|
||||
|
||||
def data(self, index, role=None):
|
||||
if role is None:
|
||||
role = QtCore.Qt.DisplayRole
|
||||
|
||||
col = index.column()
|
||||
if col == 1:
|
||||
if role in (
|
||||
QtCore.Qt.DisplayRole, QtCore.Qt.InitialSortOrderRole
|
||||
):
|
||||
role = ITEM_CREATED_AT_ROLE
|
||||
|
||||
if col != 0:
|
||||
index = self.index(index.row(), 0, index.parent())
|
||||
|
||||
return super(LoadedFilesModel, self).data(index, role)
|
||||
|
||||
def setData(self, index, value, role):
|
||||
def setData(self, index, value, role=None):
|
||||
if role is None:
|
||||
role = QtCore.Qt.EditRole
|
||||
|
||||
if role == QtCore.Qt.EditRole:
|
||||
item_id = index.data(ITEM_ID_ROLE)
|
||||
report_item = self._report_items_by_id.get(item_id)
|
||||
|
|
@ -247,6 +366,12 @@ class LoadedFilesModel(QtGui.QStandardItemModel):
|
|||
|
||||
return super(LoadedFilesModel, self).setData(index, value, role)
|
||||
|
||||
def flags(self, index):
|
||||
# Allow editable flag only for first column
|
||||
if index.column() > 0:
|
||||
return QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled
|
||||
return super(LoadedFilesModel, self).flags(index)
|
||||
|
||||
def _create_item(self, report_item):
|
||||
if report_item.id in self._items_by_id:
|
||||
return None
|
||||
|
|
@ -254,6 +379,7 @@ class LoadedFilesModel(QtGui.QStandardItemModel):
|
|||
item = QtGui.QStandardItem(report_item.label)
|
||||
item.setColumnCount(self.columnCount())
|
||||
item.setData(report_item.id, ITEM_ID_ROLE)
|
||||
item.setData(report_item.created_at, ITEM_CREATED_AT_ROLE)
|
||||
|
||||
return item
|
||||
|
||||
|
|
@ -278,16 +404,16 @@ class LoadedFilesModel(QtGui.QStandardItemModel):
|
|||
|
||||
new_items = []
|
||||
for normalized_path in filtered_paths:
|
||||
try:
|
||||
with open(normalized_path, "r") as stream:
|
||||
data = json.load(stream)
|
||||
report_item = PublishReportItem(data)
|
||||
except Exception:
|
||||
# TODO handle errors
|
||||
report_item = PublishReportItem.from_filepath(normalized_path)
|
||||
if report_item is None:
|
||||
continue
|
||||
|
||||
label = data.get("label")
|
||||
if not label:
|
||||
# Skip already added report items
|
||||
# QUESTION: Should we replace existing or skip the item?
|
||||
if report_item.id in self._items_by_id:
|
||||
continue
|
||||
|
||||
if not report_item.loaded_label:
|
||||
report_item.label = (
|
||||
os.path.splitext(os.path.basename(filepath))[0]
|
||||
)
|
||||
|
|
@ -306,15 +432,13 @@ class LoadedFilesModel(QtGui.QStandardItemModel):
|
|||
root_item.appendRows(new_items)
|
||||
|
||||
def remove_item_by_id(self, item_id):
|
||||
report_item = self._report_items_by_id.get(item_id)
|
||||
if not report_item:
|
||||
return
|
||||
self._handler.remove_report_item(item_id)
|
||||
|
||||
self._handler.remove_report_items(item_id)
|
||||
item = self._items_by_id.get(item_id)
|
||||
|
||||
parent = self.invisibleRootItem()
|
||||
parent.removeRow(item.row())
|
||||
self._report_items_by_id.pop(item_id, None)
|
||||
item = self._items_by_id.pop(item_id, None)
|
||||
if item is not None:
|
||||
parent = self.invisibleRootItem()
|
||||
parent.removeRow(item.row())
|
||||
|
||||
def get_report_by_id(self, item_id):
|
||||
report_item = self._report_items_by_id.get(item_id)
|
||||
|
|
@ -335,13 +459,18 @@ class LoadedFilesView(QtWidgets.QTreeView):
|
|||
)
|
||||
self.setIndentation(0)
|
||||
self.setAlternatingRowColors(True)
|
||||
self.setSortingEnabled(True)
|
||||
|
||||
model = LoadedFilesModel()
|
||||
self.setModel(model)
|
||||
proxy_model = QtCore.QSortFilterProxyModel()
|
||||
proxy_model.setSourceModel(model)
|
||||
self.setModel(proxy_model)
|
||||
|
||||
time_delegate = PrettyTimeDelegate()
|
||||
self.setItemDelegateForColumn(1, time_delegate)
|
||||
|
||||
self.sortByColumn(1, QtCore.Qt.AscendingOrder)
|
||||
|
||||
remove_btn = IconButton(self)
|
||||
remove_icon_path = resources.get_icon_path("delete")
|
||||
loaded_remove_image = QtGui.QImage(remove_icon_path)
|
||||
|
|
@ -356,6 +485,7 @@ class LoadedFilesView(QtWidgets.QTreeView):
|
|||
)
|
||||
|
||||
self._model = model
|
||||
self._proxy_model = proxy_model
|
||||
self._time_delegate = time_delegate
|
||||
self._remove_btn = remove_btn
|
||||
|
||||
|
|
@ -403,7 +533,8 @@ class LoadedFilesView(QtWidgets.QTreeView):
|
|||
if index.isValid():
|
||||
return
|
||||
|
||||
index = self._model.index(0, 0)
|
||||
model = self.model()
|
||||
index = model.index(0, 0)
|
||||
if index.isValid():
|
||||
self.setCurrentIndex(index)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue