Skip to content

Commit

Permalink
Make the Remove button on the ChargeManager screen actually do someth…
Browse files Browse the repository at this point in the history
…ing.

Pulled the delete functions from the test file into the primary thymed file.

Added a new screen and a warning about deleting.
  • Loading branch information
Czarified committed Aug 16, 2024
1 parent 039ba1f commit e59db82
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 76 deletions.
54 changes: 53 additions & 1 deletion src/thymed/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@
_CHARGES.touch()


# Exceptions


class ThymedError(Exception):
"""A custome Exception for Thymed."""


# Classes


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


def delete_charge(id: str = "99999999") -> None:
"""Cleanup the test ChargeCode and punch data.
This function manually removes the data. There
may be a better way to do this in the future...
"""
with open(_CHARGES) as f:
# We know it won't be blank, since we only call
# this function after we tested it already. So
# no try:except like the rest of the codebase.
codes = json.load(f, object_hook=object_decoder)

with open(_CHARGES, "w") as f:
# Remove the testing code with a pop method.
_ = codes.pop(id)
# Convert the dict of ChargeCodes into a plain dict
out = {}
for k, v in codes.items():
dict_val = v.__dict__
dict_val["__type__"] = "ChargeCode"
del dict_val["times"]
out[k] = dict_val
# Write the new set of codes back to the file.
_ = f.write(json.dumps(out, indent=2))

with open(_DATA) as f:
# We know it won't be blank, since we only call
# this function after we tested it already. So
# no try:except like the rest of the codebase.
times = json.load(f)

with open(_DATA, "w") as f:
# Remove the testing code with a pop method.
_ = times.pop(id)
# Write the rest of times back to the file.
_ = f.write(json.dumps(times, indent=2))


# TODO: Function to update _DATA global variable.
# This function should be available in the CLI,
# prompt to make the new file if non-existent,
Expand Down
12 changes: 12 additions & 0 deletions src/thymed/thymed.tcss
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ AddScreen {
align: center middle;
}

RemoveScreen {
align: center middle;
}

#dialog {
grid-size: 2;
grid-rows: 1fr 1fr 1fr 1fr 1fr 1fr;
Expand All @@ -171,6 +175,14 @@ AddScreen {
background: $surface;
}

#warning {
column-span: 2;
height: 1fr;
width: 1fr;
color: red;
content-align: center middle;
}

#question {
column-span: 2;
height: 1fr;
Expand Down
112 changes: 94 additions & 18 deletions src/thymed/tui.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,13 @@
from textual.widgets import Header
from textual.widgets import Input
from textual.widgets import Placeholder
from textual.widgets import Select
from textual.widgets import Static
from textual.widgets import Switch
from textual_plotext import PlotextPlot

import thymed
from thymed import ThymedError
from thymed import TimeCard


Expand Down Expand Up @@ -200,7 +202,7 @@ class Statblock(Container):
"""A Block of statistics."""

timecard: reactive[TimeCard | None] = reactive(None, recompose=True)
period: reactive[str | None] = reactive("Period", recompose=True)
period: reactive[str | None] = reactive("Week", recompose=True)
delta: reactive[timedelta | None] = reactive(timedelta(days=7), recompose=True)

def compose(self) -> ComposeResult:
Expand Down Expand Up @@ -283,22 +285,24 @@ def replot(self) -> None:
card = TimeCard(self.code)
end = datetime.today()
start = end - self.delta
df = card.general_report(start, end)
df["clock_in_day"] = df.clock_in.dt.strftime("%d/%m/%Y")
# TODO: Make the plot show an exact range, whether or not work entries are present. (Create a PR later.)
# Create a new date range with daily increments over the full range.
# The punches increments are variable (eg you may not work every day)
# new_clock_in_day = pd.date_range(start, end)
# Reindex the dataframe on the new range, filling blanks with an int of zero.
# df = df.set_index("clock_in_day").reindex(new_clock_in_day, fill_value=0).reset_index()
# Need to convert the clock_in to string for plotext
dates = df.clock_in_day
plt.clear_data()
plt.bar(dates, df.hours)
plt.title(self.name)
plt.xlabel("Date")
plt.ylabel("Hours")
self.plot.refresh()
try:
df = card.general_report(start, end)
df["clock_in_day"] = df.clock_in.dt.strftime("%d/%m/%Y")
dates = df.clock_in_day
# TODO: Make the plot show an exact range, whether or not work entries are present. (Create a PR later.)
plt.clear_data()
plt.bar(dates, df.hours)
plt.title(self.name)
plt.xlabel("Date")
plt.ylabel("Hours")

plt.xlim(left=datetime.strftime(start, "%d/%m/%Y"))

self.plot.refresh()
except ThymedError:
self.notify(
"Problem with that ChargeCode...", severity="error", title="Error"
)

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


class RemoveScreen(ModalScreen):
"""Screen with a dialog to remove a ChargeCode."""

def get_data(self) -> list:
"""Function to retrieve Thymed data."""
with open(thymed._CHARGES) as f:
try:
codes = json.load(f, object_hook=thymed.object_decoder)

# Sort the codes dictionary by key (code id)
sorted_codes = sorted(codes.items(), key=lambda kv: int(kv[0]))
codes = [x[1] for x in sorted_codes]
except json.JSONDecodeError: # pragma: no cover
self.notify("Got JSON Error", severity="error")
# If the file is completely blank, we will get an error
codes = [("No Codes Found", 0)]

out = []
for code in codes:
out.append((code.name, str(code.id)))

return out

def compose(self) -> ComposeResult:
yield Grid(
Title("Remove ChargeCode information", id="question"),
Static("ID Number: ", classes="right"),
Select(options=self.get_data(), id="charge_id"),
Static(
"THIS WILL IMMEDIATELY DELETE THE CODE AND ALL PUNCH DATA! IT CANNOT BE UNDONE!",
id="warning",
),
Button("DELETE IT", variant="error", id="submit"),
Button("Cancel", variant="primary", id="cancel"),
id="dialog",
)

def on_button_pressed(self, event: Button.Pressed) -> None:
charge_id = self.query_one("#charge_id").value
data = [charge_id]
if event.button.id == "submit":
self.dismiss(data)
else:
self.app.pop_screen()


class ThymedApp(App[None]):
CSS_PATH = "thymed.tcss"
TITLE = "Thymed App"
Expand Down Expand Up @@ -564,7 +614,7 @@ def option_buttons(self, event: Button.Pressed) -> None:
self.action_launch_settings()

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

self.push_screen(AddScreen(), add_code)

@textual.on(Button.Pressed, "#remove")
def remove_screen(self, event: Button.Pressed) -> None:
"""When we want to remove a chargecode.
When the RemoveScreen is dismissed, it will call the
callback function below.
"""

def remove_code(data: list):
"""Method to actually remove the ChargeCode and data.
This method gets called after the RemoveScreen is dismissed.
It takes data and calls the base Thymed methods to remove
a ChargeCode object and it's corresponding punch data.
After we finish removing the code, we call get_data on
the ChargeManager screen to refresh the table.
"""
id = data[0]
thymed.delete_charge(id)

applet = self.query_one("#applet")
applet.data = applet.get_data()

self.push_screen(RemoveScreen(), remove_code)

def action_open_link(self, link: str) -> None:
self.app.bell()
import webbrowser
Expand Down
61 changes: 4 additions & 57 deletions tests/test_timecard.py
Original file line number Diff line number Diff line change
@@ -1,70 +1,20 @@
"""Test Cases for the ChargeCode class."""

import datetime as dt
import json
import random

import pandas as pd
import pytest
from rich.console import Console

from thymed import _CHARGES
from thymed import _DATA
from thymed import ChargeCode
from thymed import TimeCard
from thymed import object_decoder
from thymed import delete_charge


# CLEANUP UTILITIES


def remove_test_charge(id: str = "99999999") -> None:
"""Cleanup the test ChargeCode.
Testing creates a ChargeCode. This function
manually removes the data, since deleting/removing
ChargeCode objects is not currently supported.
"""
with open(_CHARGES) as f:
# We know it won't be blank, since we only call
# this function after we tested it already. So
# no try:except like the rest of the codebase.
codes = json.load(f, object_hook=object_decoder)

with open(_CHARGES, "w") as f:
# Remove the testing code with a pop method.
_ = codes.pop(id)
# Convert the dict of ChargeCodes into a plain dict
out = {}
for k, v in codes.items():
dict_val = v.__dict__
dict_val["__type__"] = "ChargeCode"
del dict_val["times"]
out[k] = dict_val
# Write the new set of codes back to the file.
_ = f.write(json.dumps(out, indent=2))


def remove_test_data(id: str = "99999999") -> None:
"""Cleanup the test ChargeCode punch data.
Testing creates a ChargeCode. This function
manually removes the data, since deleting/removing
ChargeCode objects is not currently supported.
"""
with open(_DATA) as f:
# We know it won't be blank, since we only call
# this function after we tested it already. So
# no try:except like the rest of the codebase.
times = json.load(f)

with open(_DATA, "w") as f:
# Remove the testing code with a pop method.
_ = times.pop(id)
# Write the rest of times back to the file.
_ = f.write(json.dumps(times, indent=2))


# FIXTURES
@pytest.fixture
def fake_times(
Expand Down Expand Up @@ -139,8 +89,7 @@ def test_weekly(fake_times):
assert isinstance(df, pd.DataFrame)
assert not df.empty

remove_test_charge()
remove_test_data()
delete_charge("99999999")


def test_period(fake_times):
Expand All @@ -151,8 +100,7 @@ def test_period(fake_times):
assert isinstance(df, pd.DataFrame)
assert not df.empty

remove_test_charge()
remove_test_data()
delete_charge("99999999")


def test_monthly(fake_times):
Expand All @@ -163,8 +111,7 @@ def test_monthly(fake_times):
assert isinstance(df, pd.DataFrame)
assert not df.empty

remove_test_charge()
remove_test_data()
delete_charge("99999999")


if __name__ == "__main__": # pragma: no cover
Expand Down

0 comments on commit e59db82

Please sign in to comment.