Skip to content

Commit e59db82

Browse files
committed
Make the Remove button on the ChargeManager screen actually do something.
Pulled the delete functions from the test file into the primary thymed file. Added a new screen and a warning about deleting.
1 parent 039ba1f commit e59db82

File tree

4 files changed

+163
-76
lines changed

4 files changed

+163
-76
lines changed

src/thymed/__init__.py

+53-1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@
6767
_CHARGES.touch()
6868

6969

70+
# Exceptions
71+
72+
73+
class ThymedError(Exception):
74+
"""A custome Exception for Thymed."""
75+
76+
7077
# Classes
7178

7279

@@ -237,7 +244,14 @@ def general_report(self, start: dt.datetime, end: dt.datetime) -> pd.DataFrame:
237244
238245
Start and End can theoretically be any datetime object.
239246
"""
240-
df = pd.DataFrame(self.code.times, columns=["clock_in", "clock_out"])
247+
try:
248+
df = pd.DataFrame(self.code.times, columns=["clock_in", "clock_out"])
249+
except ValueError as exc:
250+
# This happens when only a clock_in time is provided.
251+
# AKA the code was created, and initialized, but not punched out.
252+
raise ThymedError(
253+
"Looks like this code doesn't have clock_in and clock_out times!"
254+
) from exc
241255
df["duration"] = df.clock_out - df.clock_in
242256
df["hours"] = df.duration.apply(
243257
lambda x: x.components.hours + round(x.components.minutes / 60, 1)
@@ -343,6 +357,44 @@ def get_code(id: int) -> Any:
343357
return code
344358

345359

360+
def delete_charge(id: str = "99999999") -> None:
361+
"""Cleanup the test ChargeCode and punch data.
362+
363+
This function manually removes the data. There
364+
may be a better way to do this in the future...
365+
"""
366+
with open(_CHARGES) as f:
367+
# We know it won't be blank, since we only call
368+
# this function after we tested it already. So
369+
# no try:except like the rest of the codebase.
370+
codes = json.load(f, object_hook=object_decoder)
371+
372+
with open(_CHARGES, "w") as f:
373+
# Remove the testing code with a pop method.
374+
_ = codes.pop(id)
375+
# Convert the dict of ChargeCodes into a plain dict
376+
out = {}
377+
for k, v in codes.items():
378+
dict_val = v.__dict__
379+
dict_val["__type__"] = "ChargeCode"
380+
del dict_val["times"]
381+
out[k] = dict_val
382+
# Write the new set of codes back to the file.
383+
_ = f.write(json.dumps(out, indent=2))
384+
385+
with open(_DATA) as f:
386+
# We know it won't be blank, since we only call
387+
# this function after we tested it already. So
388+
# no try:except like the rest of the codebase.
389+
times = json.load(f)
390+
391+
with open(_DATA, "w") as f:
392+
# Remove the testing code with a pop method.
393+
_ = times.pop(id)
394+
# Write the rest of times back to the file.
395+
_ = f.write(json.dumps(times, indent=2))
396+
397+
346398
# TODO: Function to update _DATA global variable.
347399
# This function should be available in the CLI,
348400
# prompt to make the new file if non-existent,

src/thymed/thymed.tcss

+12
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@ AddScreen {
160160
align: center middle;
161161
}
162162

163+
RemoveScreen {
164+
align: center middle;
165+
}
166+
163167
#dialog {
164168
grid-size: 2;
165169
grid-rows: 1fr 1fr 1fr 1fr 1fr 1fr;
@@ -171,6 +175,14 @@ AddScreen {
171175
background: $surface;
172176
}
173177

178+
#warning {
179+
column-span: 2;
180+
height: 1fr;
181+
width: 1fr;
182+
color: red;
183+
content-align: center middle;
184+
}
185+
174186
#question {
175187
column-span: 2;
176188
height: 1fr;

src/thymed/tui.py

+94-18
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,13 @@
3737
from textual.widgets import Header
3838
from textual.widgets import Input
3939
from textual.widgets import Placeholder
40+
from textual.widgets import Select
4041
from textual.widgets import Static
4142
from textual.widgets import Switch
4243
from textual_plotext import PlotextPlot
4344

4445
import thymed
46+
from thymed import ThymedError
4547
from thymed import TimeCard
4648

4749

@@ -200,7 +202,7 @@ class Statblock(Container):
200202
"""A Block of statistics."""
201203

202204
timecard: reactive[TimeCard | None] = reactive(None, recompose=True)
203-
period: reactive[str | None] = reactive("Period", recompose=True)
205+
period: reactive[str | None] = reactive("Week", recompose=True)
204206
delta: reactive[timedelta | None] = reactive(timedelta(days=7), recompose=True)
205207

206208
def compose(self) -> ComposeResult:
@@ -283,22 +285,24 @@ def replot(self) -> None:
283285
card = TimeCard(self.code)
284286
end = datetime.today()
285287
start = end - self.delta
286-
df = card.general_report(start, end)
287-
df["clock_in_day"] = df.clock_in.dt.strftime("%d/%m/%Y")
288-
# TODO: Make the plot show an exact range, whether or not work entries are present. (Create a PR later.)
289-
# Create a new date range with daily increments over the full range.
290-
# The punches increments are variable (eg you may not work every day)
291-
# new_clock_in_day = pd.date_range(start, end)
292-
# Reindex the dataframe on the new range, filling blanks with an int of zero.
293-
# df = df.set_index("clock_in_day").reindex(new_clock_in_day, fill_value=0).reset_index()
294-
# Need to convert the clock_in to string for plotext
295-
dates = df.clock_in_day
296-
plt.clear_data()
297-
plt.bar(dates, df.hours)
298-
plt.title(self.name)
299-
plt.xlabel("Date")
300-
plt.ylabel("Hours")
301-
self.plot.refresh()
288+
try:
289+
df = card.general_report(start, end)
290+
df["clock_in_day"] = df.clock_in.dt.strftime("%d/%m/%Y")
291+
dates = df.clock_in_day
292+
# TODO: Make the plot show an exact range, whether or not work entries are present. (Create a PR later.)
293+
plt.clear_data()
294+
plt.bar(dates, df.hours)
295+
plt.title(self.name)
296+
plt.xlabel("Date")
297+
plt.ylabel("Hours")
298+
299+
plt.xlim(left=datetime.strftime(start, "%d/%m/%Y"))
300+
301+
self.plot.refresh()
302+
except ThymedError:
303+
self.notify(
304+
"Problem with that ChargeCode...", severity="error", title="Error"
305+
)
302306

303307
@textual.on(Button.Pressed, "#period")
304308
def cycle_period(self) -> None:
@@ -421,6 +425,52 @@ def on_button_pressed(self, event: Button.Pressed) -> None:
421425
self.app.pop_screen()
422426

423427

428+
class RemoveScreen(ModalScreen):
429+
"""Screen with a dialog to remove a ChargeCode."""
430+
431+
def get_data(self) -> list:
432+
"""Function to retrieve Thymed data."""
433+
with open(thymed._CHARGES) as f:
434+
try:
435+
codes = json.load(f, object_hook=thymed.object_decoder)
436+
437+
# Sort the codes dictionary by key (code id)
438+
sorted_codes = sorted(codes.items(), key=lambda kv: int(kv[0]))
439+
codes = [x[1] for x in sorted_codes]
440+
except json.JSONDecodeError: # pragma: no cover
441+
self.notify("Got JSON Error", severity="error")
442+
# If the file is completely blank, we will get an error
443+
codes = [("No Codes Found", 0)]
444+
445+
out = []
446+
for code in codes:
447+
out.append((code.name, str(code.id)))
448+
449+
return out
450+
451+
def compose(self) -> ComposeResult:
452+
yield Grid(
453+
Title("Remove ChargeCode information", id="question"),
454+
Static("ID Number: ", classes="right"),
455+
Select(options=self.get_data(), id="charge_id"),
456+
Static(
457+
"THIS WILL IMMEDIATELY DELETE THE CODE AND ALL PUNCH DATA! IT CANNOT BE UNDONE!",
458+
id="warning",
459+
),
460+
Button("DELETE IT", variant="error", id="submit"),
461+
Button("Cancel", variant="primary", id="cancel"),
462+
id="dialog",
463+
)
464+
465+
def on_button_pressed(self, event: Button.Pressed) -> None:
466+
charge_id = self.query_one("#charge_id").value
467+
data = [charge_id]
468+
if event.button.id == "submit":
469+
self.dismiss(data)
470+
else:
471+
self.app.pop_screen()
472+
473+
424474
class ThymedApp(App[None]):
425475
CSS_PATH = "thymed.tcss"
426476
TITLE = "Thymed App"
@@ -564,7 +614,7 @@ def option_buttons(self, event: Button.Pressed) -> None:
564614
self.action_launch_settings()
565615

566616
@textual.on(Button.Pressed, "#add")
567-
def code_screen(self, event: Button.Pressed):
617+
def code_screen(self, event: Button.Pressed) -> None:
568618
"""When we want to add a chargecode.
569619
570620
When the AddScreen is dismissed, it will call the
@@ -590,6 +640,32 @@ def add_code(data: list):
590640

591641
self.push_screen(AddScreen(), add_code)
592642

643+
@textual.on(Button.Pressed, "#remove")
644+
def remove_screen(self, event: Button.Pressed) -> None:
645+
"""When we want to remove a chargecode.
646+
647+
When the RemoveScreen is dismissed, it will call the
648+
callback function below.
649+
"""
650+
651+
def remove_code(data: list):
652+
"""Method to actually remove the ChargeCode and data.
653+
654+
This method gets called after the RemoveScreen is dismissed.
655+
It takes data and calls the base Thymed methods to remove
656+
a ChargeCode object and it's corresponding punch data.
657+
658+
After we finish removing the code, we call get_data on
659+
the ChargeManager screen to refresh the table.
660+
"""
661+
id = data[0]
662+
thymed.delete_charge(id)
663+
664+
applet = self.query_one("#applet")
665+
applet.data = applet.get_data()
666+
667+
self.push_screen(RemoveScreen(), remove_code)
668+
593669
def action_open_link(self, link: str) -> None:
594670
self.app.bell()
595671
import webbrowser

tests/test_timecard.py

+4-57
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,20 @@
11
"""Test Cases for the ChargeCode class."""
22

33
import datetime as dt
4-
import json
54
import random
65

76
import pandas as pd
87
import pytest
98
from rich.console import Console
109

11-
from thymed import _CHARGES
12-
from thymed import _DATA
1310
from thymed import ChargeCode
1411
from thymed import TimeCard
15-
from thymed import object_decoder
12+
from thymed import delete_charge
1613

1714

1815
# CLEANUP UTILITIES
1916

2017

21-
def remove_test_charge(id: str = "99999999") -> None:
22-
"""Cleanup the test ChargeCode.
23-
24-
Testing creates a ChargeCode. This function
25-
manually removes the data, since deleting/removing
26-
ChargeCode objects is not currently supported.
27-
"""
28-
with open(_CHARGES) as f:
29-
# We know it won't be blank, since we only call
30-
# this function after we tested it already. So
31-
# no try:except like the rest of the codebase.
32-
codes = json.load(f, object_hook=object_decoder)
33-
34-
with open(_CHARGES, "w") as f:
35-
# Remove the testing code with a pop method.
36-
_ = codes.pop(id)
37-
# Convert the dict of ChargeCodes into a plain dict
38-
out = {}
39-
for k, v in codes.items():
40-
dict_val = v.__dict__
41-
dict_val["__type__"] = "ChargeCode"
42-
del dict_val["times"]
43-
out[k] = dict_val
44-
# Write the new set of codes back to the file.
45-
_ = f.write(json.dumps(out, indent=2))
46-
47-
48-
def remove_test_data(id: str = "99999999") -> None:
49-
"""Cleanup the test ChargeCode punch data.
50-
51-
Testing creates a ChargeCode. This function
52-
manually removes the data, since deleting/removing
53-
ChargeCode objects is not currently supported.
54-
"""
55-
with open(_DATA) as f:
56-
# We know it won't be blank, since we only call
57-
# this function after we tested it already. So
58-
# no try:except like the rest of the codebase.
59-
times = json.load(f)
60-
61-
with open(_DATA, "w") as f:
62-
# Remove the testing code with a pop method.
63-
_ = times.pop(id)
64-
# Write the rest of times back to the file.
65-
_ = f.write(json.dumps(times, indent=2))
66-
67-
6818
# FIXTURES
6919
@pytest.fixture
7020
def fake_times(
@@ -139,8 +89,7 @@ def test_weekly(fake_times):
13989
assert isinstance(df, pd.DataFrame)
14090
assert not df.empty
14191

142-
remove_test_charge()
143-
remove_test_data()
92+
delete_charge("99999999")
14493

14594

14695
def test_period(fake_times):
@@ -151,8 +100,7 @@ def test_period(fake_times):
151100
assert isinstance(df, pd.DataFrame)
152101
assert not df.empty
153102

154-
remove_test_charge()
155-
remove_test_data()
103+
delete_charge("99999999")
156104

157105

158106
def test_monthly(fake_times):
@@ -163,8 +111,7 @@ def test_monthly(fake_times):
163111
assert isinstance(df, pd.DataFrame)
164112
assert not df.empty
165113

166-
remove_test_charge()
167-
remove_test_data()
114+
delete_charge("99999999")
168115

169116

170117
if __name__ == "__main__": # pragma: no cover

0 commit comments

Comments
 (0)