# Virus Data Analysis Tool: a data reduction GUI for HETDEX/VIRUS data
# Copyright (C) 2015, 2016, 2017 "The HETDEX collaboration"
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Model and Tree view implementations to show and navigate the directory tree
used by VDAT.
The code has being written following `this tutorial
<http://www.hardcoded.net/articles/using_qtreeview_with_qabstractitemmodel>`_.
To represent the following structure
::
+- 20150622
+-- cal
+-- time_stamp_1
+-- time_stamp_2
+-- sci
+-- object_1
+-- object_2
+-- zro
+-- time_stamp_3
+-- time_stamp_4
we first create a :class:`ReductionTreeviewModel` instance and create a root
node as a :class:`ReductionNode`:
>>> model = ReductionTreeviewModel()
>>> nights = ReductionNode("Nights", '/path/to/redux', model)
>>> model.rootnode = nights
Then we need to create the node for the night:
>>> night1 = ReductionNode("20150622", '/path/to/redux/20150622',
... model, parent=nights)
>>> nights.add_subnode(night1)
and below it the nodes for the types. E.g. the calibration node would be:
>>> cal = ReductionNode("cal", '/path/to/redux/20150622/cal',
... model, parent=night1)
>>> night1.add_subnode(cal)
Finally we can add the final directories with, e.g.:
>>> cal_1 = ReductionNode('time_stamp_1', '/path/to/redux/20150622/cal',
... model, parent=cal, selectable=True, checkable=True,
... tooltip='all relevant info go here')
>>> cal.add_subnode(cal_1)
The model must then been added to the :class:`ReductionQTreeView` using the
:meth:`ReductionQTreeView.setModel` method as done in
:meth:`ReductionQTreeView.set_model`
"""
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import logging
import os
import pprint
import shutil
from qtpy import QtCore, QtGui, QtWidgets
import six
import vdat.utilities as vutils
import vdat.database as vdb
import vdat.config as vdatconfig
from vdat.gui.menus_actions import RemoveExposuresMenu
[docs]class ReductionNode(object):
"""A class to store nodes in the custom treeview model.
Parameters
----------
name : String
a label for the node to appear in the treeview
path : string
path associated with the name
model : :class:`ReductionTreeviewModel` instance
the model to attach the node to
type_ : string
type of the shot in the node
selectable : bool, optional
where the current node is selectable or not
checkable : bool, optional
where the current node can have a check box associated
parent : :class:`ReductionNode` instance, optional
parent of the current node
tooltip :
if not ``None`` converted to a string with :func:`pprint.pformat`
Attributes
----------
name, model, parent : as in the parameters
column : int
column index
subnodes : list
sub-nodes of the current node
"""
def __init__(self, name, path, model, type_='', selectable=False,
checkable=False, parent=None, tooltip=None):
self.name = name
self.path = path
self.model = model
self.type_ = type_
self.selectable = selectable
self.checkable = checkable
self.checked = False # nothing is checked by default
self.parent = parent
self.tooltip = self._stringify(tooltip)
self.column = 0
self.subnodes = []
[docs] def _stringify(self, value):
"""Unless ``None`` or a string, convert ``value`` to a string using
pprint.
Parameters
----------
value :
value to stringify
Returns
-------
string or None
``None`` if ``value == None``, string otherwise
"""
if value is not None and not isinstance(value, six.string_types):
value = pprint.pformat(value)
return value
[docs] def index(self):
"""Return the index of the node
Returns
-------
index : :class:`PyQt5.QtCore.QModelIndex` instance
the index to this node
"""
if self.parent:
row = self.parent.subnodes.index(self)
else:
row = 0
return self.model.createIndex(row, self.column, self)
[docs] def add_subnode(self, node):
"""Add a subnode to the current node
Parameters
----------
node : :class:`ReductionNode` instance
the subnode to add
"""
self.subnodes.append(node)
[docs] def subnode(self, row):
"""Get the child at ``row``
Parameters
----------
row : int
index of the child
Returns
-------
:class:`ReductionNode` instance
required child
"""
return self.subnodes[row]
def __str__(self):
"""Nice string representation of the node type"""
template = "{r}, name: {n}, type: {t}"
return template.format(r=self.__class__.__name__, n=self.name,
t=self.type_)
[docs]class ReductionTreeviewModel(QtCore.QAbstractItemModel):
"""A model that stores the tree structure for the treeview widget
Parameters
----------
parent : :class:`PyQt5.QtWidgets.QWidget` or derivate
parent object of the tree view model
column_title : string, optional
title of the column
"""
def __init__(self, parent=None, column_title="Reduction Browser"):
super(ReductionTreeviewModel, self).__init__(parent=parent)
self._rootnode = None
self.column_title = column_title
# at most one directory per type can be checked at a time
# key: type (str); value: checked node instance
self._checked = {}
@QtCore.Property(ReductionNode)
def rootnode(self):
"""Get the rootnode of this tree
Returns
-------
node : :class:`ReductionNode` instance
the index of the node you want to be root node
"""
return self._rootnode
@rootnode.setter
def rootnode(self, node):
"""Set the rootnode of this tree
Parameters
----------
node : :class:`ReductionNode` instance
the index of the node you want to be root node
"""
self._rootnode = node
@QtCore.Property(dict)
def checked_nodes(self):
"""Return the checked nodes.
:meth:`setData` makes sure that at most one node per type is selected
Returns
-------
dictionary
key: type (str) of the node ('sci', 'cal')
value: corresponding node instance
"""
return self._checked
[docs] def columnCount(self, parentIndex):
"""Return number of columns, for us this is always 1
Parameters
----------
parentIndex : :class:`PyQt5.QtCore.QModelIndex`
index to the parent node
Returns
-------
int
the number of columns under parent: always 1
"""
return 1
[docs] def rowCount(self, parentIndex):
"""Return the number of subnodes under a parent node
Parameters
----------
parentIndex : :class:`PyQt5.QtCore.QModelIndex`
index to the parent node
Returns
-------
nrows : int
the number of rows under parent
"""
if parentIndex.isValid():
node = parentIndex.internalPointer()
return len(node.subnodes)
else:
return 1
[docs] def data(self, index, role):
"""Return information about the specified node.
Parameters
----------
index : :class:`PyQt5.QtCore.QModelIndex` instance
the index of the node you want data for
role : int
flag specifying the type of info requested (i.e. a title or an
icon etc.)
Returns
-------
string or int
depending of the input role:
* ``PyQt5.QtCore.Qt.DisplayRole``: return the name of the node
associated with the index
* ``PyQt5.QtCore.Qt.CheckStateRole``: returns
``PyQt5.QtCore.Qt.Checked``: ``PyQt5.QtCore.Qt.Unchecked``
whether the check-box is checked or not
* ``PyQt5.QtCore.Qt.ToolTipRole``: if available, returns the
string to put in the tooltip
"""
if index.isValid():
node = index.internalPointer()
if role == QtCore.Qt.DisplayRole:
return node.name
elif role == QtCore.Qt.CheckStateRole:
if node.checkable:
if node.checked:
return QtCore.Qt.Checked
else:
return QtCore.Qt.Unchecked
else:
return
elif role == QtCore.Qt.ToolTipRole and node.tooltip:
return node.tooltip
else:
return
else:
return
[docs] def setData(self, index, value, role):
"""If the role is :class:`PyQt5.QtCore.Qt.CheckStateRole`, change the
check status of the ``index``, making sure that at most one element per
checkable node type is selected.
Parameters
----------
index : :class:`~PyQt5.QtCore.QModelIndex` instance
the index of the node you want data for
value :
value to set (ignored)
role : int
flag specifying the type of info requested (i.e. a title or an
icon etc.)
Returns
-------
success : Bool
``True`` if the operation worked
"""
success = False
conf = vdatconfig.get_config('main')
if index.isValid():
node = index.internalPointer() # get the node
if role == QtCore.Qt.CheckStateRole:
try:
# try to uncheck already checked nodes, if any
old_node = self._checked.pop(node.type_)
if old_node is not node:
# if the old node is not the same of the new one
old_node.checked = False
# make sure to update the GUI
self.dataChanged.emit(old_node.index(),
old_node.index())
conf.remove_option("redux_dirs", node.type_ + "_dir")
except KeyError:
# no previously checked node: do nothing
pass
# now toggle the current node
node.checked = not node.checked
if node.checked:
# if checked, add the node to the dictionary
self._checked[node.type_] = node
conf.set("redux_dirs", node.type_ + "_dir", node.path)
# update the GUI
self.dataChanged.emit(index, index)
success = True
return success
[docs] def flags(self, index):
'''Set the flag for every index according to the selectable/checkable
status of the corresponding node
Parameters
----------
index : :class:`~PyQt5.QtCore.QModelIndex` instance
the index of the node you want data for
Returns
-------
int
flags for the index as defined `here
<https://doc.qt.io/qt-5/qt.html#ItemFlag-enum>`_
'''
if index.isValid():
node = index.internalPointer()
if node.selectable and node.checkable:
return QtCore.Qt.ItemIsUserCheckable |\
QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled
elif node.selectable:
return QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled
else:
return QtCore.Qt.NoItemFlags
[docs] def index(self, row, column, parentIndex):
"""Return the index of the node with row, column and parent
Implementation from
https://www.mail-archive.com/pyqt@riverbankcomputing.com/msg19414.html
Parameters
----------
row, column : int
index of the row and the column
parentIndex : :class:`PyQt5.QtCore.QModelIndex`
index to the parent node
Returns
-------
:class:`PyQt5.QtCore.QModelIndex`
index of the node
"""
if self.hasIndex(row, column, parentIndex):
if parentIndex.isValid():
parent = parentIndex.internalPointer()
else:
parent = self.rootnode
return self.createIndex(row, column, parent.subnode(row))
return QtCore.QModelIndex()
[docs] def parent(self, index):
"""Return the index of the parent of the input
Parameters
----------
index : :class:`PyQt5.QtCore.QModelIndex` instance
the index you want the parent of
Returns
-------
:class:`PyQt5.QtCore.QModelIndex` instance
the index of the parent (if the node has a parent)
"""
if index.isValid():
node = index.internalPointer()
if node.parent:
return node.parent.index()
return QtCore.QModelIndex()
[docs] def insertRow(self, node, row, parent=QtCore.QModelIndex()):
"""Insert ``node`` in a new row after the given row.
Parameters
----------
node : class:`ReductionNode` instance
node to insert
row: int
index of the row after which add the ``node``
index : :class:`PyQt5.QtCore.QModelIndex` instance
index of the parent
Returns
-------
bool
``True`` if the insertion succeed, ``False`` otherwise, whatever
exception is raised is logged to the main logger
"""
try:
self.beginInsertRows(parent, row, row)
parent.internalPointer().add_subnode(node)
self.endInsertRows()
return True
except Exception:
log = logging.getLogger('logger')
log.exception("The node '%s' could not be added", node)
return False
[docs] def removeRows(self, row, count, parent=QtCore.QModelIndex()):
"""Remove ``count`` rows starting at ``row``
Parameters
----------
row: int
index of the first row to remove
count: int
number of rows to remove
index : :class:`PyQt5.QtCore.QModelIndex` instance
index of the parent
Returns
-------
bool
``True`` if the removal succeed, ``False`` otherwise, whatever
exception is raised is logged to the main logger
"""
try:
self.beginRemoveRows(parent, row, row + count)
parent_node = parent.internalPointer()
for i in range(count):
parent_node.subnodes.pop(row)
self.endRemoveRows()
return True
except Exception:
log = logging.getLogger('logger')
log.exception("The node(s) number [%d, %d) could not be removed",
row, row+count)
return False
[docs]class ReductionQTreeView(QtWidgets.QTreeView):
"""Custom tree view widget to display the reduction directories
.. list-table:: **Custom signals**
:header-rows: 1
* - Name
- Signature
- Description
* - :attr:`sig_selectionChanged`
- ``str, str``
- emitted when a use select a directory; the parameters are the full
path to the directory and its type (e.g. ``sci``, ``cal``, ``zro``)
* - :attr:`sig_exposure_removed`
- str
- emitted when an exposure is removed; the value is the ``expname``
from the database
.. list-table:: **Custom slot**
:header-rows: 1
* - Name
- Signature
- Description
* - :meth:`set_model`
-
- create a model from the database and set it
* - :meth:`on_press`
- :class:`PyQt5.QtCore.QModelIndex`
- put the path of the selected node into the config file and emit
:attr:`sig_selectionChanged`
* - :meth:`option_menu`
- :class:`PyQt5.QtCore.QPoint`
- create the actions for the right-click menu
* - :meth:`clone_slot`
-
- slot that triggers the cloning of the selected directory
* - :meth:`remove_slot`
-
- slot that triggers the removal of the selected directory
.. list-table:: **Connections between custom signals and/or slots**
:header-rows: 1
* - Signal
- Slot
* - :attr:`customContextMenuRequested`
- :meth:`option_menu`
* - :attr:`pressed`
- :meth:`on_press`
* - ``triggered`` signal of the "Clone" action
- :meth:`clone_slot`
* - ``triggered`` signal of the "Remove" action
- :meth:`remove_slot`
* - :attr:`sig_exposure_removed`
- :attr:`vdat.gui.RemoveExposuresMenu.sig_exposure_removed`
Parameters
----------
parent : :class:`PyQt5.QtWidgets.QWidget` or derivate
parent object of the tree view model
"""
sig_selectionChanged = QtCore.Signal(str, str)
sig_exposure_removed = QtCore.Signal(str)
def __init__(self, parent=None):
super(ReductionQTreeView, self).__init__(parent)
self.setObjectName("filebrowser_treeView")
self.set_model()
self._first_shown = True
# enable and create context menu (right click menu)
# inspired by:
# https://wiki.python.org/moin/PyQt/Creating%20a%20context%20menu%20for%20a%20tree%20view
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.option_menu)
# activated only when the selection changes: it's emitted after
# selectionChanged is called and can be used instead of if the
# deselected item is of no interest
self.pressed.connect(self.on_press)
[docs] @QtCore.Slot()
def set_model(self):
'''Create the model and set it.
This method is a pyqt slot.'''
with vdb.connect():
model = self.create_model()
# Add it to the panel
self.setModel(model)
self.setRootIndex(model.rootnode.index())
self.expandAll()
# force selecting no directory
self.setCurrentIndex(self.currentIndex())
[docs] def create_model(self):
"""Create the redux directory structure using the information stored in
the database
Returns
-------
model : :class:`ReductionTreeviewModel` instance
"""
model = ReductionTreeviewModel(parent=self)
redux_dir = vdatconfig.get_config('main',
section='general')['redux_dir']
root_node = ReductionNode("Nights", redux_dir, model)
model.rootnode = root_node
# search the nights
nights = [i.night for i in
vdb.VDATDir.select(vdb.VDATDir.night).distinct()]
if not nights: # no entries in the database
return model
nights.sort()
for inight, night in enumerate(nights):
night_path = os.path.join(redux_dir, night)
night_node = ReductionNode(night, night_path, model,
parent=root_node)
root_node.add_subnode(night_node)
qnight = vdb.VDATDir.select().where(vdb.VDATDir.night == night)
types = [i.type_
for i in qnight.select(vdb.VDATDir.type_).distinct()]
types.sort()
# loop through the image types
for itype, type_ in enumerate(types):
type_path = os.path.join(night_path, type_)
type_node = ReductionNode(type_, type_path, model,
parent=night_node)
night_node.add_subnode(type_node)
is_checkable_type = type_ in ['cal', 'zro']
# find all the shots in the night/type_
# first come the original, then the cloned
qshots = (qnight.where(vdb.VDATDir.type_ == type_)
.order_by(vdb.VDATDir.is_clone))
# loop through the shots
for ishot, shot in enumerate(qshots):
tooltip = shot.data_clean
if shot.zero_dir:
tooltip.update(zero_dir=shot.zero_dir.path)
if shot.cal_dir:
tooltip.update(cal_dir=shot.cal_dir.path)
shot_node = ReductionNode(shot.name, shot.path, model,
type_=type_, selectable=True,
checkable=is_checkable_type,
parent=type_node,
tooltip=tooltip)
type_node.add_subnode(shot_node)
return model
[docs] def keyPressEvent(self, event):
"""If enter is pressed, trigger the pressed signal with the currently
selected index
Parameters
----------
event : :class:`PyQt5.QtGui.QKeyEvent`
event happened
"""
if not event.isAutoRepeat() and event.key() == QtCore.Qt.Key_Enter:
self.pressed.emit(self.currentIndex())
else:
super(ReductionQTreeView, self).keyPressEvent(event)
[docs] @QtCore.Slot(QtCore.QModelIndex)
def on_press(self, index):
"""Add the path of the underlying node and emit the
:attr:`sig_selectionChanged` signal
Parameters
----------
index : :class:`PyQt5.QtCore.QModelIndex` instance
index of the selected directory
"""
conf = vdatconfig.get_config('main')
node = index.internalPointer()
if node:
conf.set("redux_dirs", "selected_dir", node.path)
self.sig_selectionChanged.emit(node.path, node.type_)
[docs] @QtCore.Slot()
def clone_slot(self):
'''Slot triggered when the clone action is clicked.
'''
index = self.currentIndex()
node = index.internalPointer()
self._clone_dir(index, node)
[docs] @QtCore.Slot()
def remove_slot(self):
'''Slot triggered when the remove action is clicked.
'''
index = self.currentIndex()
node = index.internalPointer()
with vdb.connect():
self._remove_dir(index, node)
[docs] def _clone_dir(self, index, node):
"""Clone the directory associated to ``node`` and add it to the tree
view and to the database
Parameters
----------
index : :class:`PyQt5.QtCore.QModelIndex` instance
index of the selected node
node : class:`ReductionNode` instance
selected node
"""
import vdat.gui.utils as gutils
parent_path = node.parent.path
with vdb.connect():
new_name = self._new_dir_name(node.name, parent_path)
if new_name is None: # don't do anything
return
gutils.run_wait_func_qthread(self._do_clone_dir, index, node, new_name,
parent=self)
[docs] def _do_clone_dir(self, index, node, new_name):
with vdb.connect(), vdb.database.atomic() as txn:
inserted = False
try:
# copy the data, remove the id and edit the name, path and
# ``is_clone`` attributes; then reinsert in the database as new
# entry
# copy VDATDir
vdat_dir = (vdb.VDATDir.select()
.where(vdb.VDATDir.path == node.path)).get()
vdat_dir.id = None
vdat_dir.name = new_name
vdat_dir.make_path()
vdat_dir.is_clone = True
vdat_dir.save(force_insert=True)
# copy VDATExposures
vdat_exps = (vdb.VDATExposures.select()
.where(vdb.VDATExposures.path == node.path))
for ve in vdat_exps:
ve.id = None
ve.name = new_name
ve.path = vdat_dir.path
ve.save(force_insert=True)
# insert a new node in the tree view
inserted = self._insert_row(index, new_name)
if not inserted:
txn.rollback()
return
# copy the directory
self._copy_dir(node.parent.path, node.name, new_name, vdat_dir)
except Exception:
if inserted: # remove the inserted row
model = self.model()
model.removeRow(model.rowCount(index.parent()),
index.parent())
txn.rollback()
new_dir = os.path.join(node.parent.path, new_name)
if os.path.exists(new_dir):
shutil.rmtree(new_dir)
log = logging.getLogger('logger')
log.exception("The cloning of node '%s' has failed", node)
[docs] def _new_dir_name(self, original_name, parent_path):
"""Ask the user for the new directory name.
Parameters
----------
original_name : string
name of the directory we are about to copy
parent_path : string
path of the parent directory of ``original_name``
Returns
-------
new_name : string
name of the new directory
"""
db_entry = (vdb.VDATDir.select()
.where(vdb.VDATDir.name % (original_name + "*")))
# create the default new name
n_entries = db_entry.count()
while True:
clone_dir_name = "{0}_{1:d}".format(original_name,
n_entries)
# if the name already exists, increase n_entries by one and retry
if db_entry.where(vdb.VDATDir.name == clone_dir_name).exists():
n_entries += 1
else:
break
# Open a popup window asking for the directory name
label_prefix = ""
while True:
new_name = self._clone_dialog(clone_dir_name,
label_prefix=label_prefix)
if new_name is None:
break
elif not new_name: # the text box was empty
label_prefix = "Please provide a name"
continue
# Check that the input name does not exist
if (vdb.VDATDir.select()
.where((vdb.VDATDir.name == new_name) &
(vdb.VDATDir.path % (parent_path + "*")))
.exists()):
label_prefix = "{} already exist, please find a new name."
label_prefix = label_prefix.format(new_name)
else:
break
return new_name
[docs] def _clone_dialog(self, default_text, label_prefix=""):
"""Create a text dialog with the default text.
Parameters
----------
default_text : string
text set by default in the dialog
label_prefix : string
if not empty added in the line before the standard label
Returns
-------
string
text from the dialog
"""
label = "Insert the name for the cloned directory"
if label_prefix:
label = label_prefix + "\n" + label
dialog = QtWidgets.QInputDialog(parent=self)
dialog.setLabelText(label)
dialog.setOkButtonText('Clone')
dialog.setTextValue(default_text)
dialog.setInputMode(QtWidgets.QInputDialog.TextInput)
dialogCode = dialog.exec_()
if dialogCode == QtWidgets.QDialog.Accepted:
return dialog.textValue()
else:
return None
[docs] def _copy_dir(self, parent_path, src, dst, db_entry):
"""Copy the directory ``src`` to ``dst``. Both are children of
``parent_path``.
Also set to true the ``is_clone`` entry in the shot file
Parameters
----------
parent_path : string
path where the original and new directories live
src, dst : string
copy ``src`` into ``dst``
db_entry : :class:`vdat.database.models.VDATDir`
database entry for the new directory
"""
src = os.path.join(parent_path, src)
dst = os.path.join(parent_path, dst)
shutil.copytree(src, dst, symlinks=False)
# edit the shot file in the dst directory
entries = vutils.read_shot_file(dst)
append = False
for l in entries:
l['is_clone'] = True
# copy the following entries from the new database entry
for k in ['name', 'path']:
l[k] = getattr(db_entry, k)
vutils.write_to_shot_file(dst, append=append, **l)
append = True
# edit the exp file in the dst directory
entries = vutils.read_exps_file(dst)
append = False
for l in entries:
# copy the following entries from the new database entry
for k in ['name', 'path']:
l[k] = getattr(db_entry, k)
vutils.write_to_exps_file(dst, append=append, **l)
append = True
[docs] def _insert_row(self, index, new_name):
"""Clone ``node``, update it and insert it
Parameters
----------
index : :class:`PyQt5.QtCore.QModelIndex` instance
index of the selected node
new_name : string
name of the new entry
Returns
-------
bool
whether the insertion is successful or not
"""
node = index.internalPointer()
# get the index and the node of the parent
parent_index = index.parent()
parent_node = parent_index.internalPointer()
# get the model
model = self.model()
# get the number of rows of the parent
n_rows = model.rowCount(parent_index)
# create the new node and append it
new_node = ReductionNode(new_name,
os.path.join(os.path.dirname(node.path),
new_name),
model, type_=node.type_,
selectable=node.selectable,
checkable=node.checkable, parent=parent_node)
return model.insertRow(new_node, n_rows, parent_index)
[docs] def _remove_dir(self, index, node):
"""Remove the directory associated to ``node`` and remove it from the tree
view and from the database.
Parameters
----------
index : :class:`PyQt5.QtCore.QModelIndex` instance
index of the selected node
node : class:`ReductionNode` instance
selected node
"""
import vdat.gui.utils as gutils
# open a dialog asking if you are sure
do_remove = self._confirm_remove_dialog(node.name)
if not do_remove:
return
with gutils.wait_cursor():
# remove from the gui
model = self.model()
model.removeRow(index.row(), index.parent())
# remove from the file system
# stop making the thumbnails to avoid crashes when removing the
# directory
# TODO: find a way to make sure that no more pngs are being created
# TODO: does the thumbnail creation uses this?
shutil.rmtree(node.path)
# remove from the database
db_entry = vdb.VDATDir.get(vdb.VDATDir.path == node.path)
db_entry.delete_instance()
for db_entry in (vdb.VDATExposures.select()
.where(vdb.VDATExposures.path == node.path)):
db_entry.delete_instance()
[docs] def _confirm_remove_dialog(self, dir_name):
"""Create the dialog to ask if you are sure
Parameters
----------
dir_name : string
name of the directory
Returns
-------
bool
where the directory can be removed
"""
box = QtWidgets.QMessageBox(parent=self)
box.setText("The directory '{}' will be removed. The removal is not"
" reversible".format(dir_name))
box.setInformativeText("Do you want to proceed?")
box.setStandardButtons(QtWidgets.QMessageBox.Yes |
QtWidgets.QMessageBox.No)
box.setDefaultButton(QtWidgets.QMessageBox.Yes)
box.setIcon(QtWidgets.QMessageBox.Information)
pressed = box.exec_()
return pressed == QtWidgets.QMessageBox.Yes
[docs] def showEvent(self, event):
'''When the :class:`ReductionQTreeView` becomes visible, this method is
triggered by Qt.
Parameters
----------
event : :class:`PyQt5.QtGui.QShowEvent`
the tab is show
'''
super(ReductionQTreeView, self).showEvent(event)
if self._first_shown and self.verticalScrollBar().isVisible():
self.collapseAll()
self._first_shown = False