Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom-dependency-tree.txt
Original file line number Diff line number Diff line change
@@ -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
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/ai/elimu/dao/LetterSoundAssessmentEventDao.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package ai.elimu.dao;

import java.util.List;

import org.springframework.dao.DataAccessException;

import ai.elimu.entity.analytics.LetterSoundAssessmentEvent;

public interface LetterSoundAssessmentEventDao extends GenericDao<LetterSoundAssessmentEvent> {

List<LetterSoundAssessmentEvent> readAll(String androidId) throws DataAccessException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package ai.elimu.dao.jpa;

import java.util.List;

import org.springframework.dao.DataAccessException;

import ai.elimu.dao.LetterSoundAssessmentEventDao;
import ai.elimu.entity.analytics.LetterSoundAssessmentEvent;

public class LetterSoundAssessmentEventDaoJpa extends GenericDaoJpa<LetterSoundAssessmentEvent> implements LetterSoundAssessmentEventDao {

@Override
public List<LetterSoundAssessmentEvent> readAll(String androidId) throws DataAccessException {
return em.createQuery(
"SELECT event " +
"FROM LetterSoundAssessmentEvent event " +
"WHERE event.androidId = :androidId " +
"ORDER BY event.timestamp")
.setParameter("androidId", androidId)
.getResultList();
}
}
57 changes: 57 additions & 0 deletions src/main/java/ai/elimu/entity/analytics/AssessmentEvent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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;

/**
* A 64-bit number (expressed as a hexadecimal string), unique to each combination of
* app-signing key, user, and device.
*
* 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.
* E.g. <code>ai.elimu.soundcards</code>.
*/
@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:
* <pre>
* {'word_ids_presented': [1,2,3], 'word_id_selected': 2}
* </pre>
*/
@Column(length = 1024)
private String additionalData;
}
3 changes: 3 additions & 0 deletions src/main/java/ai/elimu/entity/analytics/LearningEvent.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ public abstract class LearningEvent extends BaseEntity {
private Calendar timestamp;

/**
* A 64-bit number (expressed as a hexadecimal string), unique to each combination of
* app-signing key, user, and device.
*
* See https://developer.android.com/reference/android/provider/Settings.Secure#ANDROID_ID
*/
@NotNull
Expand Down
Original file line number Diff line number Diff line change
@@ -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. <code>"sh"</code>.
*/
private String letterSoundLetters;

/**
* The sequence of sounds (IPA values). E.g. <code>"ʃ"</code>.
*/
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;
Comment on lines +29 to +32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add validation constraint for mastery score range.

The JavaDoc specifies a range [0.0, 1.0] but there's no validation to enforce this constraint.

+import jakarta.validation.constraints.DecimalMax;
+import jakarta.validation.constraints.DecimalMin;

 /**
  * A value in the range [0.0, 1.0].
  */
+@DecimalMin(value = "0.0", message = "Mastery score must be between 0.0 and 1.0")
+@DecimalMax(value = "1.0", message = "Mastery score must be between 0.0 and 1.0")
 private Float masteryScore;
🤖 Prompt for AI Agents
In src/main/java/ai/elimu/entity/analytics/LetterSoundAssessmentEvent.java
around lines 29 to 32, the masteryScore field has a JavaDoc specifying it should
be in the range [0.0, 1.0], but no validation enforces this. Add a validation
annotation such as @DecimalMin("0.0") and @DecimalMax("1.0") to the masteryScore
field to ensure the value stays within the specified range.


/**
* The number of milliseconds passed between the student opening the assessment task
* and submitting a response. E.g. <code>15000</code>.
*/
private Long timeSpentMs;
Comment on lines +34 to +38
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add validation for time measurement.

Consider adding validation to ensure timeSpentMs is non-negative, as negative time values wouldn't make sense for assessment duration.

+import jakarta.validation.constraints.Min;

 /**
  * The number of milliseconds passed between the student opening the assessment task 
  * and submitting a response. E.g. <code>15000</code>.
  */
+@Min(value = 0, message = "Time spent must be non-negative")
 private Long timeSpentMs;

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/java/ai/elimu/entity/analytics/LetterSoundAssessmentEvent.java
around lines 34 to 38, add validation to ensure the timeSpentMs field is never
set to a negative value. Implement a check in the setter method or wherever
timeSpentMs is assigned to throw an exception or reject values less than zero,
enforcing that the assessment duration is always non-negative.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package ai.elimu.web.analytics.students;

import ai.elimu.dao.LetterSoundAssessmentEventDao;
import ai.elimu.dao.StudentDao;
import ai.elimu.entity.analytics.LetterSoundAssessmentEvent;
import ai.elimu.entity.analytics.students.Student;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringWriter;
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;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/analytics/students/{studentId}/letter-sound-assessment-events.csv")
@RequiredArgsConstructor
@Slf4j
public class LetterSoundAssessmentEventCsvExportController {

private final StudentDao studentDao;

private final LetterSoundAssessmentEventDao letterSoundAssessmentEventDao;

@GetMapping
public void handleRequest(
@PathVariable Long studentId,
HttpServletResponse response,
OutputStream outputStream
) throws IOException {
Comment on lines +32 to +36
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove redundant OutputStream parameter.

The OutputStream outputStream parameter is redundant since HttpServletResponse already provides access to the output stream via response.getOutputStream(). This can simplify the method signature and reduce potential confusion.

  @GetMapping
  public void handleRequest(
      @PathVariable Long studentId,
-      HttpServletResponse response,
-      OutputStream outputStream
+      HttpServletResponse response
  ) throws IOException {

Then update the output writing section to use response.getOutputStream().

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public void handleRequest(
@PathVariable Long studentId,
HttpServletResponse response,
OutputStream outputStream
) throws IOException {
@GetMapping
public void handleRequest(
@PathVariable Long studentId,
HttpServletResponse response
) throws IOException {
🤖 Prompt for AI Agents
In
src/main/java/ai/elimu/web/analytics/students/LetterSoundAssessmentEventCsvExportController.java
around lines 32 to 36, remove the redundant OutputStream outputStream parameter
from the handleRequest method signature since HttpServletResponse already
provides access to the output stream. Then update the method implementation to
write output using response.getOutputStream() instead of the removed
outputStream parameter.

log.info("handleRequest");

Student student = studentDao.read(studentId);
log.info("student.getAndroidId(): " + student.getAndroidId());

List<LetterSoundAssessmentEvent> letterSoundAssessmentEvents = letterSoundAssessmentEventDao.readAll(student.getAndroidId());
log.info("letterSoundAssessmentEvents.size(): " + letterSoundAssessmentEvents.size());
Comment on lines +39 to +43
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add null safety checks for student retrieval.

The code doesn't handle the case where the student might not exist, which could result in a NullPointerException.

  Student student = studentDao.read(studentId);
+ if (student == null) {
+   response.sendError(HttpServletResponse.SC_NOT_FOUND, "Student not found");
+   return;
+ }
  log.info("student.getAndroidId(): " + student.getAndroidId());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Student student = studentDao.read(studentId);
log.info("student.getAndroidId(): " + student.getAndroidId());
List<LetterSoundAssessmentEvent> letterSoundAssessmentEvents = letterSoundAssessmentEventDao.readAll(student.getAndroidId());
log.info("letterSoundAssessmentEvents.size(): " + letterSoundAssessmentEvents.size());
Student student = studentDao.read(studentId);
if (student == null) {
response.sendError(HttpServletResponse.SC_NOT_FOUND, "Student not found");
return;
}
log.info("student.getAndroidId(): " + student.getAndroidId());
List<LetterSoundAssessmentEvent> letterSoundAssessmentEvents =
letterSoundAssessmentEventDao.readAll(student.getAndroidId());
log.info("letterSoundAssessmentEvents.size(): " + letterSoundAssessmentEvents.size());
🤖 Prompt for AI Agents
In
src/main/java/ai/elimu/web/analytics/students/LetterSoundAssessmentEventCsvExportController.java
around lines 39 to 43, add a null check after retrieving the student with
studentDao.read(studentId). If the student is null, handle this case
appropriately, such as logging a warning and returning early or throwing a
controlled exception, to prevent a NullPointerException when accessing
student.getAndroidId().


CSVFormat csvFormat = CSVFormat.DEFAULT.builder()
.setHeader(
"id",
"timestamp",
"package_name",
"letter_sound_letters",
"letter_sound_sounds",
"letter_sound_id",
"mastery_score",
"time_spent_ms",
"additional_data"
)
.build();

StringWriter stringWriter = new StringWriter();
CSVPrinter csvPrinter = new CSVPrinter(stringWriter, csvFormat);

for (LetterSoundAssessmentEvent letterSoundAssessmentEvent : letterSoundAssessmentEvents) {
log.info("letterSoundAssessmentEvent.getId(): " + letterSoundAssessmentEvent.getId());

csvPrinter.printRecord(
letterSoundAssessmentEvent.getId(),
letterSoundAssessmentEvent.getTimestamp().getTimeInMillis(),
letterSoundAssessmentEvent.getPackageName(),
letterSoundAssessmentEvent.getLetterSoundLetters(),
letterSoundAssessmentEvent.getLetterSoundSounds(),
letterSoundAssessmentEvent.getLetterSoundId(),
letterSoundAssessmentEvent.getMasteryScore(),
letterSoundAssessmentEvent.getTimeSpentMs(),
letterSoundAssessmentEvent.getAdditionalData()
);
}
csvPrinter.flush();
csvPrinter.close();
Comment on lines +59 to +78
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Implement proper resource management using try-with-resources.

The current implementation manually closes the CSVPrinter and StringWriter, but doesn't guarantee closure in case of exceptions. Use try-with-resources for better resource management.

- StringWriter stringWriter = new StringWriter();
- CSVPrinter csvPrinter = new CSVPrinter(stringWriter, csvFormat);
-
- for (LetterSoundAssessmentEvent letterSoundAssessmentEvent : letterSoundAssessmentEvents) {
-   log.info("letterSoundAssessmentEvent.getId(): " + letterSoundAssessmentEvent.getId());
-
-   csvPrinter.printRecord(
-       letterSoundAssessmentEvent.getId(),
-       letterSoundAssessmentEvent.getTimestamp().getTimeInMillis(),
-       letterSoundAssessmentEvent.getPackageName(),
-       letterSoundAssessmentEvent.getLetterSoundLetters(),
-       letterSoundAssessmentEvent.getLetterSoundSounds(),
-       letterSoundAssessmentEvent.getLetterSoundId(),
-       letterSoundAssessmentEvent.getMasteryScore(),
-       letterSoundAssessmentEvent.getTimeSpentMs(),
-       letterSoundAssessmentEvent.getAdditionalData()
-   );
- }
- csvPrinter.flush();
- csvPrinter.close();
+ try (StringWriter stringWriter = new StringWriter();
+      CSVPrinter csvPrinter = new CSVPrinter(stringWriter, csvFormat)) {
+   
+   for (LetterSoundAssessmentEvent letterSoundAssessmentEvent : letterSoundAssessmentEvents) {
+     log.info("letterSoundAssessmentEvent.getId(): " + letterSoundAssessmentEvent.getId());
+
+     csvPrinter.printRecord(
+         letterSoundAssessmentEvent.getId(),
+         letterSoundAssessmentEvent.getTimestamp().getTimeInMillis(),
+         letterSoundAssessmentEvent.getPackageName(),
+         letterSoundAssessmentEvent.getLetterSoundLetters(),
+         letterSoundAssessmentEvent.getLetterSoundSounds(),
+         letterSoundAssessmentEvent.getLetterSoundId(),
+         letterSoundAssessmentEvent.getMasteryScore(),
+         letterSoundAssessmentEvent.getTimeSpentMs(),
+         letterSoundAssessmentEvent.getAdditionalData()
+     );
+   }
+   csvPrinter.flush();
+   
+   String csvFileContent = stringWriter.toString();
+   // Move response writing logic here
+ }
🤖 Prompt for AI Agents
In
src/main/java/ai/elimu/web/analytics/students/LetterSoundAssessmentEventCsvExportController.java
around lines 59 to 78, the CSVPrinter and StringWriter are manually closed,
which risks resource leaks if exceptions occur. Refactor the code to use
try-with-resources for both CSVPrinter and StringWriter to ensure they are
automatically closed even if an exception is thrown during processing.


String csvFileContent = stringWriter.toString();

response.setContentType("text/csv");
byte[] bytes = csvFileContent.getBytes();
response.setContentLength(bytes.length);
try {
outputStream.write(bytes);
outputStream.flush();
outputStream.close();
} catch (IOException ex) {
log.error(ex.getMessage());
}
Comment on lines +85 to +91
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve error handling and use response.getOutputStream().

The current error handling catches IOException but continues execution, and the OutputStream parameter should be replaced with response.getOutputStream().

  response.setContentType("text/csv");
  byte[] bytes = csvFileContent.getBytes();
  response.setContentLength(bytes.length);
- try {
-   outputStream.write(bytes);
-   outputStream.flush();
-   outputStream.close();
- } catch (IOException ex) {
-   log.error(ex.getMessage());
- }
+ try (OutputStream outputStream = response.getOutputStream()) {
+   outputStream.write(bytes);
+   outputStream.flush();
+ } catch (IOException ex) {
+   log.error("Failed to write CSV data to response", ex);
+   response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Failed to export CSV");
+ }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
outputStream.write(bytes);
outputStream.flush();
outputStream.close();
} catch (IOException ex) {
log.error(ex.getMessage());
}
response.setContentType("text/csv");
byte[] bytes = csvFileContent.getBytes();
response.setContentLength(bytes.length);
try (OutputStream outputStream = response.getOutputStream()) {
outputStream.write(bytes);
outputStream.flush();
} catch (IOException ex) {
log.error("Failed to write CSV data to response", ex);
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Failed to export CSV");
}
🤖 Prompt for AI Agents
In
src/main/java/ai/elimu/web/analytics/students/LetterSoundAssessmentEventCsvExportController.java
around lines 85 to 91, replace the current OutputStream parameter with
response.getOutputStream() to write the bytes. Improve error handling by not
just logging the exception message but also properly handling the IOException,
such as rethrowing it or stopping further execution to avoid inconsistent state
after a failure.

}
}
Original file line number Diff line number Diff line change
@@ -1,13 +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;
Expand All @@ -33,6 +39,9 @@ public class StudentController {

private final StudentDao studentDao;

private final LetterSoundAssessmentEventDao letterSoundAssessmentEventDao;
private final LetterSoundLearningEventDao letterSoundLearningEventDao;

private final WordLearningEventDao wordLearningEventDao;

private final StoryBookLearningEventDao storyBookLearningEventDao;
Expand All @@ -46,6 +55,17 @@ public String handleRequest(@PathVariable Long studentId, Model model) {
Student student = studentDao.read(studentId);
log.info("student.getAndroidId(): " + student.getAndroidId());


model.addAttribute("literacySkills", LiteracySkill.values());
model.addAttribute("numeracySkills", NumeracySkill.values());


List<LetterSoundAssessmentEvent> letterSoundAssessmentEvents = letterSoundAssessmentEventDao.readAll();
model.addAttribute("letterSoundAssessmentEvents", letterSoundAssessmentEvents);
Comment on lines +63 to +64
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Filter events by student to maintain consistency with controller purpose.

The current implementation fetches all letter sound assessment events without filtering by the specific student. This is inconsistent with the controller's purpose (handling requests for a specific student) and differs from how the student data is retrieved.

Apply this diff to filter events by the student's Android ID:

-List<LetterSoundAssessmentEvent> letterSoundAssessmentEvents = letterSoundAssessmentEventDao.readAll();
+List<LetterSoundAssessmentEvent> letterSoundAssessmentEvents = letterSoundAssessmentEventDao.readAllByAndroidId(student.getAndroidId());

Note: This assumes the DAO has or will implement a readAllByAndroidId method. If not available, you'll need to add this method to the DAO interface and implementation.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/java/ai/elimu/web/analytics/students/StudentController.java around
lines 54-55, the code retrieves all LetterSoundAssessmentEvent objects without
filtering by student, which is inconsistent with the controller's purpose. To
fix this, modify the code to fetch only events associated with the specific
student's Android ID by calling a method like readAllByAndroidId, assuming it
exists or add this method to the DAO. This ensures the data displayed is
relevant to the current student context.


Comment on lines +62 to +65
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add chart data preparation for consistency with other event types.

The letter sound assessment events are added to the model without any chart data preparation, unlike the other event types (Word, StoryBook, Video) which all have extensive chart preparation logic. This inconsistency suggests the implementation is incomplete.

Consider adding chart data preparation similar to other event types:

+// Prepare chart data - LetterSoundAssessmentEvents
 List<LetterSoundAssessmentEvent> letterSoundAssessmentEvents = letterSoundAssessmentEventDao.readAll();
+List<String> letterSoundMonthList = new ArrayList<>();
+List<Integer> letterSoundEventCountList = new ArrayList<>();
+if (!letterSoundAssessmentEvents.isEmpty()) {
+    // Group event count by month (e.g. "Aug-2024", "Sep-2024")
+    Map<String, Integer> eventCountByMonthMap = new HashMap<>();
+    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("MMM-yyyy");
+    for (LetterSoundAssessmentEvent event : letterSoundAssessmentEvents) {
+        String eventMonth = simpleDateFormat.format(event.getTimestamp().getTime());
+        eventCountByMonthMap.put(eventMonth, eventCountByMonthMap.getOrDefault(eventMonth, 0) + 1);
+    }
+
+    // Iterate each month from 4 years ago until now
+    Calendar calendar4YearsAgo = Calendar.getInstance();
+    calendar4YearsAgo.add(Calendar.YEAR, -4);
+    Calendar calendarNow = Calendar.getInstance();
+    Calendar month = calendar4YearsAgo;
+    while (!month.after(calendarNow)) {
+        String monthAsString = simpleDateFormat.format(month.getTime());
+        letterSoundMonthList.add(monthAsString);
+
+        letterSoundEventCountList.add(eventCountByMonthMap.getOrDefault(monthAsString, 0));
+
+        // Increase the date by 1 month
+        month.add(Calendar.MONTH, 1);
+    }
+}
+model.addAttribute("letterSoundMonthList", letterSoundMonthList);
+model.addAttribute("letterSoundEventCountList", letterSoundEventCountList);
 model.addAttribute("letterSoundAssessmentEvents", letterSoundAssessmentEvents);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
List<LetterSoundAssessmentEvent> letterSoundAssessmentEvents = letterSoundAssessmentEventDao.readAll();
model.addAttribute("letterSoundAssessmentEvents", letterSoundAssessmentEvents);
// Prepare chart data - LetterSoundAssessmentEvents
List<LetterSoundAssessmentEvent> letterSoundAssessmentEvents = letterSoundAssessmentEventDao.readAll();
List<String> letterSoundMonthList = new ArrayList<>();
List<Integer> letterSoundEventCountList = new ArrayList<>();
if (!letterSoundAssessmentEvents.isEmpty()) {
// Group event count by month (e.g. "Aug-2024", "Sep-2024")
Map<String, Integer> eventCountByMonthMap = new HashMap<>();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("MMM-yyyy");
for (LetterSoundAssessmentEvent event : letterSoundAssessmentEvents) {
String eventMonth = simpleDateFormat.format(event.getTimestamp().getTime());
eventCountByMonthMap.put(eventMonth, eventCountByMonthMap.getOrDefault(eventMonth, 0) + 1);
}
// Iterate each month from 4 years ago until now
Calendar calendar4YearsAgo = Calendar.getInstance();
calendar4YearsAgo.add(Calendar.YEAR, -4);
Calendar calendarNow = Calendar.getInstance();
Calendar month = calendar4YearsAgo;
while (!month.after(calendarNow)) {
String monthAsString = simpleDateFormat.format(month.getTime());
letterSoundMonthList.add(monthAsString);
letterSoundEventCountList.add(eventCountByMonthMap.getOrDefault(monthAsString, 0));
month.add(Calendar.MONTH, 1);
}
}
model.addAttribute("letterSoundMonthList", letterSoundMonthList);
model.addAttribute("letterSoundEventCountList", letterSoundEventCountList);
model.addAttribute("letterSoundAssessmentEvents", letterSoundAssessmentEvents);
🤖 Prompt for AI Agents
In src/main/java/ai/elimu/web/analytics/students/StudentController.java around
lines 53 to 56, the code retrieves all LetterSoundAssessmentEvent objects and
adds them to the model without generating chart data, unlike other event types.
To fix this, implement chart data preparation for letter sound assessment events
similar to other event types, ensuring consistency across the model attributes
and complete analytics visualization.

List<LetterSoundLearningEvent> letterSoundLearningEvents = letterSoundLearningEventDao.readAll();
model.addAttribute("letterSoundLearningEvents", letterSoundLearningEvents);


// Prepare chart data - WordLearningEvents
List<WordLearningEvent> wordLearningEvents = wordLearningEventDao.readAll();
Expand Down
22 changes: 22 additions & 0 deletions src/main/resources/META-INF/jpa-schema-export.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 varchar(255),
letterSoundSounds varchar(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,
Expand Down Expand Up @@ -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)
Expand Down
Loading
Loading