From e5eaccb14e23aea8a306ea6ab05d8d88500f1002 Mon Sep 17 00:00:00 2001
From: "Verbeek, J.M. (Janneke)" <j.verbeek@student.ru.nl>
Date: Mon, 2 Dec 2019 14:22:32 +0100
Subject: [PATCH] Can now read multiple files, solved some GUI bugs, can also
 write multiple files

---
 umbra/controller.py | 73 ++++++++++++++++++++++++++++++++-------------
 umbra/filereader.py | 18 +++++------
 umbra/model.py      | 10 ++++---
 umbra/utils.py      | 31 +++++++++++++++++--
 umbra/view.py       | 64 ++++++++++++++++++++++-----------------
 5 files changed, 131 insertions(+), 65 deletions(-)

diff --git a/umbra/controller.py b/umbra/controller.py
index 942c83da..b2108fee 100644
--- a/umbra/controller.py
+++ b/umbra/controller.py
@@ -32,7 +32,6 @@ class Controller:
             key(str): Identifier for view functionality
         """
         action, *type = key.split(' ')  # type is a (possibly empty) list
-
         if action == 'select':
             self._select_files(type[0])
         elif action == 'delete':
@@ -47,21 +46,17 @@ class Controller:
             self._filewriter = TxtWriter()
         elif action == 'select_folder':
             self._select_folder()
+        elif action == 'rm_all':
+            self._delete_files(type[0], True)
 
-    def _select_folder(self):
+    def _select_folder(self, type="shadow"):
         folder_path = fd.askdirectory()
         file_names = os.listdir(folder_path)
         file_paths = [folder_path + "/" + x for x in file_names]
-        self._shadow_files = file_paths # For the record
-        for file_path in file_paths:
-            if "AO" not in file_path:
-                pass  # TODO: raise error
-            else:
-                dict = self._model._multi_data_shadow
-                key = ut.shadow_regex(file_path)
-                self._filereader.path(file_path)
-                data = self._filereader.read()
-                self._model.add_to_dict(key, data, dict)
+        self._shadow_files = file_paths
+        self._view.update_files(file_paths, type)
+        self._view._button_status("select {}".format(type), "disabled")
+
 
     def _select_files(self, type):
         """Select files and add them to files list corresponding to type.
@@ -74,20 +69,29 @@ class Controller:
         # TODO: Check if not already present
         files.extend(selection)
         self._view.update_files(files, type)
+        if type == "shadow":
+            self._view._button_status("select_folder", "disable")
 
-    def _delete_files(self, type):  # TODO: Not yet implemented fully
+    def _delete_files(self, type, all=False):  # TODO: Not yet implemented fully
         """Remove selected file from files list corresponding to type.
 
         Args:
             type (str): Role of files ('source' or 'shadow')
+            all (boolean): whether to remove all files, by default false.
         """
         selection = self._view.selected(type)
         files = getattr(self, '_{}_files'.format(type))
-
         if selection is not None:  # TODO: This is supposed to make sure deletion of 'No file selected' is impossible, but does not work.
-            selection = [file for file in files if selection.lower() in file.lower()][0]
-            files.remove(selection)
-            self._view.update_files(files, type)  # TODO: File remains in drop down options
+            if all:
+                selection = [file for file in files]
+                for s in selection:
+                    files.remove(s)
+                    self._view._button_status("select {}".format(type), "normal")
+            else:
+                selection = [file for file in files if selection.lower() in file.lower()][0]
+                files.remove(selection)
+                self._view._button_status("select_folder".format(type), "normal")
+            self._view.update_files(files, type)
 
     def _compare_files(self):
         """Check source and shadow data availability, and compare."""
@@ -97,8 +101,13 @@ class Controller:
             self._view.update_message('no shadow')
         else:
             self._view.update_message('files ok')
+
+            if len(self._shadow_files) > 1:
+                self._read_folder("shadow")
+            else:
+                self._read_files("shadow")
+
             self._read_files("source")
-            self._read_files("shadow")
             self._model.compare()
             self._view.update_message('comparison complete')
 
@@ -107,9 +116,14 @@ class Controller:
         if not self._model.analysis_complete():
             self._view.update_message('no comparison')
         else:
-            path = self._view.ask_save_location()
-            results = self._model._analysis_results
-            self._filewriter.write(path, results)
+            if self._model._analysis_results is not None:
+                path = self._view.ask_save_location()
+                results = self._model._analysis_results
+                self._filewriter.write(path, results)
+            else:
+                path = fd.askdirectory()
+                results = self._model._multi_results
+                self._filewriter.write_multiple(path, results)
             # results are of form sc, sh, info
             self._view.update_message('saved')
 
@@ -129,3 +143,20 @@ class Controller:
             self._model._id = ut.id_regex(path)
         else:
             self._model._data_shadow = data
+
+    def _read_folder(self, type):
+        """
+        Read in multiple source files.
+        Extensible to consider "type". Now assume shadow
+        files.
+        """
+        for file_path in self._shadow_files:
+            if "AO" not in file_path or ".csv" not in file_path:
+                # Very dirty
+                pass  # TODO: raise error
+            else:
+                dict = self._model._multi_data_shadow
+                key = ut.shadow_regex(file_path)
+                self._filereader.path(file_path)
+                data = self._filereader.read()
+                ut.add_to_dict(key, data, dict)
diff --git a/umbra/filereader.py b/umbra/filereader.py
index 784b491f..dd5ee449 100644
--- a/umbra/filereader.py
+++ b/umbra/filereader.py
@@ -60,10 +60,6 @@ class FileReader (ABC):
         self._path = pth
 
 
-
-
-
-
 class CSVReader(FileReader):
 
     def __init__(self, path, type):
@@ -143,7 +139,6 @@ class TxtWriter(FileWriter):
             result: the results, type unknown
         """
         # For now assume source, shadow, stat format
-
         source_results, shadow_results, info = results
         final = np.empty(0)
         for i in range(len(source_results)):
@@ -193,15 +188,18 @@ class CSVWriter(FileWriter):
             writer.writerows(sc)
             writer.writerow(info)
 
+    def write_multiple(self, dir, dict):
+        for key, results in dict.items():
+            path = self._participant_path(dir, key)
+            self.write(path, results)
+
     # TODO: Test per-participant writing
-    def _participant_path(part_number):
+    def _participant_path(self, dir, key):
         """
         Create a path for the appropriate participant.
         Args: int part_number, participant number.
         Returns:
             participath: string, a path towards a directory for participant part_number
         """
-        pth = self.path+"/"+part_number
-        os.mkdir(pth)
-        participath = pth+"/"+part_number
-        return participath
+        pth = dir+"/"+key
+        return pth
diff --git a/umbra/model.py b/umbra/model.py
index ab1705a3..69027142 100644
--- a/umbra/model.py
+++ b/umbra/model.py
@@ -1,5 +1,5 @@
 from statistics import Statistics
-
+from utils import Utils as ut
 
 class Model:
     """Internal data representation and processing."""
@@ -60,21 +60,23 @@ class Model:
 
     def analysis_complete(self):
         """Check whether self._analysis_results has a value."""
-        return self._analysis_results is not None
+        return self._analysis_results is not None \
+            or self._multi_results is not None
 
     def compare(self):
         """"Run the analyses and saves the results."""
         if self.has_source() and self.has_shadow():
-            print(self._multi_data_shadow, self.has_shadow())
             if self._multi_data_shadow:
                 for key, data in self._multi_data_shadow.items():
-                    if self._id in key:
+                    print(key,self._id, ut.filter_key(key))
+                    if self._id is ut.filter_key(key):
                         result = self._stats.analyze(self._data_source, data)
                         self.add_to_dict(key+"_res", result, self._multi_results)
                     else:
                         print("passed")
                         pass
             else:
+                print("Single analysis")
                 self._analysis_results = \
                     self._stats.analyze(self._data_source, self._data_shadow)
         else:
diff --git a/umbra/utils.py b/umbra/utils.py
index 4d169ab1..6e03cf40 100644
--- a/umbra/utils.py
+++ b/umbra/utils.py
@@ -37,13 +37,38 @@ class Utils:
         return "%02i:%02i:%02i.%03i" % (h, m, s, ms)
 
     def shadow_regex(path):
-        match = re.search("AO\d", path)
+        match = re.search("\d_AO[0-9]+", path)
         if match:
-            return match.group()
+            key = match.group()
+        if ".csv" in path:
+            key = key+"csv"
+        return key
+
+    def filter_key(string):
+        match = re.search("AO[0-9]+", string)
+        if match:
+            if len(match.group()[2:]) > 2:
+                return match.group()[2:4]
+            return match.group()[2:]
 
     def id_regex(path):
         match = re.search("\d\.TextGrid", path)
         # Make sure that not different number accidentally
         if match:
             return match.group()[0]
-    
+
+
+    def add_to_dict(key, value, dict):
+        """Checks if key already exists in a dictionary
+        before adding new key-value pair
+
+        Args:
+            key: Key to add to dictionary
+            value: Corresponding value to add to dictionary
+            dict: Dictionary to add key-value pair to
+        """
+        if key not in dict:
+            dict[key] = value
+        else:
+            raise ValueError("Key '{}' already exists in {}."
+                             .format(key, dict))
diff --git a/umbra/view.py b/umbra/view.py
index c1fada4a..5878ecca 100644
--- a/umbra/view.py
+++ b/umbra/view.py
@@ -1,6 +1,7 @@
 import tkinter as tk
 from tkinter import ttk, W, filedialog  # tk.ttk yields an AttributeError
 from functools import partial
+from utils import Utils as ut
 
 class View:
     """Handle GUI and I/O elements of the program."""
@@ -12,7 +13,8 @@ class View:
         self._elements = {}  # Dict to avoid large number of attributes
         self._selected_source = "No file selected"
         self._selected_shadow = "No file selected"
-        self._options = Options(self)
+        # self._options = Options(self)
+        # Options are not redundant quite yet
         self._actionlistener = None
 
         # Initialisation of visual and iteractive elements is listed top-down
@@ -73,6 +75,7 @@ class View:
 
         if paths == []:
             filebox.set("No file selected")
+            filebox['values'] = []
         else:
             filenames = []
             for path in paths:
@@ -123,7 +126,9 @@ class View:
                           'delete shadow',
                           'compare',
                           'save',
-                          'select_folder'
+                          'select_folder',
+                          'rm_all source',
+                          'rm_all shadow'
 
                           ]
         if key in controllerkeys:
@@ -178,7 +183,17 @@ class View:
         self._create_button('delete {}'.format(type),
                            frame,
                            'Delete {}'.format(type),
-                           6, column)
+                           7, column)
+        self._create_button('rm_all {}'.format(type),
+                           frame,
+                           'Delete all {} files'.format(type),
+                           8, column)
+
+        if type == 'shadow':
+            self._create_button('select_folder', frame,
+                                'Select {} folder'.format(type),
+                                6, column)
+
 
 
     def _create_button(self, key, frame, text, row, column):
@@ -200,7 +215,7 @@ class View:
         action_function = partial(self._perform_action, key=key)
         button.bind('<Button>', action_function)
 
-        self._add_to_dict(key, button, self._elements)
+        ut.add_to_dict(key, button, self._elements)
 
     def _create_combobox(self, key, frame, type, row, column):
         """Create combobox, a drop-down file selection widget.
@@ -214,19 +229,17 @@ class View:
         Raises:
             ValueError: Key already exists
         """
-
-        selection = tk.StringVar()
-        selection.set("No file selected")  # TODO: Does not show up initially
         filebox = ttk.Combobox(frame,
-                               textvariable=selection,
                                state="readonly",
                                width=20)
+        filebox.set("No file selected")
+
         filebox.grid(column=column, row=row, sticky=W, padx=10, pady=10)
 
         action_function = partial(self._perform_action, key=key)
         filebox.bind('<Button>', action_function)
 
-        self._add_to_dict(key, filebox, self._elements)
+        ut.add_to_dict(key, filebox, self._elements)
 
     def _create_label(self, key, frame, text, row, column):
         """Create textual label and place it in the given frame.
@@ -242,23 +255,11 @@ class View:
         """
         label = tk.Label(frame, text=text)
         label.grid(column=column, row=row, columnspan=2)
-        self._add_to_dict(key, label, self._elements)
+        ut.add_to_dict(key, label, self._elements)
 
-    @staticmethod
-    def _add_to_dict(key, value, dict):  # TODO: Definitely better in utils
-        """Checks if key already exists in a dictionary
-        before adding new key-value pair
-
-        Args:
-            key: Key to add to dictionary
-            value: Corresponding value to add to dictionary
-            dict: Dictionary to add key-value pair to
-        """
-        if key not in dict:
-            dict[key] = value
-        else:
-            raise ValueError("Key '{}' already exists in {}."
-                             .format(key, dict))
+    def _button_status(self, key, mode):
+        button = self._elements[key]
+        button['state'] = mode
 
     @property
     def actionlistener(self):
@@ -270,7 +271,7 @@ class View:
 
     def selected(self, type):
         selected = self._elements['file {}'.format(type)].get()
-        if selected == "No file selected":
+        if selected == "No file selected" or not selected:
             return None
         else:
             return selected
@@ -288,19 +289,28 @@ class Options(View):
         self._build_menu()
 
     def _build_menu(self):
+        """
+        Build the menu.
+        """
         self._add_menu_tab("File", self._menu_tabs)
         self._add_menu_tab("Options", self._menu_tabs)
         self._add_command("File", "Select folder", "select_folder")
         for tab in self._menu_tabs:
             self._menu_bar.add_cascade(label=tab, menu=self._menu_tabs[tab])
         self._view._window.config(menu=self._menu_bar)
-        self._view._add_to_dict("menu", self._menu_bar, self._view._elements)
+        ut.add_to_dict("menu", self._menu_bar, self._view._elements)
 
     def _add_menu_tab(self, key, dict):
+        """
+        Add a tab to the menu.
+        """
         tab = tk.Menu(self._menu_bar, tearoff=0)
         dict[key] = tab
 
     def _add_command(self, tab, label, command):
+        """
+        Add a command to the menu.
+        """
         tab = self._menu_tabs[tab]
         action_function = partial(self._view._perform_action, key=command)
         tab.add_command(label=label)
-- 
GitLab