Merge pull request #6092 from ynput/enhancement/publisher-report-viewer-sorting

Publish report viewer: Report items sorting
This commit is contained in:
Jakub Trllo 2024-01-25 10:35:38 +01:00 committed by GitHub
commit 77bfe7e3bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 187 additions and 52 deletions

View file

@ -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):

View file

@ -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 = []

View file

@ -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)