diff options
author | Jesse Morgan <jesse@jesterpm.net> | 2013-09-09 22:39:44 -0700 |
---|---|---|
committer | Jesse Morgan <jesse@jesterpm.net> | 2013-09-09 22:39:44 -0700 |
commit | bc385cd7620df50110e57ac40bb5138f55d3b5a2 (patch) | |
tree | 1572e09a51e5d8db1bb136d8cb5df9f6b21acd22 | |
parent | 2872d474307595a96ab4373c1294d1b316ec0ae8 (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.
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; + } + } } |