-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.py
836 lines (727 loc) · 30.8 KB
/
main.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
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
#!/usr/bin/env python3
# TODO: Finish settings dialog next
import os
import wx
import wx.adv
import json
import time
import requests
import wx.grid as wxgrid
import lib.tritime as libtt
import lib.trireport as libtr
from functools import wraps
from io import BytesIO
from threading import Thread, Timer
from datetime import datetime
# Create a custom event type for debounced events
wxEVT_DEBOUNCED_TEXT = wx.NewEventType()
EVT_DEBOUNCED_TEXT = wx.PyEventBinder(wxEVT_DEBOUNCED_TEXT, 1)
_app_settings: dict[str, any] = {}
def default_app_settings() -> dict[str, any]:
return {
'allow_all_out': True,
'show_active_badges': True,
'auto_out_time': '20:30',
'pay_period_days': 14,
}
def modifies_settings(func):
@wraps
def wrapper(*args, **kwargs):
func(*args, **kwargs)
store_app_settings()
return wrapper
def debounce(wait_time):
"""
Decorator that will debounce a function for the specified amount of time.
If the decorated function is called multiple times, only the last call
will be executed after the wait_time has elapsed.
"""
def decorator(fn):
timer = None
@wraps(fn)
def debounced(*args, **kwargs):
nonlocal timer
if timer is not None:
timer.cancel()
timer = Timer(wait_time, lambda: fn(*args, **kwargs))
timer.start()
return debounced
return decorator
def get_app_settings():
try:
with open('app_settings.json', 'r') as f:
obj = json.loads(f.read())
except json.decoder.JSONDecodeError:
obj = None
except FileNotFoundError:
obj = None
return obj
def store_app_settings():
json_str = json.dumps(_app_settings)
with open('app_settings.json', 'w') as f:
f.write(json_str)
def is_json(myjson):
try:
json.loads(myjson)
except ValueError:
return False
return True
# If we have a URL (http:// or https://), download the image from the URL
def download_image(self, url, width=64, height=64):
# This method is a hot mess and needs to be cleaned up.
image = wx.Image()
image.LoadFile('unknown_badge.png', wx.BITMAP_TYPE_PNG)
valid_image = False
try:
response = requests.get(url)
if response.status_code == 200:
# Convert the image data into a wx.Bitmap
image_data = BytesIO(response.content)
image = wx.Image(image_data)
image = image.Scale(width, height, wx.IMAGE_QUALITY_HIGH)
valid_image = True
else:
image.LoadFile('unknown_badge.png', wx.BITMAP_TYPE_PNG)
valid_image = False
except: # noqa
image.LoadFile('unknown_badge.png', wx.BITMAP_TYPE_PNG)
valid_image = False
return image, valid_image
class DebouncedTextEvent(wx.PyCommandEvent):
"""Custom event for debounced text changes"""
def __init__(self, event_type, id, text=""):
super().__init__(event_type, id)
self._text = text
def GetText(self):
return self._text
class DebouncedTextCtrl(wx.TextCtrl):
"""
A TextCtrl subclass that provides debounced text change events.
Regular EVT_TEXT events fire immediately, while EVT_DEBOUNCED_TEXT
events fire after the specified delay with no intermediate input.
"""
def __init__(self, parent, id=wx.ID_ANY, value="", delay=0.5, *args, **kwargs):
super().__init__(parent, id, value, *args, **kwargs)
self.delay = delay
self._timer = None
# Bind to the regular text event
self.Bind(wx.EVT_TEXT, self._on_text)
def _on_text(self, event):
"""Handle the text change event with debouncing"""
# Cancel any pending timer
if self._timer is not None:
self._timer.cancel()
# Create new timer for delayed event
self._timer = Timer(self.delay, self._fire_debounced_event)
self._timer.start()
# Allow the regular event to propagate
event.Skip()
def _fire_debounced_event(self):
"""Fire the custom debounced event"""
evt = DebouncedTextEvent(wxEVT_DEBOUNCED_TEXT, self.GetId(), self.GetValue())
evt.SetEventObject(self)
wx.PostEvent(self, evt)
class MainWindow(wx.Frame):
def return_focus(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
result = func(self, *args, **kwargs)
self.badge_num_input.SetFocus()
return result
return wrapper
# Set up the main window for the application; this is where most controls
# get laid out.
def __init__(self, parent, id):
wx.Frame.__init__(self, parent, id,
'TriTime', size=(1024, 800))
self.Maximize(True)
bni_style = wx.TE_PROCESS_ENTER | wx.TE_MULTILINE
self.badge_num_input = DebouncedTextCtrl(self, -1, '',
delay=0.2,
style=bni_style)
self.badge_num_input.Bind(EVT_DEBOUNCED_TEXT, self.on_badge_num_change)
self.badge_num_input.Bind(wx.EVT_TEXT_ENTER, self.on_badge_num_enter)
self.badge_clear_btn = wx.Button(self, label='Clear', size=(80, 100))
self.badge_clear_btn.Bind(wx.EVT_BUTTON, self.clear_badge_input)
self.export_btn = wx.Button(self, label='Export Data')
self.export_btn.Bind(wx.EVT_BUTTON, self.export_data)
self.greeting_label = wx.StaticText(self, -1, 'Welcome to TriTime')
self.clock_display = wx.StaticText(self, -1, 'HH:mm:ss AP')
tc = wx.Font(28, wx.FONTFAMILY_TELETYPE,
wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)
self.clock_display.SetFont(tc)
self.badge_num_input.SetFont(tc)
vbox = wx.BoxSizer(wx.VERTICAL)
btn_size = (100, 100)
self.in_btn = wx.Button(self, label='In', size=btn_size)
self.in_btn.Bind(wx.EVT_BUTTON, self.punch_in)
self.out_btn = wx.Button(self, label='Out', size=btn_size)
self.out_btn.Bind(wx.EVT_BUTTON, self.punch_out)
self.check_time = wx.Button(self, label='Check Time', size=btn_size)
self.check_time.Bind(wx.EVT_BUTTON, self.check_time_dialog)
self.add_user_btn = wx.Button(self, label='Add User', size=btn_size)
self.add_user_btn.Bind(wx.EVT_BUTTON, self.add_user)
self.find_user_btn = wx.Button(self, label='Search', size=btn_size)
self.find_user_btn.Bind(wx.EVT_BUTTON, self.find_user)
self.punch_all_out_btn = wx.Button(self, label='Punch All Out!',
size=btn_size)
self.punch_all_out_btn.Bind(wx.EVT_BUTTON, self.punch_all_out)
self.edit_settings_btn = wx.Button(self, label='Settings...',
size=btn_size)
self.edit_settings_btn.Bind(wx.EVT_BUTTON, self.edit_settings)
# Disable all of the buttons; they will enable when a valid badge is
# entered.
for b in [self.in_btn, self.out_btn]:
b.Disable()
if _app_settings['allow_all_out'] is False:
self.punch_all_out_btn.Disable()
# Create a grid that lets us show everybody punched in
self.active_badge_sizer = wx.WrapSizer(wx.HORIZONTAL)
self.badge_scroller = wx.ScrolledWindow(self)
self.badge_scroller.SetScrollRate(10, 10)
self.badge_scroller.SetMinSize((800, 600))
self.badge_scroller.SetSizer(self.active_badge_sizer)
spacer_size = 20
# This lets us put a space to the left of everything by putting our
# other boxes in a horizontal box witha spacer at the beginning.
outerhbox = wx.BoxSizer(wx.HORIZONTAL)
vbox_buttons = wx.BoxSizer(wx.VERTICAL)
hbox_inout = wx.BoxSizer(wx.HORIZONTAL)
hbox_inout.Add(self.in_btn)
hbox_inout.AddSpacer(spacer_size)
hbox_inout.Add(self.out_btn)
hbox_inout.AddSpacer(spacer_size)
hbox_inout.Add(self.check_time)
hbox_inout.AddSpacer(spacer_size)
hbox_usermanage = wx.BoxSizer(wx.HORIZONTAL)
hbox_usermanage.Add(self.add_user_btn)
hbox_usermanage.AddSpacer(spacer_size)
hbox_usermanage.Add(self.find_user_btn)
hbox_usermanage.AddSpacer(spacer_size)
hbox_usermanage.Add(self.punch_all_out_btn)
hbox_usermanage.AddSpacer(spacer_size)
hbox_system = wx.BoxSizer(wx.HORIZONTAL)
hbox_system.Add(self.edit_settings_btn)
hbox_system.AddSpacer(spacer_size)
vbox_buttons.Add(hbox_inout)
vbox_buttons.AddSpacer(spacer_size)
vbox_buttons.Add(hbox_usermanage)
vbox_buttons.AddSpacer(spacer_size)
vbox_buttons.Add(hbox_system)
vbox_buttons.AddSpacer(spacer_size)
hbox_top = wx.BoxSizer(wx.HORIZONTAL)
hbox_top.Add(self.clock_display)
hbox_top.AddStretchSpacer(20)
hbox_top.Add(self.export_btn)
hbox_buttons_checkgrid = wx.BoxSizer(wx.HORIZONTAL)
hbox_buttons_checkgrid.Add(vbox_buttons)
hbox_buttons_checkgrid.Add(self.badge_scroller, 1, wx.EXPAND)
hbox_badgde_input = wx.BoxSizer(wx.HORIZONTAL)
hbox_badgde_input.Add(self.badge_num_input, 1, wx.EXPAND)
hbox_badgde_input.AddSpacer(spacer_size)
hbox_badgde_input.Add(self.badge_clear_btn)
vbox.AddSpacer(spacer_size)
vbox.Add(hbox_top, 1, wx.EXPAND)
vbox.AddSpacer(spacer_size)
vbox.Add(hbox_badgde_input, 1, wx.EXPAND)
vbox.AddSpacer(spacer_size)
vbox.Add(self.greeting_label, 0, wx.EXPAND)
vbox.AddSpacer(spacer_size)
vbox.Add(hbox_buttons_checkgrid, 0, wx.EXPAND)
vbox.AddSpacer(spacer_size)
# TODO: Add check time grid back somewhere
outerhbox.AddSpacer(spacer_size)
outerhbox.Add(vbox, wx.EXPAND)
outerhbox.AddSpacer(spacer_size)
self.outerhbox = outerhbox
# Add sizer to panel
self.SetSizerAndFit(outerhbox)
self.Layout()
self.Update()
self.Bind(wx.EVT_CLOSE, self.on_app_shutdown)
self.clock_thread_run = True
self.clock_thread = Thread(target=self.update_clock)
self.clock_thread.start()
self.update_active_badges()
self.badge_num_input.SetFocus()
self.Bind(wx.EVT_LEFT_DOWN, self.on_panel_click)
@return_focus
def on_panel_click(self, event):
return
def on_app_shutdown(self, event):
self.shutdown()
def shutdown(self):
self.clock_thread_run = False
self.clock_thread.join()
self.Destroy()
@return_focus
def export_data(self, event):
# Configure file dialog options
wildcard = (
"Excel files (*.xlsx)|*.xlsx|"
"CSV files (*.csv)|*.csv|"
"Parquet files (*.parquet)|*.parquet"
)
# Default directory and filename for export
dialog = wx.FileDialog(
self, message="Export data",
defaultDir="",
defaultFile="export.xlsx",
wildcard=wildcard,
style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT
)
# Show the dialog and get user input
if dialog.ShowModal() == wx.ID_OK:
# Get the chosen filename and path
filepath = dialog.GetPath()
wx.MessageBox(f"File chosen: {filepath}", "Export Complete")
dialog.Destroy()
libtr.export_to_excel(filepath)
def update_clock(self):
while self.clock_thread_run:
time.sleep(0.1)
current_time = time.strftime("%I:%M:%S %p")
curr_hour = datetime.now().hour
curr_mins = datetime.now().minute
# Use wx.CallAfter to update the StaticText in the main thread
wx.CallAfter(self.clock_display.SetLabel, current_time)
notused = """
auto_out_time = _app_settings.get('auto_out_time', None)
if auto_out_time is not None:
out_hour, out_min = map(int, auto_out_time.split(':'))
if out_hour > curr_hour and out_min > curr_mins:
self.punch_all_out(None)
"""
# Remove all of the active badges from the grid; this was easier than
# trying to remove the one-by-one.
def clear_active_badges(self):
self.active_badge_sizer.Clear(True)
self.Layout()
self.Update()
# Draw every punched in badge on the grid with a button to punch them out
def update_active_badges(self):
self.Freeze()
self.clear_active_badges()
if _app_settings['show_active_badges'] is False:
return
badges = libtt.get_badges()
for bnum, badge in badges.items():
if badge['status'] == 'in':
self.add_badge_to_grid(bnum)
self.Thaw()
def create_badge_card(self, badge_num, parent=None, bind_method=None):
parent = self if parent is None else parent
bind_method = self.punch_out if bind_method is None else bind_method
badges = libtt.get_badges()
badge = badges[badge_num]
badge_name = badge['display_name']
cached_image_filename = f'cached_photos/{badge_num}.png'
# If we already have a file downloaded (cached) for this badge just
# use that.
if os.path.exists(cached_image_filename):
img = wx.Image()
img.LoadFile(cached_image_filename, wx.BITMAP_TYPE_PNG)
# Otherwise we can download the image from the URL
else:
img_url = badge['photo_url']
img, should_cache = download_image(parent, img_url)
if should_cache:
if not os.path.exists('cached_photos'):
os.makedirs('cached_photos')
img.SaveFile(f'cached_photos/{badge_num}.png',
wx.BITMAP_TYPE_PNG)
img = wx.Bitmap(img)
bmp = wx.StaticBitmap(parent, -1, img)
vbox = wx.BoxSizer(wx.VERTICAL)
btn = wx.Button(parent, label=badge_name, size=(-1, 80))
btn.Bind(wx.EVT_BUTTON, lambda event: bind_method(event, badge_num))
vbox.Add(bmp, flag=wx.CENTER)
vbox.AddSpacer(10)
vbox.Add(btn, flag=wx.CENTER)
return vbox
# Draws an individual badge on the grid with a button to punch them out
def add_badge_to_grid(self, badge_num):
vbox = self.create_badge_card(badge_num,
self.badge_scroller,
self.punch_out)
self.active_badge_sizer.Add(vbox, 0, wx.ALL, border=10)
self.Layout()
self.Update()
# Reset the badge number input and set the focus back to it
def clear_input(self):
self.badge_num_input.SetValue('')
self.in_btn.Disable()
self.out_btn.Disable()
self.badge_num_input.SetFocus()
# Users can be identified by more than one code; this method will look up
# the "real" badge number if an alternate is entered.
def lookup_alt(self, badges, badge_num):
for real_badge_num, badge in badges.items():
if 'alt_keys' not in badge:
continue
if badge_num in badge['alt_keys']:
return real_badge_num
return badge_num
@return_focus
def clear_badge_input(self, event):
self.badge_num_input.SetValue('')
# This method fires whenever the badge number input changes; it will
# update the greeting label and enable/disable the buttons as needed.
def on_badge_num_change(self, event):
self.in_btn.Disable()
self.out_btn.Disable()
badge_num = self.get_entered_badge(
badge=event.GetString().strip()
)
badges = libtt.get_badges()
valid_badges = badges.keys()
if badge_num in valid_badges:
badge_data = badges[badge_num]
self.greeting_label.SetLabel(
f'Welcome {badge_data["display_name"]}'
)
status = badge_data['status']
if status != 'in':
self.in_btn.Enable()
if status != 'out':
self.out_btn.Enable()
else:
self.greeting_label.SetLabel(
'Scan badge'
)
# If the 'Enter' key is pressed in the badge input box this method fires
# We'll use this to punch in or out the badge depending on what the status
# of their badge is. Usually. We also use this to permit the app to
# reconfigure itself with JSON entered into the badge input.
# The use case there is putting the JSON data into a QR code that can
# reconfig the whole system in a jiffy!
@return_focus
def on_badge_num_enter(self, event, badge_num=None):
if badge_num is None:
badge_num = self.get_entered_badge()
if badge_num == 'quit':
self.shutdown()
if badge_num == 'fixbadges':
libtt.fix_badges()
return
if badge_num == 'debug':
import wx.lib.inspection
wx.lib.inspection.InspectionTool().Show()
return
# if is_json(badge_num):
if False: # We can skip this for now.
print(f' this is json: {badge_num}')
# Process as an app_settings.json config
global _app_settings
_app_settings = json.loads(badge_num)
store_app_settings()
return
# Otherwise we'll just handle it like a badge input
badges = libtt.get_badges()
valid_badges = badges.keys()
if badge_num in valid_badges:
badge_data = badges[badge_num]
if badge_data['status'] == 'in':
self.punch_out(event)
elif badge_data['status'] == 'out':
self.punch_in(event)
def get_entered_badge(self, badge=None) -> str:
if badge is None:
badge = self.badge_num_input.GetValue()
badge = badge.strip()
badge = self.lookup_alt(libtt.get_badges(), badge)
return badge
# Buttons to punch in will call this method; we pass off all the data
# manipulation to the libtt module.
@return_focus
def punch_in(self, event):
badge = self.get_entered_badge()
dt = datetime.now()
badges = libtt.punch_in(badge, dt)
libtt.store_badges(badges)
self.add_badge_to_grid(badge)
self.clear_input()
# Buttons to punch out will call this method; we pass off all the data
# manipulation to the libtt module.
@return_focus
def punch_out(self, event, badge_num=None):
badge_num = self.get_entered_badge(badge_num) if badge_num is None else badge_num
bni = self.badge_num_input
badge = bni.GetValue() if badge_num is None else badge_num
badge = self.lookup_alt(libtt.get_badges(), badge)
dt = datetime.now()
badges = libtt.punch_out(badge, dt)
libtt.store_badges(badges)
libtt.tabulate_badge(badge)
self.update_active_badges()
self.clear_input()
# Adds up all of the time a badge has been punched in.
@return_focus
def check_time_dialog(self, event):
# Create a dialog that has inputs for a badge number, display name,
# and photo URL. When the dialog is submitted, add the user to the
# database and update the active badges grid.
def badge_change(event):
badge = event.GetString().strip()
badge = self.lookup_alt(libtt.get_badges(), badge)
# Create a grid
punch_data = libtt.read_punches(badge)
punch_data.reverse()
curr_rows = check_time_grid.GetNumberRows()
new_rows = len(punch_data) + 1
if new_rows > curr_rows:
check_time_grid.AppendRows(new_rows-curr_rows)
elif new_rows < curr_rows:
check_time_grid.DeleteRows(new_rows, curr_rows-new_rows)
# Populate the grid with data
total_duration = 0
for row_index, row_data in enumerate(punch_data):
instr = 'N/A - Error'
outstr = 'N/A - Error'
duration = ''
if 'ts_in' in row_data:
instr = str(row_data['ts_in'])
if 'ts_out' in row_data:
outstr = str(row_data['ts_out'])
if 'duration' in row_data:
d = row_data['duration']
if d is None:
d = 0
total_duration += d
duration = str(round(d/3600, 2))
check_time_grid.SetCellValue(row_index+1, 0, instr)
check_time_grid.SetCellValue(row_index+1, 1, outstr)
check_time_grid.SetCellValue(row_index+1, 2, duration)
check_time_grid.SetCellValue(0, 2,
str(round(total_duration/3600, 2)))
check_time_grid.Show()
check_time_grid.Layout()
check_time_grid.Update()
check_time_grid.AutoSize()
checktime_dlg.SetSizerAndFit(vbox)
checktime_dlg.Layout()
checktime_dlg.Update()
checktime_dlg = wx.Dialog(self, title='Checking Time...')
check_time_grid = wxgrid.Grid(checktime_dlg)
check_time_grid.CreateGrid(0, 3)
# Set the column labels
check_time_grid.SetColLabelValue(0, 'Time In')
check_time_grid.SetColLabelValue(1, 'Time Out')
check_time_grid.SetColLabelValue(2, 'Hours')
check_time_grid.HideRowLabels()
main_app_badge = self.get_entered_badge()
badge_input = wx.TextCtrl(checktime_dlg, size=(200, -1))
badge_input.Bind(wx.EVT_TEXT, badge_change)
submit_btn = wx.Button(checktime_dlg, label='Close',
size=(80, 80))
submit_btn.Bind(wx.EVT_BUTTON,
lambda event: checktime_dlg.EndModal(True))
vbox = wx.BoxSizer(wx.VERTICAL)
vbox.AddSpacer(20)
vbox.Add(badge_input)
vbox.AddSpacer(20)
vbox.Add(check_time_grid, flag=wx.EXPAND)
vbox.AddSpacer(20)
vbox.Add(submit_btn)
badge_input.SetValue(main_app_badge)
# Fit the grid to the size of the window
checktime_dlg.SetSizerAndFit(vbox)
checktime_dlg.Layout()
checktime_dlg.Update()
checktime_dlg.ShowModal()
checktime_dlg.Destroy()
return
@return_focus
def add_user(self, event):
# Create a dialog that has inputs for a badge number, display name,
# and photo URL. When the dialog is submitted, add the user to the
# database and update the active badges grid.
self.settings_dlg = wx.Dialog(self, title='Add User')
badge_num_label = wx.StaticText(self.settings_dlg,
label='Badge Number')
badge_num_input = wx.TextCtrl(self.settings_dlg, size=(200, -1))
display_name_label = wx.StaticText(self.settings_dlg,
label='Display Name')
display_name_input = wx.TextCtrl(self.settings_dlg, size=(200, -1))
photo_url_label = wx.StaticText(self.settings_dlg,
label='Photo URL')
photo_url_input = wx.TextCtrl(self.settings_dlg, size=(400, -1))
submit_btn = wx.Button(self.settings_dlg, label='Save and Close',
size=(120, 80))
submit_btn.Bind(wx.EVT_BUTTON, lambda event: self.submit_user(
event, badge_num_input, display_name_input, photo_url_input
))
spacer_size = 20
vbox = wx.BoxSizer(wx.VERTICAL)
vbox.Add(badge_num_label)
vbox.Add(badge_num_input)
vbox.AddSpacer(spacer_size)
vbox.Add(display_name_label)
vbox.Add(display_name_input)
vbox.AddSpacer(spacer_size)
vbox.Add(photo_url_label)
vbox.Add(photo_url_input)
vbox.AddSpacer(spacer_size)
vbox.Add(submit_btn)
vbox.AddSpacer(spacer_size)
self.settings_dlg.SetSizerAndFit(vbox)
self.settings_dlg.Layout()
self.settings_dlg.Update()
self.settings_dlg.ShowModal()
self.settings_dlg.Destroy()
def submit_user(self, event, badge_num_input, display_name_input,
photo_url_input):
badge_num = self.get_entered_badge()
display_name = display_name_input.GetValue()
photo_url = photo_url_input.GetValue()
# Don't allow submit unless a name and number are in
if not all([badge_num, display_name]):
errmsg = 'Please fill in the badge number and display name fields'
wx.MessageBox(errmsg, 'Error', wx.OK | wx.ICON_ERROR)
if badge_num == '':
badge_num_input.SetFocus()
elif display_name == '':
display_name_input.SetFocus()
return
libtt.create_user(badge_num, display_name, photo_url)
self.settings_dlg.EndModal(True)
def set_badge_input(self, event, badge_num):
self.badge_num_input.ChangeValue(badge_num)
self.badge_num_input.SetFocus()
self.find_user_dlg.EndModal(True)
# If we want to auto-punch after people are selected
# in the search window we want to execute this.
if True:
evt = wx.CommandEvent(wx.EVT_TEXT_ENTER.typeId)
evt.SetEventObject(self.badge_num_input)
wx.PostEvent(self.badge_num_input, evt)
def update_find_user_search(self, search_text):
matches = {}
self.find_user_badge_sizer.Clear(True)
for num, b in self.find_user_badges.items():
if search_text in b['display_name'].lower():
matches[num] = b
vbox = self.create_badge_card(num,
self.scrolled_window,
self.set_badge_input)
self.find_user_badge_sizer.Add(vbox, 0, wx.ALL, border=10)
self.find_user_dlg.Fit()
self.find_user_dlg.Layout()
self.find_user_dlg.Update()
def find_user_input_change(self, event):
search_text = event.GetString().lower()
self.update_find_user_search(search_text)
@return_focus
def find_user(self, event):
self.find_user_badges = libtt.get_badges()
if len(self.find_user_badges) == 0:
wx.MessageBox('There are no users in the system.',
'Error', wx.OK | wx.ICON_ERROR)
return
self.find_user_dlg = wx.Dialog(self, title='Find User')
search_input = wx.TextCtrl(self.find_user_dlg, size=(200, -1))
search_input.Bind(wx.EVT_TEXT, self.find_user_input_change)
self.scrolled_window = wx.ScrolledWindow(self.find_user_dlg)
self.scrolled_window.SetScrollRate(10, 10)
self.scrolled_window.SetMinSize((800, 600))
self.find_user_badge_sizer = wx.WrapSizer(wx.HORIZONTAL)
self.scrolled_window.SetSizer(self.find_user_badge_sizer)
vbox = wx.BoxSizer(wx.VERTICAL)
vbox.SetMinSize((600, -1))
vbox.AddSpacer(20)
vbox.Add(search_input)
vbox.AddSpacer(20)
vbox.Add(self.scrolled_window, flag=wx.EXPAND, border=10)
vbox.AddSpacer(20)
self.update_find_user_search('')
self.find_user_dlg.SetSizerAndFit(vbox)
self.find_user_dlg.Layout()
self.find_user_dlg.Update()
self.find_user_dlg.ShowModal()
self.find_user_dlg.Destroy()
@return_focus
def punch_all_out(self, event):
for badge_num, badge in libtt.get_badges().items():
if badge['status'] == 'in':
self.punch_out(None, badge_num)
def submit_settings(self, event, keys: list, vfuncs: list):
for k, v in zip(keys, vfuncs):
_app_settings[k] = v()
store_app_settings()
self.settings_dlg.EndModal(True)
@return_focus
def edit_settings(self, event):
self.settings_dlg = wx.Dialog(self, title='System Settings')
allow_all_out_chk = wx.CheckBox(self.settings_dlg,
label='Allow All Out')
allow_all_out_chk.SetValue(_app_settings['allow_all_out'])
show_active_badges_chk = wx.CheckBox(self.settings_dlg,
label='Show Active Users')
show_active_badges_chk.SetValue(_app_settings['show_active_badges'])
auto_out_time_val = _app_settings['auto_out_time']
auto_out_chk = wx.CheckBox(
self.settings_dlg,
label='Auto Punch Out'
)
auto_out_chk.SetValue(auto_out_time_val is not None)
auto_out_time = wx.adv.TimePickerCtrl(self.settings_dlg)
if auto_out_time_val is not None:
auto_out_time.SetValue(
datetime.strptime(auto_out_time_val, '%H:%M')
)
auto_out_chk.Bind(wx.EVT_CHECKBOX,
lambda event: auto_out_time.Enable(event.IsChecked()))
submit_btn = wx.Button(self.settings_dlg, label='Submit',
size=(80, 80))
# I'm not thrilled with this completely untyped way of doing this, but
# it's a quick way to get the settings dialog working.
keys = ['allow_all_out',
'show_active_badges',
'auto_out_time']
# This is a list of functions that we'll call to get the values of the
# controls in the dialog.
control_values = [
allow_all_out_chk.GetValue,
show_active_badges_chk.GetValue,
lambda: (auto_out_time.GetValue()
.Format('%H:%M')
if auto_out_chk.IsChecked() else None),
]
spacer_size = 20
vbox = wx.BoxSizer(wx.VERTICAL)
vbox.Add(allow_all_out_chk)
vbox.AddSpacer(spacer_size)
vbox.Add(show_active_badges_chk)
vbox.AddSpacer(spacer_size)
vbox.Add(auto_out_chk)
vbox.AddSpacer(spacer_size)
vbox.Add(auto_out_time)
vbox.AddSpacer(spacer_size)
auto_out_time.Enable(event.IsChecked())
# Add extra settings here
# END Extra settings
vbox.Add(submit_btn)
vbox.AddSpacer(spacer_size)
submit_btn.Bind(wx.EVT_BUTTON,
lambda event: self.submit_settings(event,
keys,
control_values))
self.settings_dlg.SetSizerAndFit(vbox)
self.settings_dlg.Layout()
self.settings_dlg.Update()
self.settings_dlg.ShowModal()
self.settings_dlg.Destroy()
# Here's how we fire up the wxPython app
if __name__ == '__main__':
import sys
if hasattr(sys, 'frozen'):
import pyi_splash
pyi_splash.close()
_app_settings = get_app_settings()
if _app_settings is None:
_app_settings = default_app_settings()
store_app_settings()
app = wx.App()
frame = MainWindow(parent=None, id=-1)
frame.Show()
app.MainLoop()