Skip to content

Commit 7b06b50

Browse files
committed
Feedback reader
1 parent 0ed242d commit 7b06b50

File tree

2 files changed

+306
-0
lines changed

2 files changed

+306
-0
lines changed
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
package no.javazone.cake.redux
2+
3+
import no.javazone.cake.redux.mail.*
4+
import no.javazone.cake.redux.sleepingpill.SleepingpillCommunicator
5+
import no.javazone.cake.redux.util.*
6+
import org.jsonbuddy.JsonArray
7+
import org.jsonbuddy.JsonObject
8+
import java.io.File
9+
import java.io.PrintWriter
10+
import java.time.LocalDate
11+
import java.time.LocalDateTime
12+
13+
data class FeedBackRaw(
14+
val slotRaw:String,
15+
val enjoy:Int,
16+
val useful:Int,
17+
val comment:String?
18+
)
19+
20+
data class SleepingPillSpeaker(
21+
val name:String,
22+
val email:String,
23+
)
24+
25+
private fun toAvg(sum:Int,count:Int):String {
26+
val avg = (sum * 1000) / count
27+
var rest = "" + (avg % 1000)
28+
while (rest.length < 3) {
29+
rest = "0" + rest
30+
}
31+
val res = "${avg / 1000},$rest"
32+
return res
33+
}
34+
35+
private fun commentToExport(commentList:List<String>):String {
36+
if (commentList.isEmpty()) {
37+
return ""
38+
}
39+
val joined = commentList.map { it.replace("\"","\"\"") }.joinToString("\n")
40+
return "\"${joined}\""
41+
}
42+
43+
data class FeedBack(
44+
val talkid:String,
45+
val talktype: String,
46+
val speakerList:List<SleepingPillSpeaker>,
47+
val enjoySum:Int,
48+
val usefulSum:Int,
49+
val count:Int,
50+
val commentList:List<String>,
51+
val title:String,
52+
) {
53+
val enjoyavg:String = toAvg(enjoySum,count)
54+
val usefulavg:String = toAvg(usefulSum,count)
55+
56+
// EMAIL|SPEAKERNAME|TYPE|TITLE|COUNT|AVG_ENJOY|AVG_USEFUL|COMMENTS
57+
val asExport:String = "${speakerList.map { it.name }.joinToString(" and ")};${speakerList.map { it.email }.joinToString(",")};$talktype;$title;$count;$enjoyavg;$usefulavg;${commentToExport(commentList)}"
58+
59+
val emailParaMap:Map<FeedbackMailParameter,String> = mapOf(
60+
FeedbackMailParameter.SPEAKERNAME to speakerList.map { it.name }.joinToString(" and "),
61+
FeedbackMailParameter.TYPE to talktype,
62+
FeedbackMailParameter.TITLE to title,
63+
FeedbackMailParameter.COUNT to "$count",
64+
FeedbackMailParameter.AVG_ENJOY to enjoyavg,
65+
FeedbackMailParameter.AVG_USEFUL to usefulavg,
66+
FeedbackMailParameter.COMMENTS to commentList.map { "<li>$it</li>" }.joinToString("\n"),
67+
)
68+
69+
companion object {
70+
val exportHeader = "SPEAKERNAME;EMAIL;TYPE;TITLE;COUNT;AVG_ENJOY;AVG_USEFUL;COMMENTS"
71+
}
72+
}
73+
74+
75+
data class SleepingPillData(
76+
val talkid:String,
77+
val speakerList: List<SleepingPillSpeaker>,
78+
val room:String,
79+
val startTime:LocalDateTime,
80+
val talktype:String,
81+
val title:String,
82+
)
83+
84+
enum class FeedbackMailParameter {
85+
SPEAKERNAME,
86+
TYPE,
87+
TITLE,
88+
COUNT,
89+
AVG_ENJOY,
90+
AVG_USEFUL,
91+
COMMENTS,
92+
}
93+
94+
object FeedbackReader {
95+
private val dates:List<Pair<String,LocalDate>> = listOf (
96+
Pair("Tue",LocalDate.of(2024,9,3)),
97+
Pair("Wed",LocalDate.of(2024,9,4)),
98+
Pair("Thu",LocalDate.of(2024,9,5)),
99+
)
100+
101+
@JvmStatic
102+
fun main(args: Array<String>) {
103+
if (args.size < 4) {
104+
println("Usage <cakeconfig> <feedback> <outputcvs> <emailtemplate>")
105+
return
106+
}
107+
System.setProperty("cake-redux-config-file", args[0])
108+
val rawList: List<FeedBackRaw> = readRaw(args[1])
109+
val sleepingPillTalks:JsonArray = SleepingpillCommunicator().allTalkFromConferenceSleepingPillFormat(Configuration.videoAdminConference())
110+
val sleepingPillDataList:List<SleepingPillData> = sleepingPillTalks.objects { sleepingPillData(it) }.mapNotNull { it }
111+
val feedbackList:List<FeedBack> = generateFeedback(rawList,sleepingPillDataList)
112+
println("Got feedback " + feedbackList.size)
113+
checkMissing(sleepingPillDataList,feedbackList)
114+
//writExport(feedbackList,args[2])
115+
generateMail(feedbackList,args[3])
116+
}
117+
118+
fun generateMail(feedbackList:List<FeedBack>,emailTemplateFilename:String) {
119+
val emailTemplate:String = File(emailTemplateFilename).readText()
120+
for (feedback in feedbackList) {
121+
val emailContent = StringBuilder(emailTemplate)
122+
for (parameter in FeedbackMailParameter.values()) {
123+
val vartext = "#$parameter#"
124+
while (true) {
125+
val pos = emailContent.indexOf(vartext)
126+
if (pos == -1) {
127+
break
128+
}
129+
emailContent.replace(pos,pos+vartext.length,feedback.emailParaMap[parameter]?:"")
130+
}
131+
}
132+
val mailToSend = MailToSend(
133+
134+
"JavaZone",
135+
emptyList(),
136+
feedback.speakerList.map { it.email },
137+
"JavaZone talk feedback",
138+
emailContent.toString(),
139+
)
140+
sendMail(mailToSend)
141+
}
142+
}
143+
144+
private fun sendMail(mailToSend:MailToSend) {
145+
146+
val impl = KotlinFix().createMailImpl(mailToSend)
147+
try {
148+
impl.send()
149+
} catch (e:Exception) {
150+
println("Failed to send to ${mailToSend.to} -> ${e.message}")
151+
}
152+
}
153+
154+
private fun writExport(feedbackList: List<FeedBack>,filename: String) {
155+
val exportList: List<String> = feedbackList.map { it.asExport }
156+
PrintWriter(File(filename)).use { out ->
157+
out.println(FeedBack.exportHeader)
158+
exportList.forEach { out.println(it) }
159+
}
160+
}
161+
162+
163+
fun checkMissing(sleepingPillDataList:List<SleepingPillData>,feedbackList:List<FeedBack>) {
164+
for (sleepingPillData in sleepingPillDataList) {
165+
if (feedbackList.none { it.talkid == sleepingPillData.talkid }) {
166+
println("Missing *$sleepingPillData*")
167+
}
168+
}
169+
}
170+
171+
fun generateFeedback(rawList:List<FeedBackRaw>,sleepingPillDataList:List<SleepingPillData>):List<FeedBack> {
172+
val resultMap:MutableMap<String,FeedBack> = mutableMapOf()
173+
for (raw in rawList) {
174+
val roomSlot:Pair<String,LocalDateTime> = readRoomSlot(raw.slotRaw)
175+
val sleepingPillData:SleepingPillData? = sleepingPillDataList.firstOrNull { it.room == roomSlot.first && it.startTime == roomSlot.second }
176+
if (sleepingPillData == null) {
177+
println("Did not match ${raw.slotRaw}")
178+
continue
179+
}
180+
val currentFeedback:FeedBack? = resultMap[sleepingPillData.talkid]
181+
if (currentFeedback == null) {
182+
resultMap[sleepingPillData.talkid] = FeedBack(
183+
talkid = sleepingPillData.talkid,
184+
talktype = sleepingPillData.talktype,
185+
speakerList = sleepingPillData.speakerList,
186+
enjoySum = raw.enjoy,
187+
usefulSum = raw.useful,
188+
count = 1,
189+
commentList = listOfNotNull(raw.comment),
190+
title = sleepingPillData.title,
191+
)
192+
} else {
193+
resultMap[sleepingPillData.talkid] = currentFeedback.copy(enjoySum = currentFeedback.enjoySum + raw.enjoy,usefulSum = currentFeedback.usefulSum + raw.useful,count = currentFeedback.count + 1,commentList = currentFeedback.commentList + listOfNotNull(raw.comment))
194+
}
195+
196+
}
197+
return resultMap.values.toList()
198+
}
199+
200+
fun readRoomSlot(rawvalue:String):Pair<String,LocalDateTime> {
201+
val wordList = rawvalue.split(" ")
202+
val room = wordList[0] + " " + wordList[1]
203+
val day:LocalDate = dates.first { it.first == wordList[2] }.second
204+
val hour:Int = wordList[3].substring(0,2).toInt()
205+
val minute:Int = wordList[3].substring(3,5).toInt()
206+
val dateTime = day.atTime(hour,minute)
207+
return Pair(room,dateTime)
208+
}
209+
210+
private fun sleepingPillData(jsonObject: JsonObject):SleepingPillData? {
211+
if (jsonObject.stringValue("status").orElse(null) != "APPROVED") {
212+
return null
213+
}
214+
val room:String = jsonObject
215+
.objectValue("data").orElse(null)
216+
?.objectValue("room")?.orElse(null)
217+
?.stringValue("value")?.orElse(null)?:return null
218+
val startTime:LocalDateTime = jsonObject
219+
.objectValue("data").orElse(null)
220+
?.objectValue("startTime")?.orElse(null)
221+
?.stringValue("value")?.orElse(null)
222+
?.let { LocalDateTime.parse(it) }?:return null
223+
val talktype:String = jsonObject
224+
.objectValue("data").orElse(null)
225+
?.objectValue("format")?.orElse(null)
226+
?.stringValue("value")?.orElse(null)?:return null
227+
val speakerList:List<SleepingPillSpeaker> = jsonObject.arrayValue("speakers").orElse(null)?.objects {
228+
val name:String? = it.stringValue("name").orElse(null)
229+
val email:String? = it.stringValue("email").orElse(null)
230+
if (name != null && email != null) {
231+
SleepingPillSpeaker(name,email)
232+
} else {
233+
null
234+
}
235+
}?.mapNotNull { it }?:emptyList()
236+
if (speakerList.isEmpty()) {
237+
return null
238+
}
239+
val talkid:String = jsonObject.stringValue("id").orElse(null)?:return null
240+
val title:String = jsonObject
241+
.objectValue("data").orElse(null)
242+
?.objectValue("title")?.orElse(null)
243+
?.stringValue("value")?.orElse(null)?:return null
244+
return SleepingPillData(talkid,speakerList,room,startTime,talktype,title)
245+
}
246+
247+
248+
private fun readRaw(filename: String): List<FeedBackRaw> {
249+
val readLines: List<String> = File(filename).readLines()
250+
val result: MutableList<FeedBackRaw> = mutableListOf()
251+
var current: FeedBackRaw? = null
252+
for ((index,line) in readLines.withIndex()) {
253+
if (current != null) {
254+
if (line.endsWith("\"")) {
255+
current = current.copy(comment = current.comment + ", " + line.substring(0, line.length - 1))
256+
result.add(current)
257+
current = null
258+
continue
259+
}
260+
current = current.copy(comment = current.comment + " " + line)
261+
continue
262+
}
263+
val parts = line.split(";")
264+
if (parts.size < 7) {
265+
println("Not enough parts ($index) '$line'")
266+
continue
267+
}
268+
val slotRaw = parts[3]
269+
if (!(slotRaw.startsWith("Room") || slotRaw.startsWith("Workshop"))) {
270+
continue
271+
}
272+
val enjoy = parts[4].toInt()
273+
val useful = parts[5].toInt()
274+
var commentValue = parts[6]
275+
for (i in 7 until parts.size) {
276+
commentValue = commentValue + " " + parts[i]
277+
}
278+
val doMerge:Boolean = (commentValue.startsWith("\"") && (!commentValue.endsWith("\"")))
279+
val comment = if (doMerge) commentValue.substring(1) else commentValue
280+
val newFeedBack = FeedBackRaw(slotRaw, enjoy, useful, if (comment.trim().isEmpty()) null else comment)
281+
if (doMerge) {
282+
current = newFeedBack
283+
} else {
284+
result.add(newFeedBack)
285+
}
286+
}
287+
return result
288+
}
289+
}
290+
291+
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package no.javazone.cake.redux
2+
3+
import org.assertj.core.api.Assertions
4+
import org.assertj.core.api.Assertions.*
5+
import org.junit.Test
6+
import java.time.LocalDateTime
7+
8+
class FeedbackReaderTest {
9+
@Test
10+
fun shouldParseRoom() {
11+
val (room:String,slot:LocalDateTime) = FeedbackReader.readRoomSlot("Room 2 Wed 09:00")
12+
assertThat(room).isEqualTo("Room 2")
13+
14+
}
15+
}

0 commit comments

Comments
 (0)