Skip to content

Commit 104805f

Browse files
authored
feat: add word assessment events (#2210)
2 parents 2c7221e + 9a45000 commit 104805f

File tree

16 files changed

+613
-155
lines changed

16 files changed

+613
-155
lines changed

pom-dependency-tree.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
ai.elimu:webapp:war:2.6.25-SNAPSHOT
1+
ai.elimu:webapp:war:2.6.27-SNAPSHOT
22
+- ai.elimu:model:jar:model-2.0.97:compile
33
| \- com.google.code.gson:gson:jar:2.13.0:compile
44
| \- com.google.errorprone:error_prone_annotations:jar:2.37.0:compile
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package ai.elimu.dao;
2+
3+
import java.util.Calendar;
4+
import java.util.List;
5+
6+
import org.springframework.dao.DataAccessException;
7+
8+
import ai.elimu.entity.analytics.WordAssessmentEvent;
9+
10+
public interface WordAssessmentEventDao extends GenericDao<WordAssessmentEvent> {
11+
12+
WordAssessmentEvent read(Calendar timestamp, String androidId, String packageName) throws DataAccessException;
13+
14+
List<WordAssessmentEvent> readAll(String androidId) throws DataAccessException;
15+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package ai.elimu.dao.jpa;
2+
3+
import java.util.Calendar;
4+
import java.util.List;
5+
6+
import org.springframework.dao.DataAccessException;
7+
8+
import ai.elimu.dao.WordAssessmentEventDao;
9+
import ai.elimu.entity.analytics.WordAssessmentEvent;
10+
import jakarta.persistence.NoResultException;
11+
12+
public class WordAssessmentEventDaoJpa extends GenericDaoJpa<WordAssessmentEvent> implements WordAssessmentEventDao {
13+
14+
@Override
15+
public WordAssessmentEvent read(Calendar timestamp, String androidId, String packageName) throws DataAccessException {
16+
try {
17+
return (WordAssessmentEvent) em.createQuery(
18+
"SELECT event " +
19+
"FROM WordAssessmentEvent event " +
20+
"WHERE event.timestamp = :timestamp " +
21+
"AND event.androidId = :androidId " +
22+
"AND event.packageName = :packageName")
23+
.setParameter("timestamp", timestamp)
24+
.setParameter("androidId", androidId)
25+
.setParameter("packageName", packageName)
26+
.getSingleResult();
27+
} catch (NoResultException e) {
28+
logger.info("WordAssessmentEvent (" + timestamp.getTimeInMillis() + ", " + androidId + ", \"" + packageName + "\") was not found");
29+
return null;
30+
}
31+
}
32+
33+
@Override
34+
public List<WordAssessmentEvent> readAll(String androidId) throws DataAccessException {
35+
return em.createQuery(
36+
"SELECT event " +
37+
"FROM WordAssessmentEvent event " +
38+
"WHERE event.androidId = :androidId " +
39+
"ORDER BY event.id")
40+
.setParameter("androidId", androidId)
41+
.getResultList();
42+
}
43+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package ai.elimu.entity.analytics;
2+
3+
import jakarta.persistence.Entity;
4+
import lombok.Getter;
5+
import lombok.Setter;
6+
7+
@Getter
8+
@Setter
9+
@Entity
10+
public class WordAssessmentEvent extends AssessmentEvent {
11+
12+
/**
13+
* The word text. E.g. <code>"star"</code>.
14+
*/
15+
private String wordText;
16+
17+
/**
18+
* This field might not be included, e.g. if the assessment task was done in a
19+
* 3rd-party app that did not load the content from the elimu.ai Content Provider.
20+
* In this case, the {@link #wordId} will be {@code null}.
21+
*/
22+
private Long wordId;
23+
24+
/**
25+
* A value in the range [0.0, 1.0].
26+
*/
27+
private Float masteryScore;
28+
29+
/**
30+
* The number of milliseconds passed between the student opening the assessment task
31+
* and submitting a response. E.g. <code>15000</code>.
32+
*/
33+
private Long timeSpentMs;
34+
}
Lines changed: 69 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,12 @@
11
package ai.elimu.rest.v2.analytics;
22

3-
import ai.elimu.dao.ApplicationDao;
4-
import ai.elimu.dao.WordDao;
53
import ai.elimu.model.v2.enums.Language;
64
import ai.elimu.util.AnalyticsHelper;
75
import ai.elimu.util.ConfigHelper;
8-
import ai.elimu.util.DiscordHelper;
9-
import jakarta.servlet.http.HttpServletResponse;
106
import java.io.File;
11-
import java.io.Reader;
12-
import java.nio.file.Files;
13-
import java.nio.file.Path;
14-
import java.nio.file.Paths;
15-
import lombok.RequiredArgsConstructor;
7+
import jakarta.servlet.http.HttpServletResponse;
168
import lombok.extern.slf4j.Slf4j;
17-
import org.apache.commons.csv.CSVFormat;
18-
import org.apache.commons.csv.CSVParser;
19-
import org.apache.commons.csv.CSVRecord;
9+
2010
import org.apache.commons.io.FileUtils;
2111
import org.json.JSONObject;
2212
import org.springframework.http.HttpStatus;
@@ -27,100 +17,75 @@
2717
import org.springframework.web.bind.annotation.RestController;
2818
import org.springframework.web.multipart.MultipartFile;
2919

20+
/**
21+
* REST API endpoint for receiving word assessment events from the
22+
* <a href="https://github.com/elimu-ai/analytics">Analytics</a> application.
23+
*/
3024
@RestController
31-
@RequestMapping(value = "/rest/v2/analytics/word-assessment-events/csv", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
32-
@RequiredArgsConstructor
25+
@RequestMapping(value = "/rest/v2/analytics/word-assessment-events/csv", produces = MediaType.APPLICATION_JSON_VALUE)
3326
@Slf4j
3427
public class WordAssessmentEventsRestController {
35-
36-
// @Autowired
37-
// private WordAssessmentEventDao wordAssessmentEventDao;
38-
39-
private final ApplicationDao applicationDao;
40-
41-
private final WordDao wordDao;
42-
43-
@PostMapping
44-
public String handleUploadCsvRequest(
45-
@RequestParam("file") MultipartFile multipartFile,
46-
HttpServletResponse response
47-
) {
48-
log.info("handleUploadCsvRequest");
49-
50-
String name = multipartFile.getName();
51-
log.info("name: " + name);
52-
53-
// Expected format: "7161a85a0e4751cd_3001012_word-assessment-events_2020-04-23.csv"
54-
String originalFilename = multipartFile.getOriginalFilename();
55-
log.info("originalFilename: " + originalFilename);
56-
57-
// TODO: Send notification to the #📊-data-collection channel in Discord
58-
// Hide parts of the Android ID, e.g. "7161***51cd_3001012_word-learning-events_2020-04-23.csv"
59-
String anonymizedOriginalFilename = originalFilename.substring(0, 4) + "***" + originalFilename.substring(12);
60-
DiscordHelper.sendChannelMessage("Received dataset: `" + anonymizedOriginalFilename + "`", null, null, null, null);
61-
62-
String androidIdExtractedFromFilename = AnalyticsHelper.extractAndroidIdFromCsvFilename(originalFilename);
63-
log.info("androidIdExtractedFromFilename: \"" + androidIdExtractedFromFilename + "\"");
64-
65-
Integer versionCodeExtractedFromFilename = AnalyticsHelper.extractVersionCodeFromCsvFilename(originalFilename);
66-
log.info("versionCodeExtractedFromFilename: " + versionCodeExtractedFromFilename);
67-
68-
String contentType = multipartFile.getContentType();
69-
log.info("contentType: " + contentType);
70-
71-
JSONObject jsonObject = new JSONObject();
72-
73-
try {
74-
byte[] bytes = multipartFile.getBytes();
75-
log.info("bytes.length: " + bytes.length);
76-
77-
// Store a backup of the original CSV file on the filesystem (in case it will be needed for debugging)
78-
File elimuAiDir = new File(System.getProperty("user.home"), ".elimu-ai");
79-
File languageDir = new File(elimuAiDir, "lang-" + Language.valueOf(ConfigHelper.getProperty("content.language")));
80-
File analyticsDir = new File(languageDir, "analytics");
81-
File androidIdDir = new File(analyticsDir, "android-id-" + androidIdExtractedFromFilename);
82-
File versionCodeDir = new File(androidIdDir, "version-code-" + versionCodeExtractedFromFilename);
83-
File wordAssessmentEventsDir = new File(versionCodeDir, "word-assessment-events");
84-
wordAssessmentEventsDir.mkdirs();
85-
File csvFile = new File(wordAssessmentEventsDir, originalFilename);
86-
log.info("Storing CSV file at " + csvFile);
87-
FileUtils.writeByteArrayToFile(csvFile, bytes);
88-
log.info("csvFile.exists(): " + csvFile.exists());
89-
90-
// Iterate each row in the CSV file
91-
Path csvFilePath = Paths.get(csvFile.toURI());
92-
log.info("csvFilePath: " + csvFilePath);
93-
Reader reader = Files.newBufferedReader(csvFilePath);
94-
CSVFormat csvFormat = CSVFormat.DEFAULT
95-
.withHeader(
96-
"id", // The Room database ID
97-
"time",
98-
"android_id",
99-
"package_name",
100-
"word_id",
101-
"word_text",
102-
"mastery_score",
103-
"time_spent_ms"
104-
)
105-
.withSkipHeaderRecord();
106-
CSVParser csvParser = new CSVParser(reader, csvFormat);
107-
for (CSVRecord csvRecord : csvParser) {
108-
log.info("csvRecord: " + csvRecord);
109-
110-
// Convert from CSV to Java
111-
112-
// TODO
113-
}
114-
} catch (Exception ex) {
115-
log.error(ex.getMessage());
116-
117-
jsonObject.put("result", "error");
118-
jsonObject.put("errorMessage", ex.getMessage());
119-
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
28+
29+
@PostMapping
30+
public String handleUploadCsvRequest(
31+
@RequestParam("file") MultipartFile multipartFile,
32+
HttpServletResponse response
33+
) {
34+
log.info("handleUploadCsvRequest");
35+
36+
JSONObject jsonResponseObject = new JSONObject();
37+
try {
38+
String contentType = multipartFile.getContentType();
39+
log.info("contentType: " + contentType);
40+
41+
long size = multipartFile.getSize();
42+
log.info("size: " + size);
43+
if (size == 0) {
44+
throw new IllegalArgumentException("Empty file");
45+
}
46+
47+
// Expected format: "7161a85a0e4751cd_3001012_word-assessment-events_2020-04-23.csv"
48+
String originalFilename = multipartFile.getOriginalFilename();
49+
log.info("originalFilename: " + originalFilename);
50+
if (originalFilename.length() != "7161a85a0e4751cd_3001012_word-assessment-events_2020-04-23.csv".length()) {
51+
throw new IllegalArgumentException("Unexpected filename");
52+
}
53+
54+
String androidIdExtractedFromFilename = AnalyticsHelper.extractAndroidIdFromCsvFilename(originalFilename);
55+
log.info("androidIdExtractedFromFilename: \"" + androidIdExtractedFromFilename + "\"");
56+
57+
Integer versionCodeExtractedFromFilename = AnalyticsHelper.extractVersionCodeFromCsvFilename(originalFilename);
58+
log.info("versionCodeExtractedFromFilename: " + versionCodeExtractedFromFilename);
59+
60+
byte[] bytes = multipartFile.getBytes();
61+
log.info("bytes.length: " + bytes.length);
62+
63+
// Store the original CSV file on the filesystem
64+
File elimuAiDir = new File(System.getProperty("user.home"), ".elimu-ai");
65+
File languageDir = new File(elimuAiDir, "lang-" + Language.valueOf(ConfigHelper.getProperty("content.language")));
66+
File analyticsDir = new File(languageDir, "analytics");
67+
File androidIdDir = new File(analyticsDir, "android-id-" + androidIdExtractedFromFilename);
68+
File versionCodeDir = new File(androidIdDir, "version-code-" + versionCodeExtractedFromFilename);
69+
File wordAssessmentEventsDir = new File(versionCodeDir, "word-assessment-events");
70+
wordAssessmentEventsDir.mkdirs();
71+
File csvFile = new File(wordAssessmentEventsDir, originalFilename);
72+
log.info("Storing CSV file at " + csvFile);
73+
FileUtils.writeByteArrayToFile(csvFile, bytes);
74+
log.info("csvFile.exists(): " + csvFile.exists());
75+
76+
jsonResponseObject.put("result", "success");
77+
jsonResponseObject.put("successMessage", "The CSV file was uploaded");
78+
response.setStatus(HttpStatus.OK.value());
79+
} catch (Exception ex) {
80+
log.error(ex.getMessage());
81+
82+
jsonResponseObject.put("result", "error");
83+
jsonResponseObject.put("errorMessage", ex.getMessage());
84+
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
85+
}
86+
87+
String jsonResponse = jsonResponseObject.toString();
88+
log.info("jsonResponse: " + jsonResponse);
89+
return jsonResponse;
12090
}
121-
122-
String jsonResponse = jsonObject.toString();
123-
log.info("jsonResponse: " + jsonResponse);
124-
return jsonResponse;
125-
}
12691
}

0 commit comments

Comments
 (0)