import platform from os import environ from tempfile import mkdtemp import tkinter as tk from tkinter import ttk from tkinter import filedialog from tkinter import messagebox from tkinter.font import nametofont from datetime import datetime from . import views as v from . import models as m from .mainmenu import get_main_menu_for_os from .images import ABQ_LOGO_32, ABQ_LOGO_64 from . import network as n class Application(tk.Tk): """Application root window""" config_dirs = { 'Linux': environ.get('$XDG_CONFIG_HOME', '~/.config'), 'freebsd7': environ.get('$XDG_CONFIG_HOME', '~/.config'), 'Darwin': '~/Library/Application Support', 'Windows': '~/AppData/Local' } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.title("ABQ Data Entry Application") self.resizable(width=False, height=False) self.taskbar_icon = tk.PhotoImage(file=ABQ_LOGO_64) self.call('wm', 'iconphoto', self._w, self.taskbar_icon) self.logo = tk.PhotoImage(file=ABQ_LOGO_32) tk.Label(self, image=self.logo).grid(row=0) self.inserted_rows = [] self.updated_rows = [] datestring = datetime.today().strftime("%Y-%m-%d") default_filename = "abq_data_record_{}.csv".format(datestring) self.filename = tk.StringVar(value=default_filename) # settings model & settings config_dir = self.config_dirs.get(platform.system(), '~') self.settings_model = m.SettingsModel(path=config_dir) self.load_settings() self.set_font() self.settings['font size'].trace('w', self.set_font) style = ttk.Style() theme = self.settings.get('theme').get() if theme in style.theme_names(): style.theme_use(theme) # create data model self.database_login() if not hasattr(self, 'data_model'): self.destroy() return self.callbacks = { 'file->select': self.on_file_select, 'file->quit': self.quit, 'show_recordlist': self.show_recordlist, 'new_record': self.open_record, 'on_open_record': self.open_record, 'on_save': self.on_save, 'get_seed_sample': self.get_current_seed_sample, 'get_check_tech': self.get_tech_for_lab_check, 'update_weather_data': self.update_weather_data, 'upload_to_corporate_rest': self.upload_to_corporate_rest, 'upload_to_corporate_ftp': self.upload_to_corporate_ftp } menu_class = get_main_menu_for_os(platform.system()) menu = menu_class(self, self.settings, self.callbacks) self.config(menu=menu) # The data record form self.recordform = v.DataRecordForm( self, self.data_model.fields, self.settings, self.callbacks) self.recordform.grid(row=1, padx=10, sticky='NSEW') # The data record list self.recordlist = v.RecordList( self, self.callbacks, inserted=self.inserted_rows, updated=self.updated_rows ) self.recordlist.grid(row=1, padx=10, sticky='NSEW') self.populate_recordlist() # status bar self.status = tk.StringVar() self.statusbar = ttk.Label(self, textvariable=self.status) self.statusbar.grid(sticky="we", row=3, padx=10) self.records_saved = 0 def show_recordlist(self): """Show the recordform""" self.recordlist.tkraise() def populate_recordlist(self): try: rows = self.data_model.get_all_records() except Exception as e: messagebox.showerror( title='Error', message='Problem reading file', detail=str(e) ) else: self.recordlist.populate(rows) def open_record(self, rowkey=None): """Rowkey must be a tuple of (Date, Time, Lab, Plot)""" if rowkey is None: record = None else: try: record = self.data_model.get_record(*rowkey) except Exception as e: messagebox.showerror( title='Error', message='Problem reading file', detail=str(e) ) return self.recordform.load_record(rowkey, record) self.recordform.tkraise() def on_save(self): """Handles save button clicks""" # Check for errors first errors = self.recordform.get_errors() if errors: message = "Cannot save record" detail = "The following fields have errors: \n * {}".format( '\n * '.join(errors.keys()) ) self.status.set( "Cannot save, error in fields: {}" .format(', '.join(errors.keys())) ) messagebox.showerror(title='Error', message=message, detail=detail) return False data = self.recordform.get() try: self.data_model.save_record(data) except Exception as e: messagebox.showerror( title='Error', message='Problem saving record', detail=str(e) ) self.status.set('Problem saving record') else: self.records_saved += 1 self.status.set( "{} records saved this session".format(self.records_saved) ) key = (data['Date'], data['Time'], data['Lab'], data['Plot']) if self.data_model.last_write == 'update': self.updated_rows.append(key) else: # we just inserted a row self.inserted_rows.append(key) self.populate_recordlist() # Only reset the form when we're appending records if self.data_model.last_write == 'insert': self.recordform.reset() def on_file_select(self): """Handle the file->select action from the menu""" filename = filedialog.asksaveasfilename( title='Select the target file for saving records', defaultextension='.csv', filetypes=[('CSV', '*.csv *.CSV')] ) if filename: self.filename.set(filename) self.data_model = m.CSVModel(filename=self.filename.get()) self.populate_recordlist() self.inserted_rows = [] self.updated_rows = [] def save_settings(self, *args): """Save the current settings to a preferences file""" for key, variable in self.settings.items(): self.settings_model.set(key, variable.get()) self.settings_model.save() def load_settings(self): """Load settings into our self.settings dict.""" vartypes = { 'bool': tk.BooleanVar, 'str': tk.StringVar, 'int': tk.IntVar, 'float': tk.DoubleVar } # create our dict of settings variables from the model's settings. self.settings = {} for key, data in self.settings_model.variables.items(): vartype = vartypes.get(data['type'], tk.StringVar) self.settings[key] = vartype(value=data['value']) # put a trace on the variables so they get stored when changed. for var in self.settings.values(): var.trace('w', self.save_settings) def set_font(self, *args): font_size = self.settings['font size'].get() font_names = ('TkDefaultFont', 'TkMenuFont', 'TkTextFont') for font_name in font_names: tk_font = nametofont(font_name) tk_font.config(size=font_size) def database_login(self): """Try to login to the database and create self.data_model""" error = '' db_host = self.settings['db_host'].get() db_name = self.settings['db_name'].get() title = "Login to {} at {}".format(db_name, db_host) while True: login = v.LoginDialog(self, title, error) if not login.result: break else: username, password = login.result try: self.data_model = m.SQLModel( db_host, db_name, username, password) except m.pg.OperationalError: error = "Login Failed" else: break def get_current_seed_sample(self, *args): if not ( hasattr(self, 'recordform') and self.settings['autofill sheet data'].get() ): return data = self.recordform.get() plot = data['Plot'] lab = data['Lab'] if plot and lab: seed = self.data_model.get_current_seed_sample(lab, plot) self.recordform.inputs['Seed sample'].set(seed) self.recordform.focus_next_empty() def get_tech_for_lab_check(self, *args): if not ( hasattr(self, 'recordform') and self.settings['autofill sheet data'].get() ): return data = self.recordform.get() date = data['Date'] time = data['Time'] lab = data['Lab'] if all([date, time, lab]): check = self.data_model.get_lab_check(date, time, lab) tech = check['lab_tech'] if check else '' self.recordform.inputs['Technician'].set(tech) self.recordform.focus_next_empty() def update_weather_data(self): try: weather_data = n.get_local_weather( self.settings['weather_station'].get() ) except Exception as e: messagebox.showerror( title='Error', message='Problem retrieving weather data', detail=str(e) ) self.status.set('Problem retrieving weather data') else: self.data_model.add_weather_data(weather_data) self.status.set( 'Weather data recorded for {}' .format(weather_data['observation_time_rfc822']) ) def _create_csv_extract(self): tmpfilepath = mkdtemp() csvmodel = m.CSVModel( filename=self.filename.get(), filepath=tmpfilepath) records = self.data_model.get_all_records() if not records: return None for record in records: csvmodel.save_record(record) return csvmodel.filename def upload_to_corporate_rest(self): csvfile = self._create_csv_extract() if csvfile is None: messagebox.showwarning( title='No records', message='There are no records to upload' ) return d = v.LoginDialog( self, 'Login to ABQ Corporate REST API' ) if d.result is not None: username, password = d.result else: return try: n.upload_to_corporate_rest( csvfile, self.settings['abq_upload_url'].get(), self.settings['abq_auth_url'].get(), username, password) except n.requests.RequestException as e: messagebox.showerror('Error with your request', str(e)) except n.requests.ConnectionError as e: messagebox.showerror('Error connecting', str(e)) except Exception as e: messagebox.showerror('General Exception', str(e)) else: messagebox.showinfo( 'Success', '{} successfully uploaded to REST API.'.format(csvfile) ) def upload_to_corporate_ftp(self): csvfile = self._create_csv_extract() d = v.LoginDialog( self, 'Login to ABQ Corporate FTP' ) if d.result is not None: username, password = d.result try: n.upload_to_corporate_ftp( csvfile, self.settings['abq_ftp_host'].get(), self.settings['abq_ftp_port'].get(), username, password ) except n.ftp.all_errors as e: messagebox.showerror('Error connecting to ftp', str(e)) else: messagebox.showinfo( 'Success', '{} successfully uploaded to FTP'.format(csvfile) )