From 694d962896ee95b54f3322031f155a63ab2e65c2 Mon Sep 17 00:00:00 2001 From: denizk0461 Date: Mon, 3 Jul 2023 15:47:55 +0200 Subject: [PATCH] Added Timetable.kt which was a PAIN because SQLite does not support ADD FOREIGN KEY on ALTER TABLE, and as such, the required workaround is to create a temporary table with the new constraint added, insert the values from the old table into the new table, drop the old table, and finally rename the new table to the name of the old table so that the app thinks that nothing ever happened... UGH --- .../22.json | 387 ++++++++++++++++++ .../weserplaner/activity/MainActivity.kt | 6 + .../com/denizk0461/weserplaner/db/AppDAO.kt | 5 + .../denizk0461/weserplaner/db/AppDatabase.kt | 48 ++- .../weserplaner/db/AppRepository.kt | 4 + .../weserplaner/model/StudIPEvent.kt | 15 +- .../denizk0461/weserplaner/model/Timetable.kt | 17 + .../weserplaner/viewmodel/MainViewModel.kt | 1 + app/src/main/res/values-v29/themes.xml | 8 - 9 files changed, 480 insertions(+), 11 deletions(-) create mode 100644 app/schemas/com.denizk0461.weserplaner.db.AppDatabase/22.json create mode 100644 app/src/main/java/com/denizk0461/weserplaner/model/Timetable.kt delete mode 100644 app/src/main/res/values-v29/themes.xml diff --git a/app/schemas/com.denizk0461.weserplaner.db.AppDatabase/22.json b/app/schemas/com.denizk0461.weserplaner.db.AppDatabase/22.json new file mode 100644 index 0000000..2552759 --- /dev/null +++ b/app/schemas/com.denizk0461.weserplaner.db.AppDatabase/22.json @@ -0,0 +1,387 @@ +{ + "formatVersion": 1, + "database": { + "version": 22, + "identityHash": "d55bef3b8778cd6346bf65455993008d", + "entities": [ + { + "tableName": "timetables", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "studip_events", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`eventId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timetableId` INTEGER NOT NULL, `title` TEXT NOT NULL, `lecturer` TEXT NOT NULL, `room` TEXT NOT NULL, `day` INTEGER NOT NULL, `timeslotStart` TEXT NOT NULL, `timeslotEnd` TEXT NOT NULL, `timeslotId` INTEGER NOT NULL, `colour` INTEGER NOT NULL, FOREIGN KEY(`timetableId`) REFERENCES `timetables`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "eventId", + "columnName": "eventId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timetableId", + "columnName": "timetableId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lecturer", + "columnName": "lecturer", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "room", + "columnName": "room", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "day", + "columnName": "day", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timeslotStart", + "columnName": "timeslotStart", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timeslotEnd", + "columnName": "timeslotEnd", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timeslotId", + "columnName": "timeslotId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "colour", + "columnName": "colour", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "eventId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "timetables", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "timetableId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "offer_date", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `date` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "offer_canteen", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `canteen` TEXT NOT NULL, `openingHours` TEXT NOT NULL, `news` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canteen", + "columnName": "canteen", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "openingHours", + "columnName": "openingHours", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "news", + "columnName": "news", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "offer_category", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `dateId` INTEGER NOT NULL, `canteenId` INTEGER NOT NULL, `category` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`dateId`) REFERENCES `offer_date`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`canteenId`) REFERENCES `offer_canteen`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dateId", + "columnName": "dateId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canteenId", + "columnName": "canteenId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "offer_date", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "dateId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "offer_canteen", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "canteenId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "offer_item", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`itemId` INTEGER NOT NULL, `categoryId` INTEGER NOT NULL, `title` TEXT NOT NULL, `price` TEXT NOT NULL, `dietary_preferences` TEXT NOT NULL, `allergens` TEXT NOT NULL, PRIMARY KEY(`itemId`), FOREIGN KEY(`categoryId`) REFERENCES `offer_category`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "itemId", + "columnName": "itemId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "categoryId", + "columnName": "categoryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "price", + "columnName": "price", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dietaryPreferences", + "columnName": "dietary_preferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "allergens", + "columnName": "allergens", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "itemId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "offer_category", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "categoryId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "event_tasks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`taskId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `eventId` INTEGER NOT NULL, `dueDate` INTEGER NOT NULL, `notifyDate` INTEGER NOT NULL, `title` TEXT NOT NULL, `notes` TEXT NOT NULL, `room` TEXT NOT NULL, `isFinished` INTEGER NOT NULL, FOREIGN KEY(`eventId`) REFERENCES `studip_events`(`eventId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "taskId", + "columnName": "taskId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eventId", + "columnName": "eventId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dueDate", + "columnName": "dueDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notifyDate", + "columnName": "notifyDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notes", + "columnName": "notes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "room", + "columnName": "room", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isFinished", + "columnName": "isFinished", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "taskId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "studip_events", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "eventId" + ], + "referencedColumns": [ + "eventId" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd55bef3b8778cd6346bf65455993008d')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizk0461/weserplaner/activity/MainActivity.kt b/app/src/main/java/com/denizk0461/weserplaner/activity/MainActivity.kt index e5eff5c..4ce7e3c 100644 --- a/app/src/main/java/com/denizk0461/weserplaner/activity/MainActivity.kt +++ b/app/src/main/java/com/denizk0461/weserplaner/activity/MainActivity.kt @@ -37,6 +37,12 @@ class MainActivity : FragmentActivity() { binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) +// if (viewModel.getTimetableCount() == 0) { +// viewModel.insertTimetable( +// Timetable(0, "default") +// ) +// } + /* * Check whether the user is launching the app for the first time in order to show a * tutorial. This value will be changed to 'false' upon completing the tutorial, thus not diff --git a/app/src/main/java/com/denizk0461/weserplaner/db/AppDAO.kt b/app/src/main/java/com/denizk0461/weserplaner/db/AppDAO.kt index cc33d14..7d068ba 100644 --- a/app/src/main/java/com/denizk0461/weserplaner/db/AppDAO.kt +++ b/app/src/main/java/com/denizk0461/weserplaner/db/AppDAO.kt @@ -73,6 +73,11 @@ interface AppDAO { @Query("DELETE FROM studip_events") fun nukeEvents() + /* --- timetables --- */ + + @Insert + fun insertTimetable(timetable: Timetable) + /* --- event tasks --- */ /** diff --git a/app/src/main/java/com/denizk0461/weserplaner/db/AppDatabase.kt b/app/src/main/java/com/denizk0461/weserplaner/db/AppDatabase.kt index f51b683..d445aa9 100644 --- a/app/src/main/java/com/denizk0461/weserplaner/db/AppDatabase.kt +++ b/app/src/main/java/com/denizk0461/weserplaner/db/AppDatabase.kt @@ -14,6 +14,7 @@ import com.denizk0461.weserplaner.model.* */ @Database( entities = [ + Timetable::class, StudIPEvent::class, OfferDate::class, OfferCanteen::class, @@ -21,10 +22,11 @@ import com.denizk0461.weserplaner.model.* OfferItem::class, EventTask::class, ], - version = 21, + version = 22, exportSchema = true, autoMigrations = [ AutoMigration(from = 20, to = 21), +// AutoMigration(from = 21, to = 22), ], ) abstract class AppDatabase : RoomDatabase() { @@ -58,6 +60,7 @@ abstract class AppDatabase : RoomDatabase() { "event_db", ).addMigrations( migrationAddNewsToOfferCanteen_19_20, + migration_21_22, // migrationAddTimetableId_20_21, // migrationAddEventTask_20_21, ) @@ -79,5 +82,48 @@ abstract class AppDatabase : RoomDatabase() { database.execSQL("ALTER TABLE offer_canteen ADD COLUMN news TEXT NOT NULL DEFAULT ''") } } + + private val migration_21_22 = object : Migration(21, 22) { + override fun migrate(database: SupportSQLiteDatabase) { + // Add 'timetables' table + database.execSQL("CREATE TABLE 'timetables' ('id' INTEGER NOT NULL UNIQUE, 'name' TEXT NOT NULL, PRIMARY KEY('id' AUTOINCREMENT));") + + // Insert a timetable + database.execSQL("INSERT INTO timetables VALUES (0, 'default')") + + /* + * Create temporary Stud.IP event table. This is necessary because SQLite doesn't + * implement ADD FOREIGN KEY on ALTER TABLE >:( + */ + database.execSQL(""" + CREATE TABLE IF NOT EXISTS studip_events_temp ( + 'eventId' INTEGER NOT NULL UNIQUE, + 'timetableId' INTEGER NOT NULL, + 'title' TEXT NOT NULL, + 'lecturer' TEXT NOT NULL, + 'room' TEXT NOT NULL, + 'day' INTEGER NOT NULL, + 'timeslotStart' TEXT NOT NULL, + 'timeslotEnd' TEXT NOT NULL, + 'timeslotId' INTEGER NOT NULL, + 'colour' INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY('timetableId') REFERENCES 'timetables'('id') ON DELETE CASCADE, + PRIMARY KEY('eventId' AUTOINCREMENT) + ); + """.trimIndent()) + + // Copy data from old table to new table + database.execSQL(""" + INSERT INTO studip_events_temp (eventId, timetableId, title, lecturer, room, day, timeslotStart, timeslotEnd, timeslotId, colour) + SELECT eventId, timetableId, title, lecturer, room, day, timeslotStart, timeslotEnd, timeslotId, colour FROM studip_events + """.trimIndent()) + + // Delete old table + database.execSQL("DROP TABLE studip_events") + + // Rename new table to the old table's name + database.execSQL("ALTER TABLE studip_events_temp RENAME TO studip_events") + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/denizk0461/weserplaner/db/AppRepository.kt b/app/src/main/java/com/denizk0461/weserplaner/db/AppRepository.kt index d72281a..e5e3053 100644 --- a/app/src/main/java/com/denizk0461/weserplaner/db/AppRepository.kt +++ b/app/src/main/java/com/denizk0461/weserplaner/db/AppRepository.kt @@ -159,6 +159,10 @@ class AppRepository(app: Application) { dao.delete(event) } + fun insertTimetable(timetable: Timetable) { + dao.insertTimetable(timetable) + } + /** * Retrieves all canteen offer date objects. * diff --git a/app/src/main/java/com/denizk0461/weserplaner/model/StudIPEvent.kt b/app/src/main/java/com/denizk0461/weserplaner/model/StudIPEvent.kt index b75e204..6f0adf0 100644 --- a/app/src/main/java/com/denizk0461/weserplaner/model/StudIPEvent.kt +++ b/app/src/main/java/com/denizk0461/weserplaner/model/StudIPEvent.kt @@ -4,6 +4,7 @@ import android.os.Parcel import android.os.Parcelable import androidx.room.ColumnInfo import androidx.room.Entity +import androidx.room.ForeignKey import androidx.room.PrimaryKey /** @@ -20,10 +21,20 @@ import androidx.room.PrimaryKey * @param timeslotId minute the event starts at - used for ordering * @param colour user-defined colour the event will be shown in - UNIMPLEMENTED */ -@Entity(tableName = "studip_events") +@Entity( + tableName = "studip_events", + foreignKeys = [ + ForeignKey( + Timetable::class, + ["id"], + ["timetableId"], + ForeignKey.CASCADE, + ), + ], +) data class StudIPEvent( @PrimaryKey(autoGenerate = true) val eventId: Int = 0, - @ColumnInfo(defaultValue = "0") val timetableId: Int, + val timetableId: Int, val title: String, val lecturer: String, val room: String, diff --git a/app/src/main/java/com/denizk0461/weserplaner/model/Timetable.kt b/app/src/main/java/com/denizk0461/weserplaner/model/Timetable.kt new file mode 100644 index 0000000..f5a5988 --- /dev/null +++ b/app/src/main/java/com/denizk0461/weserplaner/model/Timetable.kt @@ -0,0 +1,17 @@ +package com.denizk0461.weserplaner.model + +import androidx.room.Entity +import androidx.room.PrimaryKey + +/** + * Timetable parent object that [StudIPEvent]s are assigned to. These can be named to denote + * different semesters. + * + * @param id unique primary key value + * @param name title of the timetable + */ +@Entity(tableName = "timetables") +data class Timetable( + @PrimaryKey(autoGenerate = true) val id: Int, + val name: String, +) diff --git a/app/src/main/java/com/denizk0461/weserplaner/viewmodel/MainViewModel.kt b/app/src/main/java/com/denizk0461/weserplaner/viewmodel/MainViewModel.kt index bc9d165..7203ef6 100644 --- a/app/src/main/java/com/denizk0461/weserplaner/viewmodel/MainViewModel.kt +++ b/app/src/main/java/com/denizk0461/weserplaner/viewmodel/MainViewModel.kt @@ -1,6 +1,7 @@ package com.denizk0461.weserplaner.viewmodel import android.app.Application +import com.denizk0461.weserplaner.model.Timetable import com.denizk0461.weserplaner.values.AppLayout /** diff --git a/app/src/main/res/values-v29/themes.xml b/app/src/main/res/values-v29/themes.xml deleted file mode 100644 index 5b2a3a2..0000000 --- a/app/src/main/res/values-v29/themes.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - \ No newline at end of file