-
#(slot.startDate) – #sessionEnd(slot.date, first(slot.duration, slot.presentation.duration, slot.activity.duration))
-
+
#(slot.startDate) – #sessionEnd(slot.startDate, first(slot.duration, slot.presentation.duration, slot.activity.duration))
+
#if(slot.presentation.id):
#if(first(slot.presentation.duration, slot.duration) <= 15.0):
Lightning
diff --git a/Sources/App/Features/Event Day/Migrations/EventDay+Migration+V1.swift b/Sources/App/Features/Event Day/Migrations/EventDay+Migration+V1.swift
index b936c113..43a1835a 100644
--- a/Sources/App/Features/Event Day/Migrations/EventDay+Migration+V1.swift
+++ b/Sources/App/Features/Event Day/Migrations/EventDay+Migration+V1.swift
@@ -1,3 +1,4 @@
+import Foundation
import Fluent
final class EventDayMigrationV1: AsyncMigration {
@@ -10,14 +11,26 @@ final class EventDayMigrationV1: AsyncMigration {
.field("end_time", .string, .required)
.field("name", .string, .required)
.create()
-
+
+ // We use this local-only model (instead of the 'real' Slot) to solve a migration step problem,
+ // whilst also allowing us to drop event and date from Slot
+ final class MigrationSlot: Model {
+ static let schema = Schema.slot
+
+ @ID(key: .id) var id: UUID?
+ @Field(key: "date") var date: Date?
+ @Parent(key: "event_id") var event: Event
+
+ init() {}
+ }
+
// Custom migratory code to automatically seed the `event_days` table with previous years information
let events = try await Event.query(on: database).all()
- let slots = try await Slot.query(on: database).with(\.$event).all()
-
+ let slots = try await MigrationSlot.query(on: database).with(\.$event).all()
+
for event in events {
print("[Migrator] Processing event: \(event.name)")
- let eventSlots = slots.filter { $0.event?.id == event.id }
+ let eventSlots = slots.filter { $0.event.id == event.id }
let uniqueDays = Set(eventSlots.compactMap { $0.date?.withoutTime }).sorted()
print("[Migrator] Found \(eventSlots.count) slots over \(uniqueDays.count) days")
diff --git a/Sources/App/Features/Slots/Controllers/SlotRouteController.swift b/Sources/App/Features/Slots/Controllers/SlotRouteController.swift
index 5a775fb3..5aab31d1 100644
--- a/Sources/App/Features/Slots/Controllers/SlotRouteController.swift
+++ b/Sources/App/Features/Slots/Controllers/SlotRouteController.swift
@@ -87,9 +87,7 @@ struct SlotRouteController: RouteCollection {
let mutableSlot = slot ?? Slot()
mutableSlot.startDate = input.startTime
- mutableSlot.date = inputDate
mutableSlot.duration = duration
- mutableSlot.$event.id = try event.requireID()
mutableSlot.$day.id = try eventDay.requireID()
if let activity {
diff --git a/Sources/App/Features/Slots/Migrations/Slot+Migration+v3.swift b/Sources/App/Features/Slots/Migrations/Slot+Migration+v3.swift
index abf3b81d..827408bb 100644
--- a/Sources/App/Features/Slots/Migrations/Slot+Migration+v3.swift
+++ b/Sources/App/Features/Slots/Migrations/Slot+Migration+v3.swift
@@ -6,10 +6,21 @@ struct SlotMigrationV3: AsyncMigration {
try await database.schema(Schema.slot)
.field("day_id", .uuid, .references(Schema.eventDay, "id"))
.update()
-
- let slots = try await Slot.query(on: database).all()
+
+ // Define a local-only model to access the old structure
+ final class MigrationSlot: Model {
+ static let schema = Schema.slot
+
+ @ID(key: .id) var id: UUID?
+ @Field(key: "date") var date: Date?
+ @OptionalParent(key: "day_id") var day: EventDay?
+
+ init() {}
+ }
+
+ let slots = try await MigrationSlot.query(on: database).all()
let days = try await EventDay.query(on: database).all()
-
+
for slot in slots {
if let day = days.first(where: { $0.date.withoutTime == slot.date?.withoutTime }) {
slot.$day.id = try day.requireID()
@@ -19,7 +30,7 @@ struct SlotMigrationV3: AsyncMigration {
}
}
}
-
+
func revert(on database: Database) async throws {
try await database.schema(Schema.slot)
.deleteField("day_id")
diff --git a/Sources/App/Features/Slots/Migrations/Slot+Migration+v4.swift b/Sources/App/Features/Slots/Migrations/Slot+Migration+v4.swift
index c73f1749..25ff6917 100644
--- a/Sources/App/Features/Slots/Migrations/Slot+Migration+v4.swift
+++ b/Sources/App/Features/Slots/Migrations/Slot+Migration+v4.swift
@@ -3,13 +3,38 @@ import Fluent
struct SlotMigrationV4: AsyncMigration {
func prepare(on database: Database) async throws {
- fatalError("Read comment from Aug 2024 below")
+
+ final class MigrationSlot: Model {
+ static let schema = Schema.slot
+
+ @ID(key: .id) var id: UUID?
+ @OptionalField(key: "duration") var duration: Double?
+ @OptionalParent(key: "presentation_id") var presentation: Presentation?
+ @OptionalParent(key: "activity_id") var activity: Activity?
+ }
+
+ final class MigrationPresentation: Model {
+ static let schema = Schema.presentation
+
+ @ID(key: .id) var id: UUID?
+ @OptionalField(key: "duration") var duration: Double?
+ @OptionalField(key: "slot_id") var slotID: UUID?
+ }
+
+ final class MigrationActivity: Model {
+ static let schema = Schema.activity
+
+ @ID(key: .id) var id: UUID?
+ @OptionalField(key: "duration") var duration: Double?
+ @OptionalField(key: "slot_id") var slotID: UUID?
+ }
+
+ // fatalError("Read comment from Aug 2024 below")
// Aug 2024: This migrator has been partially commented out as the field it relies on has been removed (as it was no longer needed).
// In order to migrate past this point, you need to go to an earlier commit, migrate, and then come to a more recent commit before finishing
// the migration.
// Or, alternatively, just take a backup of production and apply that locally so you're up to date without playing git games.
-
- /*
+
try await database.schema(Schema.slot)
.field("presentation_id", .uuid, .references(Schema.presentation, "id"))
.field("activity_id", .uuid, .references(Schema.activity, "id"))
@@ -25,36 +50,35 @@ struct SlotMigrationV4: AsyncMigration {
// Data Migrator
- let slots = try await Slot.query(on: database).all()
- let presentations = try await Presentation.query(on: database).with(\.$slot).all()
- let activities = try await Activity.query(on: database).with(\.$slot).all()
-
+ let slots = try await MigrationSlot.query(on: database).all()
+ let presentations = try await MigrationPresentation.query(on: database).all()
+ let activities = try await MigrationActivity.query(on: database).all()
+
for presentation in presentations {
- if let slot = slots.first(where: { $0.id == presentation.slot?.id }) {
+ if let slot = slots.first(where: { $0.id == presentation.slotID }) {
let slotDuration = slot.duration
slot.$presentation.id = try presentation.requireID()
- slot.duration = 0 // easy to set to nil in future cleanup
+ slot.duration = 0
try await slot.update(on: database)
-
- presentation.$slot.id = nil
+
+ presentation.slotID = nil
presentation.duration = slotDuration ?? 0
try await presentation.update(on: database)
}
}
-
+
for activity in activities {
- if let slot = slots.first(where: { $0.id == activity.slot?.id }) {
+ if let slot = slots.first(where: { $0.id == activity.slotID }) {
let slotDuration = slot.duration
slot.$activity.id = try activity.requireID()
- slot.duration = 0 // easy to set to nil in future cleanup
+ slot.duration = 0
try await slot.update(on: database)
-
- activity.$slot.id = nil
+
+ activity.slotID = nil
activity.duration = slotDuration ?? 0
try await activity.update(on: database)
}
}
- */
}
func revert(on database: Database) async throws {
diff --git a/Sources/App/Features/Slots/Migrations/Slot+Migration+v6.swift b/Sources/App/Features/Slots/Migrations/Slot+Migration+v6.swift
new file mode 100644
index 00000000..a95461f4
--- /dev/null
+++ b/Sources/App/Features/Slots/Migrations/Slot+Migration+v6.swift
@@ -0,0 +1,19 @@
+import Foundation
+import Fluent
+
+struct SlotMigrationV6: AsyncMigration {
+ func prepare(on db: Database) async throws {
+ try await db.schema(Schema.slot)
+ .deleteField("date")
+ .deleteField("event_id")
+ .update()
+ }
+
+ func revert(on db: Database) async throws {
+ try await db.schema(Schema.slot)
+ .field("date", .datetime)
+ .field("event_id", .uuid, .references(Schema.event, "id"))
+ .update()
+
+ }
+}
diff --git a/Sources/App/Features/Slots/Models/Slot.swift b/Sources/App/Features/Slots/Models/Slot.swift
index d4a47e61..1d392bdc 100644
--- a/Sources/App/Features/Slots/Models/Slot.swift
+++ b/Sources/App/Features/Slots/Models/Slot.swift
@@ -12,19 +12,9 @@ final class Slot: Codable, Model, Content, @unchecked Sendable {
@Field(key: "start_date")
var startDate: String
-
- // DO NOT USE (June 2024)
- // TODO: This will be removed in a future PR as part of a cleanup
- @Field(key: "date")
- var date: Date?
@Field(key: "duration")
var duration: Double?
-
- // DO NOT USE (June 2024)
- // TODO: This will be removed in a future PR as part of a cleanup - it needs to be done this way for safe migrations.
- @OptionalParent(key: "event_id")
- var event: Event?
@OptionalParent(key: "day_id")
var day: EventDay?
@@ -40,23 +30,26 @@ final class Slot: Codable, Model, Content, @unchecked Sendable {
init(
id: IDValue?,
startDate: String,
- date: Date,
duration: Double?
) {
self.id = id
self.startDate = startDate
- self.date = date
self.duration = duration
}
}
extension Array where Element == Slot {
var schedule: [[Slot]] {
- let dates = Set(compactMap { $0.date?.withoutTime }).sorted(by: (<))
+ let dates = Set(compactMap { $0.day?.date.withoutTime }).sorted()
var slots: [[Slot]] = []
for date in dates {
- slots.append(filter { Calendar.current.compare(date, to: $0.date ?? Date(), toGranularity: .day) == .orderedSame })
+ slots.append(
+ filter {
+ guard let slotDate = $0.day?.date else { return false }
+ return Calendar.current.isDate(slotDate, inSameDayAs: date)
+ }
+ )
}
return slots
diff --git a/Sources/App/Features/Slots/Transformers/SlotTransformer.swift b/Sources/App/Features/Slots/Transformers/SlotTransformer.swift
index 16088021..2e068395 100644
--- a/Sources/App/Features/Slots/Transformers/SlotTransformer.swift
+++ b/Sources/App/Features/Slots/Transformers/SlotTransformer.swift
@@ -28,7 +28,7 @@ enum SlotTransformer: Transformer {
return .init(
id: id,
startTime: entity.startDate,
- date: entity.date,
+ date: entity.day?.date,
duration: entity.presentation?.duration ?? entity.activity?.duration ?? entity.duration ?? 0,
presentation: presentation,
activity: activity
diff --git a/Sources/App/Migrations.swift b/Sources/App/Migrations.swift
index 71c130b3..9768767e 100644
--- a/Sources/App/Migrations.swift
+++ b/Sources/App/Migrations.swift
@@ -83,6 +83,7 @@ class Migrations {
app.migrations.add(SpeakerMigrationV2()) // Add more social link options
app.migrations.add(SlotMigrationV5()) // Remove unneeded slot_id params
app.migrations.add(PresentationMigrationV6()) // Add video visibility
+ app.migrations.add(SlotMigrationV6()) // Remove legacy date and event_id fields
do {
guard let url = Environment.get("DATABASE_URL") else {
diff --git a/Sources/App/Tag/SessionEndTag.swift b/Sources/App/Tag/SessionEndTag.swift
index 08e10d9f..d8395a4d 100644
--- a/Sources/App/Tag/SessionEndTag.swift
+++ b/Sources/App/Tag/SessionEndTag.swift
@@ -1,22 +1,25 @@
-import Foundation
import Leaf
+import Foundation
struct SessionEndTag: LeafTag {
- let formatter = DateFormatter()
+ let formatter: DateFormatter = {
+ let f = DateFormatter()
+ f.dateFormat = "HH:mm"
+ f.timeZone = .init(identifier: "UTC")
+ f.locale = .init(identifier: "en_US_POSIX")
+ return f
+ }()
func render(_ ctx: LeafContext) throws -> LeafData {
guard
- let startDate = ctx.parameters[0].double,
- let duration = ctx.parameters[1].double
- else { return .string("") }
-
- formatter.dateFormat = "HH:mm"
- formatter.timeZone = .init(identifier: "UTC")
- formatter.locale = .init(identifier: "en_US_POSIX")
-
- let referenceDate = Date(timeIntervalSince1970: startDate)
- let endDate = referenceDate.addingTimeInterval(.init(Int(duration) * 60))
+ let startString = ctx.parameters[0].string, // e.g., "09:30"
+ let duration = ctx.parameters[1].double, // duration in minutes
+ let startDate = formatter.date(from: startString)
+ else {
+ return .string("")
+ }
+ let endDate = startDate.addingTimeInterval(duration * 60)
return .string(formatter.string(from: endDate))
}
}
diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift
index 132ee8f2..1c64a130 100644
--- a/Sources/App/routes.swift
+++ b/Sources/App/routes.swift
@@ -86,12 +86,15 @@ func routes(_ app: Application) throws {
.all()
let slots = try await Slot
.query(on: request.db)
- .sort(\.$date)
.sort(\.$startDate)
.with(\.$day)
.with(\.$presentation)
.with(\.$activity)
.all()
+ .sorted {
+ guard let d1 = $0.day?.date, let d2 = $1.day?.date else { return false }
+ return d1 < d2
+ }
let activities = try await Activity
.query(on: request.db)
.sort(\.$event.$id, .descending) // This moves 'Reusable' events to the top of the filtered view