SyncServer GUI - multiselect for Summary

This commit is contained in:
Petr Kalis 2021-04-22 19:26:10 +02:00
parent 502f30be12
commit 4036eefe7b
3 changed files with 201 additions and 140 deletions

View file

@ -24,6 +24,7 @@ ProgressRole = QtCore.Qt.UserRole + 4
DateRole = QtCore.Qt.UserRole + 6 DateRole = QtCore.Qt.UserRole + 6
FailedRole = QtCore.Qt.UserRole + 8 FailedRole = QtCore.Qt.UserRole + 8
HeaderNameRole = QtCore.Qt.UserRole + 10 HeaderNameRole = QtCore.Qt.UserRole + 10
FullItemRole = QtCore.Qt.UserRole + 12
@six.add_metaclass(abc.ABCMeta) @six.add_metaclass(abc.ABCMeta)

View file

@ -120,7 +120,7 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel):
self.query = self.get_query(load_records) self.query = self.get_query(load_records)
representations = self.dbcon.aggregate(self.query) representations = self.dbcon.aggregate(self.query)
self.add_page_records(self.local_site, self.remote_site, self.add_page_records(self.active_site, self.remote_site,
representations) representations)
self.endResetModel() self.endResetModel()
self.refresh_finished.emit() self.refresh_finished.emit()
@ -158,7 +158,7 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel):
self._rec_loaded, self._rec_loaded,
self._rec_loaded + items_to_fetch - 1) self._rec_loaded + items_to_fetch - 1)
self.add_page_records(self.local_site, self.remote_site, self.add_page_records(self.active_site, self.remote_site,
representations) representations)
self.endInsertRows() self.endInsertRows()
@ -283,7 +283,7 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel):
""" """
self._project = project self._project = project
self.sync_server.set_sync_project_settings() self.sync_server.set_sync_project_settings()
self.local_site = self.sync_server.get_active_site(self.project) self.active_site = self.sync_server.get_active_site(self.project)
self.remote_site = self.sync_server.get_remote_site(self.project) self.remote_site = self.sync_server.get_remote_site(self.project)
self.refresh() self.refresh()
@ -410,7 +410,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
self.sync_server = sync_server self.sync_server = sync_server
# TODO think about admin mode # TODO think about admin mode
# this is for regular user, always only single local and single remote # this is for regular user, always only single local and single remote
self.local_site = self.sync_server.get_active_site(self.project) self.active_site = self.sync_server.get_active_site(self.project)
self.remote_site = self.sync_server.get_remote_site(self.project) self.remote_site = self.sync_server.get_remote_site(self.project)
self.sort = self.DEFAULT_SORT self.sort = self.DEFAULT_SORT
@ -428,6 +428,9 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
def data(self, index, role): def data(self, index, role):
item = self._data[index.row()] item = self._data[index.row()]
if role == lib.FullItemRole:
return item
header_value = self._header[index.column()] header_value = self._header[index.column()]
if role == lib.ProviderRole: if role == lib.ProviderRole:
if header_value == 'local_site': if header_value == 'local_site':
@ -585,7 +588,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
}}, }},
'order_local': { 'order_local': {
'$filter': {'input': '$files.sites', 'as': 'p', '$filter': {'input': '$files.sites', 'as': 'p',
'cond': {'$eq': ['$$p.name', self.local_site]} 'cond': {'$eq': ['$$p.name', self.active_site]}
}} }}
}}, }},
{'$addFields': { {'$addFields': {
@ -714,7 +717,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
""" """
base_match = { base_match = {
"type": "representation", "type": "representation",
'files.sites.name': {'$all': [self.local_site, 'files.sites.name': {'$all': [self.active_site,
self.remote_site]} self.remote_site]}
} }
if not self._word_filter: if not self._word_filter:
@ -889,7 +892,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
self.sync_server = sync_server self.sync_server = sync_server
# TODO think about admin mode # TODO think about admin mode
# this is for regular user, always only single local and single remote # this is for regular user, always only single local and single remote
self.local_site = self.sync_server.get_active_site(self.project) self.active_site = self.sync_server.get_active_site(self.project)
self.remote_site = self.sync_server.get_remote_site(self.project) self.remote_site = self.sync_server.get_remote_site(self.project)
self.sort = self.DEFAULT_SORT self.sort = self.DEFAULT_SORT
@ -1042,7 +1045,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
}}, }},
'order_local': { 'order_local': {
'$filter': {'input': '$files.sites', 'as': 'p', '$filter': {'input': '$files.sites', 'as': 'p',
'cond': {'$eq': ['$$p.name', self.local_site]} 'cond': {'$eq': ['$$p.name', self.active_site]}
}} }}
}}, }},
{'$addFields': { {'$addFields': {

View file

@ -161,9 +161,7 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
self.sync_server = sync_server self.sync_server = sync_server
self._selected_id = None # keep last selected _id self._selected_ids = [] # keep last selected _id
self.representation_id = None
self.site_name = None # to pause/unpause representation
self.txt_filter = QtWidgets.QLineEdit() self.txt_filter = QtWidgets.QLineEdit()
self.txt_filter.setPlaceholderText("Quick filter representations..") self.txt_filter.setPlaceholderText("Quick filter representations..")
@ -183,7 +181,7 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
self.table_view.setModel(model) self.table_view.setModel(model)
self.table_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.table_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.table_view.setSelectionMode( self.table_view.setSelectionMode(
QtWidgets.QAbstractItemView.SingleSelection) QtWidgets.QAbstractItemView.ExtendedSelection)
self.table_view.setSelectionBehavior( self.table_view.setSelectionBehavior(
QtWidgets.QAbstractItemView.SelectRows) QtWidgets.QAbstractItemView.SelectRows)
self.table_view.horizontalHeader().setSortIndicator( self.table_view.horizontalHeader().setSortIndicator(
@ -228,10 +226,12 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
idx = model.get_header_index(column_name) idx = model.get_header_index(column_name)
self.table_view.setColumnWidth(idx, width) self.table_view.setColumnWidth(idx, width)
def _selection_changed(self, _new_selection): def _selection_changed(self, new_selected, all_selected):
index = self.selection_model.currentIndex() idxs = self.selection_model.selectedRows()
self._selected_id = \ self._selected_ids = []
self.model.data(index, Qt.UserRole)
for index in idxs:
self._selected_ids.append(self.model.data(index, Qt.UserRole))
def _set_selection(self): def _set_selection(self):
""" """
@ -239,14 +239,16 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
Keep selection during model refresh. Keep selection during model refresh.
""" """
if self._selected_id: existing_ids = []
index = self.model.get_index(self._selected_id) for selected_id in self._selected_ids:
index = self.model.get_index(selected_id)
if index and index.isValid(): if index and index.isValid():
mode = QtCore.QItemSelectionModel.Select | \ mode = QtCore.QItemSelectionModel.Select | \
QtCore.QItemSelectionModel.Rows QtCore.QItemSelectionModel.Rows
self.selection_model.setCurrentIndex(index, mode) self.selection_model.select(index, mode)
else: existing_ids.append(selected_id)
self._selected_id = None
self._selected_ids = existing_ids
def _double_clicked(self, index): def _double_clicked(self, index):
""" """
@ -256,59 +258,62 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
detail_window = SyncServerDetailWindow( detail_window = SyncServerDetailWindow(
self.sync_server, _id, self.model.project) self.sync_server, _id, self.model.project)
detail_window.exec() detail_window.exec()
def _on_context_menu(self, point): def _on_context_menu(self, point):
""" """
Shows menu with loader actions on Right-click. Shows menu with loader actions on Right-click.
Supports multiple selects - adds all available actions, each
action handles if it appropriate for item itself, if not it skips.
""" """
is_multi = len(self._selected_ids) > 1
point_index = self.table_view.indexAt(point) point_index = self.table_view.indexAt(point)
if not point_index.isValid(): if not point_index.isValid() and not is_multi:
return return
self.item = self.model._data[point_index.row()] if is_multi:
self.representation_id = self.item._id index = self.model.get_index(self._selected_ids[0])
log.debug("menu representation _id:: {}". self.item = self.model.data(index, lib.FullItemRole)
format(self.representation_id)) else:
self.item = self.model.data(point_index, lib.FullItemRole)
menu = QtWidgets.QMenu() menu = QtWidgets.QMenu()
actions_mapping = {} actions_mapping = {}
actions_kwargs_mapping = {} action_kwarg_map = {}
active_site = self.model.active_site
remote_site = self.model.remote_site
local_site = self.item.local_site
local_progress = self.item.local_progress local_progress = self.item.local_progress
remote_site = self.item.remote_site
remote_progress = self.item.remote_progress remote_progress = self.item.remote_progress
for site, progress in {local_site: local_progress, project = self.model.project
for site, progress in {active_site: local_progress,
remote_site: remote_progress}.items(): remote_site: remote_progress}.items():
project = self.model.project provider = self.sync_server.get_provider_for_site(project, site)
provider = self.sync_server.get_provider_for_site(project,
site)
if provider == 'local_drive': if provider == 'local_drive':
if 'studio' in site: if 'studio' in site:
txt = " studio version" txt = " studio version"
else: else:
txt = " local version" txt = " local version"
action = QtWidgets.QAction("Open in explorer" + txt) action = QtWidgets.QAction("Open in explorer" + txt)
if progress == 1.0: if progress == 1.0 or is_multi:
actions_mapping[action] = self._open_in_explorer actions_mapping[action] = self._open_in_explorer
actions_kwargs_mapping[action] = {'site': site} action_kwarg_map[action] = \
self._get_action_kwargs(site)
menu.addAction(action) menu.addAction(action)
# progress smaller then 1.0 --> in progress or queued if self.item.status in [lib.STATUS[0], lib.STATUS[1]] or is_multi:
if local_progress < 1.0: action = QtWidgets.QAction("Pause in queue")
self.site_name = local_site
else:
self.site_name = remote_site
if self.item.status in [lib.STATUS[0], lib.STATUS[1]]:
action = QtWidgets.QAction("Pause")
actions_mapping[action] = self._pause actions_mapping[action] = self._pause
# pause handles which site_name it will pause itself
action_kwarg_map[action] = {"repre_ids": self._selected_ids}
menu.addAction(action) menu.addAction(action)
if self.item.status == lib.STATUS[3]: if self.item.status == lib.STATUS[3] or is_multi:
action = QtWidgets.QAction("Unpause") action = QtWidgets.QAction("Unpause in queue")
actions_mapping[action] = self._unpause actions_mapping[action] = self._unpause
action_kwarg_map[action] = {"repre_ids": self._selected_ids}
menu.addAction(action) menu.addAction(action)
# if self.item.status == lib.STATUS[1]: # if self.item.status == lib.STATUS[1]:
@ -316,24 +321,29 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
# actions_mapping[action] = self._show_detail # actions_mapping[action] = self._show_detail
# menu.addAction(action) # menu.addAction(action)
if remote_progress == 1.0: if remote_progress == 1.0 or is_multi:
action = QtWidgets.QAction("Re-sync Active site") action = QtWidgets.QAction("Re-sync Active site")
actions_mapping[action] = self._reset_local_site action_kwarg_map[action] = self._get_action_kwargs(active_site)
actions_mapping[action] = self._reset_site
menu.addAction(action) menu.addAction(action)
if local_progress == 1.0: if local_progress == 1.0 or is_multi:
action = QtWidgets.QAction("Re-sync Remote site") action = QtWidgets.QAction("Re-sync Remote site")
actions_mapping[action] = self._reset_remote_site action_kwarg_map[action] = self._get_action_kwargs(remote_site)
actions_mapping[action] = self._reset_site
menu.addAction(action) menu.addAction(action)
if local_site != self.sync_server.DEFAULT_SITE: if active_site == get_local_site_id():
action = QtWidgets.QAction("Completely remove from local") action = QtWidgets.QAction("Completely remove from local")
action_kwarg_map[action] = self._get_action_kwargs(active_site)
actions_mapping[action] = self._remove_site actions_mapping[action] = self._remove_site
menu.addAction(action) menu.addAction(action)
else:
action = QtWidgets.QAction("Mark for sync to local") # # temp for testing only !!!
actions_mapping[action] = self._add_site # action = QtWidgets.QAction("Download")
menu.addAction(action) # action_kwarg_map[action] = self._get_action_kwargs(active_site)
# actions_mapping[action] = self._add_site
# menu.addAction(action)
if not actions_mapping: if not actions_mapping:
action = QtWidgets.QAction("< No action >") action = QtWidgets.QAction("< No action >")
@ -343,46 +353,65 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
result = menu.exec_(QtGui.QCursor.pos()) result = menu.exec_(QtGui.QCursor.pos())
if result: if result:
to_run = actions_mapping[result] to_run = actions_mapping[result]
to_run_kwargs = actions_kwargs_mapping.get(result, {}) to_run_kwargs = action_kwarg_map.get(result, {})
if to_run: if to_run:
to_run(**to_run_kwargs) to_run(**to_run_kwargs)
self.model.refresh() self.model.refresh()
def _pause(self): def _pause(self, repre_ids=None):
self.sync_server.pause_representation(self.model.project, log.debug("Pause {}".format(repre_ids))
self.representation_id, for representation_id in repre_ids:
self.site_name) item = self._get_item_by_repre_id(representation_id)
self.site_name = None if item.status not in [lib.STATUS[0], lib.STATUS[1]]:
self.message_generated.emit("Paused {}".format(self.representation_id)) continue
for site_name in [self.model.active_site, self.model.remote_site]:
check_progress = self._get_progress(item, site_name)
if check_progress < 1:
self.sync_server.pause_representation(self.model.project,
representation_id,
site_name)
def _unpause(self): self.message_generated.emit("Paused {}".format(representation_id))
self.sync_server.unpause_representation(
self.model.project, def _unpause(self, repre_ids=None):
self.representation_id, log.debug("UnPause {}".format(repre_ids))
self.site_name) for representation_id in repre_ids:
self.site_name = None item = self._get_item_by_repre_id(representation_id)
self.message_generated.emit("Unpaused {}".format( if item.status not in lib.STATUS[3]:
self.representation_id)) continue
for site_name in [self.model.active_site, self.model.remote_site]:
check_progress = self._get_progress(item, site_name)
if check_progress < 1:
self.sync_server.unpause_representation(
self.model.project,
representation_id,
site_name)
self.message_generated.emit("Unpause {}".format(representation_id))
# temporary here for testing, will be removed TODO # temporary here for testing, will be removed TODO
def _add_site(self): def _add_site(self, repre_ids=None, site_name=None):
log.info(self.representation_id) log.debug("Add site {}:{}".format(repre_ids, site_name))
project_name = self.model.project for representation_id in repre_ids:
local_site_name = get_local_site_id() item = self._get_item_by_repre_id(representation_id)
try: if item.local_site == site_name or item.remote_site == site_name:
self.sync_server.add_site( # site already exists skip
project_name, continue
self.representation_id,
local_site_name
)
self.message_generated.emit(
"Site {} added for {}".format(local_site_name,
self.representation_id))
except ValueError as exp:
self.message_generated.emit("Error {}".format(str(exp)))
def _remove_site(self): try:
self.sync_server.add_site(
self.model.project,
representation_id,
site_name
)
self.message_generated.emit(
"Site {} added for {}".format(site_name,
representation_id))
except ValueError as exp:
self.message_generated.emit("Error {}".format(str(exp)))
def _remove_site(self, repre_ids=None, site_name=None):
""" """
Removes site record AND files. Removes site record AND files.
@ -392,65 +421,93 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
This could only happen when artist work on local machine, not This could only happen when artist work on local machine, not
connected to studio mounted drives. connected to studio mounted drives.
""" """
log.info("Removing {}".format(self.representation_id)) log.debug("Remove site {}:{}".format(repre_ids, site_name))
try: for representation_id in repre_ids:
local_site = get_local_site_id() log.info("Removing {}".format(representation_id))
self.sync_server.remove_site( try:
self.sync_server.remove_site(
self.model.project,
representation_id,
site_name,
True)
self.message_generated.emit(
"Site {} removed".format(site_name))
except ValueError as exp:
self.message_generated.emit("Error {}".format(str(exp)))
self.model.refresh(
load_records=self.model._rec_loaded)
def _reset_site(self, repre_ids=None, site_name=None):
"""
Removes errors or success metadata for particular file >> forces
redo of upload/download
"""
log.debug("Reset site {}:{}".format(repre_ids, site_name))
for representation_id in repre_ids:
item = self._get_item_by_repre_id(representation_id)
check_progress = self._get_progress(item, site_name, True)
# do not reset if opposite side is not fully there
if check_progress != 1:
log.debug("Not fully available {} on other side, skipping".
format(check_progress))
continue
self.sync_server.reset_provider_for_file(
self.model.project, self.model.project,
self.representation_id, representation_id,
local_site, site_name=site_name,
True) force=True)
self.message_generated.emit("Site {} removed".format(local_site))
except ValueError as exp:
self.message_generated.emit("Error {}".format(str(exp)))
self.model.refresh( self.model.refresh(
load_records=self.model._rec_loaded) load_records=self.model._rec_loaded)
def _reset_local_site(self): def _open_in_explorer(self, repre_ids=None, site_name=None):
""" log.debug("Open in Explorer {}:{}".format(repre_ids, site_name))
Removes errors or success metadata for particular file >> forces for representation_id in repre_ids:
redo of upload/download item = self._get_item_by_repre_id(representation_id)
""" if not item:
self.sync_server.reset_provider_for_file( return
self.model.project,
self.representation_id,
'local')
self.model.refresh(
load_records=self.model._rec_loaded)
def _reset_remote_site(self): fpath = item.path
""" project = self.model.project
Removes errors or success metadata for particular file >> forces fpath = self.sync_server.get_local_file_path(project,
redo of upload/download site_name,
""" fpath)
self.sync_server.reset_provider_for_file(
self.model.project,
self.representation_id,
'remote')
self.model.refresh(
load_records=self.model._rec_loaded)
def _open_in_explorer(self, site): fpath = os.path.normpath(os.path.dirname(fpath))
if not self.item: if os.path.isdir(fpath):
return if 'win' in sys.platform: # windows
subprocess.Popen('explorer "%s"' % fpath)
elif sys.platform == 'darwin': # macOS
subprocess.Popen(['open', fpath])
else: # linux
try:
subprocess.Popen(['xdg-open', fpath])
except OSError:
raise OSError('unsupported xdg-open call??')
fpath = self.item.path def _get_progress(self, item, site_name, opposite=False):
project = self.model.project """Returns progress value according to site (side)"""
fpath = self.sync_server.get_local_file_path(project, progress = {'local': item.local_progress,
site, 'remote': item.remote_progress}
fpath) side = 'remote'
if site_name == self.model.active_site:
side = 'local'
if opposite:
side = 'remote' if side == 'local' else 'local'
fpath = os.path.normpath(os.path.dirname(fpath)) return progress[side]
if os.path.isdir(fpath):
if 'win' in sys.platform: # windows def _get_item_by_repre_id(self, representation_id):
subprocess.Popen('explorer "%s"' % fpath) index = self.model.get_index(representation_id)
elif sys.platform == 'darwin': # macOS item = self.model.data(index, lib.FullItemRole)
subprocess.Popen(['open', fpath]) return item
else: # linux
try: def _get_action_kwargs(self, site_name):
subprocess.Popen(['xdg-open', fpath]) """Default format of kwargs for action"""
except OSError: return {"repre_ids": self._selected_ids, "site_name": site_name}
raise OSError('unsupported xdg-open call??')
def _save_scrollbar(self): def _save_scrollbar(self):
self._scrollbar_pos = self.table_view.verticalScrollBar().value() self._scrollbar_pos = self.table_view.verticalScrollBar().value()
@ -599,7 +656,7 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget):
menu = QtWidgets.QMenu() menu = QtWidgets.QMenu()
actions_mapping = {} actions_mapping = {}
actions_kwargs_mapping = {} action_kwarg_map = {}
local_site = self.item.local_site local_site = self.item.local_site
local_progress = self.item.local_progress local_progress = self.item.local_progress
@ -619,7 +676,7 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget):
action = QtWidgets.QAction("Open in explorer" + txt) action = QtWidgets.QAction("Open in explorer" + txt)
if progress == 1: if progress == 1:
actions_mapping[action] = self._open_in_explorer actions_mapping[action] = self._open_in_explorer
actions_kwargs_mapping[action] = {'site': site} action_kwarg_map[action] = {'site': site}
menu.addAction(action) menu.addAction(action)
if self.item.status == lib.STATUS[2]: if self.item.status == lib.STATUS[2]:
@ -645,7 +702,7 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget):
result = menu.exec_(QtGui.QCursor.pos()) result = menu.exec_(QtGui.QCursor.pos())
if result: if result:
to_run = actions_mapping[result] to_run = actions_mapping[result]
to_run_kwargs = actions_kwargs_mapping.get(result, {}) to_run_kwargs = action_kwarg_map.get(result, {})
if to_run: if to_run:
to_run(**to_run_kwargs) to_run(**to_run_kwargs)