summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJesse Morgan <jesse@jesterpm.net>2013-09-09 22:39:44 -0700
committerJesse Morgan <jesse@jesterpm.net>2013-09-09 22:39:44 -0700
commitbc385cd7620df50110e57ac40bb5138f55d3b5a2 (patch)
tree1572e09a51e5d8db1bb136d8cb5df9f6b21acd22
parent2872d474307595a96ab4373c1294d1b316ec0ae8 (diff)
Adding Playlist Support.
The TrainingRecordResource now builds a playlist of videos for each user. The playlist is a map of video id to meta-data (completion date, completed, required). Some videos are required, others are not. TrainingPageResource now redirects to the earliest uncompleted chapter, based on information in the playlist. SurveyResultsResource now caches computed scores.
-rw-r--r--src/com/p4square/grow/backend/resources/Playlist.java134
-rw-r--r--src/com/p4square/grow/backend/resources/Score.java15
-rw-r--r--src/com/p4square/grow/backend/resources/SurveyResultsResource.java20
-rw-r--r--src/com/p4square/grow/backend/resources/TrainingRecordResource.java95
-rw-r--r--src/com/p4square/grow/backend/resources/VideoRecord.java56
-rw-r--r--src/com/p4square/grow/frontend/TrainingPageResource.java54
6 files changed, 344 insertions, 30 deletions
diff --git a/src/com/p4square/grow/backend/resources/Playlist.java b/src/com/p4square/grow/backend/resources/Playlist.java
new file mode 100644
index 0000000..f3d2f08
--- /dev/null
+++ b/src/com/p4square/grow/backend/resources/Playlist.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.backend.resources;
+
+import java.io.IOException;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.codehaus.jackson.map.ObjectMapper;
+import org.codehaus.jackson.type.TypeReference;
+
+import com.p4square.grow.backend.db.CassandraDatabase;
+
+/**
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+class Playlist {
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ /**
+ * Load a Playlist from the database.
+ */
+ public static Playlist load(CassandraDatabase db, String userId) throws IOException {
+ String playlistString = db.getKey("training", userId, "playlist");
+
+ if (playlistString == null) {
+ return null;
+ }
+
+ Map<String, Map<String, VideoRecord>> playlist =
+ MAPPER.readValue(playlistString, new TypeReference<Map<String, Map<String, VideoRecord>>>() { });
+
+ return new Playlist(playlist);
+
+ }
+
+ /**
+ * Persist the Playlist for the given user.
+ * @return The String serialization of the playlist.
+ */
+ public static String save(CassandraDatabase db, String userId, Playlist playlist) throws IOException {
+ String playlistString = MAPPER.writeValueAsString(playlist.mPlaylist);
+ db.putKey("training", userId, "playlist", playlistString);
+ return playlistString;
+ }
+
+
+ private Map<String, Map<String, VideoRecord>> mPlaylist;
+
+ /**
+ * Construct an empty playlist.
+ */
+ public Playlist() {
+ mPlaylist = new HashMap<String, Map<String, VideoRecord>>();
+ }
+
+ /**
+ * Constructor for database initialization.
+ */
+ private Playlist(Map<String, Map<String, VideoRecord>> playlist) {
+ mPlaylist = playlist;
+ }
+
+ public VideoRecord find(String videoId) {
+ for (Map<String, VideoRecord> chapter : mPlaylist.values()) {
+ VideoRecord r = chapter.get(videoId);
+
+ if (r != null) {
+ return r;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Add a video to the playlist.
+ */
+ public VideoRecord add(String chapter, String videoId) {
+ Map<String, VideoRecord> chapterMap = mPlaylist.get(chapter);
+
+ if (chapterMap == null) {
+ chapterMap = new HashMap<String, VideoRecord>();
+ mPlaylist.put(chapter, chapterMap);
+ }
+
+ VideoRecord r = new VideoRecord();
+ chapterMap.put(videoId, r);
+ return r;
+ }
+
+ /**
+ * @return The last chapter to be completed.
+ */
+ public Map<String, Boolean> getChapterStatuses() {
+ Map<String, Boolean> completed = new HashMap<String, Boolean>();
+
+ for (String chapter : mPlaylist.keySet()) {
+ completed.put(chapter, isChapterComplete(chapter));
+ }
+
+ return completed;
+ }
+
+ public boolean isChapterComplete(String chapterId) {
+ boolean complete = true;
+
+ Map<String, VideoRecord> chapter = mPlaylist.get(chapterId);
+ if (chapter != null) {
+ for (VideoRecord r : chapter.values()) {
+ if (r.getRequired() && !r.getComplete()) {
+ return false;
+ }
+ }
+ }
+
+ return complete;
+ }
+
+ @Override
+ public String toString() {
+ try {
+ return MAPPER.writeValueAsString(mPlaylist);
+
+ } catch (IOException e) {
+ return super.toString();
+ }
+ }
+
+}
diff --git a/src/com/p4square/grow/backend/resources/Score.java b/src/com/p4square/grow/backend/resources/Score.java
index c7e5ecc..6f52c02 100644
--- a/src/com/p4square/grow/backend/resources/Score.java
+++ b/src/com/p4square/grow/backend/resources/Score.java
@@ -10,6 +10,21 @@ package com.p4square.grow.backend.resources;
* @author Jesse Morgan <jesse@jesterpm.net>
*/
class Score {
+ /**
+ * Return the integer value for the given Score String.
+ */
+ public static int numericScore(String score) {
+ if ("teacher".equals(score)) {
+ return 4;
+ } else if ("disciple".equals(score)) {
+ return 3;
+ } else if ("believer".equals(score)) {
+ return 2;
+ } else {
+ return 1;
+ }
+ }
+
double sum;
int count;
diff --git a/src/com/p4square/grow/backend/resources/SurveyResultsResource.java b/src/com/p4square/grow/backend/resources/SurveyResultsResource.java
index 5e4a8bb..e93e253 100644
--- a/src/com/p4square/grow/backend/resources/SurveyResultsResource.java
+++ b/src/com/p4square/grow/backend/resources/SurveyResultsResource.java
@@ -31,7 +31,7 @@ import com.p4square.grow.backend.db.CassandraDatabase;
public class SurveyResultsResource extends ServerResource {
private final static Logger cLog = Logger.getLogger(SurveyResultsResource.class);
- private final static ObjectMapper cMapper = new ObjectMapper();
+ private final static ObjectMapper MAPPER = new ObjectMapper();
static enum RequestType {
ASSESSMENT, ANSWER
@@ -72,7 +72,10 @@ public class SurveyResultsResource extends ServerResource {
break;
case ASSESSMENT:
- result = buildAssessment();
+ result = mDb.getKey("assessments", mUserId, "summary");
+ if (result == null) {
+ result = buildAssessment();
+ }
break;
}
@@ -135,7 +138,7 @@ public class SurveyResultsResource extends ServerResource {
if (!row.isEmpty()) {
Score score = new Score();
for (Column<String> c : row) {
- if (c.getName().equals("lastAnswered")) {
+ if (c.getName().equals("lastAnswered") || c.getName().equals("summary")) {
continue;
}
@@ -153,7 +156,12 @@ public class SurveyResultsResource extends ServerResource {
}
sb.append(" }");
- return sb.toString();
+ String summary = sb.toString();
+
+ // Persist summary
+ mDb.putKey("assessments", mUserId, "summary", summary);
+
+ return summary;
}
private boolean scoreQuestion(final Score score, final String questionId,
@@ -162,8 +170,8 @@ public class SurveyResultsResource extends ServerResource {
final String data = mDb.getKey("strings", "/questions/" + questionId);
try {
- final Map<?,?> questionMap = cMapper.readValue(data, Map.class);
- final Map<?,?> answerMap = cMapper.readValue(answerJson, Map.class);
+ final Map<?,?> questionMap = MAPPER.readValue(data, Map.class);
+ final Map<?,?> answerMap = MAPPER.readValue(answerJson, Map.class);
final Question question = new Question((Map<String, Object>) questionMap);
final String answerId = (String) answerMap.get("answerId");
diff --git a/src/com/p4square/grow/backend/resources/TrainingRecordResource.java b/src/com/p4square/grow/backend/resources/TrainingRecordResource.java
index 93f4fbc..8447c16 100644
--- a/src/com/p4square/grow/backend/resources/TrainingRecordResource.java
+++ b/src/com/p4square/grow/backend/resources/TrainingRecordResource.java
@@ -4,12 +4,18 @@
package com.p4square.grow.backend.resources;
+import java.io.IOException;
+
+import java.util.LinkedList;
+import java.util.List;
import java.util.Map;
import java.util.HashMap;
import com.netflix.astyanax.model.Column;
import com.netflix.astyanax.model.ColumnList;
+import org.codehaus.jackson.map.ObjectMapper;
+
import org.restlet.data.MediaType;
import org.restlet.data.Status;
import org.restlet.resource.ServerResource;
@@ -22,16 +28,19 @@ import com.p4square.grow.backend.GrowBackend;
import com.p4square.grow.backend.db.CassandraDatabase;
/**
- *
+ *
* @author Jesse Morgan <jesse@jesterpm.net>
*/
public class TrainingRecordResource extends ServerResource {
- private final static Logger cLog = Logger.getLogger(TrainingRecordResource.class);
+ private static final String[] CHAPTERS = { "seeker", "believer", "disciple", "teacher" };
+
+ private static final Logger LOG = Logger.getLogger(TrainingRecordResource.class);
+ private static final ObjectMapper MAPPER = new ObjectMapper();
static enum RequestType {
SUMMARY, VIDEO
}
-
+
private GrowBackend mBackend;
private CassandraDatabase mDb;
@@ -76,7 +85,7 @@ public class TrainingRecordResource extends ServerResource {
setStatus(Status.CLIENT_ERROR_NOT_FOUND);
return null;
}
-
+
return new StringRepresentation(result);
}
@@ -92,10 +101,20 @@ public class TrainingRecordResource extends ServerResource {
try {
mDb.putKey("training", mUserId, mVideoId, entity.getText());
mDb.putKey("training", mUserId, "lastVideo", mVideoId);
+
+ Playlist playlist = Playlist.load(mDb, mUserId);
+ if (playlist != null) {
+ VideoRecord r = playlist.find(mVideoId);
+ if (r != null && !r.getComplete()) {
+ r.complete();
+ Playlist.save(mDb, mUserId, playlist);
+ }
+ }
+
success = true;
} catch (Exception e) {
- cLog.warn("Caught exception updating training record: " + e.getMessage(), e);
+ LOG.warn("Caught exception updating training record: " + e.getMessage(), e);
}
break;
@@ -119,19 +138,20 @@ public class TrainingRecordResource extends ServerResource {
private String buildSummary() {
StringBuilder sb = new StringBuilder("{ ");
- // Last question answered
+ // Last watch video
final String lastVideo = mDb.getKey("training", mUserId, "lastVideo");
if (lastVideo != null) {
sb.append("\"lastVideo\": \"" + lastVideo + "\", ");
}
- // List of videos watched
+ // Get the user's video history
sb.append("\"videos\": { ");
ColumnList<String> row = mDb.getRow("training", mUserId);
if (!row.isEmpty()) {
boolean first = true;
for (Column<String> c : row) {
- if ("lastVideo".equals(c.getName())) {
+ if ("lastVideo".equals(c.getName()) ||
+ "playlist".equals(c.getName())) {
continue;
}
@@ -142,14 +162,69 @@ public class TrainingRecordResource extends ServerResource {
sb.append(", \"" + c.getName() + "\": ");
}
- sb.append(c.getStringValue());
+ sb.append(c.getStringValue());
}
}
sb.append(" }");
+ // Get the user's playlist
+ try {
+ Playlist playlist = Playlist.load(mDb, mUserId);
+ if (playlist == null) {
+ playlist = createInitialPlaylist();
+ }
+
+ sb.append(", \"playlist\": ");
+ sb.append(playlist.toString());
+
+ // Last Completed Section
+ Map<String, Boolean> chapters = playlist.getChapterStatuses();
+ String chaptersString = MAPPER.writeValueAsString(chapters);
+ sb.append(", \"chapters\":");
+ sb.append(chaptersString);
+
+
+ } catch (IOException e) {
+ LOG.warn("IOException loading playlist for user " + mUserId, e);
+ }
+
sb.append(" }");
return sb.toString();
}
-
+
+ /**
+ * Create the user's initial playlist.
+ *
+ * @return Returns the String representation of the initial playlist.
+ */
+ private Playlist createInitialPlaylist() throws IOException {
+ Playlist playlist = new Playlist();
+
+ // Get assessment score
+ String summaryString = mDb.getKey("assessments", mUserId, "summary");
+ if (summaryString == null) {
+ return null;
+ }
+ Map<?,?> summary = MAPPER.readValue(summaryString, Map.class);
+ double score = (Double) summary.get("score");
+
+ // Get videos for each section and build playlist
+ for (String chapter : CHAPTERS) {
+ // Chapter required if the floor of the score is <= the chapter's numeric value.
+ boolean required = score < Score.numericScore(chapter) + 1;
+
+ ColumnList<String> row = mDb.getRow("strings", "/training/" + chapter);
+ if (!row.isEmpty()) {
+ for (Column<String> c : row) {
+ VideoRecord r = playlist.add(chapter, c.getName());
+ r.setRequired(required);
+ }
+ }
+ }
+
+ Playlist.save(mDb, mUserId, playlist);
+
+ return playlist;
+ }
}
diff --git a/src/com/p4square/grow/backend/resources/VideoRecord.java b/src/com/p4square/grow/backend/resources/VideoRecord.java
new file mode 100644
index 0000000..2ba28c3
--- /dev/null
+++ b/src/com/p4square/grow/backend/resources/VideoRecord.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.backend.resources;
+
+import java.util.Date;
+
+/**
+ * Simple bean containing video completion data.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+class VideoRecord {
+ private boolean mComplete;
+ private boolean mRequired;
+ private Date mCompletionDate;
+
+ public VideoRecord() {
+ mComplete = false;
+ mRequired = true;
+ mCompletionDate = null;
+ }
+
+ public boolean getComplete() {
+ return mComplete;
+ }
+
+ public void setComplete(boolean complete) {
+ mComplete = complete;
+ }
+
+ public boolean getRequired() {
+ return mRequired;
+ }
+
+ public void setRequired(boolean complete) {
+ mRequired = complete;
+ }
+
+ public Date getCompletionDate() {
+ return mCompletionDate;
+ }
+
+ public void setCompletionDate(Date date) {
+ mCompletionDate = date;
+ }
+
+ /**
+ * Convenience method to mark a video complete.
+ */
+ public void complete() {
+ mComplete = true;
+ mCompletionDate = new Date();
+ }
+}
diff --git a/src/com/p4square/grow/frontend/TrainingPageResource.java b/src/com/p4square/grow/frontend/TrainingPageResource.java
index 6615295..c6c86c3 100644
--- a/src/com/p4square/grow/frontend/TrainingPageResource.java
+++ b/src/com/p4square/grow/frontend/TrainingPageResource.java
@@ -16,6 +16,7 @@ import org.restlet.data.MediaType;
import org.restlet.data.Status;
import org.restlet.ext.freemarker.TemplateRepresentation;
import org.restlet.representation.Representation;
+import org.restlet.representation.StringRepresentation;
import org.restlet.resource.ServerResource;
import org.apache.log4j.Logger;
@@ -70,10 +71,34 @@ public class TrainingPageResource extends FreeMarkerPageResource {
@Override
protected Representation get() {
try {
- // Get the current chapter.
+ // Get the training summary
+ Map<String, Object> trainingRecord = null;
+ Map<String, Object> completedVideos = new HashMap<String, Object>();
+ Map<String, Boolean> chapters = null;
+ {
+ JsonResponse response = backendGet("/accounts/" + mUserId + "/training");
+ if (response.getStatus().isSuccess()) {
+ trainingRecord = response.getMap();
+ completedVideos = (Map<String, Object>) trainingRecord.get("videos");
+ chapters = (Map<String, Boolean>) trainingRecord.get("chapters");
+ }
+ }
+
+ // Get the current chapter (the lowest, incomplete chapter)
if (mChapter == null) {
- // TODO: Get user's current question
- mChapter = "seeker";
+ int min = Integer.MAX_VALUE;
+ for (Map.Entry<String, Boolean> chapter : chapters.entrySet()) {
+ int index = chapterIndex(chapter.getKey());
+ if (!chapter.getValue() && index < min) {
+ min = index;
+ mChapter = chapter.getKey();
+ }
+ }
+
+ String nextPage = mConfig.getString("dynamicRoot", "");
+ nextPage += "/account/training/" + mChapter;
+ getResponse().redirectSeeOther(nextPage);
+ return new StringRepresentation("Redirecting to " + nextPage);
}
// Get videos for the chapter.
@@ -87,17 +112,6 @@ public class TrainingPageResource extends FreeMarkerPageResource {
videos = (List<Map<String, Object>>) response.getMap().get("videos");
}
- // Get list of completed videos
- Map<String, Object> trainingRecord = null;
- Map<String, Object> completedVideos = new HashMap<String, Object>();
- {
- JsonResponse response = backendGet("/accounts/" + mUserId + "/training");
- if (response.getStatus().isSuccess()) {
- trainingRecord = response.getMap();
- completedVideos = (Map<String, Object>) trainingRecord.get("videos");
- }
- }
-
// Mark the completed videos as completed
int chapterProgress = 0;
for (Map<String, Object> video : videos) {
@@ -158,4 +172,16 @@ public class TrainingPageResource extends FreeMarkerPageResource {
return response;
}
+
+ int chapterIndex(String chapter) {
+ if ("teacher".equals(chapter)) {
+ return 4;
+ } else if ("disciple".equals(chapter)) {
+ return 3;
+ } else if ("believer".equals(chapter)) {
+ return 2;
+ } else {
+ return 1;
+ }
+ }
}