Skip to content

Commit 65978e7

Browse files
authored
Merge pull request #1654 from joreilly/droidcon_italy_2025
Droidcon Italy 2025 import
2 parents cfdc442 + 8a89873 commit 65978e7

File tree

7 files changed

+563
-1
lines changed

7 files changed

+563
-1
lines changed

backend/datastore/src/jvmMain/kotlin/dev/johnoreilly/confetti/backend/datastore/ConferenceId.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ enum class ConferenceId(val id: String) {
4545
DroidconNYC2025("droidconnyc2025"),
4646
DroidConLondon2025("droidconlondon2025"),
4747
DevFestVenice2025("devfestvenice2025"),
48+
DroidconItaly2025("droidconitaly2025"),
4849
;
4950

5051
companion object {

backend/service-import/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ kotlin {
2626
implementation(libs.kaml)
2727
implementation(libs.bare.graphQL)
2828
implementation(libs.kotlinx.serialization)
29-
29+
implementation(libs.kotlin.csv)
3030
implementation(projects.backend.datastore)
3131
}
3232
}
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
package dev.johnoreilly.confetti.backend.import
2+
3+
import com.github.doyaaaaaken.kotlincsv.dsl.csvReader
4+
import dev.johnoreilly.confetti.backend.datastore.*
5+
import kotlinx.datetime.LocalDate
6+
import kotlinx.datetime.LocalDateTime
7+
import java.nio.file.Files
8+
import java.nio.file.Path
9+
import java.nio.file.Paths
10+
11+
/**
12+
* CSV-based importer for droidcon Italy 2025 using
13+
* - droidcon-2025_speakers.csv
14+
* - droidcon-2025_sessions.csv
15+
*/
16+
object DroidconItaly2025 {
17+
private val csvReader = csvReader()
18+
private const val SPEAKERS_FILE = "droidcon-2025_speakers.csv"
19+
private const val SESSIONS_FILE = "droidcon-2025_sessions.csv"
20+
21+
22+
private val config = DConfig(
23+
id = ConferenceId.DroidconItaly2025.id,
24+
name = "droidcon Italy 2025",
25+
timeZone = "Europe/Rome",
26+
days = listOf(
27+
LocalDate(2025, 11, 19),
28+
LocalDate(2025, 11, 20)
29+
)
30+
)
31+
32+
private val venue = DVenue(
33+
id = "main",
34+
name = "UCI Cinema Lingotto",
35+
address = "Via Nizza 262, 10126 Turin",
36+
latitude = null,
37+
longitude = null,
38+
description = emptyMap(),
39+
imageUrl = "https://flutterheroes.com/2025/wp-content/uploads/sites/6/venue_pg3.jpg",
40+
floorPlanUrl = null
41+
)
42+
43+
suspend fun import(): Int {
44+
val sessionsCsvText = javaClass.classLoader.getResourceAsStream(SESSIONS_FILE).use { it.reader().readText() }
45+
val speakersCsvText = javaClass.classLoader.getResourceAsStream(SPEAKERS_FILE).use { it.reader().readText() }
46+
47+
val sessionsRows = csvReader.readAll(sessionsCsvText).drop(1)
48+
val speakersRows = csvReader.readAll(speakersCsvText).drop(1)
49+
50+
data class Speaker(
51+
val id: String,
52+
val name: String,
53+
val bio: String?,
54+
val picture: String?,
55+
val company: String?,
56+
val jobTitle: String?,
57+
val xUrl: String?,
58+
val linkedinUrl: String?,
59+
val githubUrl: String?,
60+
)
61+
62+
val speakers = speakersRows.mapNotNull { row ->
63+
// Expected columns: ID,Name,Biography,Picture,Proposal titles,Your company,Your job title,X (user URL),LinkedIn (user URL),Github (user URL)
64+
if (row.isEmpty()) return@mapNotNull null
65+
val id = row.getOrNull(0)?.trim().orEmpty()
66+
if (id.isBlank()) return@mapNotNull null
67+
Speaker(
68+
id = id,
69+
name = row.getOrNull(1)?.trim().orEmpty(),
70+
bio = row.getOrNull(2)?.trim().takeIf { !it.isNullOrBlank() },
71+
picture = row.getOrNull(3)?.trim().takeIf { !it.isNullOrBlank() },
72+
company = row.getOrNull(5)?.trim().takeIf { !it.isNullOrBlank() },
73+
jobTitle = row.getOrNull(6)?.trim().takeIf { !it.isNullOrBlank() },
74+
xUrl = row.getOrNull(7)?.trim().takeIf { !it.isNullOrBlank() },
75+
linkedinUrl = row.getOrNull(8)?.trim().takeIf { !it.isNullOrBlank() },
76+
githubUrl = row.getOrNull(9)?.trim().takeIf { !it.isNullOrBlank() },
77+
)
78+
}
79+
80+
// Map by name for resolving speakers in sessions file
81+
val speakerByName: Map<String, Speaker> = speakers.associateBy { it.name }
82+
83+
data class Session(
84+
val id: String,
85+
val title: String,
86+
val tags: List<String>,
87+
val abstractText: String?,
88+
val speakerNames: List<String>,
89+
val level: String?,
90+
val date: String?,
91+
val startTime: String?,
92+
val duration: String?,
93+
val room: String?,
94+
)
95+
96+
val sessions = sessionsRows.mapNotNull { row ->
97+
// Expected columns: ID,Proposal title,Tags,Abstract,Speaker names,Level,Date,Start Time,Duration,Room
98+
if (row.isEmpty()) return@mapNotNull null
99+
val id = row.getOrNull(0)?.trim().orEmpty()
100+
if (id.isBlank()) return@mapNotNull null
101+
val title = row.getOrNull(1)?.trim().orEmpty()
102+
val tags = row.getOrNull(2)?.split(',')?.map { it.trim() }?.filter { it.isNotEmpty() } ?: emptyList()
103+
val abstractText = row.getOrNull(3)?.trim().takeIf { !it.isNullOrBlank() }
104+
val speakerNames = row.getOrNull(4)?.split(',')?.map { it.trim() }?.filter { it.isNotEmpty() } ?: emptyList()
105+
val level = row.getOrNull(5)?.trim().takeIf { !it.isNullOrBlank() }
106+
val date = row.getOrNull(6)?.trim().takeIf { !it.isNullOrBlank() }
107+
val startTime = row.getOrNull(7)?.trim().takeIf { !it.isNullOrBlank() }
108+
val duration = row.getOrNull(8)?.trim().takeIf { !it.isNullOrBlank() }
109+
val room = row.getOrNull(9)?.trim().takeIf { !it.isNullOrBlank() }
110+
Session(id, title, tags, abstractText, speakerNames, level, date, startTime, duration, room)
111+
}
112+
113+
// Resolve speakers for sessions
114+
val datastoreSessions = sessions.map { s ->
115+
val speakerIds = s.speakerNames.mapNotNull { speakerByName[it]?.id }
116+
117+
// Parse start and end times from CSV data
118+
val start = if (s.date != null && s.startTime != null) {
119+
LocalDateTime.parse("${s.date}T${s.startTime}")
120+
} else {
121+
defaultStart()
122+
}
123+
124+
val end = if (s.date != null && s.startTime != null && s.duration != null) {
125+
// Duration is in format HH:MM, parse and add to start
126+
val durationParts = s.duration.split(":")
127+
val durationHours = durationParts.getOrNull(0)?.toIntOrNull() ?: 0
128+
val durationMinutes = durationParts.getOrNull(1)?.toIntOrNull() ?: 0
129+
130+
// Add duration to start time
131+
var endHour = start.hour + durationHours
132+
var endMinute = start.minute + durationMinutes
133+
if (endMinute >= 60) {
134+
endHour += 1
135+
endMinute -= 60
136+
}
137+
138+
LocalDateTime(start.year, start.monthNumber, start.dayOfMonth, endHour, endMinute)
139+
} else {
140+
defaultEnd()
141+
}
142+
143+
val rooms = if (s.room != null) listOf(s.room) else listOf("Main")
144+
145+
DSession(
146+
id = s.id,
147+
type = "talk",
148+
title = s.title,
149+
description = s.abstractText,
150+
shortDescription = null,
151+
language = null,
152+
start = start,
153+
end = end,
154+
complexity = s.level,
155+
feedbackId = null,
156+
tags = s.tags,
157+
rooms = rooms,
158+
speakers = speakerIds,
159+
links = emptyList()
160+
)
161+
}
162+
163+
// Build speakers with back-linked sessions
164+
val speakerSessionsMap: Map<String, List<String>> = datastoreSessions
165+
.flatMap { session -> session.speakers.map { it to session.id } }
166+
.groupBy({ it.first }, { it.second })
167+
168+
val datastoreSpeakers = speakers.map { sp ->
169+
DSpeaker(
170+
id = sp.id,
171+
name = sp.name,
172+
bio = sp.bio,
173+
tagline = sp.jobTitle,
174+
company = sp.company,
175+
companyLogoUrl = null,
176+
city = null,
177+
links = listOfNotNull(
178+
sp.xUrl?.let { DLink("twitter", it) },
179+
sp.linkedinUrl?.let { DLink("linkedin", it) },
180+
sp.githubUrl?.let { DLink("github", it) },
181+
),
182+
photoUrl = sp.picture,
183+
sessions = speakerSessionsMap[sp.id]
184+
)
185+
}
186+
187+
DataStore().write(
188+
sessions = datastoreSessions,
189+
rooms = listOf(
190+
DRoom("Sala 7", "Sala 7"),
191+
DRoom("Sala 8", "Sala 8")
192+
),
193+
speakers = datastoreSpeakers,
194+
partnerGroups = emptyList(),
195+
config = config,
196+
venues = listOf(venue)
197+
)
198+
199+
return sessions.size
200+
}
201+
202+
private fun defaultStart(): LocalDateTime {
203+
// Placeholder: Day 1 09:00
204+
return LocalDateTime.parse("2025-11-19T09:00")
205+
}
206+
207+
private fun defaultEnd(): LocalDateTime {
208+
// Placeholder: Day 1 10:00
209+
return LocalDateTime.parse("2025-11-19T10:00")
210+
}
211+
212+
private fun readFileFlexible(vararg candidates: String): String {
213+
// Try multiple relative paths to be resilient to working directory
214+
for (c in candidates) {
215+
val p: Path = Paths.get(c)
216+
if (Files.exists(p)) {
217+
return Files.readString(p)
218+
}
219+
}
220+
// Also try from project root if launched from module dir
221+
val moduleRoot = Paths.get("backend", "service-import")
222+
for (c in candidates) {
223+
val p = moduleRoot.resolve(Paths.get(c).fileName)
224+
if (Files.exists(p)) {
225+
return Files.readString(p)
226+
}
227+
}
228+
error("CSV file not found. Tried: ${candidates.joinToString()}")
229+
}
230+
231+
}

backend/service-import/src/jvmMain/kotlin/dev/johnoreilly/confetti/backend/import/Main.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ private suspend fun update(conf: String?): Int {
148148
ConferenceId.KotlinConf2025 -> Sessionize.importKotlinConf2025()
149149
ConferenceId.DroidConLondon2025 -> importDroidconLondon2025()
150150
ConferenceId.DevFestVenice2025 -> importDevFestVenice2025()
151+
ConferenceId.DroidconItaly2025 -> DroidconItaly2025.import()
151152
null -> error("")
152153
}
153154
}

0 commit comments

Comments
 (0)