''' This module contains class DataTabFrame that creates tab with all data.
'''
import xlrd
import os
from tkinter import StringVar, E, N, W, S, Toplevel
from tkinter.ttk import Frame, Button, LabelFrame
from tkinter.filedialog import askopenfilename, asksaveasfilename
from tkinter.messagebox import askyesno
from pyDEA.core.gui_modules.table_gui import TableFrameWithInputOutputBox
from pyDEA.core.gui_modules.navigation_frame_gui import NavigationForTableFrame
from pyDEA.core.gui_modules.table_modifier_gui import TableModifierFrame
from pyDEA.core.utils.dea_utils import TEXT_FOR_PANEL, FILE_TYPES, center_window
from pyDEA.core.utils.dea_utils import calculate_nb_pages
from pyDEA.core.data_processing.read_data_from_xls import read_data
from pyDEA.core.gui_modules.load_xls_gui import AskSheetName
from pyDEA.core.data_processing.save_data_to_file import save_data_to_xls
[docs]def remove_star(file_name):
data_file = file_name
if file_name:
if '*' in file_name:
data_file = data_file[:-1]
return data_file
[docs]class DataTabFrame(Frame):
''' This class creates frame with all widgets needed for working with data
(e.g. loading, saving, modifying, etc.)
Attributes:
parent (Tk object): parent of the frame.
data_from_params_file (StringVar): StringVar object that contains
name of data file. It is used for communication between this
frame and parameters frame.
When parameters are uploaded from file, then file name with
data might be updated,
and, hence, this frame must also be updated. If file name is set
to empty string, then
this framed is cleared (everything will be deleted). If file
name points to actual file,
then data from this file will be uploaded and displayed on
this frame
panel (LabelFrame): frame that holds table with data and name of
the currently loaded data file
table (TableFrameWithInputOutputBox): object that knows how to
display data in table and how to modify it
params_frame (Notebook): frame with parameters. When all data is
cleared, then we also need to clear parameters
data (list of list of str or float): stores all loaded or entered
data without categories,
for example:
>>> [["A", 1, 2, 3], ["B", 1, "", 5], ["C", "", "a", "b"]]
if_text_modified_str (StringVar): is used for adding star to file
name if it was modified.
Technically it is not necessary to store this variable,
but it is used in unit tests.
sheet_name (str): name of the sheet in xls-file from where data
has been loaded
navigation_frame (NavigationForTableFrame): frame that knows how
to navigate table with data
table_modifier_frame (TableModifierFrame): frame that adds and
removes rows and columns of data table
Args:
parent (Tk object): parent of this frame
params_frame (Notebook): frame with parameters. When all data is
cleared, then we also need to clear parameters
current_categories (list of str): list with categories, it is
shared and modified by several objects
data_from_params_file (StringVar): StringVar object that contains
name of data file.
It is used for communication between this frame and
parameters frame
str_var_for_input_output_boxes (ObserverStringVar): object that
contains current input and output categories and notifies
table with data when data is loaded
and some categories need to be checked
'''
def __init__(self, parent, params_frame, current_categories,
data_from_params_file,
str_var_for_input_output_boxes, *args, **kw):
super().__init__(parent, *args, **kw)
self.parent = parent
data_from_params_file.trace("w", self.on_params_file_change)
self.data_from_params_file = data_from_params_file
self.panel = None
self.table = None
self.params_frame = params_frame
self.data = []
self.if_text_modified_str = StringVar()
self.if_text_modified_str.trace('w', self.on_data_modify)
self.sheet_name = ''
self.navigation_frame = None
self.table_modifier_frame = None
self.create_widgets(current_categories, str_var_for_input_output_boxes)
def _create_btns(self):
''' Creates load and save buttons.
'''
load_button = Button(self, text='Load', command=self.load_file)
load_button.grid(row=0, column=0, pady=5, padx=5, sticky=W+N)
frame_for_save_btns = Frame(self)
save_button = Button(frame_for_save_btns, text='Save',
command=self.save_data)
save_button.grid(row=0, column=0, pady=2, padx=5, sticky=E+N)
save_as_button = Button(frame_for_save_btns, text='Save as...',
command=self.save_data_as)
save_as_button.grid(row=1, column=0, pady=2, padx=5, sticky=E+N)
frame_for_save_btns.grid(row=0, column=1, pady=5, padx=5, sticky=E+N)
def _create_label_frame(self):
''' Creates label frame.
'''
self.panel = panel = LabelFrame(self, text=TEXT_FOR_PANEL)
panel.columnconfigure(0, weight=1)
panel.rowconfigure(0, weight=1)
panel.grid(row=1, column=0,
padx=5, pady=5, sticky=E+W+S+N)
[docs] def get_data_file_name(self):
''' Returns name of a file with data if it was loaded.
File name is stored in label frame.
'''
data_file = self.panel.cget('text')
nb_letters = len(TEXT_FOR_PANEL)
return data_file[nb_letters:]
[docs] def remove_star_from_panel(self):
''' Removes star from data file name if it was there.
'''
data_file = self.get_data_file_name()
data_file = remove_star(data_file)
self.panel.config(text=TEXT_FOR_PANEL + data_file)
[docs] def save_data(self):
''' Saves data to an existing or new file.
This method is called when user presses "Save" button.
If user modified open data file, then modifications will
be saved in this file.
If user typed data, this method redirects user to
save_data_as() method.
'''
data_file = self.get_data_file_name()
if data_file == '*': # when file was created and modified
self.save_data_as()
elif data_file:
self.save_data_to_given_file(remove_star(data_file),
sheet_name=self.sheet_name)
self.remove_star_from_panel()
[docs] def save_data_as(self):
''' Saves data to a new file.
Calls dialogue to ask user where to save file and saves
data file there. Default extension is .xls.
'''
file_name = self._call_file_save_as_dialog()
if file_name:
self.save_data_to_given_file(file_name)
self.panel.config(text=TEXT_FOR_PANEL + file_name)
def _call_file_save_as_dialog(self):
''' Calls save as dialogue.
This method is redefined in unit tests.
'''
return asksaveasfilename(filetypes=FILE_TYPES, defaultextension='.xls')
[docs] def save_data_to_given_file(self, data_file, sheet_name='Data'):
''' Saves data to a given file.
Args:
data_file (str): file name where data should be stored.
sheet_name (str): sheet name where data should be stored.
Defaults to "Data".
'''
categories = [self.table.cells[0][col].get() for col
in range(self.table.nb_cols)]
save_data_to_xls(data_file, categories, self.data, sheet_name)
[docs] def on_data_modify(self, *args):
''' Adds star to label frame when data was modified.
Args:
*args: are not used in this method, but are provided by
StringVar trace routine.
'''
data_file = self.panel.cget('text')
if data_file and '*' not in data_file:
self.panel.config(text=data_file + '*')
[docs] def load_file(self):
''' Asks user which data file should be loaded and loads specified file.
Only xls, xlsx and csv files are allowed.
'''
file_name = self._call_open_file_dialogue()
if file_name:
self.show_loaded_data(file_name)
def _call_open_file_dialogue(self):
''' Calls open file dialogue. This method is overridden in unit tests.
Returns:
(str): file name of file that should be loaded or empty string
if the user pressed cancel
'''
return askopenfilename(title='Choose a file', filetypes=FILE_TYPES)
[docs] def clear_all_on_btn_click(self):
''' Clears all data if user agrees.
Asks the user if he/she wants to clear all data, clears all data
if user agrees.
This method is called when user presses button "Clear all".
'''
if (self.data and
askyesno("Verify",
"Are sure you want to delete all input data?")):
self.clear_all()
[docs] def clear_all(self):
''' Clears all data.
'''
self.table.clear_all_data()
self.table.deselect_all_boxes()
self.params_frame.clear_all()
self.panel.config(text=TEXT_FOR_PANEL)
self.parent.reset_progress_bar()
self.sheet_name = ''
self.navigation_frame.reset_navigation()
[docs] def on_params_file_change(self, *args):
''' Loads data or clears all after parameters are loaded from file.
If file with parameters contains data file, then this data
will be attempted to load.
If file with parameters doesn't contain data file
(or user specified to load parameters
without data), then all previously loaded data will remain.
Args:
*args: are not used in this methods and are provided by
trace method of StringVar
'''
params_file = self.data_from_params_file.get()
if params_file:
self.show_loaded_data(params_file)
[docs] def show_loaded_data(self, file_name):
''' Loads data file.
This method asks user from what sheet data must be loaded if
there are more then 1 sheet
in data file. Changes name of data file in label frame and
calls method that displays
data in the table.
Args:
file_name (str): data file
'''
just_name, extension = os.path.splitext(file_name)
should_proceed = False
if extension in ['.xls', '.xlsx']:
book = xlrd.open_workbook(file_name)
names = book.sheet_names()
nb_names = len(names)
if nb_names == 0:
return
elif nb_names > 1:
self.sheet_name = self.call_dialog(names)
else:
self.sheet_name = names[0]
if self.sheet_name:
should_proceed = True
else:
self.sheet_name = '' # csv does not support sheets
should_proceed = True
if should_proceed:
sheet_name_copy = self.sheet_name
self.clear_all()
# set data file name in parameters
# (this field is cleared in clear_all())
self.params_frame.params.update_parameter('DATA_FILE', file_name)
self.sheet_name = sheet_name_copy
self.panel.config(text=TEXT_FOR_PANEL + file_name)
categories, coefficients, dmu_name, self.sheet_name = read_data(
file_name, self.sheet_name)
self.show_data(categories, coefficients, dmu_name)
self.remove_star_from_panel()
[docs] def call_dialog(self, names):
''' Calls dialogue that asks user to specify sheet name from where
data should be loaded.
Args:
names (list of str): list of sheet names
Returns:
(str): name of the selected sheet if sheet was selected,
empty string if user pressed cancel
'''
win = Toplevel()
win.withdraw()
ask_sheet_name_frame = AskSheetName(win, names)
var = ask_sheet_name_frame.sheet_name_str
ask_sheet_name_frame.pack()
center_window(win)
win.grab_set()
win.focus_set()
win.wait_window()
return var.get()
[docs] def show_data(self, categories, coefficients, dmu_name):
''' Fills table with data.
Args:
categories (list of str): list with categories that will
appear in the first table row
coefficients (list of list of str or float): data that will
appear after categories.
Each contained list will be displayed on a new table row
dmu_name (str): text that appear in the same line with
categories before the categories appear
'''
self.table.cells[0][0].insert(0, dmu_name)
nb_needed_cols = len(categories) + 1
for i in range(self.table.nb_cols, nb_needed_cols):
self.table.add_column()
# print categories
for count, category in enumerate(categories):
self.table.cells[0][count + 1].insert(0, category)
# print data
self.data.clear()
for value in coefficients:
self.data.append(value)
self.table.load_visible_data()
nb_data_pages = calculate_nb_pages(len(self.data), self.table.nb_rows)
self.navigation_frame.set_navigation(nb_data_pages)
[docs] def read_coefficients(self):
''' Converts coefficients into a proper data structure
used for running algorithms.
Returns:
see read_data_from_xls for details
'''
return self.table.read_coefficients()