mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge pull request #1444 from pypeclub/feature/sync_server_priority
This commit is contained in:
commit
e3689f259d
6 changed files with 440 additions and 124 deletions
|
|
@ -83,6 +83,7 @@ class SyncServerModule(PypeModule, ITrayModule):
|
|||
DEFAULT_SITE = 'studio'
|
||||
LOCAL_SITE = 'local'
|
||||
LOG_PROGRESS_SEC = 5 # how often log progress to DB
|
||||
DEFAULT_PRIORITY = 50 # higher is better, allowed range 1 - 1000
|
||||
|
||||
name = "sync_server"
|
||||
label = "Sync Queue"
|
||||
|
|
@ -472,6 +473,7 @@ class SyncServerModule(PypeModule, ITrayModule):
|
|||
|
||||
try:
|
||||
self.sync_server_thread = SyncServerThread(self)
|
||||
|
||||
from .tray.app import SyncServerWindow
|
||||
self.widget = SyncServerWindow(self)
|
||||
except ValueError:
|
||||
|
|
@ -662,7 +664,7 @@ class SyncServerModule(PypeModule, ITrayModule):
|
|||
self.connection.Session["AVALON_PROJECT"] = collection
|
||||
# retry_cnt - number of attempts to sync specific file before giving up
|
||||
retries_arr = self._get_retries_arr(collection)
|
||||
query = {
|
||||
match = {
|
||||
"type": "representation",
|
||||
"$or": [
|
||||
{"$and": [
|
||||
|
|
@ -700,10 +702,47 @@ class SyncServerModule(PypeModule, ITrayModule):
|
|||
]}
|
||||
]
|
||||
}
|
||||
|
||||
aggr = [
|
||||
{"$match": match},
|
||||
{'$unwind': '$files'},
|
||||
{'$addFields': {
|
||||
'order_remote': {
|
||||
'$filter': {'input': '$files.sites', 'as': 'p',
|
||||
'cond': {'$eq': ['$$p.name', remote_site]}
|
||||
}},
|
||||
'order_local': {
|
||||
'$filter': {'input': '$files.sites', 'as': 'p',
|
||||
'cond': {'$eq': ['$$p.name', active_site]}
|
||||
}},
|
||||
}},
|
||||
{'$addFields': {
|
||||
'priority': {
|
||||
'$cond': [
|
||||
{'$size': '$order_local.priority'},
|
||||
{'$first': '$order_local.priority'},
|
||||
{'$cond': [
|
||||
{'$size': '$order_remote.priority'},
|
||||
{'$first': '$order_remote.priority'},
|
||||
self.DEFAULT_PRIORITY]}
|
||||
]
|
||||
},
|
||||
}},
|
||||
{'$group': {
|
||||
'_id': '$_id',
|
||||
# pass through context - same for representation
|
||||
'context': {'$addToSet': '$context'},
|
||||
'data': {'$addToSet': '$data'},
|
||||
# pass through files as a list
|
||||
'files': {'$addToSet': '$files'},
|
||||
'priority': {'$max': "$priority"},
|
||||
}},
|
||||
{"$sort": {'priority': -1, '_id': 1}},
|
||||
]
|
||||
log.debug("active_site:{} - remote_site:{}".format(active_site,
|
||||
remote_site))
|
||||
log.debug("query: {}".format(query))
|
||||
representations = self.connection.find(query)
|
||||
log.debug("query: {}".format(aggr))
|
||||
representations = self.connection.aggregate(aggr)
|
||||
|
||||
return representations
|
||||
|
||||
|
|
@ -749,7 +788,7 @@ class SyncServerModule(PypeModule, ITrayModule):
|
|||
return SyncStatus.DO_NOTHING
|
||||
|
||||
def update_db(self, collection, new_file_id, file, representation,
|
||||
site, error=None, progress=None):
|
||||
site, error=None, progress=None, priority=None):
|
||||
"""
|
||||
Update 'provider' portion of records in DB with success (file_id)
|
||||
or error (exception)
|
||||
|
|
@ -763,12 +802,16 @@ class SyncServerModule(PypeModule, ITrayModule):
|
|||
site (string): label ('gdrive', 'S3')
|
||||
error (string): exception message
|
||||
progress (float): 0-1 of progress of upload/download
|
||||
priority (int): 0-100 set priority
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
representation_id = representation.get("_id")
|
||||
file_id = file.get("_id")
|
||||
file_id = None
|
||||
if file:
|
||||
file_id = file.get("_id")
|
||||
|
||||
query = {
|
||||
"_id": representation_id
|
||||
}
|
||||
|
|
@ -780,6 +823,8 @@ class SyncServerModule(PypeModule, ITrayModule):
|
|||
update["$unset"] = self._get_error_dict("", "", "")
|
||||
elif progress is not None:
|
||||
update["$set"] = self._get_progress_dict(progress)
|
||||
elif priority is not None:
|
||||
update["$set"] = self._get_priority_dict(priority, file_id)
|
||||
else:
|
||||
tries = self._get_tries_count(file, site)
|
||||
tries += 1
|
||||
|
|
@ -787,9 +832,10 @@ class SyncServerModule(PypeModule, ITrayModule):
|
|||
update["$set"] = self._get_error_dict(error, tries)
|
||||
|
||||
arr_filter = [
|
||||
{'s.name': site},
|
||||
{'f._id': ObjectId(file_id)}
|
||||
{'s.name': site}
|
||||
]
|
||||
if file_id:
|
||||
arr_filter.append({'f._id': ObjectId(file_id)})
|
||||
|
||||
self.connection.database[collection].update_one(
|
||||
query,
|
||||
|
|
@ -798,7 +844,7 @@ class SyncServerModule(PypeModule, ITrayModule):
|
|||
array_filters=arr_filter
|
||||
)
|
||||
|
||||
if progress is not None:
|
||||
if progress is not None or priority is not None:
|
||||
return
|
||||
|
||||
status = 'failed'
|
||||
|
|
@ -1192,6 +1238,21 @@ class SyncServerModule(PypeModule, ITrayModule):
|
|||
val = {"files.$[f].sites.$[s].progress": progress}
|
||||
return val
|
||||
|
||||
def _get_priority_dict(self, priority, file_id):
|
||||
"""
|
||||
Provide priority metadata to be stored in Db.
|
||||
Used during upload/download for GUI to show.
|
||||
Args:
|
||||
priority: (int) - priority for file(s)
|
||||
Returns:
|
||||
(dictionary)
|
||||
"""
|
||||
if file_id:
|
||||
str_key = "files.$[f].sites.$[s].priority"
|
||||
else:
|
||||
str_key = "files.$[].sites.$[s].priority"
|
||||
return {str_key: int(priority)}
|
||||
|
||||
def _get_retries_arr(self, project_name):
|
||||
"""
|
||||
Returns array with allowed values in 'tries' field. If repre
|
||||
|
|
|
|||
|
|
@ -85,8 +85,26 @@ class SyncServerWindow(QtWidgets.QDialog):
|
|||
self.projects.current_project))
|
||||
|
||||
self.pause_btn.clicked.connect(self._pause)
|
||||
self.pause_btn.setAutoDefault(False)
|
||||
self.pause_btn.setDefault(False)
|
||||
repres.message_generated.connect(self._update_message)
|
||||
|
||||
self.representationWidget = repres
|
||||
|
||||
def showEvent(self, event):
|
||||
self.representationWidget.model.set_project(
|
||||
self.projects.current_project)
|
||||
self._set_running(True)
|
||||
super().showEvent(event)
|
||||
|
||||
def closeEvent(self, event):
|
||||
self._set_running(False)
|
||||
super().closeEvent(event)
|
||||
|
||||
def _set_running(self, running):
|
||||
self.representationWidget.model.is_running = running
|
||||
self.representationWidget.model.timer.setInterval(0)
|
||||
|
||||
def _pause(self):
|
||||
if self.sync_server.is_paused():
|
||||
self.sync_server.unpause_server()
|
||||
|
|
|
|||
116
openpype/modules/sync_server/tray/delegates.py
Normal file
116
openpype/modules/sync_server/tray/delegates.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import os
|
||||
from Qt import QtCore, QtWidgets, QtGui
|
||||
|
||||
from openpype.lib import PypeLogger
|
||||
from openpype.modules.sync_server.tray import lib
|
||||
|
||||
log = PypeLogger().get_logger("SyncServer")
|
||||
|
||||
|
||||
class PriorityDelegate(QtWidgets.QStyledItemDelegate):
|
||||
"""Creates editable line edit to set priority on representation"""
|
||||
def paint(self, painter, option, index):
|
||||
super(PriorityDelegate, self).paint(painter, option, index)
|
||||
|
||||
if option.widget.selectionModel().isSelected(index) or \
|
||||
option.state & QtWidgets.QStyle.State_MouseOver:
|
||||
edit_icon = index.data(lib.EditIconRole)
|
||||
if not edit_icon:
|
||||
return
|
||||
|
||||
state = QtGui.QIcon.On
|
||||
mode = QtGui.QIcon.Selected
|
||||
|
||||
icon_side = 16
|
||||
icon_rect = QtCore.QRect(
|
||||
option.rect.left() + option.rect.width() - icon_side - 4,
|
||||
option.rect.top() + ((option.rect.height() - icon_side) / 2),
|
||||
icon_side,
|
||||
icon_side
|
||||
)
|
||||
|
||||
edit_icon.paint(
|
||||
painter, icon_rect,
|
||||
QtCore.Qt.AlignRight, mode, state
|
||||
)
|
||||
|
||||
def createEditor(self, parent, option, index):
|
||||
editor = PriorityLineEdit(
|
||||
parent,
|
||||
option.widget.selectionModel().selectedRows())
|
||||
editor.setFocus(True)
|
||||
return editor
|
||||
|
||||
def setModelData(self, editor, model, index):
|
||||
for index in editor.selected_idxs:
|
||||
try:
|
||||
val = int(editor.text())
|
||||
except ValueError:
|
||||
val = model.sync_server.DEFAULT_PRIORITY
|
||||
model.set_priority_data(index, val)
|
||||
|
||||
|
||||
class PriorityLineEdit(QtWidgets.QLineEdit):
|
||||
"""Special LineEdit to consume Enter and store selected indexes"""
|
||||
def __init__(self, parent=None, selected_idxs=None):
|
||||
self.selected_idxs = selected_idxs
|
||||
super(PriorityLineEdit, self).__init__(parent)
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
result = super(PriorityLineEdit, self).keyPressEvent(event)
|
||||
if (
|
||||
event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter)
|
||||
):
|
||||
return event.accept()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class ImageDelegate(QtWidgets.QStyledItemDelegate):
|
||||
"""
|
||||
Prints icon of site and progress of synchronization
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(ImageDelegate, self).__init__(parent)
|
||||
self.icons = {}
|
||||
|
||||
def paint(self, painter, option, index):
|
||||
super(ImageDelegate, self).paint(painter, option, index)
|
||||
option = QtWidgets.QStyleOptionViewItem(option)
|
||||
option.showDecorationSelected = True
|
||||
|
||||
provider = index.data(lib.ProviderRole)
|
||||
value = index.data(lib.ProgressRole)
|
||||
date_value = index.data(lib.DateRole)
|
||||
is_failed = index.data(lib.FailedRole)
|
||||
|
||||
if not self.icons.get(provider):
|
||||
resource_path = os.path.dirname(__file__)
|
||||
resource_path = os.path.join(resource_path, "..",
|
||||
"providers", "resources")
|
||||
pix_url = "{}/{}.png".format(resource_path, provider)
|
||||
pixmap = QtGui.QPixmap(pix_url)
|
||||
self.icons[provider] = pixmap
|
||||
else:
|
||||
pixmap = self.icons[provider]
|
||||
|
||||
padding = 10
|
||||
point = QtCore.QPoint(option.rect.x() + padding,
|
||||
option.rect.y() +
|
||||
(option.rect.height() - pixmap.height()) / 2)
|
||||
painter.drawPixmap(point, pixmap)
|
||||
|
||||
overlay_rect = option.rect.translated(0, 0)
|
||||
overlay_rect.setHeight(overlay_rect.height() * (1.0 - float(value)))
|
||||
painter.fillRect(overlay_rect,
|
||||
QtGui.QBrush(QtGui.QColor(0, 0, 0, 100)))
|
||||
text_rect = option.rect.translated(10, 0)
|
||||
painter.drawText(text_rect,
|
||||
QtCore.Qt.AlignCenter,
|
||||
date_value)
|
||||
|
||||
if is_failed:
|
||||
overlay_rect = option.rect.translated(0, 0)
|
||||
painter.fillRect(overlay_rect,
|
||||
QtGui.QBrush(QtGui.QColor(255, 0, 0, 35)))
|
||||
|
|
@ -25,6 +25,7 @@ DateRole = QtCore.Qt.UserRole + 6
|
|||
FailedRole = QtCore.Qt.UserRole + 8
|
||||
HeaderNameRole = QtCore.Qt.UserRole + 10
|
||||
FullItemRole = QtCore.Qt.UserRole + 12
|
||||
EditIconRole = QtCore.Qt.UserRole + 14
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@ from Qt import QtCore
|
|||
from Qt.QtCore import Qt
|
||||
|
||||
from avalon.tools.delegates import pretty_timestamp
|
||||
from avalon.vendor import qtawesome
|
||||
|
||||
from openpype.lib import PypeLogger
|
||||
from openpype.api import get_local_site_id
|
||||
|
||||
from openpype.modules.sync_server.tray import lib
|
||||
|
||||
|
|
@ -41,6 +43,9 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel):
|
|||
PAGE_SIZE = 20 # default page size to query for
|
||||
REFRESH_SEC = 5000 # in seconds, requery DB for new status
|
||||
|
||||
refresh_started = QtCore.Signal()
|
||||
refresh_finished = QtCore.Signal()
|
||||
|
||||
@property
|
||||
def dbcon(self):
|
||||
"""
|
||||
|
|
@ -60,6 +65,14 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel):
|
|||
def column_filtering(self):
|
||||
return self._column_filtering
|
||||
|
||||
@property
|
||||
def is_running(self):
|
||||
return self._is_running
|
||||
|
||||
@is_running.setter
|
||||
def is_running(self, state):
|
||||
self._is_running = state
|
||||
|
||||
def rowCount(self, _index):
|
||||
return len(self._data)
|
||||
|
||||
|
|
@ -78,7 +91,20 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel):
|
|||
if orientation == Qt.Horizontal:
|
||||
return self.COLUMN_LABELS[section][0] # return name
|
||||
|
||||
@property
|
||||
def can_edit(self):
|
||||
"""Returns true if some site is user local site, eg. could edit"""
|
||||
return get_local_site_id() in (self.active_site, self.remote_site)
|
||||
|
||||
def get_column(self, index):
|
||||
"""
|
||||
Returns info about column
|
||||
|
||||
Args:
|
||||
index (QModelIndex)
|
||||
Returns:
|
||||
(tuple): (COLUMN_NAME: COLUMN_LABEL)
|
||||
"""
|
||||
return self.COLUMN_LABELS[index]
|
||||
|
||||
def get_header_index(self, value):
|
||||
|
|
@ -108,8 +134,7 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel):
|
|||
actually queried (scrolled a couple of times to list more
|
||||
than single page of records)
|
||||
"""
|
||||
if self.sync_server.is_paused() or \
|
||||
self.sync_server.is_project_paused(self.project):
|
||||
if self.is_editing or not self.is_running:
|
||||
return
|
||||
self.refresh_started.emit()
|
||||
self.beginResetModel()
|
||||
|
|
@ -191,7 +216,7 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel):
|
|||
self.sort = {self.SORT_BY_COLUMN[index]: order} # reset
|
||||
# add last one
|
||||
for key, val in backup_sort.items():
|
||||
if key != '_id':
|
||||
if key != '_id' and key != self.SORT_BY_COLUMN[index]:
|
||||
self.sort[key] = val
|
||||
break
|
||||
# add default one
|
||||
|
|
@ -363,7 +388,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
"updated_dt_remote", # remote created_dt
|
||||
"files_count", # count of files
|
||||
"files_size", # file size of all files
|
||||
"context.asset", # priority TODO
|
||||
"priority", # priority
|
||||
"status" # status
|
||||
]
|
||||
|
||||
|
|
@ -374,6 +399,8 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
'representation': lib.MultiSelectFilter('representation')
|
||||
}
|
||||
|
||||
EDITABLE_COLUMNS = ["priority"]
|
||||
|
||||
refresh_started = QtCore.Signal()
|
||||
refresh_finished = QtCore.Signal()
|
||||
|
||||
|
|
@ -403,8 +430,8 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
status = attr.ib(default=None)
|
||||
path = attr.ib(default=None)
|
||||
|
||||
def __init__(self, sync_server, header, project=None):
|
||||
super(SyncRepresentationSummaryModel, self).__init__()
|
||||
def __init__(self, sync_server, header, project=None, parent=None):
|
||||
super(SyncRepresentationSummaryModel, self).__init__(parent=parent)
|
||||
self._header = header
|
||||
self._data = []
|
||||
self._project = project
|
||||
|
|
@ -412,10 +439,13 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
self._total_records = 0 # how many documents query actually found
|
||||
self._word_filter = None
|
||||
self._column_filtering = {}
|
||||
self._is_running = False
|
||||
|
||||
self.edit_icon = qtawesome.icon("fa.edit", color="white")
|
||||
self.is_editing = False
|
||||
|
||||
self._word_filter = None
|
||||
|
||||
self._initialized = False
|
||||
if not self._project or self._project == lib.DUMMY_PROJECT:
|
||||
return
|
||||
|
||||
|
|
@ -472,12 +502,17 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
return item.status == lib.STATUS[2] and \
|
||||
item.remote_progress < 1
|
||||
|
||||
if role == Qt.DisplayRole:
|
||||
if role in (Qt.DisplayRole, Qt.EditRole):
|
||||
# because of ImageDelegate
|
||||
if header_value in ['remote_site', 'local_site']:
|
||||
return ""
|
||||
|
||||
return attr.asdict(item)[self._header[index.column()]]
|
||||
|
||||
if role == lib.EditIconRole:
|
||||
if self.can_edit and header_value in self.EDITABLE_COLUMNS:
|
||||
return self.edit_icon
|
||||
|
||||
if role == Qt.UserRole:
|
||||
return item._id
|
||||
|
||||
|
|
@ -549,7 +584,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
avg_progress_remote,
|
||||
repre.get("files_count", 1),
|
||||
lib.pretty_size(repre.get("files_size", 0)),
|
||||
1,
|
||||
repre.get("priority"),
|
||||
lib.STATUS[repre.get("status", -1)],
|
||||
files[0].get('path')
|
||||
)
|
||||
|
|
@ -668,6 +703,16 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
'$cond': [{'$size': "$order_local.paused"},
|
||||
1,
|
||||
0]},
|
||||
'priority': {
|
||||
'$cond': [
|
||||
{'$size': '$order_local.priority'},
|
||||
{'$first': '$order_local.priority'},
|
||||
{'$cond': [
|
||||
{'$size': '$order_remote.priority'},
|
||||
{'$first': '$order_remote.priority'},
|
||||
self.sync_server.DEFAULT_PRIORITY]}
|
||||
]
|
||||
},
|
||||
}},
|
||||
{'$group': {
|
||||
'_id': '$_id',
|
||||
|
|
@ -690,7 +735,8 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
'failed_local_tries': {'$sum': '$failed_local_tries'},
|
||||
'paused_remote': {'$sum': '$paused_remote'},
|
||||
'paused_local': {'$sum': '$paused_local'},
|
||||
'updated_dt_local': {'$max': "$updated_dt_local"}
|
||||
'updated_dt_local': {'$max': "$updated_dt_local"},
|
||||
'priority': {'$max': "$priority"},
|
||||
}},
|
||||
{"$project": self.projection}
|
||||
]
|
||||
|
|
@ -772,6 +818,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
'updated_dt_local': 1,
|
||||
'paused_remote': 1,
|
||||
'paused_local': 1,
|
||||
'priority': 1,
|
||||
'status': {
|
||||
'$switch': {
|
||||
'branches': [
|
||||
|
|
@ -818,6 +865,35 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
}
|
||||
}
|
||||
|
||||
def set_priority_data(self, index, value):
|
||||
"""
|
||||
Sets 'priority' flag and value on local site for selected reprs.
|
||||
|
||||
Args:
|
||||
index (QItemIndex): selected index from View
|
||||
value (int): priority value
|
||||
|
||||
Updates DB.
|
||||
Potentially should allow set priority to any site when user
|
||||
management is implemented.
|
||||
"""
|
||||
if not self.can_edit:
|
||||
return
|
||||
|
||||
repre_id = self.data(index, Qt.UserRole)
|
||||
|
||||
representation = list(self.dbcon.find({"type": "representation",
|
||||
"_id": repre_id}))
|
||||
if representation:
|
||||
self.sync_server.update_db(self.project, None, None,
|
||||
representation.pop(),
|
||||
get_local_site_id(),
|
||||
priority=value)
|
||||
self.is_editing = False
|
||||
|
||||
# all other approaches messed up selection to 0th index
|
||||
self.timer.setInterval(0)
|
||||
|
||||
|
||||
class SyncRepresentationDetailModel(_SyncRepresentationModel):
|
||||
"""
|
||||
|
|
@ -852,7 +928,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
|
|||
"updated_dt_local", # local created_dt
|
||||
"updated_dt_remote", # remote created_dt
|
||||
"size", # remote progress
|
||||
"size", # priority TODO
|
||||
"priority", # priority
|
||||
"status" # status
|
||||
]
|
||||
|
||||
|
|
@ -861,8 +937,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
|
|||
'file': lib.RegexTextFilter('file'),
|
||||
}
|
||||
|
||||
refresh_started = QtCore.Signal()
|
||||
refresh_finished = QtCore.Signal()
|
||||
EDITABLE_COLUMNS = ["priority"]
|
||||
|
||||
@attr.s
|
||||
class SyncRepresentationDetail:
|
||||
|
|
@ -898,8 +973,11 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
|
|||
self._total_records = 0 # how many documents query actually found
|
||||
self._word_filter = None
|
||||
self._id = _id
|
||||
self._initialized = False
|
||||
self._column_filtering = {}
|
||||
self._is_running = False
|
||||
|
||||
self.is_editing = False
|
||||
self.edit_icon = qtawesome.icon("fa.edit", color="white")
|
||||
|
||||
self.sync_server = sync_server
|
||||
# TODO think about admin mode
|
||||
|
|
@ -952,11 +1030,17 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
|
|||
return item.status == lib.STATUS[2] and \
|
||||
item.remote_progress < 1
|
||||
|
||||
if role == Qt.DisplayRole:
|
||||
if role in (Qt.DisplayRole, Qt.EditRole):
|
||||
# because of ImageDelegate
|
||||
if header_value in ['remote_site', 'local_site']:
|
||||
return ""
|
||||
|
||||
return attr.asdict(item)[self._header[index.column()]]
|
||||
|
||||
if role == lib.EditIconRole:
|
||||
if self.can_edit and header_value in self.EDITABLE_COLUMNS:
|
||||
return self.edit_icon
|
||||
|
||||
if role == Qt.UserRole:
|
||||
return item._id
|
||||
|
||||
|
|
@ -1026,7 +1110,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
|
|||
local_progress,
|
||||
remote_progress,
|
||||
lib.pretty_size(file.get('size', 0)),
|
||||
1,
|
||||
repre.get("priority"),
|
||||
lib.STATUS[repre.get("status", -1)],
|
||||
repre.get("tries"),
|
||||
'\n'.join(errors),
|
||||
|
|
@ -1144,7 +1228,17 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
|
|||
"$order_remote.tries",
|
||||
[]
|
||||
]}
|
||||
]}}
|
||||
]}},
|
||||
'priority': {
|
||||
'$cond': [
|
||||
{'$size': '$order_local.priority'},
|
||||
{'$first': '$order_local.priority'},
|
||||
{'$cond': [
|
||||
{'$size': '$order_remote.priority'},
|
||||
{'$first': '$order_remote.priority'},
|
||||
self.sync_server.DEFAULT_PRIORITY]}
|
||||
]
|
||||
},
|
||||
}},
|
||||
{"$project": self.projection}
|
||||
]
|
||||
|
|
@ -1210,6 +1304,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
|
|||
'failed_remote_error': 1,
|
||||
'failed_local_error': 1,
|
||||
'tries': 1,
|
||||
'priority': 1,
|
||||
'status': {
|
||||
'$switch': {
|
||||
'branches': [
|
||||
|
|
@ -1261,3 +1356,37 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
|
|||
},
|
||||
'data.path': 1
|
||||
}
|
||||
|
||||
def set_priority_data(self, index, value):
|
||||
"""
|
||||
Sets 'priority' flag and value on local site for selected reprs.
|
||||
|
||||
Args:
|
||||
index (QItemIndex): selected index from View
|
||||
value (int): priority value
|
||||
|
||||
Updates DB
|
||||
"""
|
||||
if not self.can_edit:
|
||||
return
|
||||
|
||||
file_id = self.data(index, Qt.UserRole)
|
||||
|
||||
updated_file = None
|
||||
# conversion from cursor to list
|
||||
representations = list(self.dbcon.find({"type": "representation",
|
||||
"_id": self._id}))
|
||||
|
||||
representation = representations.pop()
|
||||
for repre_file in representation["files"]:
|
||||
if repre_file["_id"] == file_id:
|
||||
updated_file = repre_file
|
||||
break
|
||||
|
||||
if representation and updated_file:
|
||||
self.sync_server.update_db(self.project, None, updated_file,
|
||||
representation, get_local_site_id(),
|
||||
priority=value)
|
||||
self.is_editing = False
|
||||
# all other approaches messed up selection to 0th index
|
||||
self.timer.setInterval(0)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ from openpype.modules.sync_server.tray.models import (
|
|||
)
|
||||
|
||||
from openpype.modules.sync_server.tray import lib
|
||||
from openpype.modules.sync_server.tray import delegates
|
||||
|
||||
log = PypeLogger().get_logger("SyncServer")
|
||||
|
||||
|
|
@ -94,16 +95,19 @@ class SyncProjectListWidget(ProjectListWidget):
|
|||
|
||||
self.project_name = point_index.data(QtCore.Qt.DisplayRole)
|
||||
|
||||
menu = QtWidgets.QMenu()
|
||||
menu = QtWidgets.QMenu(self)
|
||||
actions_mapping = {}
|
||||
|
||||
if self.sync_server.is_project_paused(self.project_name):
|
||||
action = QtWidgets.QAction("Unpause")
|
||||
actions_mapping[action] = self._unpause
|
||||
else:
|
||||
action = QtWidgets.QAction("Pause")
|
||||
actions_mapping[action] = self._pause
|
||||
menu.addAction(action)
|
||||
can_edit = self.model.can_edit
|
||||
|
||||
if can_edit:
|
||||
if self.sync_server.is_project_paused(self.project_name):
|
||||
action = QtWidgets.QAction("Unpause")
|
||||
actions_mapping[action] = self._unpause
|
||||
else:
|
||||
action = QtWidgets.QAction("Pause")
|
||||
actions_mapping[action] = self._pause
|
||||
menu.addAction(action)
|
||||
|
||||
if self.local_site == get_local_site_id():
|
||||
action = QtWidgets.QAction("Clear local project")
|
||||
|
|
@ -145,10 +149,10 @@ class _SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
|
||||
def _selection_changed(self, _new_selected, _all_selected):
|
||||
idxs = self.selection_model.selectedRows()
|
||||
self._selected_ids = []
|
||||
self._selected_ids = set()
|
||||
|
||||
for index in idxs:
|
||||
self._selected_ids.append(self.model.data(index, Qt.UserRole))
|
||||
self._selected_ids.add(self.model.data(index, Qt.UserRole))
|
||||
|
||||
def _set_selection(self):
|
||||
"""
|
||||
|
|
@ -156,14 +160,14 @@ class _SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
|
||||
Keep selection during model refresh.
|
||||
"""
|
||||
existing_ids = []
|
||||
existing_ids = set()
|
||||
for selected_id in self._selected_ids:
|
||||
index = self.model.get_index(selected_id)
|
||||
if index and index.isValid():
|
||||
mode = QtCore.QItemSelectionModel.Select | \
|
||||
QtCore.QItemSelectionModel.Rows
|
||||
self.selection_model.select(index, mode)
|
||||
existing_ids.append(selected_id)
|
||||
existing_ids.add(selected_id)
|
||||
|
||||
self._selected_ids = existing_ids
|
||||
|
||||
|
|
@ -171,9 +175,17 @@ class _SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
"""
|
||||
Opens representation dialog with all files after doubleclick
|
||||
"""
|
||||
# priority editing
|
||||
if self.model.can_edit:
|
||||
column_name = self.model.get_column(index.column())
|
||||
if column_name[0] in self.model.EDITABLE_COLUMNS:
|
||||
self.model.is_editing = True
|
||||
self.table_view.openPersistentEditor(index)
|
||||
return
|
||||
|
||||
_id = self.model.data(index, Qt.UserRole)
|
||||
detail_window = SyncServerDetailWindow(
|
||||
self.sync_server, _id, self.model.project)
|
||||
self.sync_server, _id, self.model.project, parent=self)
|
||||
detail_window.exec()
|
||||
|
||||
def _on_context_menu(self, point):
|
||||
|
|
@ -189,13 +201,15 @@ class _SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
return
|
||||
|
||||
if is_multi:
|
||||
index = self.model.get_index(self._selected_ids[0])
|
||||
index = self.model.get_index(list(self._selected_ids)[0])
|
||||
item = self.model.data(index, lib.FullItemRole)
|
||||
else:
|
||||
item = self.model.data(point_index, lib.FullItemRole)
|
||||
|
||||
can_edit = self.model.can_edit
|
||||
action_kwarg_map, actions_mapping, menu = self._prepare_menu(item,
|
||||
is_multi)
|
||||
is_multi,
|
||||
can_edit)
|
||||
|
||||
result = menu.exec_(QtGui.QCursor.pos())
|
||||
if result:
|
||||
|
|
@ -206,8 +220,8 @@ class _SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
|
||||
self.model.refresh()
|
||||
|
||||
def _prepare_menu(self, item, is_multi):
|
||||
menu = QtWidgets.QMenu()
|
||||
def _prepare_menu(self, item, is_multi, can_edit):
|
||||
menu = QtWidgets.QMenu(self)
|
||||
|
||||
actions_mapping = {}
|
||||
action_kwarg_map = {}
|
||||
|
|
@ -235,24 +249,30 @@ class _SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
self._get_action_kwargs(site)
|
||||
menu.addAction(action)
|
||||
|
||||
if remote_progress == 1.0 or is_multi:
|
||||
if can_edit and (remote_progress == 1.0 or is_multi):
|
||||
action = QtWidgets.QAction("Re-sync Active site")
|
||||
action_kwarg_map[action] = self._get_action_kwargs(active_site)
|
||||
actions_mapping[action] = self._reset_site
|
||||
menu.addAction(action)
|
||||
|
||||
if local_progress == 1.0 or is_multi:
|
||||
if can_edit and (local_progress == 1.0 or is_multi):
|
||||
action = QtWidgets.QAction("Re-sync Remote site")
|
||||
action_kwarg_map[action] = self._get_action_kwargs(remote_site)
|
||||
actions_mapping[action] = self._reset_site
|
||||
menu.addAction(action)
|
||||
|
||||
if active_site == get_local_site_id():
|
||||
if can_edit and active_site == get_local_site_id():
|
||||
action = QtWidgets.QAction("Completely remove from local")
|
||||
action_kwarg_map[action] = self._get_action_kwargs(active_site)
|
||||
actions_mapping[action] = self._remove_site
|
||||
menu.addAction(action)
|
||||
|
||||
if can_edit:
|
||||
action = QtWidgets.QAction("Change priority")
|
||||
action_kwarg_map[action] = self._get_action_kwargs(active_site)
|
||||
actions_mapping[action] = self._change_priority
|
||||
menu.addAction(action)
|
||||
|
||||
# # temp for testing only !!!
|
||||
# action = QtWidgets.QAction("Download")
|
||||
# action_kwarg_map[action] = self._get_action_kwargs(active_site)
|
||||
|
|
@ -397,6 +417,16 @@ class _SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
except OSError:
|
||||
raise OSError('unsupported xdg-open call??')
|
||||
|
||||
def _change_priority(self, **kwargs):
|
||||
"""Open editor to change priority on first selected row"""
|
||||
if self._selected_ids:
|
||||
# get_index returns dummy index with column equals to 0
|
||||
index = self.model.get_index(list(self._selected_ids)[0])
|
||||
column_no = self.model.get_header_index("priority") # real column
|
||||
real_index = self.model.index(index.row(), column_no)
|
||||
self.model.is_editing = True
|
||||
self.table_view.openPersistentEditor(real_index)
|
||||
|
||||
def _get_progress(self, item, site_name, opposite=False):
|
||||
"""Returns progress value according to site (side)"""
|
||||
progress = {'local': item.local_progress,
|
||||
|
|
@ -441,7 +471,7 @@ class SyncRepresentationSummaryWidget(_SyncRepresentationWidget):
|
|||
|
||||
self.sync_server = sync_server
|
||||
|
||||
self._selected_ids = [] # keep last selected _id
|
||||
self._selected_ids = set() # keep last selected _id
|
||||
|
||||
txt_filter = QtWidgets.QLineEdit()
|
||||
txt_filter.setPlaceholderText("Quick filter representations..")
|
||||
|
|
@ -459,7 +489,8 @@ class SyncRepresentationSummaryWidget(_SyncRepresentationWidget):
|
|||
table_view = QtWidgets.QTableView()
|
||||
headers = [item[0] for item in self.default_widths]
|
||||
|
||||
model = SyncRepresentationSummaryModel(sync_server, headers, project)
|
||||
model = SyncRepresentationSummaryModel(sync_server, headers, project,
|
||||
parent=self)
|
||||
table_view.setModel(model)
|
||||
table_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
table_view.setSelectionMode(
|
||||
|
|
@ -470,15 +501,20 @@ class SyncRepresentationSummaryWidget(_SyncRepresentationWidget):
|
|||
-1, Qt.AscendingOrder)
|
||||
table_view.setAlternatingRowColors(True)
|
||||
table_view.verticalHeader().hide()
|
||||
table_view.viewport().setAttribute(QtCore.Qt.WA_Hover, True)
|
||||
|
||||
column = table_view.model().get_header_index("local_site")
|
||||
delegate = ImageDelegate(self)
|
||||
delegate = delegates.ImageDelegate(self)
|
||||
table_view.setItemDelegateForColumn(column, delegate)
|
||||
|
||||
column = table_view.model().get_header_index("remote_site")
|
||||
delegate = ImageDelegate(self)
|
||||
delegate = delegates.ImageDelegate(self)
|
||||
table_view.setItemDelegateForColumn(column, delegate)
|
||||
|
||||
column = table_view.model().get_header_index("priority")
|
||||
priority_delegate = delegates.PriorityDelegate(self)
|
||||
table_view.setItemDelegateForColumn(column, priority_delegate)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addLayout(top_bar_layout)
|
||||
|
|
@ -508,18 +544,19 @@ class SyncRepresentationSummaryWidget(_SyncRepresentationWidget):
|
|||
self.selection_model = self.table_view.selectionModel()
|
||||
self.selection_model.selectionChanged.connect(self._selection_changed)
|
||||
|
||||
def _prepare_menu(self, item, is_multi):
|
||||
def _prepare_menu(self, item, is_multi, can_edit):
|
||||
action_kwarg_map, actions_mapping, menu = \
|
||||
super()._prepare_menu(item, is_multi)
|
||||
super()._prepare_menu(item, is_multi, can_edit)
|
||||
|
||||
if item.status in [lib.STATUS[0], lib.STATUS[1]] or is_multi:
|
||||
if can_edit and (
|
||||
item.status in [lib.STATUS[0], lib.STATUS[1]] or is_multi):
|
||||
action = QtWidgets.QAction("Pause in queue")
|
||||
actions_mapping[action] = self._pause
|
||||
# pause handles which site_name it will pause itself
|
||||
action_kwarg_map[action] = {"selected_ids": self._selected_ids}
|
||||
menu.addAction(action)
|
||||
|
||||
if item.status == lib.STATUS[3] or is_multi:
|
||||
if can_edit and (item.status == lib.STATUS[3] or is_multi):
|
||||
action = QtWidgets.QAction("Unpause in queue")
|
||||
actions_mapping[action] = self._unpause
|
||||
action_kwarg_map[action] = {"selected_ids": self._selected_ids}
|
||||
|
|
@ -598,7 +635,7 @@ class SyncRepresentationDetailWidget(_SyncRepresentationWidget):
|
|||
self.sync_server = sync_server
|
||||
|
||||
self.representation_id = _id
|
||||
self._selected_ids = []
|
||||
self._selected_ids = set()
|
||||
|
||||
self.txt_filter = QtWidgets.QLineEdit()
|
||||
self.txt_filter.setPlaceholderText("Quick filter representation..")
|
||||
|
|
@ -616,6 +653,8 @@ class SyncRepresentationDetailWidget(_SyncRepresentationWidget):
|
|||
|
||||
model = SyncRepresentationDetailModel(sync_server, headers, _id,
|
||||
project)
|
||||
model.is_running = True
|
||||
|
||||
table_view.setModel(model)
|
||||
table_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
table_view.setSelectionMode(
|
||||
|
|
@ -628,13 +667,18 @@ class SyncRepresentationDetailWidget(_SyncRepresentationWidget):
|
|||
table_view.verticalHeader().hide()
|
||||
|
||||
column = model.get_header_index("local_site")
|
||||
delegate = ImageDelegate(self)
|
||||
delegate = delegates.ImageDelegate(self)
|
||||
table_view.setItemDelegateForColumn(column, delegate)
|
||||
|
||||
column = model.get_header_index("remote_site")
|
||||
delegate = ImageDelegate(self)
|
||||
delegate = delegates.ImageDelegate(self)
|
||||
table_view.setItemDelegateForColumn(column, delegate)
|
||||
|
||||
if model.can_edit:
|
||||
column = table_view.model().get_header_index("priority")
|
||||
priority_delegate = delegates.PriorityDelegate(self)
|
||||
table_view.setItemDelegateForColumn(column, priority_delegate)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addLayout(top_bar_layout)
|
||||
|
|
@ -658,12 +702,25 @@ class SyncRepresentationDetailWidget(_SyncRepresentationWidget):
|
|||
|
||||
self.txt_filter.textChanged.connect(lambda: model.set_word_filter(
|
||||
self.txt_filter.text()))
|
||||
table_view.doubleClicked.connect(self._double_clicked)
|
||||
table_view.customContextMenuRequested.connect(self._on_context_menu)
|
||||
|
||||
model.refresh_started.connect(self._save_scrollbar)
|
||||
model.refresh_finished.connect(self._set_scrollbar)
|
||||
model.modelReset.connect(self._set_selection)
|
||||
|
||||
def _double_clicked(self, index):
|
||||
"""
|
||||
Opens representation dialog with all files after doubleclick
|
||||
"""
|
||||
# priority editing
|
||||
if self.model.can_edit:
|
||||
column_name = self.model.get_column(index.column())
|
||||
if column_name[0] in self.model.EDITABLE_COLUMNS:
|
||||
self.model.is_editing = True
|
||||
self.table_view.openPersistentEditor(index)
|
||||
return
|
||||
|
||||
def _show_detail(self, selected_ids=None):
|
||||
"""
|
||||
Shows windows with error message for failed sync of a file.
|
||||
|
|
@ -672,10 +729,10 @@ class SyncRepresentationDetailWidget(_SyncRepresentationWidget):
|
|||
|
||||
detail_window.exec()
|
||||
|
||||
def _prepare_menu(self, item, is_multi):
|
||||
def _prepare_menu(self, item, is_multi, can_edit):
|
||||
"""Adds view (and model) dependent actions to default ones"""
|
||||
action_kwarg_map, actions_mapping, menu = \
|
||||
super()._prepare_menu(item, is_multi)
|
||||
super()._prepare_menu(item, is_multi, can_edit)
|
||||
|
||||
if item.status == lib.STATUS[2] or is_multi:
|
||||
action = QtWidgets.QAction("Open error detail")
|
||||
|
|
@ -778,72 +835,6 @@ class SyncRepresentationErrorWidget(QtWidgets.QWidget):
|
|||
layout.addWidget(text_area)
|
||||
|
||||
|
||||
class ImageDelegate(QtWidgets.QStyledItemDelegate):
|
||||
"""
|
||||
Prints icon of site and progress of synchronization
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(ImageDelegate, self).__init__(parent)
|
||||
self.icons = {}
|
||||
|
||||
def paint(self, painter, option, index):
|
||||
super(ImageDelegate, self).paint(painter, option, index)
|
||||
option = QtWidgets.QStyleOptionViewItem(option)
|
||||
option.showDecorationSelected = True
|
||||
|
||||
provider = index.data(lib.ProviderRole)
|
||||
value = index.data(lib.ProgressRole)
|
||||
date_value = index.data(lib.DateRole)
|
||||
is_failed = index.data(lib.FailedRole)
|
||||
|
||||
if not self.icons.get(provider):
|
||||
resource_path = os.path.dirname(__file__)
|
||||
resource_path = os.path.join(resource_path, "..",
|
||||
"providers", "resources")
|
||||
pix_url = "{}/{}.png".format(resource_path, provider)
|
||||
pixmap = QtGui.QPixmap(pix_url)
|
||||
self.icons[provider] = pixmap
|
||||
else:
|
||||
pixmap = self.icons[provider]
|
||||
|
||||
padding = 10
|
||||
point = QtCore.QPoint(option.rect.x() + padding,
|
||||
option.rect.y() +
|
||||
(option.rect.height() - pixmap.height()) / 2)
|
||||
painter.drawPixmap(point, pixmap)
|
||||
|
||||
overlay_rect = option.rect.translated(0, 0)
|
||||
overlay_rect.setHeight(overlay_rect.height() * (1.0 - float(value)))
|
||||
painter.fillRect(overlay_rect,
|
||||
QtGui.QBrush(QtGui.QColor(0, 0, 0, 100)))
|
||||
text_rect = option.rect.translated(10, 0)
|
||||
painter.drawText(text_rect,
|
||||
QtCore.Qt.AlignCenter,
|
||||
date_value)
|
||||
|
||||
if is_failed:
|
||||
overlay_rect = option.rect.translated(0, 0)
|
||||
painter.fillRect(overlay_rect,
|
||||
QtGui.QBrush(QtGui.QColor(255, 0, 0, 35)))
|
||||
|
||||
|
||||
class TransparentWidget(QtWidgets.QWidget):
|
||||
"""Used for header cell for resizing to work properly"""
|
||||
clicked = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, column_name, *args, **kwargs):
|
||||
super(TransparentWidget, self).__init__(*args, **kwargs)
|
||||
self.column_name = column_name
|
||||
# self.setStyleSheet("background: red;")
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
self.clicked.emit(self.column_name)
|
||||
|
||||
super(TransparentWidget, self).mouseReleaseEvent(event)
|
||||
|
||||
|
||||
class HorizontalHeader(QtWidgets.QHeaderView):
|
||||
"""Reiplemented QHeaderView to contain clickable changeable button"""
|
||||
def __init__(self, parent=None):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue