From 195de75b7816c26acfa626da70b1dbc28255081b Mon Sep 17 00:00:00 2001 From: Greg Sarjeant Date: Sat, 9 Aug 2025 14:49:56 +0000 Subject: [PATCH] Add tick delete function (#64) Reviewed-on: https://gitea.subcultureofone.org/greg/tkr/pulls/64 Co-authored-by: Greg Sarjeant Co-committed-by: Greg Sarjeant --- public/css/default.css | 14 ++++ .../TickController/TickController.php | 31 ++++++++ src/Framework/Router/Router.php | 1 + src/Model/SettingsModel/SettingsModel.php | 2 + src/Model/TickModel/TickModel.php | 42 ++++++++++- src/View/TicksView/TicksView.php | 10 ++- .../TickController/TickControllerTest.php | 1 - tests/Model/TickModel/TickModelTest.php | 75 +++++++++++++++++++ 8 files changed, 172 insertions(+), 4 deletions(-) create mode 100644 tests/Model/TickModel/TickModelTest.php diff --git a/public/css/default.css b/public/css/default.css index ca54f5b..738995b 100644 --- a/public/css/default.css +++ b/public/css/default.css @@ -376,6 +376,20 @@ time { text-decoration: none; } +.delete-tick-form { + display: inline; + margin: 0; +} + +.delete-tick-button { + background: none; + border: none; + padding: 0; + margin: 0; + cursor: pointer; + font-size: inherit; +} + /* Mood selection page */ .mood-option input { position: absolute; diff --git a/src/Controller/TickController/TickController.php b/src/Controller/TickController/TickController.php index fb8d17a..f33aa24 100644 --- a/src/Controller/TickController/TickController.php +++ b/src/Controller/TickController/TickController.php @@ -27,4 +27,35 @@ class TickController extends Controller{ echo '

500 - Internal Server Error

'; } } + + public function handleDelete(string $id){ + global $app; + + $id = (int) $id; + Log::debug("Attempting to delete tick with ID: {$id}"); + + try { + $tickModel = new TickModel($app['db'], $app['settings']); + + // TickModel->delete() handles validation and sets flash messages: + // - "Tick not found" if tick doesn't exist + // - "Tick is too old to delete" if outside deletion window + // - "Deleted: '{content}'" on success + $success = $tickModel->delete($id); + + if ($success) { + Log::info("Successfully deleted tick {$id}"); + } else { + Log::warning("Failed to delete tick {$id}"); + } + + } catch (Exception $e) { + Log::error("Exception while deleting tick {$id}: " . $e->getMessage()); + Session::setFlashMessage('error', 'An error occurred while deleting the tick'); + } + + // Redirect back to homepage + header('Location: ' . Util::buildRelativeUrl($app['settings']->basePath, '')); + exit(); + } } \ No newline at end of file diff --git a/src/Framework/Router/Router.php b/src/Framework/Router/Router.php index f0bb6e0..66f5f29 100644 --- a/src/Framework/Router/Router.php +++ b/src/Framework/Router/Router.php @@ -25,6 +25,7 @@ class Router { ['tkr-setup', 'AdminController@showSetup'], ['tkr-setup', 'AdminController@handleSetup', ['POST']], ['tick/{id}', 'TickController'], + ['tick/{id}/delete', 'TickController@handleDelete', ['POST']], ['css/custom/{filename}.css', 'CssController@serveCustomCss'], ]; diff --git a/src/Model/SettingsModel/SettingsModel.php b/src/Model/SettingsModel/SettingsModel.php index 539332d..a9b714b 100644 --- a/src/Model/SettingsModel/SettingsModel.php +++ b/src/Model/SettingsModel/SettingsModel.php @@ -12,6 +12,8 @@ class SettingsModel { public ?int $cssId = null; public bool $strictAccessibility = true; public ?int $logLevel = null; + // not currently configurable + public int $tickDeleteHours = 1; public function __construct(private PDO $db) {} diff --git a/src/Model/TickModel/TickModel.php b/src/Model/TickModel/TickModel.php index 4896fbc..74bab9f 100644 --- a/src/Model/TickModel/TickModel.php +++ b/src/Model/TickModel/TickModel.php @@ -7,8 +7,17 @@ class TickModel { public function getPage(int $limit, int $offset = 0): array { $stmt = $this->db->prepare("SELECT id, timestamp, tick FROM tick ORDER BY timestamp DESC LIMIT ? OFFSET ?"); $stmt->execute([$limit, $offset]); - - return $stmt->fetchAll(PDO::FETCH_ASSOC); + + $ticks = $stmt->fetchAll(PDO::FETCH_ASSOC); + + return array_map(function($tick) { + $tickTime = new DateTimeImmutable($tick['timestamp'], new DateTimeZone('UTC')); + $now = new DateTimeImmutable('now', new DateTimeZone('UTC')); + $hoursSinceCreation = ($now->getTimestamp() - $tickTime->getTimestamp()) / 3600; + + $tick['can_delete'] = $hoursSinceCreation <= $this->settings->tickDeleteHours; + return $tick; + }, $ticks); } public function insert(string $tick, ?DateTimeImmutable $datetime = null): void { @@ -35,4 +44,33 @@ class TickModel { 'settings' => $this->settings, ]; } + + public function delete(int $id): bool { + // Get tick and validate + $stmt = $this->db->prepare("SELECT tick, timestamp FROM tick WHERE id=?"); + $stmt->execute([$id]); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($row === false || empty($row)) { + Session::setFlashMessage('error', 'Tick not found'); + return false; + } + + // Check deletion window + $tickTime = new DateTimeImmutable($row['timestamp'], new DateTimeZone('UTC')); + $now = new DateTimeImmutable('now', new DateTimeZone('UTC')); + $hoursSinceCreation = ($now->getTimestamp() - $tickTime->getTimestamp()) / 3600; + + if ($hoursSinceCreation > $this->settings->tickDeleteHours) { + Session::setFlashMessage('error', 'Tick is too old to delete'); + return false; + } + + // Delete and set success message + $stmt = $this->db->prepare("DELETE FROM tick WHERE id=?"); + $stmt->execute([$id]); + + Session::setFlashMessage('success', "Deleted: '{$row['tick']}'"); + return true; + } } diff --git a/src/View/TicksView/TicksView.php b/src/View/TicksView/TicksView.php index 1e434a6..d9e1646 100644 --- a/src/View/TicksView/TicksView.php +++ b/src/View/TicksView/TicksView.php @@ -23,7 +23,15 @@ class TicksView { $relativeTime = Util::relative_time($tick['timestamp']); ?>
  • - 🗑️ + +
    " + class="delete-tick-form"> + + +
    + +
  • diff --git a/tests/Controller/TickController/TickControllerTest.php b/tests/Controller/TickController/TickControllerTest.php index 0f90d8f..0401d93 100644 --- a/tests/Controller/TickController/TickControllerTest.php +++ b/tests/Controller/TickController/TickControllerTest.php @@ -152,5 +152,4 @@ class TickControllerTest extends TestCase // Should return 500 error $this->assertStringContainsString('500 - Internal Server Error', $output); } - } \ No newline at end of file diff --git a/tests/Model/TickModel/TickModelTest.php b/tests/Model/TickModel/TickModelTest.php new file mode 100644 index 0000000..e0ddfb3 --- /dev/null +++ b/tests/Model/TickModel/TickModelTest.php @@ -0,0 +1,75 @@ +mockPdo = $this->createMock(PDO::class); + $this->settings = new SettingsModel($this->mockPdo); + $this->settings->tickDeleteHours = 1; // 1 hour deletion window + } + + public function testDeleteWithRecentTick(): void + { + // Mock successful deletion of recent tick + $recentTimestamp = (new DateTimeImmutable())->format('Y-m-d H:i:s'); + + $mockStatement1 = $this->createMock(PDOStatement::class); + $mockStatement1->method('execute')->with([123]); + $mockStatement1->method('fetch')->willReturn([ + 'tick' => 'Test content', + 'timestamp' => $recentTimestamp + ]); + + $mockStatement2 = $this->createMock(PDOStatement::class); + $mockStatement2->method('execute')->with([123]); + + $this->mockPdo->method('prepare') + ->willReturnOnConsecutiveCalls($mockStatement1, $mockStatement2); + + $tickModel = new TickModel($this->mockPdo, $this->settings); + $result = $tickModel->delete(123); + + $this->assertTrue($result); + } + + public function testDeleteWithNonexistentTick(): void + { + $mockStatement = $this->createMock(PDOStatement::class); + $mockStatement->method('execute')->with([999]); + $mockStatement->method('fetch')->willReturn(false); + + $this->mockPdo->method('prepare')->willReturn($mockStatement); + + $tickModel = new TickModel($this->mockPdo, $this->settings); + $result = $tickModel->delete(999); + + $this->assertFalse($result); + } + + public function testDeleteWithOldTick(): void + { + // Tick from 3 hours ago (outside 1-hour window) + $oldTimestamp = (new DateTimeImmutable('-3 hours'))->format('Y-m-d H:i:s'); + + $mockStatement = $this->createMock(PDOStatement::class); + $mockStatement->method('execute')->with([456]); + $mockStatement->method('fetch')->willReturn([ + 'tick' => 'Old content', + 'timestamp' => $oldTimestamp + ]); + + $this->mockPdo->method('prepare')->willReturn($mockStatement); + + $tickModel = new TickModel($this->mockPdo, $this->settings); + $result = $tickModel->delete(456); + + $this->assertFalse($result); + } +} \ No newline at end of file