Skip to content

Commit d300ff4

Browse files
committed
Add chapter 10 code
1 parent c8e0d2f commit d300ff4

28 files changed

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

Chapter10/ABQ_Data_Entry/abq_data_entry/__init__.py

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