Add tick delete function (#64)
Reviewed-on: https://gitea.subcultureofone.org/greg/tkr/pulls/64 Co-authored-by: Greg Sarjeant <greg@subcultureofone.org> Co-committed-by: Greg Sarjeant <greg@subcultureofone.org>
This commit is contained in:
parent
16f8631fd4
commit
195de75b78
@ -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;
|
||||
|
@ -27,4 +27,35 @@ class TickController extends Controller{
|
||||
echo '<h1>500 - Internal Server Error</h1>';
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
@ -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'],
|
||||
];
|
||||
|
||||
|
@ -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) {}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,15 @@ class TicksView {
|
||||
$relativeTime = Util::relative_time($tick['timestamp']);
|
||||
?>
|
||||
<li class="tick" tabindex="0">
|
||||
🗑️ <time datetime="<?php echo $datetime->format('c') ?>"><?php echo Util::escape_html($relativeTime) ?></time>
|
||||
<?php if ($tick['can_delete']): ?>
|
||||
<form method="post"
|
||||
action="<?= Util::buildRelativeUrl($settings->basePath, "tick/{$tick['id']}/delete") ?>"
|
||||
class="delete-tick-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= Util::escape_html($_SESSION['csrf_token']) ?>">
|
||||
<button type="submit" class="delete-tick-button">🗑️</button>
|
||||
</form>
|
||||
<?php endif ?>
|
||||
<time datetime="<?php echo $datetime->format('c') ?>"><?php echo Util::escape_html($relativeTime) ?></time>
|
||||
<span class="tick-text"><?php echo Util::linkify(Util::escape_html($tick['tick'])) ?></span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
|
@ -152,5 +152,4 @@ class TickControllerTest extends TestCase
|
||||
// Should return 500 error
|
||||
$this->assertStringContainsString('500 - Internal Server Error', $output);
|
||||
}
|
||||
|
||||
}
|
75
tests/Model/TickModel/TickModelTest.php
Normal file
75
tests/Model/TickModel/TickModelTest.php
Normal file
@ -0,0 +1,75 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class TickModelTest extends TestCase
|
||||
{
|
||||
private $mockPdo;
|
||||
private $settings;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user