-
-
Notifications
You must be signed in to change notification settings - Fork 69
feat: add student entity #2186
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add student entity #2186
Changes from 4 commits
dc4efd6
d0d846f
701a411
8b761f3
a9c29c2
c117eba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package ai.elimu.dao; | ||
|
|
||
| import org.springframework.dao.DataAccessException; | ||
| import ai.elimu.entity.analytics.students.Student; | ||
|
|
||
| public interface StudentDao extends GenericDao<Student> { | ||
|
|
||
| Student read(String androidId) throws DataAccessException; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| package ai.elimu.dao.jpa; | ||
|
|
||
| import jakarta.persistence.NoResultException; | ||
| import ai.elimu.dao.StudentDao; | ||
| import ai.elimu.entity.analytics.students.Student; | ||
| import org.springframework.dao.DataAccessException; | ||
|
|
||
| public class StudentDaoJpa extends GenericDaoJpa<Student> implements StudentDao { | ||
|
|
||
| @Override | ||
| public Student read(String androidId) throws DataAccessException { | ||
| try { | ||
| return (Student) em.createQuery( | ||
| "SELECT s " + | ||
| "FROM Student s " + | ||
| "WHERE s.androidId = :androidId") | ||
| .setParameter("androidId", androidId) | ||
| .getSingleResult(); | ||
| } catch (NoResultException e) { | ||
| return null; | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,7 @@ | |
| @Entity | ||
| @Getter | ||
| @Setter | ||
| @Deprecated | ||
| public class Device extends BaseEntity { | ||
|
|
||
| @NotNull | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package ai.elimu.entity.analytics.students; | ||
|
|
||
| import ai.elimu.entity.BaseEntity; | ||
| import jakarta.persistence.Column; | ||
| import jakarta.persistence.Entity; | ||
| import jakarta.validation.constraints.NotNull; | ||
| import lombok.Getter; | ||
| import lombok.Setter; | ||
|
|
||
| @Entity | ||
| @Getter | ||
| @Setter | ||
| public class Student extends BaseEntity { | ||
|
|
||
| /** | ||
| * 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 | ||
| @Column(unique = true) | ||
| private String androidId; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| package ai.elimu.tasks.analytics; | ||
|
|
||
| import ai.elimu.dao.StudentDao; | ||
| import ai.elimu.dao.StoryBookLearningEventDao; | ||
| import ai.elimu.entity.analytics.StoryBookLearningEvent; | ||
| import ai.elimu.entity.analytics.students.Student; | ||
| import ai.elimu.model.v2.enums.Language; | ||
| import ai.elimu.rest.v2.analytics.StoryBookLearningEventsRestController; | ||
| import ai.elimu.util.ConfigHelper; | ||
| import ai.elimu.util.csv.CsvAnalyticsExtractionHelper; | ||
| import java.io.File; | ||
| import java.util.List; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.scheduling.annotation.Scheduled; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| /** | ||
| * Extracts learning events from CSV files previously received by the {@link StoryBookLearningEventsRestController}, and imports them into the database. | ||
| * <p/> | ||
| * <p> | ||
| * Expected folder structure: | ||
| * <pre> | ||
| * ├── lang-ENG | ||
| * │ ├── analytics | ||
| * │ │ ├── android-id-e387e38700000001 | ||
| * │ │ │ └── version-code-3001018 | ||
| * │ │ │ └── storybook-learning-events | ||
| * │ │ │ ├── e387e38700000001_3001018_storybook-learning-events_2024-10-09.csv | ||
| * │ │ │ ├── e387e38700000001_3001018_storybook-learning-events_2024-10-10.csv | ||
| * │ │ │ ├── e387e38700000001_3001018_storybook-learning-events_2024-10-11.csv | ||
| * │ │ │ ├── e387e38700000001_3001018_storybook-learning-events_2024-10-14.csv | ||
| * │ │ │ ├── e387e38700000001_3001018_storybook-learning-events_2024-10-18.csv | ||
| * │ │ │ └── e387e38700000001_3001018_storybook-learning-events_2024-10-20.csv | ||
| * │ │ ├── android-id-e387e38700000002 | ||
| * │ │ │ └── version-code-3001018 | ||
| * │ │ │ └── storybook-learning-events | ||
| * │ │ │ ├── e387e38700000002_3001018_storybook-learning-events_2024-10-09.csv | ||
| * │ │ │ ├── e387e38700000002_3001018_storybook-learning-events_2024-10-10.csv | ||
| * │ │ │ ├── e387e38700000002_3001018_storybook-learning-events_2024-10-11.csv | ||
| * </pre> | ||
| */ | ||
| @Service | ||
| @RequiredArgsConstructor | ||
| @Slf4j | ||
| public class StoryBookLearningEventImportScheduler { | ||
|
|
||
| private final StoryBookLearningEventDao storyBookLearningEventDao; | ||
|
|
||
| private final StudentDao studentDao; | ||
|
|
||
| @Scheduled(cron = "00 35 * * * *") // 35 minutes past every hour | ||
| public synchronized void execute() { | ||
| log.info("execute"); | ||
|
|
||
| // Lookup CSV files stored 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"); | ||
| log.info("analyticsDir: " + analyticsDir); | ||
| analyticsDir.mkdirs(); | ||
| for (File analyticsDirFile : analyticsDir.listFiles()) { | ||
| if (analyticsDirFile.getName().startsWith("android-id-")) { | ||
| File androidIdDir = new File(analyticsDir, analyticsDirFile.getName()); | ||
| for (File androidIdDirFile : androidIdDir.listFiles()) { | ||
| if (androidIdDirFile.getName().startsWith("version-code-")) { | ||
| File versionCodeDir = new File(androidIdDir, androidIdDirFile.getName()); | ||
| for (File versionCodeDirFile : versionCodeDir.listFiles()) { | ||
| if (versionCodeDirFile.getName().equals("storybook-learning-events")) { | ||
| File storyBookLearningEventsDir = new File(versionCodeDir, versionCodeDirFile.getName()); | ||
| for (File csvFile : storyBookLearningEventsDir.listFiles()) { | ||
| log.info("csvFile: " + csvFile); | ||
|
|
||
| // Convert from CSV to Java | ||
| List<StoryBookLearningEvent> events = CsvAnalyticsExtractionHelper.extractStoryBookLearningEvents(csvFile); | ||
| log.info("events.size(): " + events.size()); | ||
|
|
||
| // Store in database | ||
| for (StoryBookLearningEvent event : events) { | ||
| // Check if the event has already been stored in the database | ||
| StoryBookLearningEvent existingStoryBookLearningEvent = storyBookLearningEventDao.read(event.getTimestamp(), event.getAndroidId(), event.getApplication(), event.getStoryBook()); | ||
| if (existingStoryBookLearningEvent != null) { | ||
| log.warn("The event has already been stored in the database. Skipping data import."); | ||
| continue; | ||
| } | ||
|
|
||
| // Generate Student ID | ||
| Student existingStudent = studentDao.read(event.getAndroidId()); | ||
| if (existingStudent == null) { | ||
| Student student = new Student(); | ||
| student.setAndroidId(event.getAndroidId()); | ||
| studentDao.create(student); | ||
| log.info("Stored Student in database with ID " + student.getId()); | ||
| } | ||
|
|
||
| // Store the event in the database | ||
| storyBookLearningEventDao.create(event); | ||
| log.info("Stored event in database with ID " + event.getId()); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| log.info("execute complete"); | ||
| } | ||
| } | ||
|
Comment on lines
+1
to
+110
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Significant code duplication across scheduler classes. This class is almost identical to Consider creating an abstract base class that handles the common functionality, with concrete implementations for each event type. This would eliminate most of the duplication and improve maintainability. public abstract class LearningEventImportScheduler<T> {
protected abstract String getEventDirectoryName();
protected abstract List<T> extractEventsFromCsv(File csvFile);
protected abstract T findExistingEvent(T event);
protected abstract void createEvent(T event);
protected final StudentDao studentDao;
// Common execute method with template pattern
public synchronized void execute() {
// Common directory traversal code
// Call abstract methods for type-specific behavior
}
// Common method to create Student if needed
protected void ensureStudentExists(String androidId) {
// Implementation
}
}
// Concrete implementation example
@Service
@RequiredArgsConstructor
@Slf4j
public class StoryBookLearningEventImportScheduler extends LearningEventImportScheduler<StoryBookLearningEvent> {
private final StoryBookLearningEventDao storyBookLearningEventDao;
@Override
protected String getEventDirectoryName() {
return "storybook-learning-events";
}
@Override
protected List<StoryBookLearningEvent> extractEventsFromCsv(File csvFile) {
return CsvAnalyticsExtractionHelper.extractStoryBookLearningEvents(csvFile);
}
// Other overridden methods
@Scheduled(cron = "00 35 * * * *")
@Override
public synchronized void execute() {
super.execute();
}
} |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add null checks for listFiles() results.
The code calls
listFiles()multiple times without checking if the result is null, which could lead to NullPointerExceptions if a directory is empty or inaccessible.Similar changes should be made for all other
listFiles()calls in this file.