Skip to content

Commit

Permalink
feat(analytics): add video learning events to dashboard (#1923)
Browse files Browse the repository at this point in the history
  • Loading branch information
nya-elimu authored Oct 30, 2024
2 parents 55112e6 + d514186 commit 00818e2
Show file tree
Hide file tree
Showing 10 changed files with 292 additions and 1 deletion.
10 changes: 10 additions & 0 deletions src/main/java/ai/elimu/util/AnalyticsHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,14 @@ public static Integer extractVersionCodeFromCsvFilename(String filename) {
Integer versionCode = Integer.valueOf(versionCodeAsString);
return versionCode;
}

/**
* E.g. "7161a85a0e4751cd" --> "7161***51cd"
*
* @param androidId The Android ID
* @return The redacted version of the Android ID
*/
public static String redactAndroidId(String androidId) {
return androidId.substring(0, 4) + "***" + androidId.substring(12);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;
import java.util.TimeZone;

import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
Expand Down Expand Up @@ -137,7 +139,7 @@ public static List<VideoLearningEvent> extractVideoLearningEvents(File csvFile)
VideoLearningEvent videoLearningEvent = new VideoLearningEvent();

long timestampInMillis = Long.valueOf(csvRecord.get("timestamp"));
Calendar timestamp = Calendar.getInstance();
Calendar timestamp = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
timestamp.setTimeInMillis(timestampInMillis);
videoLearningEvent.setTimestamp(timestamp);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import ai.elimu.dao.LetterLearningEventDao;
import ai.elimu.dao.StoryBookLearningEventDao;
import ai.elimu.dao.VideoLearningEventDao;
import ai.elimu.dao.WordLearningEventDao;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
Expand All @@ -26,13 +27,17 @@ public class MainAnalyticsController {
@Autowired
private StoryBookLearningEventDao storyBookLearningEventDao;

@Autowired
private VideoLearningEventDao videoLearningEventDao;

@RequestMapping(method = RequestMethod.GET)
public String handleRequest(Model model) {
logger.info("handleRequest");

model.addAttribute("letterLearningEventCount", letterLearningEventDao.readCount());
model.addAttribute("wordLearningEventCount", wordLearningEventDao.readCount());
model.addAttribute("storyBookLearningEventCount", storyBookLearningEventDao.readCount());
model.addAttribute("videoLearningEventCount", videoLearningEventDao.readCount());

return "analytics/main";
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package ai.elimu.web.analytics;

import ai.elimu.dao.VideoLearningEventDao;
import ai.elimu.model.analytics.VideoLearningEvent;
import ai.elimu.util.AnalyticsHelper;

import java.io.IOException;
import java.io.OutputStream;
import java.io.StringWriter;
import java.util.List;

import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.http.HttpServletResponse;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVPrinter;
import org.apache.logging.log4j.LogManager;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping("/analytics/video-learning-event/list")
public class VideoLearningEventCsvExportController {

private final Logger logger = LogManager.getLogger();

@Autowired
private VideoLearningEventDao videoLearningEventDao;

@RequestMapping(value="/video-learning-events.csv", method = RequestMethod.GET)
public void handleRequest(
HttpServletResponse response,
OutputStream outputStream
) throws IOException {
logger.info("handleRequest");

List<VideoLearningEvent> videoLearningEvents = videoLearningEventDao.readAll();
logger.info("videoLearningEvents.size(): " + videoLearningEvents.size());

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

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

for (VideoLearningEvent videoLearningEvent : videoLearningEvents) {
logger.info("videoLearningEvent.getId(): " + videoLearningEvent.getId());

videoLearningEvent.setAndroidId(AnalyticsHelper.redactAndroidId(videoLearningEvent.getAndroidId()));

csvPrinter.printRecord(
videoLearningEvent.getId(),
videoLearningEvent.getTimestamp().getTimeInMillis(),
videoLearningEvent.getAndroidId(),
videoLearningEvent.getPackageName(),
videoLearningEvent.getVideoId(),
videoLearningEvent.getVideoTitle(),
videoLearningEvent.getLearningEventType(),
videoLearningEvent.getAdditionalData()
);
csvPrinter.flush();
}
csvPrinter.close();

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) {
logger.error(ex);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package ai.elimu.web.analytics;

import ai.elimu.dao.VideoLearningEventDao;
import ai.elimu.model.analytics.VideoLearningEvent;
import ai.elimu.util.AnalyticsHelper;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping("/analytics/video-learning-event/list")
public class VideoLearningEventListController {

private final Logger logger = LogManager.getLogger();

@Autowired
private VideoLearningEventDao videoLearningEventDao;

@RequestMapping(method = RequestMethod.GET)
public String handleRequest(Model model) {
logger.info("handleRequest");

List<VideoLearningEvent> videoLearningEvents = videoLearningEventDao.readAll();
for (VideoLearningEvent videoLearningEvent : videoLearningEvents) {
videoLearningEvent.setAndroidId(AnalyticsHelper.redactAndroidId(videoLearningEvent.getAndroidId()));
}
model.addAttribute("videoLearningEvents", videoLearningEvents);

// Prepare chart data
List<String> monthList = new ArrayList<>();
List<Integer> eventCountList = new ArrayList<>();
if (!videoLearningEvents.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 (VideoLearningEvent event : videoLearningEvents) {
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());
monthList.add(monthAsString);

eventCountList.add(eventCountByMonthMap.getOrDefault(monthAsString, 0));

// Increase the date by 1 month
month.add(Calendar.MONTH, 1);
}
}
model.addAttribute("monthList", monthList);
model.addAttribute("eventCountList", eventCountList);

return "analytics/video-learning-event/list";
}
}
1 change: 1 addition & 0 deletions src/main/webapp/WEB-INF/jsp/analytics/layout.jsp
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
<li><a href="<spring:url value='/analytics/letter-learning-event/list' />"><i class="material-icons left">text_format</i><fmt:message key="letters" /></a></li>
<li><a href="<spring:url value='/analytics/word-learning-event/list' />"><i class="material-icons left">sms</i><fmt:message key="words" /></a></li>
<li><a href="<spring:url value='/analytics/storybook-learning-event/list' />"><i class="material-icons left">book</i><fmt:message key="storybooks" /></a></li>
<li><a href="<spring:url value='/analytics/video-learning-event/list' />"><i class="material-icons left">movie</i>Videos</a></li>
</ul>
<a id="navButton" href="<spring:url value='/analytics' />" data-activates="nav-mobile" class="waves-effect waves-light"><i class="material-icons">dehaze</i></a>
</div>
Expand Down
11 changes: 11 additions & 0 deletions src/main/webapp/WEB-INF/jsp/analytics/main.jsp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,17 @@
</div>
</div>
</div>

<div class="col s12 m6">
<div class="card">
<div class="card-content">
<span class="card-title"><i class="material-icons">movie</i> Videos</span>
</div>
<div class="card-action">
<a id="videoLearningEventsLink" href="<spring:url value='/analytics/video-learning-event/list' />">View list (${videoLearningEventCount})</a>
</div>
</div>
</div>

<div class="col s12">
<h5><fmt:message key="assessment.events" /></h5>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<content:title>
VideoLearningEvents (${fn:length(videoLearningEvents)})
</content:title>

<content:section cssId="videoLearningEventsPage">
<div class="section row">
<div class="card-panel">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"></script>
<canvas id="chart"></canvas>
<script>
const labels = [
<c:forEach var="month" items="${monthList}">'${month}',</c:forEach>
];
const data = {
labels: labels,
datasets: [{
data: <c:out value="${eventCountList}" />,
label: 'Learning events',
backgroundColor: 'rgba(149,117,205, 0.5)', // #9575cd deep-purple lighten-2
borderColor: 'rgba(149,117,205, 0.5)', // #9575cd deep-purple lighten-2
tension: 0.5,
fill: true
}]
};
const config = {
type: 'line',
data: data,
options: {}
};
var ctx = document.getElementById('chart');
new Chart(ctx, config);
</script>
</div>
</div>

<div class="section row">
<a id="exportToCsvButton" class="right btn waves-effect waves-light grey-text white"
href="<spring:url value='/analytics/video-learning-event/list/video-learning-events.csv' />">
<fmt:message key="export.to.csv" /><i class="material-icons right">vertical_align_bottom</i>
</a>
<script>
$(function() {
$('#exportToCsvButton').click(function() {
console.info('#exportToCsvButton click');
Materialize.toast('Preparing CSV file. Please wait...', 4000, 'rounded');
});
});
</script>

<table class="bordered highlight">
<thead>
<th><code>timestamp</code></th>
<th><code>android_id</code></th>
<th><code>package_name</code></th>
<th><code>video_title</code></th>
<th><code>learning_event_type</code></th>
</thead>
<tbody>
<c:forEach var="videoLearningEvent" items="${videoLearningEvents}">
<tr class="videoLearningEvent">
<td>
<fmt:formatDate value="${videoLearningEvent.timestamp.time}" pattern="yyyy-MM-dd HH:mm:ss" />
</td>
<td>
${videoLearningEvent.androidId}
</td>
<td>
${videoLearningEvent.packageName}
</td>
<td>
<c:choose>
<c:when test="${not empty videoLearningEvent.video}">
"<a href="<spring:url value='/content/video/edit/${videoLearningEvent.video.id}' />">${videoLearningEvent.video.title}</a>"
</c:when>
<c:otherwise>
"${videoLearningEvent.videoTitle}"
</c:otherwise>
</c:choose>
</td>
<td>
${videoLearningEvent.learningEventType}
</td>
</tr>
</c:forEach>
</tbody>
</table>
</div>
</content:section>
5 changes: 5 additions & 0 deletions src/main/webapp/static/css/styles.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
html {
font-family: monospace, sans-serif;
}
code {
background-color: rgba(103,58,183, 0.05); /* deep-purple */
border-radius: 4px;
padding: 4px;
}

body {
background: #EEE;
Expand Down
6 changes: 6 additions & 0 deletions src/test/java/ai/elimu/util/AnalyticsHelperTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,10 @@ public void testExtractVersionCodeFromCsvFilename() {
filename = "7161a85a0e4751cd_3001012_word-learning-events_2020-04-23.csv";
assertEquals(3001012, AnalyticsHelper.extractVersionCodeFromCsvFilename(filename));
}

@Test
public void testRedactAndroidId() {
String androidId = "745f90e7aae26423";
assertEquals("745f***6423", AnalyticsHelper.redactAndroidId(androidId));
}
}

0 comments on commit 00818e2

Please sign in to comment.