import os
import subprocess
import sys
from functools import partial
from qtpy import QtWidgets, QtCore, QtGui
import qtawesome
from openpype.tools.settings import style
from openpype.lib import Logger, get_local_site_id
from openpype.tools.utils.delegates import pretty_timestamp
from .models import (
SyncRepresentationSummaryModel,
SyncRepresentationDetailModel
)
from . import lib
from . import delegates
from openpype.tools.utils.constants import (
LOCAL_PROGRESS_ROLE,
REMOTE_PROGRESS_ROLE,
HEADER_NAME_ROLE,
STATUS_ROLE,
PATH_ROLE,
LOCAL_SITE_NAME_ROLE,
REMOTE_SITE_NAME_ROLE,
LOCAL_DATE_ROLE,
REMOTE_DATE_ROLE,
ERROR_ROLE,
TRIES_ROLE
)
log = Logger.get_logger("SyncServer")
class SyncProjectListWidget(QtWidgets.QWidget):
"""
Lists all projects that are synchronized to choose from
"""
project_changed = QtCore.Signal()
message_generated = QtCore.Signal(str)
refresh_msec = 10000
show_only_enabled = True
def __init__(self, sync_server, parent):
super(SyncProjectListWidget, self).__init__(parent)
self.setObjectName("ProjectListWidget")
self._parent = parent
label_widget = QtWidgets.QLabel("Projects", self)
project_list = QtWidgets.QListView(self)
project_model = QtGui.QStandardItemModel()
project_list.setModel(project_model)
project_list.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
# Do not allow editing
project_list.setEditTriggers(
QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers
)
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(3)
layout.addWidget(label_widget, 0)
layout.addWidget(project_list, 1)
project_list.customContextMenuRequested.connect(self._on_context_menu)
project_list.selectionModel().selectionChanged.connect(
self._on_selection_changed
)
self.project_model = project_model
self.project_list = project_list
self.sync_server = sync_server
self.current_project = None
self.project_name = None
self.local_site = None
self.remote_site = None
self.icons = {}
self._selection_changed = False
self._model_reset = False
timer = QtCore.QTimer()
timer.setInterval(self.refresh_msec)
timer.timeout.connect(self.refresh)
timer.start()
self.timer = timer
def _on_selection_changed(self, new_selection, _old_selection):
# block involuntary selection changes
if self._selection_changed or self._model_reset:
return
indexes = new_selection.indexes()
if not indexes:
return
project_name = indexes[0].data(QtCore.Qt.DisplayRole)
if self.current_project == project_name:
return
self._selection_changed = True
self.current_project = project_name
self.project_changed.emit()
self.refresh()
self._selection_changed = False
def refresh(self):
selected_index = None
model = self.project_model
self._model_reset = True
model.clear()
self._model_reset = False
selected_item = None
sync_settings = self.sync_server.sync_project_settings
for project_name in sync_settings.keys():
if self.sync_server.is_paused() or \
self.sync_server.is_project_paused(project_name):
icon = self._get_icon("paused")
elif not sync_settings[project_name]["enabled"]:
if self.show_only_enabled:
continue
icon = self._get_icon("disabled")
else:
icon = self._get_icon("synced")
if project_name in self.sync_server.projects_processed:
icon = self._get_icon("refresh")
item = QtGui.QStandardItem(icon, project_name)
model.appendRow(item)
if self.current_project == project_name:
selected_item = item
if model.item(0) is None:
return
if selected_item:
selected_index = model.indexFromItem(selected_item)
if not self.current_project:
self.current_project = model.item(0).data(QtCore.Qt.DisplayRole)
self.project_model = model
if selected_index and \
selected_index.isValid() and \
not self._selection_changed:
mode = (
QtCore.QItemSelectionModel.Select
| QtCore.QItemSelectionModel.Rows
)
self.project_list.selectionModel().select(selected_index, mode)
if self.current_project:
self.local_site = self.sync_server.get_active_site(
self.current_project)
self.remote_site = self.sync_server.get_remote_site(
self.current_project)
def _can_edit(self):
"""Returns true if some site is user local site, eg. could edit"""
return get_local_site_id() in (self.local_site, self.remote_site)
def _get_icon(self, status):
if not self.icons.get(status):
resource_path = os.path.dirname(__file__)
resource_path = os.path.join(resource_path, "..",
"resources")
pix_url = "{}/{}.png".format(resource_path, status)
icon = QtGui.QIcon(pix_url)
self.icons[status] = icon
else:
icon = self.icons[status]
return icon
def _on_context_menu(self, point):
point_index = self.project_list.indexAt(point)
if not point_index.isValid():
return
self.project_name = point_index.data(QtCore.Qt.DisplayRole)
menu = QtWidgets.QMenu(self)
actions_mapping = {}
if self._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")
actions_mapping[action] = self._clear_project
menu.addAction(action)
if self.project_name not in self.sync_server.projects_processed:
action = QtWidgets.QAction("Validate files on active site")
actions_mapping[action] = self._validate_site
menu.addAction(action)
result = menu.exec_(QtGui.QCursor.pos())
if result:
to_run = actions_mapping[result]
if to_run:
to_run()
def _pause(self):
if self.project_name:
self.sync_server.pause_project(self.project_name)
self.project_name = None
self.refresh()
def _unpause(self):
if self.project_name:
self.sync_server.unpause_project(self.project_name)
self.project_name = None
self.refresh()
def _clear_project(self):
if self.project_name:
self.sync_server.clear_project(self.project_name, self.local_site)
self.project_name = None
self.refresh()
def _validate_site(self):
if self.project_name:
self.sync_server.create_validate_project_task(self.project_name,
self.local_site)
self.project_name = None
self.refresh()
class _SyncRepresentationWidget(QtWidgets.QWidget):
"""
Summary dialog with list of representations that matches current
settings 'local_site' and 'remote_site'.
"""
active_changed = QtCore.Signal() # active index changed
message_generated = QtCore.Signal(str)
def set_project(self, project):
self.model.set_project(project)
def _selection_changed(self, _new_selected, _all_selected):
idxs = self.selection_model.selectedRows()
self._selected_ids = set()
for index in idxs:
self._selected_ids.add(self.model.data(index, QtCore.Qt.UserRole))
def _set_selection(self):
"""
Sets selection to 'self._selected_id' if exists.
Keep selection during model refresh.
"""
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.add(selected_id)
self._selected_ids = existing_ids
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
_id = self.model.data(index, QtCore.Qt.UserRole)
detail_window = SyncServerDetailWindow(
self.sync_server, _id, self.model.project, parent=self)
detail_window.exec()
def _on_context_menu(self, point):
"""
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)
if not point_index.isValid() and not is_multi:
return
if is_multi:
index = self.model.get_index(list(self._selected_ids)[0])
local_progress = self.model.data(index, LOCAL_PROGRESS_ROLE)
remote_progress = self.model.data(index, REMOTE_PROGRESS_ROLE)
status = self.model.data(index, STATUS_ROLE)
else:
local_progress = self.model.data(point_index, LOCAL_PROGRESS_ROLE)
remote_progress = self.model.data(point_index,
REMOTE_PROGRESS_ROLE)
status = self.model.data(point_index, STATUS_ROLE)
can_edit = self.model.can_edit
action_kwarg_map, actions_mapping, menu = self._prepare_menu(
local_progress, remote_progress, is_multi, can_edit, status)
result = menu.exec_(QtGui.QCursor.pos())
if result:
to_run = actions_mapping[result]
to_run_kwargs = action_kwarg_map.get(result, {})
if to_run:
to_run(**to_run_kwargs)
self.model.refresh()
def _prepare_menu(self, local_progress, remote_progress,
is_multi, can_edit, status=None):
menu = QtWidgets.QMenu(self)
actions_mapping = {}
action_kwarg_map = {}
active_site = self.model.active_site
remote_site = self.model.remote_site
for site, progress in {active_site: local_progress,
remote_site: remote_progress}.items():
provider = self.sync_server.get_provider_for_site(site=site)
if provider == 'local_drive':
if 'studio' in site:
txt = " studio version"
else:
txt = " local version"
action = QtWidgets.QAction("Open in explorer" + txt)
if progress == 1.0 or is_multi:
actions_mapping[action] = self._open_in_explorer
action_kwarg_map[action] = \
self._get_action_kwargs(site)
menu.addAction(action)
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 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 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)
if not actions_mapping:
action = QtWidgets.QAction("< No action >")
actions_mapping[action] = None
menu.addAction(action)
return action_kwarg_map, actions_mapping, menu
def _pause(self, selected_ids=None):
log.debug("Pause {}".format(selected_ids))
for representation_id in selected_ids:
status = lib.get_value_from_id_by_role(self.model,
representation_id,
STATUS_ROLE)
if status not in [lib.STATUS[0], lib.STATUS[1]]:
continue
for site_name in [self.model.active_site, self.model.remote_site]:
check_progress = self._get_progress(self.model,
representation_id,
site_name)
if check_progress < 1:
self.sync_server.pause_representation(self.model.project,
representation_id,
site_name)
self.message_generated.emit("Paused {}".format(representation_id))
def _unpause(self, selected_ids=None):
log.debug("UnPause {}".format(selected_ids))
for representation_id in selected_ids:
status = lib.get_value_from_id_by_role(self.model,
representation_id,
STATUS_ROLE)
if status not in lib.STATUS[3]:
continue
for site_name in [self.model.active_site, self.model.remote_site]:
check_progress = self._get_progress(self.model,
representation_id,
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
def _add_site(self, selected_ids=None, site_name=None):
log.debug("Add site {}:{}".format(selected_ids, site_name))
for representation_id in selected_ids:
item_local_site = lib.get_value_from_id_by_role(
self.model, representation_id, LOCAL_SITE_NAME_ROLE)
item_remote_site = lib.get_value_from_id_by_role(
self.model, representation_id, REMOTE_SITE_NAME_ROLE)
if site_name in [item_local_site, item_remote_site]:
# site already exists skip
continue
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)))
self.sync_server.reset_timer()
def _remove_site(self, selected_ids=None, site_name=None):
"""
Removes site record AND files.
This is ONLY for representations stored on local site, which
cannot be same as SyncServer.DEFAULT_SITE.
This could only happen when artist work on local machine, not
connected to studio mounted drives.
"""
log.debug("Remove site {}:{}".format(selected_ids, site_name))
for representation_id in selected_ids:
log.info("Removing {}".format(representation_id))
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)
self.sync_server.reset_timer()
def _reset_site(self, selected_ids=None, site_name=None):
"""
Removes errors or success metadata for particular file >> forces
redo of upload/download
"""
log.debug("Reset site {}:{}".format(selected_ids, site_name))
for representation_id in selected_ids:
check_progress = self._get_progress(self.model, representation_id,
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_site_on_representation(
self.model.project,
representation_id,
site_name=site_name,
force=True)
self.model.refresh(
load_records=self.model._rec_loaded)
self.sync_server.reset_timer()
def _open_in_explorer(self, selected_ids=None, site_name=None):
log.debug("Open in Explorer {}:{}".format(selected_ids, site_name))
for selected_id in selected_ids:
fpath = lib.get_value_from_id_by_role(self.model, selected_id,
PATH_ROLE)
project = self.model.project
fpath = self.sync_server.get_local_file_path(project,
site_name,
fpath)
fpath = os.path.normpath(os.path.dirname(fpath))
if os.path.isdir(fpath):
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??')
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, model, representation_id,
site_name, opposite=False):
"""Returns progress value according to site (side)"""
local_progress = lib.get_value_from_id_by_role(model,
representation_id,
LOCAL_PROGRESS_ROLE)
remote_progress = lib.get_value_from_id_by_role(model,
representation_id,
REMOTE_PROGRESS_ROLE)
progress = {'local': local_progress,
'remote': remote_progress}
side = 'remote'
if site_name == self.model.active_site:
side = 'local'
if opposite:
side = 'remote' if side == 'local' else 'local'
return progress[side]
def _get_action_kwargs(self, site_name):
"""Default format of kwargs for action"""
return {"selected_ids": self._selected_ids, "site_name": site_name}
def _save_scrollbar(self):
self._scrollbar_pos = self.table_view.verticalScrollBar().value()
def _set_scrollbar(self):
if self._scrollbar_pos:
self.table_view.verticalScrollBar().setValue(self._scrollbar_pos)
class SyncRepresentationSummaryWidget(_SyncRepresentationWidget):
default_widths = (
("asset", 190),
("subset", 170),
("version", 60),
("representation", 145),
("local_site", 160),
("remote_site", 160),
("files_count", 50),
("files_size", 60),
("priority", 70),
("status", 110)
)
def __init__(self, sync_server, project=None, parent=None):
super(SyncRepresentationSummaryWidget, self).__init__(parent)
self.sync_server = sync_server
self._selected_ids = set() # keep last selected _id
txt_filter = QtWidgets.QLineEdit()
txt_filter.setPlaceholderText("Quick filter representations..")
txt_filter.setClearButtonEnabled(True)
txt_filter.addAction(
qtawesome.icon("fa.filter", color="gray"),
QtWidgets.QLineEdit.LeadingPosition)
self.txt_filter = txt_filter
self._scrollbar_pos = None
top_bar_layout = QtWidgets.QHBoxLayout()
top_bar_layout.addWidget(self.txt_filter)
table_view = QtWidgets.QTableView()
headers = [item[0] for item in self.default_widths]
model = SyncRepresentationSummaryModel(sync_server, headers, project,
parent=self)
table_view.setModel(model)
table_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
table_view.setSelectionMode(
QtWidgets.QAbstractItemView.ExtendedSelection)
table_view.setSelectionBehavior(
QtWidgets.QAbstractItemView.SelectRows)
table_view.horizontalHeader().setSortIndicator(
-1, QtCore.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 = delegates.ImageDelegate(self, side="local")
table_view.setItemDelegateForColumn(column, delegate)
column = table_view.model().get_header_index("remote_site")
delegate = delegates.ImageDelegate(self, side="remote")
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)
layout.addWidget(table_view)
self.table_view = table_view
self.model = model
horizontal_header = HorizontalHeader(self)
table_view.setHorizontalHeader(horizontal_header)
table_view.setSortingEnabled(True)
for column_name, width in self.default_widths:
idx = model.get_header_index(column_name)
table_view.setColumnWidth(idx, width)
table_view.doubleClicked.connect(self._double_clicked)
self.txt_filter.textChanged.connect(lambda: model.set_word_filter(
self.txt_filter.text()))
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)
self.selection_model = self.table_view.selectionModel()
self.selection_model.selectionChanged.connect(self._selection_changed)
def _prepare_menu(self, local_progress, remote_progress,
is_multi, can_edit, status=None):
action_kwarg_map, actions_mapping, menu = \
super()._prepare_menu(local_progress, remote_progress,
is_multi, can_edit)
if can_edit and (
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 can_edit and (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}
menu.addAction(action)
return action_kwarg_map, actions_mapping, menu
class SyncServerDetailWindow(QtWidgets.QDialog):
"""Wrapper window for SyncRepresentationDetailWidget
Creates standalone window with list of files for selected repre_id.
"""
def __init__(self, sync_server, _id, project, parent=None):
log.debug(
"!!! SyncServerDetailWindow _id:: {}".format(_id))
super(SyncServerDetailWindow, self).__init__(parent)
self.setWindowFlags(QtCore.Qt.Window)
self.setFocusPolicy(QtCore.Qt.StrongFocus)
self.setStyleSheet(style.load_stylesheet())
self.setWindowIcon(QtGui.QIcon(style.app_icon_path()))
self.resize(1000, 400)
body = QtWidgets.QWidget()
footer = QtWidgets.QWidget()
footer.setFixedHeight(20)
container = SyncRepresentationDetailWidget(sync_server, _id, project,
parent=self)
body_layout = QtWidgets.QHBoxLayout(body)
body_layout.addWidget(container)
body_layout.setContentsMargins(0, 0, 0, 0)
self.message = QtWidgets.QLabel()
self.message.hide()
footer_layout = QtWidgets.QVBoxLayout(footer)
footer_layout.addWidget(self.message)
footer_layout.setContentsMargins(0, 0, 0, 0)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(body)
layout.addWidget(footer)
self.setWindowTitle("Sync Representation Detail")
class SyncRepresentationDetailWidget(_SyncRepresentationWidget):
"""
Widget to display list of synchronizable files for single repre.
Args:
_id (str): representation _id
project (str): name of project with repre
parent (QDialog): SyncServerDetailWindow
"""
active_changed = QtCore.Signal() # active index changed
default_widths = (
("file", 290),
("local_site", 185),
("remote_site", 185),
("size", 60),
("priority", 60),
("status", 110)
)
def __init__(self, sync_server, _id=None, project=None, parent=None):
super(SyncRepresentationDetailWidget, self).__init__(parent)
log.debug("Representation_id:{}".format(_id))
self.project = project
self.sync_server = sync_server
self.representation_id = _id
self._selected_ids = set()
self.txt_filter = QtWidgets.QLineEdit()
self.txt_filter.setPlaceholderText("Quick filter representation..")
self.txt_filter.setClearButtonEnabled(True)
self.txt_filter.addAction(qtawesome.icon("fa.filter", color="gray"),
QtWidgets.QLineEdit.LeadingPosition)
self._scrollbar_pos = None
top_bar_layout = QtWidgets.QHBoxLayout()
top_bar_layout.addWidget(self.txt_filter)
table_view = QtWidgets.QTableView()
headers = [item[0] for item in self.default_widths]
model = SyncRepresentationDetailModel(sync_server, headers, _id,
project)
model.is_running = True
table_view.setModel(model)
table_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
table_view.setSelectionMode(
QtWidgets.QAbstractItemView.ExtendedSelection)
table_view.setSelectionBehavior(
QtWidgets.QTableView.SelectRows)
table_view.horizontalHeader().setSortIndicator(
-1, QtCore.Qt.AscendingOrder)
table_view.horizontalHeader().setSortIndicatorShown(True)
table_view.setAlternatingRowColors(True)
table_view.verticalHeader().hide()
column = model.get_header_index("local_site")
delegate = delegates.ImageDelegate(self, side="local")
table_view.setItemDelegateForColumn(column, delegate)
column = model.get_header_index("remote_site")
delegate = delegates.ImageDelegate(self, side="remote")
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)
layout.addWidget(table_view)
self.model = model
self.selection_model = table_view.selectionModel()
self.selection_model.selectionChanged.connect(self._selection_changed)
horizontal_header = HorizontalHeader(self)
table_view.setHorizontalHeader(horizontal_header)
table_view.setSortingEnabled(True)
for column_name, width in self.default_widths:
idx = model.get_header_index(column_name)
table_view.setColumnWidth(idx, width)
self.table_view = table_view
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.
"""
detail_window = SyncRepresentationErrorWindow(self.model, selected_ids)
detail_window.exec()
def _prepare_menu(self, local_progress, remote_progress,
is_multi, can_edit, status=None):
"""Adds view (and model) dependent actions to default ones"""
action_kwarg_map, actions_mapping, menu = \
super()._prepare_menu(local_progress, remote_progress,
is_multi, can_edit, status)
if status == lib.STATUS[2] or is_multi:
action = QtWidgets.QAction("Open error detail")
actions_mapping[action] = self._show_detail
action_kwarg_map[action] = {"selected_ids": self._selected_ids}
menu.addAction(action)
return action_kwarg_map, actions_mapping, menu
def _reset_site(self, selected_ids=None, site_name=None):
"""
Removes errors or success metadata for particular file >> forces
redo of upload/download
"""
for file_id in selected_ids:
check_progress = self._get_progress(self.model, file_id,
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_site_on_representation(
self.model.project,
self.representation_id,
site_name=site_name,
file_id=file_id,
force=True)
self.model.refresh(
load_records=self.model._rec_loaded)
class SyncRepresentationErrorWindow(QtWidgets.QDialog):
"""Wrapper window to show errors during sync on file(s)"""
def __init__(self, model, selected_ids, parent=None):
super(SyncRepresentationErrorWindow, self).__init__(parent)
self.setWindowFlags(QtCore.Qt.Window)
self.setFocusPolicy(QtCore.Qt.StrongFocus)
self.setStyleSheet(style.load_stylesheet())
self.setWindowIcon(QtGui.QIcon(style.app_icon_path()))
self.resize(900, 150)
body = QtWidgets.QWidget()
container = SyncRepresentationErrorWidget(model,
selected_ids,
parent=self)
body_layout = QtWidgets.QHBoxLayout(body)
body_layout.addWidget(container)
body_layout.setContentsMargins(0, 0, 0, 0)
message = QtWidgets.QLabel()
message.hide()
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(body)
self.setLayout(body_layout)
self.setWindowTitle("Sync Representation Error Detail")
class SyncRepresentationErrorWidget(QtWidgets.QWidget):
"""
Dialog to show when sync error happened, prints formatted error message
"""
def __init__(self, model, selected_ids, parent=None):
super(SyncRepresentationErrorWidget, self).__init__(parent)
layout = QtWidgets.QVBoxLayout(self)
no_errors = True
for file_id in selected_ids:
created_dt = lib.get_value_from_id_by_role(model, file_id,
LOCAL_DATE_ROLE)
sync_dt = lib.get_value_from_id_by_role(model, file_id,
REMOTE_DATE_ROLE)
errors = lib.get_value_from_id_by_role(model, file_id,
ERROR_ROLE)
if not created_dt or not sync_dt or not errors:
continue
tries = lib.get_value_from_id_by_role(model, file_id,
TRIES_ROLE)
no_errors = False
dt = max(created_dt, sync_dt)
txts = []
txts.append("{}: {}
".format("Last update date",
pretty_timestamp(dt)))
txts.append("{}: {}
".format("Retries",
str(tries)))
txts.append("{}: {}
".format("Error message",
errors))
text_area = QtWidgets.QTextEdit("\n\n".join(txts))
text_area.setReadOnly(True)
layout.addWidget(text_area)
if no_errors:
text_area = QtWidgets.QTextEdit()
text_area.setText("