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) {} | ||||
| 
 | ||||
|  | ||||
| @ -8,7 +8,16 @@ class TickModel { | ||||
|         $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