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