Add database migration. Add accessibility and mood settings. Add mood to tick format.
This commit is contained in:
		
							parent
							
								
									d6673c7ed2
								
							
						
					
					
						commit
						efe9688289
					
				| @ -66,6 +66,8 @@ function confirm_setup(): void { | ||||
|     validate_storage_subdirs(); | ||||
|     validate_tables(); | ||||
|     validate_table_contents(); | ||||
|     migrate_db(); | ||||
|     migrate_tick_files(); | ||||
| } | ||||
| 
 | ||||
| // Make sure the storage/ directory exists and is writable
 | ||||
| @ -114,6 +116,38 @@ function validate_storage_subdirs(): void { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| function migrate_tick_files() { | ||||
|     $files = new RecursiveIteratorIterator( | ||||
|         new RecursiveDirectoryIterator(TICKS_DIR, RecursiveDirectoryIterator::SKIP_DOTS) | ||||
|     ); | ||||
| 
 | ||||
|     foreach ($files as $file) { | ||||
|         if ($file->isFile() && str_ends_with($file->getFilename(), '.txt')) { | ||||
|             migrate_tick_file($file->getPathname()); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| function migrate_tick_file($filepath) { | ||||
|     $lines = file($filepath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); | ||||
|     $modified = false; | ||||
| 
 | ||||
|     foreach ($lines as &$line) { | ||||
|         $fields = explode('|', $line); | ||||
|         if (count($fields) === 2) { | ||||
|             // Convert id|text to id|emoji|text
 | ||||
|             $line = $fields[0] . '||' . $fields[1]; | ||||
|             $modified = true; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     if ($modified) { | ||||
|         file_put_contents($filepath, implode("\n", $lines) . "\n"); | ||||
|         // TODO: log properly
 | ||||
|         //echo "Migrated: " . basename($filepath) . "\n";
 | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| function get_db(): PDO { | ||||
|     try { | ||||
|         // SQLite will just create this if it doesn't exist.
 | ||||
| @ -132,6 +166,93 @@ function get_db(): PDO { | ||||
|     return $db; | ||||
| } | ||||
| 
 | ||||
| // The database version will just be an int
 | ||||
| // stored as PRAGMA user_version. It will
 | ||||
| // correspond to the most recent migration file applied to the db.
 | ||||
| function get_db_version(): int { | ||||
|     $db = get_db(); | ||||
| 
 | ||||
|     return $db->query("PRAGMA user_version")->fetchColumn() ?? 0; | ||||
| } | ||||
| 
 | ||||
| function migration_number_from_file(string $filename): int { | ||||
|     $basename = basename($filename, '.sql'); | ||||
|     $parts = explode('_', $basename); | ||||
|     return (int) $parts[0]; | ||||
| } | ||||
| 
 | ||||
| function set_db_version(int $newVersion): void { | ||||
|     $currentVersion = get_db_version(); | ||||
| 
 | ||||
|     if ($newVersion <= $currentVersion){ | ||||
|         throw new SetupException( | ||||
|             "New version ($newVersion) must be greater than current version ($currentVersion)", | ||||
|             'db_migration' | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     $db = get_db(); | ||||
|     $db->exec("PRAGMA user_version = $newVersion"); | ||||
| } | ||||
| 
 | ||||
| function get_pending_migrations(): array { | ||||
|     $currentVersion = get_db_version(); | ||||
|     $files = glob(DATA_DIR . '/migrations/*.sql'); | ||||
| 
 | ||||
|     $pending = []; | ||||
|     foreach ($files as $file) { | ||||
|         $version = migration_number_from_file($file); | ||||
|         if ($version > $currentVersion) { | ||||
|             $pending[$version] = $file; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     ksort($pending); | ||||
|     return $pending; | ||||
| } | ||||
| 
 | ||||
| function migrate_db(): void { | ||||
|     $migrations = get_pending_migrations(); | ||||
| 
 | ||||
|     if (empty($migrations)) { | ||||
|         # TODO: log
 | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     $db = get_db(); | ||||
|     $db->beginTransaction(); | ||||
| 
 | ||||
|     try { | ||||
|         foreach ($migrations as $version => $file) { | ||||
|             $filename = basename($file); | ||||
|             // TODO: log properly
 | ||||
| 
 | ||||
|             $sql = file_get_contents($file); | ||||
|             if ($sql === false) { | ||||
|                 throw new Exception("Could not read migration file: $file"); | ||||
|             } | ||||
| 
 | ||||
|             // Execute the migration SQL
 | ||||
|             $db->exec($sql); | ||||
|         } | ||||
| 
 | ||||
|         // Update db version
 | ||||
|         $db->commit(); | ||||
|         set_db_version($version); | ||||
|         //TODO: log properly
 | ||||
|         //echo "All migrations completed successfully.\n";
 | ||||
| 
 | ||||
|     } catch (Exception $e) { | ||||
|         $db->rollBack(); | ||||
|         throw new SetupException( | ||||
|             "Migration failed: $filename", | ||||
|             'db_migration', | ||||
|             0, | ||||
|             $e | ||||
|         ); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| function create_tables(): void { | ||||
|     $db = get_db(); | ||||
| 
 | ||||
|  | ||||
| @ -72,6 +72,16 @@ select { | ||||
|     box-sizing: border-box; | ||||
| } | ||||
| 
 | ||||
| input[type="checkbox"] { | ||||
|     width: auto; | ||||
|     height: 2rem; | ||||
|     aspect-ratio: 1; | ||||
|     margin: 0; | ||||
|     cursor: pointer; | ||||
|     justify-self: start; | ||||
|     align-self: center; | ||||
| } | ||||
| 
 | ||||
| /* A bit of custom styling for the file input */ | ||||
| input[type="file"] { | ||||
|     border-style: dashed; | ||||
| @ -429,6 +439,13 @@ time { | ||||
|         - Once the width exceeds that (e.g. desktops), it will convert to horizontal alignment | ||||
| */ | ||||
| @media (min-width: 600px) { | ||||
|     input[type="checkbox"] { | ||||
|         height: 100%; | ||||
|         /*grid-column: 2;*/ | ||||
|         justify-self: start; | ||||
|         align-self: center; | ||||
|     } | ||||
| 
 | ||||
|     label { | ||||
|         text-align: right; | ||||
|         padding-top: 10px; | ||||
|  | ||||
| @ -52,22 +52,24 @@ class AdminController extends Controller { | ||||
|         if ($_SERVER['REQUEST_METHOD'] === 'POST') { | ||||
|             $errors = []; | ||||
| 
 | ||||
|             // UserModel profile
 | ||||
|             $username        = trim($_POST['username'] ?? ''); | ||||
|             $displayName     = trim($_POST['display_name'] ?? ''); | ||||
|             $about           = trim($_POST['about'] ?? ''); | ||||
|             $website         = trim($_POST['website'] ?? ''); | ||||
|             // User profile
 | ||||
|             $username    = trim($_POST['username'] ?? ''); | ||||
|             $displayName = trim($_POST['display_name'] ?? ''); | ||||
|             $about       = trim($_POST['about'] ?? ''); | ||||
|             $website     = trim($_POST['website'] ?? ''); | ||||
| 
 | ||||
|             // Site settings
 | ||||
|             $siteTitle       = trim($_POST['site_title']) ?? ''; | ||||
|             $siteDescription = trim($_POST['site_description']) ?? ''; | ||||
|             $baseUrl         = trim($_POST['base_url'] ?? ''); | ||||
|             $basePath        = trim($_POST['base_path'] ?? '/'); | ||||
|             $itemsPerPage    = (int) ($_POST['items_per_page'] ?? 25); | ||||
|             $siteTitle           = trim($_POST['site_title']) ?? ''; | ||||
|             $siteDescription     = trim($_POST['site_description']) ?? ''; | ||||
|             $baseUrl             = trim($_POST['base_url'] ?? ''); | ||||
|             $basePath            = trim($_POST['base_path'] ?? '/'); | ||||
|             $itemsPerPage        = (int) ($_POST['items_per_page'] ?? 25); | ||||
|             $strictAccessibility = isset($_POST['strict_accessibility']); | ||||
|             $showTickMood = isset($_POST['strict_accessibility']); | ||||
| 
 | ||||
|             // Password
 | ||||
|             $password                = $_POST['password'] ?? ''; | ||||
|             $confirmPassword         = $_POST['confirm_password'] ?? ''; | ||||
|             $password        = $_POST['password'] ?? ''; | ||||
|             $confirmPassword = $_POST['confirm_password'] ?? ''; | ||||
| 
 | ||||
|             // Validate user profile
 | ||||
|             if (!$username) { | ||||
| @ -112,6 +114,8 @@ class AdminController extends Controller { | ||||
|                 $config->baseUrl = $baseUrl; | ||||
|                 $config->basePath = $basePath; | ||||
|                 $config->itemsPerPage = $itemsPerPage; | ||||
|                 $config->strictAccessibility = $strictAccessibility; | ||||
|                 $config->showTickMood = $showTickMood; | ||||
| 
 | ||||
|                 // Save site settings and reload config from database
 | ||||
|                 // TODO - raise and handle exception on failure
 | ||||
|  | ||||
| @ -8,6 +8,8 @@ class ConfigModel { | ||||
|     public int $itemsPerPage = 25; | ||||
|     public string $timezone = 'relative'; | ||||
|     public ?int $cssId = null; | ||||
|     public bool $strictAccessibility = true; | ||||
|     public bool $showTickMood = true; | ||||
| 
 | ||||
|     // load config from sqlite database
 | ||||
|     public static function load(): self { | ||||
| @ -17,7 +19,16 @@ class ConfigModel { | ||||
|         $c->basePath = ($c->basePath === '') ? $init['base_path'] : $c->basePath; | ||||
| 
 | ||||
|         global $db; | ||||
|         $stmt = $db->query("SELECT site_title, site_description, base_url, base_path, items_per_page, css_id FROM settings WHERE id=1"); | ||||
|         $stmt = $db->query("SELECT site_title,
 | ||||
|                                    site_description, | ||||
|                                    base_url, | ||||
|                                    base_path, | ||||
|                                    items_per_page, | ||||
|                                    css_id, | ||||
|                                    strict_accessibility, | ||||
|                                    show_tick_mood | ||||
|                             FROM settings WHERE id=1");
 | ||||
| 
 | ||||
|         $row = $stmt->fetch(PDO::FETCH_ASSOC); | ||||
| 
 | ||||
|         if ($row) { | ||||
| @ -26,7 +37,8 @@ class ConfigModel { | ||||
|             $c->baseUrl = $row['base_url']; | ||||
|             $c->basePath = $row['base_path']; | ||||
|             $c->itemsPerPage = (int) $row['items_per_page']; | ||||
|             $c->cssId = (int) $row['css_id']; | ||||
|             $c->strictAccessibility = (bool) $row['strict_accessibility']; | ||||
|             $c->showTickMood = (bool) $row['show_tick_mood']; | ||||
|         } | ||||
| 
 | ||||
|         return $c; | ||||
| @ -49,11 +61,39 @@ class ConfigModel { | ||||
|         $settingsCount = (int) $db->query("SELECT COUNT(*) FROM settings")->fetchColumn(); | ||||
| 
 | ||||
|         if ($settingsCount === 0){ | ||||
|             $stmt = $db->prepare("INSERT INTO settings (id, site_title, site_description, base_url, base_path, items_per_page, css_id) VALUES (1, ?, ?, ?, ?, ?, ?)"); | ||||
|             $stmt = $db->prepare("INSERT INTO settings (
 | ||||
|                 id, | ||||
|                 site_title, | ||||
|                 site_description, | ||||
|                 base_url, | ||||
|                 base_path, | ||||
|                 items_per_page, | ||||
|                 css_id, | ||||
|                 strict_accessibility, | ||||
|                 show_tick_mood | ||||
|                 ) | ||||
|                 VALUES (1, ?, ?, ?, ?, ?, ?)");
 | ||||
|         } else { | ||||
|             $stmt = $db->prepare("UPDATE settings SET site_title=?, site_description=?, base_url=?, base_path=?, items_per_page=?, css_id=? WHERE id=1"); | ||||
|             $stmt = $db->prepare("UPDATE settings SET
 | ||||
|                 site_title=?, | ||||
|                 site_description=?, | ||||
|                 base_url=?, | ||||
|                 base_path=?, | ||||
|                 items_per_page=?, | ||||
|                 css_id=?, | ||||
|                 strict_accessibility=?, | ||||
|                 show_tick_mood=? | ||||
|                 WHERE id=1");
 | ||||
|         } | ||||
|         $stmt->execute([$this->siteTitle, $this->siteDescription, $this->baseUrl, $this->basePath, $this->itemsPerPage, $this->cssId]); | ||||
|         $stmt->execute([$this->siteTitle, | ||||
|                         $this->siteDescription, | ||||
|                         $this->baseUrl, | ||||
|                         $this->basePath, | ||||
|                         $this->itemsPerPage, | ||||
|                         $this->cssId, | ||||
|                         $this->strictAccessibility, | ||||
|                         $this->showTickMood | ||||
|                     ]); | ||||
| 
 | ||||
|         return self::load(); | ||||
|     } | ||||
|  | ||||
| @ -34,9 +34,7 @@ class TickModel { | ||||
| 
 | ||||
|                 // Ticks are pipe-delimited: timestamp|text
 | ||||
|                 // But just in case a tick contains a pipe, only split on the first one that occurs
 | ||||
|                 $tickParts = explode('|', $line, 2); | ||||
|                 $time = $tickParts[0]; | ||||
|                 $tick = $tickParts[1]; | ||||
|                 list($time, $emoji, $tick) = explode('|', $line, 3); | ||||
| 
 | ||||
|                 // Build the timestamp from the date and time
 | ||||
|                 // Ticks are always stored in UTC
 | ||||
| @ -90,10 +88,13 @@ class TickModel { | ||||
|         $lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); | ||||
|         foreach ($lines as $line) { | ||||
|             if (str_starts_with($line, $timestamp)) { | ||||
|                 $tick = explode('|', $line)[1]; | ||||
|                 echo $line; | ||||
|                 exit; | ||||
|                 list($time, $emoji, $tick) = explode('|', $line, 3); | ||||
| 
 | ||||
|                 return [ | ||||
|                     'tickTime' => $tickTime, | ||||
|                     'emoji' => $emoji, | ||||
|                     'tick' => $tick, | ||||
|                     'config' => ConfigModel::load(), | ||||
|                 ]; | ||||
|  | ||||
							
								
								
									
										2
									
								
								storage/db/migrations/001_add_accessibility_setting.sql
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										2
									
								
								storage/db/migrations/001_add_accessibility_setting.sql
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,2 @@ | ||||
| ALTER TABLE settings | ||||
| ADD COLUMN strict_accessibility BOOLEAN DEFAULT TRUE; | ||||
							
								
								
									
										2
									
								
								storage/db/migrations/002_add_show_tick_mood_setting.sql
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										2
									
								
								storage/db/migrations/002_add_show_tick_mood_setting.sql
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,2 @@ | ||||
| ALTER TABLE settings | ||||
| ADD COLUMN show_tick_mood BOOLEAN DEFAULT TRUE; | ||||
							
								
								
									
										
											BIN
										
									
								
								storage/db/tkr.sqlite.bak
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								storage/db/tkr.sqlite.bak
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							| @ -57,6 +57,18 @@ | ||||
|                             name="items_per_page" | ||||
|                             value="<?= $config->itemsPerPage ?>" min="1" max="50" | ||||
|                             required> | ||||
|                         <label>Strict accessibility</label> | ||||
|                         <input type="checkbox" | ||||
|                                id="strict_accessibility" | ||||
|                                name="strict_accessibility" | ||||
|                                value="1" | ||||
|                                <?php if ($config->strictAccessibility): ?> checked <?php endif; ?>>
 | ||||
|                         <label>Show tick mood</label> | ||||
|                         <input type="checkbox" | ||||
|                                id="show_tick_mood" | ||||
|                                name="show_tick_mood" | ||||
|                                value="1" | ||||
|                                <?php if ($config->showTickMood): ?> checked <?php endif; ?>>
 | ||||
|                     </div> | ||||
|                 </fieldset> | ||||
|                 <fieldset> | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user