Skip to content
2 changes: 1 addition & 1 deletion Resources/Views/Admin/schedule.leaf
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
#endif
</div>
<span class="d-inline-block fs-sm text-muted pe-2 me-2">
#(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))
</span>
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions Resources/Views/Schedule/_schedule.leaf
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
<div class="#if(isLast):#else:border-bottom#endif #if(isFirst):pb-4#endif #if(isLast):pt-4#endif #if(!isFirst && !isLast):py-4#endif">
<div class="row pb-1 pb-xl-3">
<div class="col-sm-4 mb-3 mb-sm-0">
<div class="h5 mb-1">#(slot.startDate) – #sessionEnd(slot.date, first(slot.duration, slot.presentation.duration, slot.activity.duration))</div>
<div class="h5 mb-1">#(slot.startDate) – #sessionEnd(slot.startDate, first(slot.duration, slot.presentation.duration, slot.activity.duration))</div>

#if(slot.presentation.id):
#if(first(slot.presentation.duration, slot.duration) <= 15.0):
<span class="badge bg-danger shadow-danger fs-sm">Lightning</span>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Foundation
import Fluent

final class EventDayMigrationV1: AsyncMigration {
Expand All @@ -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")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
19 changes: 15 additions & 4 deletions Sources/App/Features/Slots/Migrations/Slot+Migration+v3.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -19,7 +30,7 @@ struct SlotMigrationV3: AsyncMigration {
}
}
}

func revert(on database: Database) async throws {
try await database.schema(Schema.slot)
.deleteField("day_id")
Expand Down
58 changes: 41 additions & 17 deletions Sources/App/Features/Slots/Migrations/Slot+Migration+v4.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand All @@ -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 {
Expand Down
19 changes: 19 additions & 0 deletions Sources/App/Features/Slots/Migrations/Slot+Migration+v6.swift
Original file line number Diff line number Diff line change
@@ -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()

}
}
21 changes: 7 additions & 14 deletions Sources/App/Features/Slots/Models/Slot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions Sources/App/Migrations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
27 changes: 15 additions & 12 deletions Sources/App/Tag/SessionEndTag.swift
Original file line number Diff line number Diff line change
@@ -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))
}
}
5 changes: 4 additions & 1 deletion Sources/App/routes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading