Skip to content

Commit 7dddc06

Browse files
committed
Added Chapter 8 code
1 parent 27bacf1 commit 7dddc06

File tree

15 files changed

+1574
-0
lines changed

15 files changed

+1574
-0
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*.pyc
2+
__pycache__/
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
============================
2+
ABQ Data Entry Application
3+
============================
4+
5+
Description
6+
===========
7+
8+
This program provides a data entry form for ABQ Agrilabs laboratory data.
9+
10+
Features
11+
--------
12+
13+
* Provides a validated entry form to ensure correct data
14+
* Stores data to ABQ-format CSV files
15+
* Auto-fills form fields whenever possible
16+
17+
Authors
18+
=======
19+
20+
Alan D Moore, 2021
21+
22+
Requirements
23+
============
24+
25+
* Python 3.7 or higher
26+
* Tkinter
27+
28+
Usage
29+
=====
30+
31+
To start the application, run::
32+
33+
python3 ABQ_Data_Entry/abq_data_entry.py
34+
35+
36+
General Notes
37+
=============
38+
39+
The CSV file will be saved to your current directory in the format
40+
``abq_data_record_CURRENTDATE.csv``, where CURRENTDATE is today's date in ISO format.
41+
42+
This program only appends to the CSV file. You should have a spreadsheet program
43+
installed in case you need to edit or check the file.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from abq_data_entry.application import Application
2+
3+
app = Application()
4+
app.mainloop()

Chapter08/ABQ_Data_Entry/abq_data_entry/__init__.py

Whitespace-only changes.
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
"""The application/controller class for ABQ Data Entry"""
2+
3+
import tkinter as tk
4+
from tkinter import ttk
5+
from tkinter import messagebox
6+
from tkinter import filedialog
7+
8+
from . import views as v
9+
from . import models as m
10+
from .mainmenu import MainMenu
11+
12+
class Application(tk.Tk):
13+
"""Application root window"""
14+
15+
16+
def __init__(self, *args, **kwargs):
17+
super().__init__(*args, **kwargs)
18+
19+
# Hide window while GUI is built
20+
self.withdraw()
21+
22+
# Authenticate
23+
if not self._show_login():
24+
self.destroy()
25+
return
26+
27+
# show the window
28+
self.deiconify()
29+
30+
# Create model
31+
self.model = m.CSVModel()
32+
33+
# Load settings
34+
# self.settings = {
35+
# 'autofill date': tk.BooleanVar(),
36+
# 'autofill sheet data': tk.BoleanVar()
37+
# }
38+
self.settings_model = m.SettingsModel()
39+
self._load_settings()
40+
41+
# Begin building GUI
42+
self.title("ABQ Data Entry Application")
43+
self.columnconfigure(0, weight=1)
44+
45+
# Create the menu
46+
menu = MainMenu(self, self.settings)
47+
self.config(menu=menu)
48+
event_callbacks = {
49+
'<<FileSelect>>': self._on_file_select,
50+
'<<FileQuit>>': lambda _: self.quit(),
51+
# new for ch8
52+
'<<ShowRecordlist>>': self._show_recordlist,
53+
'<<NewRecord>>': self._new_record,
54+
55+
}
56+
for sequence, callback in event_callbacks.items():
57+
self.bind(sequence, callback)
58+
ttk.Label(
59+
self,
60+
text="ABQ Data Entry Application",
61+
font=("TkDefaultFont", 16)
62+
).grid(row=0)
63+
64+
# The notebook
65+
self.notebook = ttk.Notebook(self)
66+
self.notebook.enable_traversal()
67+
self.notebook.grid(row=1, padx=10, sticky='NSEW')
68+
69+
# The data record form
70+
self.recordform = v.DataRecordForm(self, self.model, self.settings)
71+
self.recordform.bind('<<SaveRecord>>', self._on_save)
72+
self.notebook.add(self.recordform, text='Entry Form')
73+
74+
75+
# The data record list
76+
# new for ch8
77+
self.recordlist = v.RecordList(self)
78+
self.notebook.insert(0, self.recordlist, text='Records')
79+
self._populate_recordlist()
80+
self.recordlist.bind('<<OpenRecord>>', self._open_record)
81+
82+
83+
self._show_recordlist()
84+
85+
# status bar
86+
self.status = tk.StringVar()
87+
self.statusbar = ttk.Label(self, textvariable=self.status)
88+
self.statusbar.grid(sticky=(tk.W + tk.E), row=3, padx=10)
89+
90+
91+
self.records_saved = 0
92+
93+
94+
def _on_save(self, *_):
95+
"""Handles file-save requests"""
96+
97+
# Check for errors first
98+
99+
errors = self.recordform.get_errors()
100+
if errors:
101+
self.status.set(
102+
"Cannot save, error in fields: {}"
103+
.format(', '.join(errors.keys()))
104+
)
105+
message = "Cannot save record"
106+
detail = "The following fields have errors: \n * {}".format(
107+
'\n * '.join(errors.keys())
108+
)
109+
messagebox.showerror(
110+
title='Error',
111+
message=message,
112+
detail=detail
113+
)
114+
return False
115+
116+
data = self.recordform.get()
117+
rownum = self.recordform.current_record
118+
self.model.save_record(data, rownum)
119+
self.records_saved += 1
120+
self.status.set(
121+
"{} records saved this session".format(self.records_saved)
122+
)
123+
self.recordform.reset()
124+
self._populate_recordlist()
125+
126+
def _on_file_select(self, *_):
127+
"""Handle the file->select action"""
128+
129+
filename = filedialog.asksaveasfilename(
130+
title='Select the target file for saving records',
131+
defaultextension='.csv',
132+
filetypes=[('CSV', '*.csv *.CSV')]
133+
)
134+
if filename:
135+
self.model = m.CSVModel(filename=filename)
136+
self._populate_recordlist()
137+
138+
@staticmethod
139+
def _simple_login(username, password):
140+
"""A basic authentication backend with a hardcoded user and password"""
141+
return username == 'abq' and password == 'Flowers'
142+
143+
def _show_login(self):
144+
"""Show login dialog and attempt to login"""
145+
error = ''
146+
title = "Login to ABQ Data Entry"
147+
while True:
148+
login = v.LoginDialog(self, title, error)
149+
if not login.result: # User canceled
150+
return False
151+
username, password = login.result
152+
if self._simple_login(username, password):
153+
return True
154+
error = 'Login Failed' # loop and redisplay
155+
156+
def _load_settings(self):
157+
"""Load settings into our self.settings dict."""
158+
159+
vartypes = {
160+
'bool': tk.BooleanVar,
161+
'str': tk.StringVar,
162+
'int': tk.IntVar,
163+
'float': tk.DoubleVar
164+
}
165+
166+
# create our dict of settings variables from the model's settings.
167+
self.settings = dict()
168+
for key, data in self.settings_model.fields.items():
169+
vartype = vartypes.get(data['type'], tk.StringVar)
170+
self.settings[key] = vartype(value=data['value'])
171+
172+
# put a trace on the variables so they get stored when changed.
173+
for var in self.settings.values():
174+
var.trace_add('write', self._save_settings)
175+
176+
def _save_settings(self, *_):
177+
"""Save the current settings to a preferences file"""
178+
179+
for key, variable in self.settings.items():
180+
self.settings_model.set(key, variable.get())
181+
self.settings_model.save()
182+
183+
# new for ch8
184+
def _show_recordlist(self, *_):
185+
"""Show the recordform"""
186+
self.notebook.select(self.recordlist)
187+
188+
def _populate_recordlist(self):
189+
try:
190+
rows = self.model.get_all_records()
191+
except Exception as e:
192+
messagebox.showerror(
193+
title='Error',
194+
message='Problem reading file',
195+
detail=str(e)
196+
)
197+
else:
198+
self.recordlist.populate(rows)
199+
200+
def _new_record(self, *_):
201+
"""Open the record form with a blank record"""
202+
self.recordform.load_record(None, None)
203+
self.notebook.select(self.recordform)
204+
205+
206+
def _open_record(self, *_):
207+
"""Open the Record selected recordlist id in the recordform"""
208+
rowkey = self.recordlist.selected_id
209+
try:
210+
record = self.model.get_record(rowkey)
211+
except Exception as e:
212+
messagebox.showerror(
213+
title='Error', message='Problem reading file', detail=str(e)
214+
)
215+
return
216+
self.recordform.load_record(rowkey, record)
217+
self.notebook.select(self.recordform)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""Global constants and classes needed by other modules in ABQ Data Entry"""
2+
from enum import Enum, auto
3+
4+
class FieldTypes(Enum):
5+
string = auto()
6+
string_list = auto()
7+
short_string_list = auto()
8+
iso_date_string = auto()
9+
long_string = auto()
10+
decimal = auto()
11+
integer = auto()
12+
boolean = auto()
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"""The Main Menu class for ABQ Data Entry"""
2+
3+
import tkinter as tk
4+
from tkinter import messagebox
5+
6+
class MainMenu(tk.Menu):
7+
"""The Application's main menu"""
8+
9+
def _event(self, sequence):
10+
"""Return a callback function that generates the sequence"""
11+
def callback(*_):
12+
root = self.master.winfo_toplevel()
13+
root.event_generate(sequence)
14+
15+
return callback
16+
17+
def __init__(self, parent, settings, **kwargs):
18+
"""Constructor for MainMenu
19+
20+
arguments:
21+
parent - The parent widget
22+
settings - a dict containing Tkinter variables
23+
"""
24+
super().__init__(parent, **kwargs)
25+
self.settings = settings
26+
27+
# The help menu
28+
help_menu = tk.Menu(self, tearoff=False)
29+
help_menu.add_command(label='About…', command=self.show_about)
30+
31+
# The file menu
32+
file_menu = tk.Menu(self, tearoff=False)
33+
file_menu.add_command(
34+
label="Select file…",
35+
command=self._event('<<FileSelect>>')
36+
)
37+
38+
file_menu.add_separator()
39+
file_menu.add_command(
40+
label="Quit",
41+
command=self._event('<<FileQuit>>')
42+
)
43+
44+
# The options menu
45+
options_menu = tk.Menu(self, tearoff=False)
46+
options_menu.add_checkbutton(
47+
label='Autofill Date',
48+
variable=self.settings['autofill date']
49+
)
50+
options_menu.add_checkbutton(
51+
label='Autofill Sheet data',
52+
variable=self.settings['autofill sheet data']
53+
)
54+
55+
# switch from recordlist to recordform
56+
go_menu = tk.Menu(self, tearoff=False)
57+
go_menu.add_command(
58+
label="Record List",
59+
command=self._event('<<ShowRecordlist>>')
60+
)
61+
go_menu.add_command(
62+
label="New Record",
63+
command=self._event('<<NewRecord>>')
64+
)
65+
66+
# add the menus in order to the main menu
67+
self.add_cascade(label='File', menu=file_menu)
68+
self.add_cascade(label='Go', menu=go_menu)
69+
self.add_cascade(label='Options', menu=options_menu)
70+
self.add_cascade(label='Help', menu=help_menu)
71+
72+
73+
def show_about(self):
74+
"""Show the about dialog"""
75+
76+
about_message = 'ABQ Data Entry'
77+
about_detail = (
78+
'by Alan D Moore\n'
79+
'For assistance please contact the author.'
80+
)
81+
82+
messagebox.showinfo(title='About', message=about_message, detail=about_detail)

0 commit comments

Comments
 (0)