Skip to content

Commit d9ee9c3

Browse files
committed
Added chapter12 code
1 parent fee2c12 commit d9ee9c3

31 files changed

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

Chapter12/ABQ_Data_Entry/abq_data_entry/__init__.py

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