Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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.22-SNAPSHOT
ai.elimu:webapp:war:2.6.23-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
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public List<LetterSoundAssessmentEvent> readAll(String androidId) throws DataAcc
"SELECT event " +
"FROM LetterSoundAssessmentEvent event " +
"WHERE event.androidId = :androidId " +
"ORDER BY event.timestamp")
"ORDER BY event.id")
.setParameter("androidId", androidId)
.getResultList();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public List<LetterSoundLearningEvent> readAll(String androidId) throws DataAcces
"SELECT event " +
"FROM LetterSoundLearningEvent event " +
"WHERE event.androidId = :androidId " +
"ORDER BY event.timestamp")
"ORDER BY event.id")
.setParameter("androidId", androidId)
.getResultList();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public List<StoryBookLearningEvent> readAll(String androidId) throws DataAccessE
"SELECT event " +
"FROM StoryBookLearningEvent event " +
"WHERE event.androidId = :androidId " +
"ORDER BY event.timestamp")
"ORDER BY event.id")
.setParameter("androidId", androidId)
.getResultList();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public List<VideoLearningEvent> readAll(String androidId) throws DataAccessExcep
"SELECT event " +
"FROM VideoLearningEvent event " +
"WHERE event.androidId = :androidId " +
"ORDER BY event.timestamp")
"ORDER BY event.id")
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

Consider the implications of ordering by ID instead of timestamp.

Changing from ORDER BY event.timestamp to ORDER BY event.id may not preserve chronological ordering. While IDs are typically sequential, they don't guarantee chronological order if:

  • Events are inserted out of sequence
  • There are concurrent insertions
  • Database transactions complete in different orders

Since this affects how events are displayed in analytics and exported in CSV files, consider whether chronological ordering is important for your use case.

If chronological ordering is required, consider reverting to timestamp ordering:

-            "ORDER BY event.id")
+            "ORDER BY event.timestamp ASC")

Or use a compound ordering strategy:

-            "ORDER BY event.id")
+            "ORDER BY event.timestamp ASC, event.id ASC")
🤖 Prompt for AI Agents
In src/main/java/ai/elimu/dao/jpa/VideoLearningEventDaoJpa.java at line 51, the
query orders events by event.id which may not reflect the true chronological
order due to possible out-of-sequence inserts or concurrent transactions. To
ensure events are displayed and exported in the correct chronological order,
change the ORDER BY clause to use event.timestamp instead of event.id, or use a
compound ordering such as ORDER BY event.timestamp, event.id to maintain
consistent and accurate event sequencing.

.setParameter("androidId", androidId)
.getResultList();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public List<WordLearningEvent> readAll(String androidId) throws DataAccessExcept
"SELECT event " +
"FROM WordLearningEvent event " +
"WHERE event.androidId = :androidId " +
"ORDER BY event.timestamp")
"ORDER BY event.id")
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

Ordering by ID instead of timestamp may affect chronological sequence.

This change mirrors the same pattern in other DAO classes, but ordering by event.id instead of event.timestamp could break chronological ordering of learning events. This is particularly important for analytics and learning progression tracking.

Consider maintaining chronological ordering:

-            "ORDER BY event.id")
+            "ORDER BY event.timestamp ASC")
📝 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
"ORDER BY event.id")
- "ORDER BY event.id")
+ "ORDER BY event.timestamp ASC")
🤖 Prompt for AI Agents
In src/main/java/ai/elimu/dao/jpa/WordLearningEventDaoJpa.java at line 43, the
query orders results by event.id, which may disrupt chronological order. To fix
this, change the ORDER BY clause to use event.timestamp instead of event.id to
ensure learning events are returned in the correct chronological sequence.

.setParameter("androidId", androidId)
.getResultList();
}
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package ai.elimu.web.analytics;
package ai.elimu.web.analytics.students;

import ai.elimu.dao.StoryBookLearningEventDao;
import ai.elimu.dao.StudentDao;
import ai.elimu.entity.analytics.StoryBookLearningEvent;
import ai.elimu.entity.analytics.students.Student;
import ai.elimu.util.AnalyticsHelper;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
Expand All @@ -14,38 +16,45 @@
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/storybook-learning-event/list/storybook-learning-events.csv")
@RequestMapping("/analytics/students/{studentId}/storybook-learning-events.csv")
@RequiredArgsConstructor
@Slf4j
public class StoryBookLearningEventCsvExportController {
public class StoryBookLearningEventsCsvExportController {

private final StudentDao studentDao;

private final StoryBookLearningEventDao storyBookLearningEventDao;

@GetMapping
public void handleRequest(
@PathVariable Long studentId,
HttpServletResponse response,
OutputStream outputStream
) throws IOException {
log.info("handleRequest");

List<StoryBookLearningEvent> storyBookLearningEvents = storyBookLearningEventDao.readAll();
Student student = studentDao.read(studentId);
log.info("student.getAndroidId(): " + student.getAndroidId());

List<StoryBookLearningEvent> storyBookLearningEvents = storyBookLearningEventDao.readAll(student.getAndroidId());
log.info("storyBookLearningEvents.size(): " + storyBookLearningEvents.size());
Comment on lines +40 to 44
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 checking for student lookup.

The code doesn't handle the case where a student with the given ID doesn't exist, which could result in a NullPointerException.

Add null checking to handle non-existent students:

 Student student = studentDao.read(studentId);
+if (student == null) {
+    response.setStatus(HttpServletResponse.SC_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<StoryBookLearningEvent> storyBookLearningEvents = storyBookLearningEventDao.readAll(student.getAndroidId());
log.info("storyBookLearningEvents.size(): " + storyBookLearningEvents.size());
Student student = studentDao.read(studentId);
if (student == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
log.info("student.getAndroidId(): " + student.getAndroidId());
List<StoryBookLearningEvent> storyBookLearningEvents = storyBookLearningEventDao.readAll(student.getAndroidId());
log.info("storyBookLearningEvents.size(): " + storyBookLearningEvents.size());
🤖 Prompt for AI Agents
In
src/main/java/ai/elimu/web/analytics/students/StoryBookLearningEventsCsvExportController.java
around lines 40 to 44, add a null check after retrieving the student with
studentDao.read(studentId) to handle the case where the student is not found. If
the student is null, handle this scenario appropriately (e.g., log a warning and
return or throw an exception) before accessing student.getAndroidId() to prevent
a NullPointerException.

for (StoryBookLearningEvent storyBookLearningEvent : storyBookLearningEvents) {
storyBookLearningEvent.setAndroidId(AnalyticsHelper.redactAndroidId(storyBookLearningEvent.getAndroidId()));
}

CSVFormat csvFormat = CSVFormat.DEFAULT.builder()
.setHeader(
"id", // The Room database ID
"id",
"timestamp",
"android_id",
"package_name",
"storybook_id",
"storybook_title",
"learning_event_type"
"storybook_id",
"learning_event_type",
"additional_data"
)
.build();

Expand All @@ -58,14 +67,14 @@ public void handleRequest(
csvPrinter.printRecord(
storyBookLearningEvent.getId(),
storyBookLearningEvent.getTimestamp().getTimeInMillis(),
storyBookLearningEvent.getAndroidId(),
storyBookLearningEvent.getPackageName(),
(storyBookLearningEvent.getStoryBook() == null) ? null : storyBookLearningEvent.getStoryBook().getId(),
storyBookLearningEvent.getStoryBookTitle(),
storyBookLearningEvent.getLearningEventType()
storyBookLearningEvent.getStoryBookId(),
storyBookLearningEvent.getLearningEventType(),
storyBookLearningEvent.getAdditionalData()
);
csvPrinter.flush();
}
csvPrinter.flush();
csvPrinter.close();

String csvFileContent = stringWriter.toString();
Expand Down
Loading