Merge pull request #1444 from pypeclub/feature/sync_server_priority

This commit is contained in:
Milan Kolar 2021-05-04 16:22:12 +02:00 committed by GitHub
commit e3689f259d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 440 additions and 124 deletions

View file

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

View file

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

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

View file

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

View file

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

View file

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