-
Notifications
You must be signed in to change notification settings - Fork 0
/
Assignment06.py
292 lines (236 loc) · 10.8 KB
/
Assignment06.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# ------------------------------------------------------------------------------------------ #
# Title: Assignment06
# Desc: This assignment demonstrates using functions, classes, and separations of concern
# Change Log:
# Patrick Moynihan: 2024-04-10 Created script
# Patrick Moynihan: 2024-04-08 Added ability to save data to CSV file
# Patrick Moynihan: 2024-04-25 Added menu functionality
# Patrick Moynihan: 2024-05-02 Added ability to read data from CSV file
# Patrick Moynihan: 2024-05-10 Refactored to use dictionaries and JSON file format
# Patrick Moynihan: 2024-05-18 Refactored to use functions and classes
# ------------------------------------------------------------------------------------------ #
import json
from typing import IO
# Define the Data Constants
MENU: str = '''
------ Course Registration Program ------
Select from the following menu:
1. Register a Student for a Course.
2. Show current data.
3. Save data to a file.
4. Exit the program.
-----------------------------------------
'''
FILE_NAME: str = "Enrollments.json"
KEYS: list = ["FirstName", "LastName", "CourseName"]
# Define the global data variables
menu_choice: str = '' # Hold the choice made by the user.
students: list = [] # List of data for all students
saved: bool = True # Tracks whether newly added data has been saved
# Define the classes
class FileProcessor:
"""
Functions for reading and writing JSON files.
ChangeLog:
Patrick Moynihan, 2024-05-18: Created class
"""
@staticmethod
def read_data_from_file(file_name: str, student_data: list) -> list:
"""
Reads the specified JSON file and stores it in a list.
ChangeLog:
Patrick Moynihan, 2024-05-18: Created method
:param file_name: string representing the name of the JSON file
:param student_data: list to which student data will be stored
:return: list of data loaded from file
"""
file: IO # Holds a reference to an opened file.
print(f">>> Loading data from {file_name}")
try:
file = open(file_name, "r")
student_data = json.load(file)
file.close()
print(f">>> Loaded {len(student_data)} records.")
for i, item in enumerate(student_data, start=1):
# Check to see if the keys we are expecting exist in the data
if not all(key in item for key in KEYS):
raise Exception(
f">>> Missing an expected key ({KEYS}) in record {i}. Please check {file_name} for errors.")
return student_data
# Let the user know we couldn't find the file
except FileNotFoundError:
IO.output_error_messages(f">>> {file_name} not found. A new file will be created.")
file = open(file_name, "w")
file.close()
# Let the user know some other problem occurred when loading the file
except Exception as e:
IO.output_error_messages(
f">>> There was an error loading the data from {file_name}. Please check {file_name} and try again.")
IO.output_error_messages(e, e.__doc__)
exit()
# If the file is still open for some reason, close it
finally:
if not file.closed:
print(">>> Closing file.")
file.close()
@staticmethod
def write_data_to_file(file_name: str, student_data: list) -> bool:
"""
Writes the specified JSON file and stores it in a list.
ChangeLog:
Patrick Moynihan, 2024-05-18: Created method
:param file_name: string representing the name of the JSON file
:param student_data: list from which student data will be saved
:return: bool representing whether or not the data was saved
"""
file: IO = None
json_data: str = '' # Holds combined string data separated by a comma.
try:
# Save JSON to file
file = open(file_name, 'w')
json.dump(student_data, file, indent=4)
file.close()
print(f">>> Wrote registration data to filename {file_name}\n")
# Print JSON data to terminal
json_data = json.dumps(students, indent=4)
print(json_data)
return True
except Exception as e:
IO.output_error_messages(">>> There was an error writing the registration data. Is the file read-only?")
IO.output_error_messages(f">>> {e}", e.__doc__)
finally:
# Does file have a value other than None? If so, is the file open? If so, close the file.
if file and not file.closed:
file.close()
class IO:
"""
Functions for handling user input and output.
ChangeLog:
Patrick Moynihan, 2024-05-18: Created class
"""
@staticmethod
def output_menu(menu: str) -> None:
"""
Displays the menu options
ChangeLog:
Patrick Moynihan, 2024-05-18: Created method
:param menu: string to be printed as the menu
"""
print(menu)
@staticmethod
def input_menu_choice() -> str:
"""
Retrieves user input from the menu
ChangeLog:
Patrick Moynihan, 2024-05-18: Created method
:return: string representing the user input
"""
choice = input("Enter your choice: ")
return choice
@staticmethod
def output_student_courses(student_data: list) -> None:
"""
Prints out the student registration data in human-readable format.
ChangeLog:
Patrick Moynihan, 2024-05-18: Created method
:param student_data: list from which student data will be presented
"""
print(">>> The current data is:\n")
print("First Name Last Name Course Name ")
print("------------------------------------------------------------")
for item in student_data:
# Print each row of the table inside 20 character wide columns
print(f"{item['FirstName'][:20]:<20}{item['LastName'][:20]:<20}{item['CourseName'][:20]:<20}")
print("------------------------------------------------------------")
@staticmethod
def input_student_data(student_data: list) -> None:
"""
Reads the student registration data from the user and appends it to a list
ChangeLog:
Patrick Moynihan, 2024-05-18: Created method
:param student_data: list to which student data will be appended
"""
student_first_name: str = '' # Holds the first name of a student entered by the user.
student_last_name: str = '' # Holds the last name of a student entered by the user.
course_name: str = '' # Holds the name of a course entered by the user.
# Input user data, allow only alpha characters
while True:
try:
student_first_name = input("Enter student's first name: ")
if not student_first_name.isalpha():
raise ValueError(f">>> Please use only letters. Try again.\n")
else:
break
except ValueError as e:
IO.output_error_messages(e)
while True:
try:
student_last_name = input("Enter student's last name: ")
if not student_last_name.isalpha():
raise ValueError(">>> Please use only letters. Try again.\n")
else:
break
except ValueError as e:
IO.output_error_messages(e)
course_name = input("Enter the course name: ")
# Create dictionary using captured data
data = {"FirstName": student_first_name, "LastName": student_last_name, "CourseName": course_name}
# Append the entered data to the passed-in list
student_data.append(data)
print(f">>> Registered {student_first_name} {student_last_name} for {course_name}.\n")
@staticmethod
def output_error_messages(message: str, error: Exception = None) -> None:
"""
Presents custom error message to user, along with Python's technical error.
ChangeLog:
Patrick Moynihan, 2024-05-18: Created method
:param message: The custom error message to present to the user
:param error: The technical error message from Python
"""
# if we get two arguments, print the custom error and the Python technical error
if error:
print(f"{message}")
print(f">>> Python technical error: {error}")
# otherwise just print the custom error message
else:
print(f"{message}")
# Load data from enrollment JSON file into students
students = FileProcessor.read_data_from_file(file_name=FILE_NAME, student_data=students)
# Main program loop
while True:
# Present the menu of choices
IO.output_menu(MENU)
menu_choice = IO.input_menu_choice()
if menu_choice == '1':
# Ingest student registration data from user
IO.input_student_data(student_data=students)
saved = False # Set the saved flag to false, so we can remind user to save
continue
elif menu_choice == '2':
# Display the data in a human-friendly format
IO.output_student_courses(students)
continue
elif menu_choice == '3':
# Save the data to a file and set saved flag to True if save was successful
if FileProcessor.write_data_to_file(file_name=FILE_NAME, student_data=students) == True:
saved = True
continue
elif menu_choice == '4':
# Exit if data has already been saved or was unmodified (i.e. saved = undefined)
if saved is False:
save_confirm = input(">>> New registration data not saved. Save it now? (Y/N): ")
if save_confirm.capitalize() == 'Y':
if FileProcessor.write_data_to_file(file_name=FILE_NAME, student_data=students) == True:
print(">>> Have a nice day!\n")
exit()
else:
continue # File was not successfully saved, so return to main menu
elif save_confirm.capitalize() == 'N':
print(">>> Newly entered data not saved.")
print(">>> Have a nice day!\n")
exit()
else:
print(">>> Have a nice day!\n")
exit()
else:
print("Please only choose option 1, 2, 3, or 4.")