Skip to content

Commit f24653d

Browse files
committed
Added chapter 11 code
1 parent 3f70033 commit f24653d

32 files changed

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

Chapter11/ABQ_Data_Entry/abq_data_entry/__init__.py

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