Skip to content

Commit c8e0d2f

Browse files
committed
Add chapter 9 code
1 parent 4c84d5f commit c8e0d2f

33 files changed

+1943
-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()

Chapter09/ABQ_Data_Entry/abq_data_entry/__init__.py

Whitespace-only changes.
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
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+
from tkinter import font
8+
9+
from . import views as v
10+
from . import models as m
11+
from .mainmenu import MainMenu
12+
from . import images
13+
14+
class Application(tk.Tk):
15+
"""Application root window"""
16+
17+
18+
def __init__(self, *args, **kwargs):
19+
super().__init__(*args, **kwargs)
20+
21+
# Hide window while GUI is built
22+
self.withdraw()
23+
24+
# Authenticate
25+
if not self._show_login():
26+
self.destroy()
27+
return
28+
29+
# show the window
30+
self.deiconify()
31+
32+
# Create model
33+
self.model = m.CSVModel()
34+
35+
# Load settings
36+
# self.settings = {
37+
# 'autofill date': tk.BooleanVar(),
38+
# 'autofill sheet data': tk.BoleanVar()
39+
# }
40+
self.settings_model = m.SettingsModel()
41+
self._load_settings()
42+
43+
self.inserted_rows = []
44+
self.updated_rows = []
45+
46+
# Begin building GUI
47+
self.title("ABQ Data Entry Application")
48+
self.columnconfigure(0, weight=1)
49+
50+
# Set taskbar icon
51+
self.taskbar_icon = tk.PhotoImage(file=images.ABQ_LOGO_64)
52+
self.iconphoto(True, self.taskbar_icon)
53+
54+
# Create the menu
55+
menu = MainMenu(self, self.settings)
56+
self.config(menu=menu)
57+
event_callbacks = {
58+
'<<FileSelect>>': self._on_file_select,
59+
'<<FileQuit>>': lambda _: self.quit(),
60+
'<<ShowRecordlist>>': self._show_recordlist,
61+
'<<NewRecord>>': self._new_record,
62+
63+
}
64+
for sequence, callback in event_callbacks.items():
65+
self.bind(sequence, callback)
66+
67+
# new for ch9
68+
self.logo = tk.PhotoImage(file=images.ABQ_LOGO_32)
69+
ttk.Label(
70+
self,
71+
text="ABQ Data Entry Application",
72+
font=("TkDefaultFont", 16),
73+
image=self.logo,
74+
compound=tk.LEFT
75+
).grid(row=0)
76+
77+
# The notebook
78+
self.notebook = ttk.Notebook(self)
79+
self.notebook.enable_traversal()
80+
self.notebook.grid(row=1, padx=10, sticky='NSEW')
81+
82+
# The data record form
83+
self.recordform_icon = tk.PhotoImage(file=images.FORM_ICON)
84+
self.recordform = v.DataRecordForm(self, self.model, self.settings)
85+
self.notebook.add(
86+
self.recordform, text='Entry Form',
87+
image=self.recordform_icon, compound=tk.LEFT
88+
)
89+
self.recordform.bind('<<SaveRecord>>', self._on_save)
90+
91+
92+
# The data record list
93+
self.recordlist_icon = tk.PhotoImage(file=images.LIST_ICON)
94+
self.recordlist = v.RecordList(
95+
self, self.inserted_rows, self.updated_rows
96+
)
97+
self.notebook.insert(
98+
0, self.recordlist, text='Records',
99+
image=self.recordlist_icon, compound=tk.LEFT
100+
)
101+
self._populate_recordlist()
102+
self.recordlist.bind('<<OpenRecord>>', self._open_record)
103+
104+
105+
self._show_recordlist()
106+
107+
# status bar
108+
self.status = tk.StringVar()
109+
self.statusbar = ttk.Label(self, textvariable=self.status)
110+
self.statusbar.grid(sticky=(tk.W + tk.E), row=3, padx=10)
111+
112+
113+
self.records_saved = 0
114+
115+
116+
def _on_save(self, *_):
117+
"""Handles file-save requests"""
118+
119+
# Check for errors first
120+
121+
errors = self.recordform.get_errors()
122+
if errors:
123+
self.status.set(
124+
"Cannot save, error in fields: {}"
125+
.format(', '.join(errors.keys()))
126+
)
127+
message = "Cannot save record"
128+
detail = "The following fields have errors: \n * {}".format(
129+
'\n * '.join(errors.keys())
130+
)
131+
messagebox.showerror(
132+
title='Error',
133+
message=message,
134+
detail=detail
135+
)
136+
return False
137+
138+
data = self.recordform.get()
139+
rownum = self.recordform.current_record
140+
self.model.save_record(data, rownum)
141+
if rownum is not None:
142+
self.updated_rows.append(rownum)
143+
else:
144+
rownum = len(self.model.get_all_records()) -1
145+
self.inserted_rows.append(rownum)
146+
self.records_saved += 1
147+
self.status.set(
148+
"{} records saved this session".format(self.records_saved)
149+
)
150+
self.recordform.reset()
151+
self._populate_recordlist()
152+
153+
def _on_file_select(self, *_):
154+
"""Handle the file->select action"""
155+
156+
filename = filedialog.asksaveasfilename(
157+
title='Select the target file for saving records',
158+
defaultextension='.csv',
159+
filetypes=[('CSV', '*.csv *.CSV')]
160+
)
161+
if filename:
162+
self.model = m.CSVModel(filename=filename)
163+
self.inserted_rows.clear()
164+
self.updated_rows.clear()
165+
self._populate_recordlist()
166+
167+
@staticmethod
168+
def _simple_login(username, password):
169+
"""A basic authentication backend with a hardcoded user and password"""
170+
return username == 'abq' and password == 'Flowers'
171+
172+
def _show_login(self):
173+
"""Show login dialog and attempt to login"""
174+
error = ''
175+
title = "Login to ABQ Data Entry"
176+
while True:
177+
login = v.LoginDialog(self, title, error)
178+
if not login.result: # User canceled
179+
return False
180+
username, password = login.result
181+
if self._simple_login(username, password):
182+
return True
183+
error = 'Login Failed' # loop and redisplay
184+
185+
def _load_settings(self):
186+
"""Load settings into our self.settings dict."""
187+
188+
vartypes = {
189+
'bool': tk.BooleanVar,
190+
'str': tk.StringVar,
191+
'int': tk.IntVar,
192+
'float': tk.DoubleVar
193+
}
194+
195+
# create our dict of settings variables from the model's settings.
196+
self.settings = dict()
197+
for key, data in self.settings_model.fields.items():
198+
vartype = vartypes.get(data['type'], tk.StringVar)
199+
self.settings[key] = vartype(value=data['value'])
200+
201+
# put a trace on the variables so they get stored when changed.
202+
for var in self.settings.values():
203+
var.trace_add('write', self._save_settings)
204+
205+
# update font settings after loading them
206+
self._set_font()
207+
self.settings['font size'].trace_add('write', self._set_font)
208+
self.settings['font family'].trace_add('write', self._set_font)
209+
210+
# process theme
211+
style = ttk.Style()
212+
theme = self.settings.get('theme').get()
213+
if theme in style.theme_names():
214+
style.theme_use(theme)
215+
216+
def _save_settings(self, *_):
217+
"""Save the current settings to a preferences file"""
218+
219+
for key, variable in self.settings.items():
220+
self.settings_model.set(key, variable.get())
221+
self.settings_model.save()
222+
223+
def _show_recordlist(self, *_):
224+
"""Show the recordform"""
225+
self.notebook.select(self.recordlist)
226+
227+
def _populate_recordlist(self):
228+
try:
229+
rows = self.model.get_all_records()
230+
except Exception as e:
231+
messagebox.showerror(
232+
title='Error',
233+
message='Problem reading file',
234+
detail=str(e)
235+
)
236+
else:
237+
self.recordlist.populate(rows)
238+
239+
def _new_record(self, *_):
240+
"""Open the record form with a blank record"""
241+
self.recordform.load_record(None, None)
242+
self.notebook.select(self.recordform)
243+
244+
245+
def _open_record(self, *_):
246+
"""Open the Record selected recordlist id in the recordform"""
247+
rowkey = self.recordlist.selected_id
248+
try:
249+
record = self.model.get_record(rowkey)
250+
except Exception as e:
251+
messagebox.showerror(
252+
title='Error', message='Problem reading file', detail=str(e)
253+
)
254+
return
255+
self.recordform.load_record(rowkey, record)
256+
self.notebook.select(self.recordform)
257+
258+
# new chapter 9
259+
def _set_font(self, *_):
260+
"""Set the application's font"""
261+
font_size = self.settings['font size'].get()
262+
font_family = self.settings['font family'].get()
263+
font_names = ('TkDefaultFont', 'TkMenuFont', 'TkTextFont')
264+
for font_name in font_names:
265+
tk_font = font.nametofont(font_name)
266+
tk_font.config(size=font_size, family=font_family)
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: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from pathlib import Path
2+
3+
# This gives us the parent directory of this file (__init__.py)
4+
IMAGE_DIRECTORY = Path(__file__).parent
5+
6+
ABQ_LOGO_16 = IMAGE_DIRECTORY / 'abq_logo-16x10.png'
7+
ABQ_LOGO_32 = IMAGE_DIRECTORY / 'abq_logo-32x20.png'
8+
ABQ_LOGO_64 = IMAGE_DIRECTORY / 'abq_logo-64x40.png'
9+
10+
# PNG icons
11+
12+
SAVE_ICON = IMAGE_DIRECTORY / 'file-2x.png'
13+
RESET_ICON = IMAGE_DIRECTORY / 'reload-2x.png'
14+
LIST_ICON = IMAGE_DIRECTORY / 'list-2x.png'
15+
FORM_ICON = IMAGE_DIRECTORY / 'browser-2x.png'
16+
17+
18+
# BMP icons
19+
QUIT_BMP = IMAGE_DIRECTORY / 'x-2x.xbm'
20+
ABOUT_BMP = IMAGE_DIRECTORY / 'question-mark-2x.xbm'
1.31 KB
Loading
2.58 KB
Loading
5.29 KB
Loading

0 commit comments

Comments
 (0)