From 93f04d2c3632066dea7e2e6c0bf4d7f02b6d4c38 Mon Sep 17 00:00:00 2001 From: "Jo G." <1451036+jo-elimu@users.noreply.github.com> Date: Sat, 24 May 2025 19:12:06 +0700 Subject: [PATCH 1/7] feat: assessment event #2191 --- pom-dependency-tree.txt | 2 +- .../entity/analytics/AssessmentEvent.java | 52 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 src/main/java/ai/elimu/entity/analytics/AssessmentEvent.java diff --git a/pom-dependency-tree.txt b/pom-dependency-tree.txt index a72ca2ca8..ca2c41403 100644 --- a/pom-dependency-tree.txt +++ b/pom-dependency-tree.txt @@ -1,4 +1,4 @@ -ai.elimu:webapp:war:2.6.15-SNAPSHOT +ai.elimu:webapp:war:2.6.16-SNAPSHOT +- ai.elimu:model:jar:model-2.0.97:compile | \- com.google.code.gson:gson:jar:2.13.0:compile | \- com.google.errorprone:error_prone_annotations:jar:2.37.0:compile diff --git a/src/main/java/ai/elimu/entity/analytics/AssessmentEvent.java b/src/main/java/ai/elimu/entity/analytics/AssessmentEvent.java new file mode 100644 index 000000000..e4807474d --- /dev/null +++ b/src/main/java/ai/elimu/entity/analytics/AssessmentEvent.java @@ -0,0 +1,52 @@ +package ai.elimu.entity.analytics; + +import ai.elimu.entity.BaseEntity; +import ai.elimu.entity.application.Application; +import jakarta.persistence.Column; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.Temporal; +import jakarta.persistence.TemporalType; +import jakarta.validation.constraints.NotNull; +import java.util.Calendar; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@MappedSuperclass +public abstract class AssessmentEvent extends BaseEntity { + + @NotNull + @Temporal(TemporalType.TIMESTAMP) + private Calendar timestamp; + + /** + * See https://developer.android.com/reference/android/provider/Settings.Secure#ANDROID_ID + */ + @NotNull + private String androidId; + + /** + * The package name of the {@link #application} where the assessment event occurred. + */ + @NotNull + private String packageName; + + /** + * This field will only be populated if a corresponding {@link Application} can be found in the database for the {@link #packageName}. + */ + @ManyToOne + private Application application; + + /** + * Any additional data should be stored in the format of a JSON object. + * + * Example: + *
+   * {'word_ids_presented': [1,2,3], 'word_id_selected': 2}
+   * 
+ */ + @Column(length = 1024) + private String additionalData; +} From 6a53986a345a7338233eeab560ee2ddb48d058ec Mon Sep 17 00:00:00 2001 From: "Jo G." <1451036+jo-elimu@users.noreply.github.com> Date: Sat, 24 May 2025 19:38:58 +0700 Subject: [PATCH 2/7] feat: letter-sound assessment event #2191 --- .../entity/analytics/AssessmentEvent.java | 4 +- .../analytics/LetterSoundAssessmentEvent.java | 39 +++++++++++++++++++ .../resources/META-INF/jpa-schema-export.sql | 22 +++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 src/main/java/ai/elimu/entity/analytics/LetterSoundAssessmentEvent.java diff --git a/src/main/java/ai/elimu/entity/analytics/AssessmentEvent.java b/src/main/java/ai/elimu/entity/analytics/AssessmentEvent.java index e4807474d..d9fbc1fca 100644 --- a/src/main/java/ai/elimu/entity/analytics/AssessmentEvent.java +++ b/src/main/java/ai/elimu/entity/analytics/AssessmentEvent.java @@ -29,12 +29,14 @@ public abstract class AssessmentEvent extends BaseEntity { /** * The package name of the {@link #application} where the assessment event occurred. + * E.g. ai.elimu.soundcards. */ @NotNull private String packageName; /** - * This field will only be populated if a corresponding {@link Application} can be found in the database for the {@link #packageName}. + * This field will only be populated if a corresponding {@link Application} can be + * found in the database for the {@link #packageName}. */ @ManyToOne private Application application; diff --git a/src/main/java/ai/elimu/entity/analytics/LetterSoundAssessmentEvent.java b/src/main/java/ai/elimu/entity/analytics/LetterSoundAssessmentEvent.java new file mode 100644 index 000000000..d7690fb91 --- /dev/null +++ b/src/main/java/ai/elimu/entity/analytics/LetterSoundAssessmentEvent.java @@ -0,0 +1,39 @@ +package ai.elimu.entity.analytics; + +import jakarta.persistence.Entity; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +public class LetterSoundAssessmentEvent extends AssessmentEvent { + + /** + * The sequence of letters. E.g. ["s", "h"]. + */ + private String[] letterSoundLetters; + + /** + * The sequence of sounds (IPA values). E.g. ["ʃ"]. + */ + private String [] letterSoundSounds; + + /** + * This field might not be included, e.g. if the assessment task was done in a + * 3rd-party app that did not load the content from the elimu.ai Content Provider. + * In this case, the {@link #letterSoundId} will be {@code null}. + */ + private Long letterSoundId; + + /** + * A value in the range [0.0, 1.0]. + */ + private Float masteryScore; + + /** + * The number of milliseconds passed between the student opening the assessment task + * and submitting a response. E.g. 15000. + */ + private Long timeSpentMs; +} diff --git a/src/main/resources/META-INF/jpa-schema-export.sql b/src/main/resources/META-INF/jpa-schema-export.sql index 96cf69fae..7439d5a00 100644 --- a/src/main/resources/META-INF/jpa-schema-export.sql +++ b/src/main/resources/META-INF/jpa-schema-export.sql @@ -43,6 +43,8 @@ drop table if exists LetterSound_Sound; + drop table if exists LetterSoundAssessmentEvent; + drop table if exists LetterSoundContributionEvent; drop table if exists LetterSoundLearningEvent; @@ -304,6 +306,21 @@ primary key (LetterSound_id, sounds_ORDER) ) type=MyISAM; + create table LetterSoundAssessmentEvent ( + id bigint not null auto_increment, + additionalData text, + androidId varchar(255), + packageName varchar(255), + timestamp datetime, + letterSoundId bigint, + letterSoundLetters varbinary(255), + letterSoundSounds varbinary(255), + masteryScore float(23), + timeSpentMs bigint, + application_id bigint, + primary key (id) + ) type=MyISAM; + create table LetterSoundContributionEvent ( id bigint not null auto_increment, comment text, @@ -755,6 +772,11 @@ foreign key (LetterSound_id) references LetterSound (id); + alter table LetterSoundAssessmentEvent + add constraint FKehf1rjbixnmdjol91didaj4b5 + foreign key (application_id) + references Application (id); + alter table LetterSoundContributionEvent add constraint FK5uk320agfa13pvh52v6n6ncbs foreign key (contributor_id) From 27de0847b31ebd4246b47491cb1414b976afd9ba Mon Sep 17 00:00:00 2001 From: "Jo G." <1451036+jo-elimu@users.noreply.github.com> Date: Mon, 26 May 2025 19:24:10 +0700 Subject: [PATCH 3/7] feat: letter-sound assessment events - dao #2191 --- .../java/ai/elimu/dao/LetterSoundAssessmentEventDao.java | 8 ++++++++ .../elimu/dao/jpa/LetterSoundAssessmentEventDaoJpa.java | 9 +++++++++ .../elimu/web/analytics/students/StudentController.java | 8 ++++++++ .../webapp/WEB-INF/spring/applicationContext-jpa.xml | 1 + 4 files changed, 26 insertions(+) create mode 100644 src/main/java/ai/elimu/dao/LetterSoundAssessmentEventDao.java create mode 100644 src/main/java/ai/elimu/dao/jpa/LetterSoundAssessmentEventDaoJpa.java diff --git a/src/main/java/ai/elimu/dao/LetterSoundAssessmentEventDao.java b/src/main/java/ai/elimu/dao/LetterSoundAssessmentEventDao.java new file mode 100644 index 000000000..daa2b0173 --- /dev/null +++ b/src/main/java/ai/elimu/dao/LetterSoundAssessmentEventDao.java @@ -0,0 +1,8 @@ +package ai.elimu.dao; + +import ai.elimu.entity.analytics.LetterSoundAssessmentEvent; + +public interface LetterSoundAssessmentEventDao extends GenericDao { + + +} diff --git a/src/main/java/ai/elimu/dao/jpa/LetterSoundAssessmentEventDaoJpa.java b/src/main/java/ai/elimu/dao/jpa/LetterSoundAssessmentEventDaoJpa.java new file mode 100644 index 000000000..17a9fd7fc --- /dev/null +++ b/src/main/java/ai/elimu/dao/jpa/LetterSoundAssessmentEventDaoJpa.java @@ -0,0 +1,9 @@ +package ai.elimu.dao.jpa; + +import ai.elimu.dao.LetterSoundAssessmentEventDao; +import ai.elimu.entity.analytics.LetterSoundAssessmentEvent; + +public class LetterSoundAssessmentEventDaoJpa extends GenericDaoJpa implements LetterSoundAssessmentEventDao { + + +} diff --git a/src/main/java/ai/elimu/web/analytics/students/StudentController.java b/src/main/java/ai/elimu/web/analytics/students/StudentController.java index 74e9b6ee3..c1b4fdd58 100644 --- a/src/main/java/ai/elimu/web/analytics/students/StudentController.java +++ b/src/main/java/ai/elimu/web/analytics/students/StudentController.java @@ -1,9 +1,11 @@ package ai.elimu.web.analytics.students; +import ai.elimu.dao.LetterSoundAssessmentEventDao; import ai.elimu.dao.StoryBookLearningEventDao; import ai.elimu.dao.StudentDao; import ai.elimu.dao.VideoLearningEventDao; import ai.elimu.dao.WordLearningEventDao; +import ai.elimu.entity.analytics.LetterSoundAssessmentEvent; import ai.elimu.entity.analytics.StoryBookLearningEvent; import ai.elimu.entity.analytics.VideoLearningEvent; import ai.elimu.entity.analytics.WordLearningEvent; @@ -33,6 +35,8 @@ public class StudentController { private final StudentDao studentDao; + private final LetterSoundAssessmentEventDao letterSoundAssessmentEventDao; + private final WordLearningEventDao wordLearningEventDao; private final StoryBookLearningEventDao storyBookLearningEventDao; @@ -46,6 +50,10 @@ public String handleRequest(@PathVariable Long studentId, Model model) { Student student = studentDao.read(studentId); log.info("student.getAndroidId(): " + student.getAndroidId()); + + List letterSoundAssessmentEvents = letterSoundAssessmentEventDao.readAll(); + model.addAttribute("letterSoundAssessmentEvents", letterSoundAssessmentEvents); + // Prepare chart data - WordLearningEvents List wordLearningEvents = wordLearningEventDao.readAll(); diff --git a/src/main/webapp/WEB-INF/spring/applicationContext-jpa.xml b/src/main/webapp/WEB-INF/spring/applicationContext-jpa.xml index 3a2002cb3..99f5925cb 100644 --- a/src/main/webapp/WEB-INF/spring/applicationContext-jpa.xml +++ b/src/main/webapp/WEB-INF/spring/applicationContext-jpa.xml @@ -54,6 +54,7 @@ + From bfd07fad837a5427efaaa2ccdfb8e34f1ec71e61 Mon Sep 17 00:00:00 2001 From: "Jo G." <1451036+jo-elimu@users.noreply.github.com> Date: Wed, 28 May 2025 20:19:38 +0700 Subject: [PATCH 4/7] test: add ui test for /analytics/students/ #2191 --- .../analytics/students/StudentController.java | 12 ++ .../WEB-INF/jsp/analytics/students/id.jsp | 165 +++++++++++++++++- .../WEB-INF/jsp/analytics/students/list.jsp | 10 +- src/main/webapp/static/img/student-ENG.png | Bin 0 -> 700411 bytes src/main/webapp/static/img/student-HIN.png | Bin 0 -> 416708 bytes src/main/webapp/static/img/student-TGL.png | Bin 0 -> 599084 bytes src/main/webapp/static/img/student-THA.png | Bin 0 -> 726057 bytes src/main/webapp/static/img/student-VIE.png | Bin 0 -> 590547 bytes src/test/java/selenium/WelcomePageTest.java | 1 + .../analytics/MainAnalyticsPageTest.java | 1 + .../StoryBookLearningEventsPageTest.java | 1 + .../VideoLearningEventsPageTest.java | 1 + .../analytics/WordLearningEventsPageTest.java | 1 + .../analytics/students/StudentListPage.java | 29 +++ .../analytics/students/StudentPage.java | 15 ++ .../analytics/students/StudentTest.java | 56 ++++++ .../selenium/application/ApplicationTest.java | 1 + .../selenium/content/emoji/EmojiTest.java | 1 + .../selenium/content/image/ImageTest.java | 1 + .../selenium/content/letter/LetterTest.java | 1 + .../content/letter_sound/LetterSoundTest.java | 1 + .../selenium/content/number/NumberTest.java | 1 + .../selenium/content/sound/SoundTest.java | 1 + .../content/storybook/StoryBookTest.java | 1 + .../selenium/content/video/VideoTest.java | 1 + .../java/selenium/content/word/WordTest.java | 1 + .../contributor/ContributorPageTest.java | 1 + 27 files changed, 289 insertions(+), 14 deletions(-) create mode 100644 src/main/webapp/static/img/student-ENG.png create mode 100644 src/main/webapp/static/img/student-HIN.png create mode 100644 src/main/webapp/static/img/student-TGL.png create mode 100644 src/main/webapp/static/img/student-THA.png create mode 100644 src/main/webapp/static/img/student-VIE.png create mode 100644 src/test/java/selenium/analytics/students/StudentListPage.java create mode 100644 src/test/java/selenium/analytics/students/StudentPage.java create mode 100644 src/test/java/selenium/analytics/students/StudentTest.java diff --git a/src/main/java/ai/elimu/web/analytics/students/StudentController.java b/src/main/java/ai/elimu/web/analytics/students/StudentController.java index c1b4fdd58..f7f446c0b 100644 --- a/src/main/java/ai/elimu/web/analytics/students/StudentController.java +++ b/src/main/java/ai/elimu/web/analytics/students/StudentController.java @@ -1,15 +1,19 @@ package ai.elimu.web.analytics.students; import ai.elimu.dao.LetterSoundAssessmentEventDao; +import ai.elimu.dao.LetterSoundLearningEventDao; import ai.elimu.dao.StoryBookLearningEventDao; import ai.elimu.dao.StudentDao; import ai.elimu.dao.VideoLearningEventDao; import ai.elimu.dao.WordLearningEventDao; import ai.elimu.entity.analytics.LetterSoundAssessmentEvent; +import ai.elimu.entity.analytics.LetterSoundLearningEvent; import ai.elimu.entity.analytics.StoryBookLearningEvent; import ai.elimu.entity.analytics.VideoLearningEvent; import ai.elimu.entity.analytics.WordLearningEvent; import ai.elimu.entity.analytics.students.Student; +import ai.elimu.model.v2.enums.content.LiteracySkill; +import ai.elimu.model.v2.enums.content.NumeracySkill; import ai.elimu.util.AnalyticsHelper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -36,6 +40,7 @@ public class StudentController { private final StudentDao studentDao; private final LetterSoundAssessmentEventDao letterSoundAssessmentEventDao; + private final LetterSoundLearningEventDao letterSoundLearningEventDao; private final WordLearningEventDao wordLearningEventDao; @@ -51,9 +56,16 @@ public String handleRequest(@PathVariable Long studentId, Model model) { log.info("student.getAndroidId(): " + student.getAndroidId()); + model.addAttribute("literacySkills", LiteracySkill.values()); + model.addAttribute("numeracySkills", NumeracySkill.values()); + + List letterSoundAssessmentEvents = letterSoundAssessmentEventDao.readAll(); model.addAttribute("letterSoundAssessmentEvents", letterSoundAssessmentEvents); + List letterSoundLearningEvents = letterSoundLearningEventDao.readAll(); + model.addAttribute("letterSoundLearningEvents", letterSoundLearningEvents); + // Prepare chart data - WordLearningEvents List wordLearningEvents = wordLearningEventDao.readAll(); diff --git a/src/main/webapp/WEB-INF/jsp/analytics/students/id.jsp b/src/main/webapp/WEB-INF/jsp/analytics/students/id.jsp index 7d1af0b44..88802f395 100644 --- a/src/main/webapp/WEB-INF/jsp/analytics/students/id.jsp +++ b/src/main/webapp/WEB-INF/jsp/analytics/students/id.jsp @@ -1,16 +1,139 @@ - 🎓 Student ${student.id} + 🎓 Student #${student.id} -

-
${student.androidId}
+
+
+
+ " /> + + + +
+
+

Student ID

+ #${student.id}
+
+

Android ID

+ ${student.androidId} +
+
+
+
+
+ + + +
+
+
+ +
+
Literacy skills
+
+ + + + + + + + + + + + + + + +
SkillMastery
+ ${literacySkill} + + 0% + +
+
+
+
+
+
+
+
Numeracy skills
+
+ + + + + + + + + + + + + + + +
SkillMastery
+ ${numeracySkill} + + 0% + +
+
+
+
+
+
+
- +
🎼 Letter-sounds
+
+
Assessment events (${fn:length(letterSoundAssessmentEvents)})
+ ... + +
+ +
Learning events (${fn:length(letterSoundLearningEvents)})
+ ... +
+
+
🔤 Words
-
🔤 Word learning events (${fn:length(wordLearningEvents)})
- +
Assessment events (${fn:length(wordAssessmentEvents)})
+ ... + +
+ +
Learning events (${fn:length(wordLearningEvents)})
+
+
🔢 Numbers
-
📚 Storybook learning events
- +
Assessment events (${fn:length(numberAssessmentEvents)})
+ ... + +
+ +
Learning events (${fn:length(numberLearningEvents)})
+ ... +
+
+ +
📚 Storybooks
+
+
Assessment events (${fn:length(storyBookAssessmentEvents)})
+ ... + +
+ +
Learning events (${fn:length(storyBookLearningEvents)})
+
+
🎬 Videos
-
🎬 Video learning events
+
Assessment events (${fn:length(videoAssessmentEvents)})
+ ... + +
+
Learning events (${fn:length(videoLearningEvents)})
Assessment events (${fn:length(letterSoundAssessmentEvents)})
... From 7a9c5081d0c8d3b9d3e48729aab33511e3741482 Mon Sep 17 00:00:00 2001 From: "Jo G." <1451036+jo-elimu@users.noreply.github.com> Date: Wed, 28 May 2025 22:08:27 +0700 Subject: [PATCH 6/7] Update LetterSoundAssessmentEventCsvExportController.java --- .../students/LetterSoundAssessmentEventCsvExportController.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/ai/elimu/web/analytics/students/LetterSoundAssessmentEventCsvExportController.java b/src/main/java/ai/elimu/web/analytics/students/LetterSoundAssessmentEventCsvExportController.java index c9638a5c4..fe8421b6c 100644 --- a/src/main/java/ai/elimu/web/analytics/students/LetterSoundAssessmentEventCsvExportController.java +++ b/src/main/java/ai/elimu/web/analytics/students/LetterSoundAssessmentEventCsvExportController.java @@ -11,6 +11,7 @@ import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVPrinter; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; From c30663a3a424e7f246c5a9da7f46fb5a92131472 Mon Sep 17 00:00:00 2001 From: "Jo G." <1451036+jo-elimu@users.noreply.github.com> Date: Wed, 28 May 2025 22:46:07 +0700 Subject: [PATCH 7/7] feat: rest api for letter-sound assessment events #2191 --- ...erSoundAssessmentEventsRestController.java | 91 +++++++++++++++++++ ...undAssessmentEventsRestControllerTest.java | 58 ++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 src/main/java/ai/elimu/rest/v2/analytics/LetterSoundAssessmentEventsRestController.java create mode 100644 src/test/java/ai/elimu/rest/v2/analytics/LetterSoundAssessmentEventsRestControllerTest.java diff --git a/src/main/java/ai/elimu/rest/v2/analytics/LetterSoundAssessmentEventsRestController.java b/src/main/java/ai/elimu/rest/v2/analytics/LetterSoundAssessmentEventsRestController.java new file mode 100644 index 000000000..9c18a80bb --- /dev/null +++ b/src/main/java/ai/elimu/rest/v2/analytics/LetterSoundAssessmentEventsRestController.java @@ -0,0 +1,91 @@ +package ai.elimu.rest.v2.analytics; + +import ai.elimu.model.v2.enums.Language; +import ai.elimu.util.AnalyticsHelper; +import ai.elimu.util.ConfigHelper; +import java.io.File; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; + +import org.apache.commons.io.FileUtils; +import org.json.JSONObject; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +/** + * REST API endpoint for receiving letter-sound assessment events from the + * Analytics application. + */ +@RestController +@RequestMapping(value = "/rest/v2/analytics/letter-sound-assessment-events/csv", produces = MediaType.APPLICATION_JSON_VALUE) +@Slf4j +public class LetterSoundAssessmentEventsRestController { + + @PostMapping + public String handleUploadCsvRequest( + @RequestParam("file") MultipartFile multipartFile, + HttpServletResponse response + ) { + log.info("handleUploadCsvRequest"); + + JSONObject jsonResponseObject = new JSONObject(); + try { + String contentType = multipartFile.getContentType(); + log.info("contentType: " + contentType); + + long size = multipartFile.getSize(); + log.info("size: " + size); + if (size == 0) { + throw new IllegalArgumentException("Empty file"); + } + + // Expected format: "7161a85a0e4751cd_3002023_letter-sound-assessment-events_2025-05-28.csv" + String originalFilename = multipartFile.getOriginalFilename(); + log.info("originalFilename: " + originalFilename); + if (originalFilename.length() != "7161a85a0e4751cd_3002023_letter-sound-assessment-events_2025-05-28.csv".length()) { + throw new IllegalArgumentException("Unexpected filename"); + } + + String androidIdExtractedFromFilename = AnalyticsHelper.extractAndroidIdFromCsvFilename(originalFilename); + log.info("androidIdExtractedFromFilename: \"" + androidIdExtractedFromFilename + "\""); + + Integer versionCodeExtractedFromFilename = AnalyticsHelper.extractVersionCodeFromCsvFilename(originalFilename); + log.info("versionCodeExtractedFromFilename: " + versionCodeExtractedFromFilename); + + byte[] bytes = multipartFile.getBytes(); + log.info("bytes.length: " + bytes.length); + + // Store the original CSV file on the filesystem + File elimuAiDir = new File(System.getProperty("user.home"), ".elimu-ai"); + File languageDir = new File(elimuAiDir, "lang-" + Language.valueOf(ConfigHelper.getProperty("content.language"))); + File analyticsDir = new File(languageDir, "analytics"); + File androidIdDir = new File(analyticsDir, "android-id-" + androidIdExtractedFromFilename); + File versionCodeDir = new File(androidIdDir, "version-code-" + versionCodeExtractedFromFilename); + File letterSoundAssessmentEventsDir = new File(versionCodeDir, "letter-sound-assessment-events"); + letterSoundAssessmentEventsDir.mkdirs(); + File csvFile = new File(letterSoundAssessmentEventsDir, originalFilename); + log.info("Storing CSV file at " + csvFile); + FileUtils.writeByteArrayToFile(csvFile, bytes); + log.info("csvFile.exists(): " + csvFile.exists()); + + jsonResponseObject.put("result", "success"); + jsonResponseObject.put("successMessage", "The CSV file was uploaded"); + response.setStatus(HttpStatus.OK.value()); + } catch (Exception ex) { + log.error(ex.getMessage()); + + jsonResponseObject.put("result", "error"); + jsonResponseObject.put("errorMessage", ex.getMessage()); + response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); + } + + String jsonResponse = jsonResponseObject.toString(); + log.info("jsonResponse: " + jsonResponse); + return jsonResponse; + } +} diff --git a/src/test/java/ai/elimu/rest/v2/analytics/LetterSoundAssessmentEventsRestControllerTest.java b/src/test/java/ai/elimu/rest/v2/analytics/LetterSoundAssessmentEventsRestControllerTest.java new file mode 100644 index 000000000..34631d4db --- /dev/null +++ b/src/test/java/ai/elimu/rest/v2/analytics/LetterSoundAssessmentEventsRestControllerTest.java @@ -0,0 +1,58 @@ +package ai.elimu.rest.v2.analytics; + +import org.json.JSONObject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.servlet.http.HttpServletResponse; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +public class LetterSoundAssessmentEventsRestControllerTest { + + @InjectMocks + private LetterSoundAssessmentEventsRestController letterSoundAssessmentEventsRestController; + + @Mock + private HttpServletResponse response; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void testHandleUploadCsvRequest_invalidFilename() { + MultipartFile multipartFile = new MockMultipartFile( + "file", + "invalid_filename.csv", + "text/csv", + "test content".getBytes() + ); + String jsonResponse = letterSoundAssessmentEventsRestController.handleUploadCsvRequest(multipartFile, response); + JSONObject jsonObject = new JSONObject(jsonResponse); + assertEquals("error", jsonObject.getString("result")); + assertEquals("Unexpected filename", jsonObject.getString("errorMessage")); + verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + + @Test + public void testHandleUploadCsvRequest_emptyFile() { + MultipartFile multipartFile = new MockMultipartFile( + "file", + "7161a85a0e4751cd_3002023_letter-sound-assessment-events_2025-05-28.csv", + "text/csv", + "".getBytes() + ); + String jsonResponse = letterSoundAssessmentEventsRestController.handleUploadCsvRequest(multipartFile, response); + JSONObject jsonObject = new JSONObject(jsonResponse); + assertEquals("error", jsonObject.getString("result")); + assertEquals("Empty file", jsonObject.getString("errorMessage")); + verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } +}