summaryrefslogtreecommitdiff
path: root/src/com/p4square/grow
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/p4square/grow')
-rw-r--r--src/com/p4square/grow/backend/GrowBackend.java30
-rw-r--r--src/com/p4square/grow/backend/db/CassandraKey.java8
-rw-r--r--src/com/p4square/grow/backend/db/CassandraProviderImpl.java12
-rw-r--r--src/com/p4square/grow/backend/db/CassandraTrainingRecordProvider.java71
-rw-r--r--src/com/p4square/grow/backend/resources/Playlist.java134
-rw-r--r--src/com/p4square/grow/backend/resources/SurveyResource.java2
-rw-r--r--src/com/p4square/grow/backend/resources/SurveyResultsResource.java2
-rw-r--r--src/com/p4square/grow/backend/resources/TrainingRecordResource.java202
-rw-r--r--src/com/p4square/grow/frontend/ChapterCompletePage.java24
-rw-r--r--src/com/p4square/grow/frontend/TrainingPageResource.java53
-rw-r--r--src/com/p4square/grow/frontend/VideosResource.java2
-rw-r--r--src/com/p4square/grow/model/Chapter.java79
-rw-r--r--src/com/p4square/grow/model/Playlist.java171
-rw-r--r--src/com/p4square/grow/model/Question.java6
-rw-r--r--src/com/p4square/grow/model/TrainingRecord.java49
-rw-r--r--src/com/p4square/grow/model/VideoRecord.java (renamed from src/com/p4square/grow/backend/resources/VideoRecord.java)41
-rw-r--r--src/com/p4square/grow/provider/JsonEncodedProvider.java12
-rw-r--r--src/com/p4square/grow/provider/ProvidesQuestions.java19
-rw-r--r--src/com/p4square/grow/provider/ProvidesTrainingRecords.java19
-rw-r--r--src/com/p4square/grow/provider/TrainingRecordProvider.java41
20 files changed, 648 insertions, 329 deletions
diff --git a/src/com/p4square/grow/backend/GrowBackend.java b/src/com/p4square/grow/backend/GrowBackend.java
index 45e0fa2..195554e 100644
--- a/src/com/p4square/grow/backend/GrowBackend.java
+++ b/src/com/p4square/grow/backend/GrowBackend.java
@@ -4,6 +4,8 @@
package com.p4square.grow.backend;
+import java.io.IOException;
+
import org.apache.log4j.Logger;
import org.restlet.Application;
@@ -17,10 +19,15 @@ import com.p4square.grow.config.Config;
import com.p4square.grow.backend.db.CassandraDatabase;
import com.p4square.grow.backend.db.CassandraKey;
import com.p4square.grow.backend.db.CassandraProviderImpl;
+import com.p4square.grow.backend.db.CassandraTrainingRecordProvider;
import com.p4square.grow.model.Question;
+import com.p4square.grow.model.TrainingRecord;
+import com.p4square.grow.model.Playlist;
import com.p4square.grow.provider.Provider;
+import com.p4square.grow.provider.ProvidesQuestions;
+import com.p4square.grow.provider.ProvidesTrainingRecords;
import com.p4square.grow.provider.QuestionProvider;
import com.p4square.grow.backend.resources.AccountResource;
@@ -35,7 +42,8 @@ import com.p4square.grow.backend.resources.TrainingResource;
*
* @author Jesse Morgan <jesse@jesterpm.net>
*/
-public class GrowBackend extends Application {
+public class GrowBackend extends Application
+ implements ProvidesQuestions, ProvidesTrainingRecords {
private static final String DEFAULT_COLUMN = "value";
private final static Logger LOG = Logger.getLogger(GrowBackend.class);
@@ -44,6 +52,7 @@ public class GrowBackend extends Application {
private final CassandraDatabase mDatabase;
private final Provider<String, Question> mQuestionProvider;
+ private final CassandraTrainingRecordProvider mTrainingRecordProvider;
public GrowBackend() {
this(new Config());
@@ -53,12 +62,14 @@ public class GrowBackend extends Application {
mConfig = config;
mDatabase = new CassandraDatabase();
- mQuestionProvider = new QuestionProvider<CassandraKey>(new CassandraProviderImpl<Question>(mDatabase, "strings", Question.class)) {
+ mQuestionProvider = new QuestionProvider<CassandraKey>(new CassandraProviderImpl<Question>(mDatabase, Question.class)) {
@Override
public CassandraKey makeKey(String questionId) {
- return new CassandraKey("/questions/" + questionId, DEFAULT_COLUMN);
+ return new CassandraKey("strings", "/questions/" + questionId, DEFAULT_COLUMN);
}
};
+
+ mTrainingRecordProvider = new CassandraTrainingRecordProvider(mDatabase);
}
@Override
@@ -120,10 +131,23 @@ public class GrowBackend extends Application {
return mDatabase;
}
+ @Override
public Provider<String, Question> getQuestionProvider() {
return mQuestionProvider;
}
+ @Override
+ public Provider<String, TrainingRecord> getTrainingRecordProvider() {
+ return mTrainingRecordProvider;
+ }
+
+ /**
+ * @return the Default Playlist.
+ */
+ public Playlist getDefaultPlaylist() throws IOException {
+ return mTrainingRecordProvider.getDefaultPlaylist();
+ }
+
/**
* Stand-alone main for testing.
*/
diff --git a/src/com/p4square/grow/backend/db/CassandraKey.java b/src/com/p4square/grow/backend/db/CassandraKey.java
index 8e23087..853fe96 100644
--- a/src/com/p4square/grow/backend/db/CassandraKey.java
+++ b/src/com/p4square/grow/backend/db/CassandraKey.java
@@ -10,14 +10,20 @@ package com.p4square.grow.backend.db;
* @author Jesse Morgan <jesse@jesterpm.net>
*/
public class CassandraKey {
+ private final String mColumnFamily;
private final String mId;
private final String mColumn;
- public CassandraKey(String id, String column) {
+ public CassandraKey(String columnFamily, String id, String column) {
+ mColumnFamily = columnFamily;
mId = id;
mColumn = column;
}
+ public String getColumnFamily() {
+ return mColumnFamily;
+ }
+
public String getId() {
return mId;
}
diff --git a/src/com/p4square/grow/backend/db/CassandraProviderImpl.java b/src/com/p4square/grow/backend/db/CassandraProviderImpl.java
index fb6e34e..c1f6e6d 100644
--- a/src/com/p4square/grow/backend/db/CassandraProviderImpl.java
+++ b/src/com/p4square/grow/backend/db/CassandraProviderImpl.java
@@ -6,10 +6,6 @@ package com.p4square.grow.backend.db;
import java.io.IOException;
-import org.codehaus.jackson.map.DeserializationConfig;
-import org.codehaus.jackson.map.ObjectMapper;
-import org.codehaus.jackson.map.SerializationConfig;
-
import com.p4square.grow.provider.JsonEncodedProvider;
/**
@@ -19,24 +15,22 @@ import com.p4square.grow.provider.JsonEncodedProvider;
*/
public class CassandraProviderImpl<V> extends JsonEncodedProvider<CassandraKey, V> {
private final CassandraDatabase mDb;
- private final String mColumnFamily;
- public CassandraProviderImpl(CassandraDatabase db, String columnFamily, Class<V> clazz) {
+ public CassandraProviderImpl(CassandraDatabase db, Class<V> clazz) {
super(clazz);
mDb = db;
- mColumnFamily = columnFamily;
}
@Override
public V get(CassandraKey key) throws IOException {
- String blob = mDb.getKey(mColumnFamily, key.getId(), key.getColumn());
+ String blob = mDb.getKey(key.getColumnFamily(), key.getId(), key.getColumn());
return decode(blob);
}
@Override
public void put(CassandraKey key, V obj) throws IOException {
String blob = encode(obj);
- mDb.putKey(mColumnFamily, key.getId(), key.getColumn(), blob);
+ mDb.putKey(key.getColumnFamily(), key.getId(), key.getColumn(), blob);
}
}
diff --git a/src/com/p4square/grow/backend/db/CassandraTrainingRecordProvider.java b/src/com/p4square/grow/backend/db/CassandraTrainingRecordProvider.java
new file mode 100644
index 0000000..4face52
--- /dev/null
+++ b/src/com/p4square/grow/backend/db/CassandraTrainingRecordProvider.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.backend.db;
+
+import java.io.IOException;
+
+import com.p4square.grow.model.Playlist;
+import com.p4square.grow.model.TrainingRecord;
+
+import com.p4square.grow.provider.JsonEncodedProvider;
+import com.p4square.grow.provider.Provider;
+
+/**
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class CassandraTrainingRecordProvider implements Provider<String, TrainingRecord> {
+ private static final CassandraKey DEFAULT_PLAYLIST_KEY = new CassandraKey("strings", "defaultPlaylist", "value");
+
+ private static final String COLUMN_FAMILY = "training";
+ private static final String PLAYLIST_KEY = "playlist";
+ private static final String LAST_VIDEO_KEY = "lastVideo";
+
+ private final CassandraDatabase mDb;
+ private final Provider<CassandraKey, Playlist> mPlaylistProvider;
+
+ public CassandraTrainingRecordProvider(CassandraDatabase db) {
+ mDb = db;
+ mPlaylistProvider = new CassandraProviderImpl<>(db, Playlist.class);
+ }
+
+ @Override
+ public TrainingRecord get(String userid) throws IOException {
+ Playlist playlist = mPlaylistProvider.get(new CassandraKey(COLUMN_FAMILY, userid, PLAYLIST_KEY));
+
+ if (playlist == null) {
+ // We consider no playlist to mean no record whatsoever.
+ return null;
+ }
+
+ TrainingRecord r = new TrainingRecord();
+ r.setPlaylist(playlist);
+ r.setLastVideo(mDb.getKey(COLUMN_FAMILY, userid, LAST_VIDEO_KEY));
+
+ return r;
+ }
+
+ @Override
+ public void put(String userid, TrainingRecord record) throws IOException {
+ String lastVideo = record.getLastVideo();
+ Playlist playlist = record.getPlaylist();
+
+ mDb.putKey(COLUMN_FAMILY, userid, LAST_VIDEO_KEY, lastVideo);
+ mPlaylistProvider.put(new CassandraKey(COLUMN_FAMILY, userid, PLAYLIST_KEY), playlist);
+ }
+
+ /**
+ * @return the default playlist stored in the database.
+ */
+ public Playlist getDefaultPlaylist() throws IOException {
+ Playlist playlist = mPlaylistProvider.get(DEFAULT_PLAYLIST_KEY);
+
+ if (playlist == null) {
+ playlist = new Playlist();
+ }
+
+ return playlist;
+ }
+}
diff --git a/src/com/p4square/grow/backend/resources/Playlist.java b/src/com/p4square/grow/backend/resources/Playlist.java
deleted file mode 100644
index f3d2f08..0000000
--- a/src/com/p4square/grow/backend/resources/Playlist.java
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * 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/SurveyResource.java b/src/com/p4square/grow/backend/resources/SurveyResource.java
index 83c4cad..497978f 100644
--- a/src/com/p4square/grow/backend/resources/SurveyResource.java
+++ b/src/com/p4square/grow/backend/resources/SurveyResource.java
@@ -9,7 +9,7 @@ import java.io.IOException;
import java.util.Map;
import java.util.HashMap;
-import org.codehaus.jackson.map.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectMapper;
import org.restlet.data.MediaType;
import org.restlet.data.Status;
diff --git a/src/com/p4square/grow/backend/resources/SurveyResultsResource.java b/src/com/p4square/grow/backend/resources/SurveyResultsResource.java
index 91d4d0f..e87126d 100644
--- a/src/com/p4square/grow/backend/resources/SurveyResultsResource.java
+++ b/src/com/p4square/grow/backend/resources/SurveyResultsResource.java
@@ -10,7 +10,7 @@ import java.util.HashMap;
import com.netflix.astyanax.model.Column;
import com.netflix.astyanax.model.ColumnList;
-import org.codehaus.jackson.map.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectMapper;
import org.restlet.data.MediaType;
import org.restlet.data.Status;
diff --git a/src/com/p4square/grow/backend/resources/TrainingRecordResource.java b/src/com/p4square/grow/backend/resources/TrainingRecordResource.java
index 6de9507..e42456e 100644
--- a/src/com/p4square/grow/backend/resources/TrainingRecordResource.java
+++ b/src/com/p4square/grow/backend/resources/TrainingRecordResource.java
@@ -14,7 +14,7 @@ import java.util.HashMap;
import com.netflix.astyanax.model.Column;
import com.netflix.astyanax.model.ColumnList;
-import org.codehaus.jackson.map.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectMapper;
import org.restlet.data.MediaType;
import org.restlet.data.Status;
@@ -22,11 +22,21 @@ import org.restlet.resource.ServerResource;
import org.restlet.representation.Representation;
import org.restlet.representation.StringRepresentation;
+import org.restlet.ext.jackson.JacksonRepresentation;
+
import org.apache.log4j.Logger;
import com.p4square.grow.backend.GrowBackend;
import com.p4square.grow.backend.db.CassandraDatabase;
+import com.p4square.grow.model.Playlist;
+import com.p4square.grow.model.VideoRecord;
+import com.p4square.grow.model.TrainingRecord;
+
+import com.p4square.grow.provider.Provider;
+import com.p4square.grow.provider.ProvidesTrainingRecords;
+import com.p4square.grow.provider.JsonEncodedProvider;
+
import com.p4square.grow.model.Score;
/**
@@ -43,23 +53,41 @@ public class TrainingRecordResource extends ServerResource {
SUMMARY, VIDEO
}
- private GrowBackend mBackend;
private CassandraDatabase mDb;
+ private Provider<String, TrainingRecord> mTrainingRecordProvider;
private RequestType mRequestType;
private String mUserId;
private String mVideoId;
+ private TrainingRecord mRecord;
@Override
public void doInit() {
super.doInit();
- mBackend = (GrowBackend) getApplication();
- mDb = mBackend.getDatabase();
+ mDb = ((GrowBackend) getApplication()).getDatabase();
+ mTrainingRecordProvider = ((ProvidesTrainingRecords) getApplication()).getTrainingRecordProvider();
mUserId = getAttribute("userId");
mVideoId = getAttribute("videoId");
+ try {
+ Playlist defaultPlaylist = ((GrowBackend) getApplication()).getDefaultPlaylist();
+
+ mRecord = mTrainingRecordProvider.get(mUserId);
+ if (mRecord == null) {
+ mRecord = new TrainingRecord();
+ mRecord.setPlaylist(defaultPlaylist);
+ } else {
+ // Merge the playlist with the most recent version.
+ mRecord.getPlaylist().merge(defaultPlaylist);
+ }
+
+ } catch (IOException e) {
+ LOG.error("IOException loading TrainingRecord: " + e.getMessage(), e);
+ mRecord = null;
+ }
+
mRequestType = RequestType.SUMMARY;
if (mVideoId != null) {
mRequestType = RequestType.VIDEO;
@@ -71,24 +99,35 @@ public class TrainingRecordResource extends ServerResource {
*/
@Override
protected Representation get() {
- String result = null;
+ JacksonRepresentation<?> rep = null;
+
+ if (mRecord == null) {
+ setStatus(Status.SERVER_ERROR_INTERNAL);
+ return null;
+ }
switch (mRequestType) {
case VIDEO:
- result = mDb.getKey("training", mUserId, mVideoId);
+ VideoRecord video = mRecord.getPlaylist().find(mVideoId);
+ if (video == null) {
+ break; // Fall through and return 404
+ }
+ rep = new JacksonRepresentation<VideoRecord>(video);
break;
case SUMMARY:
- result = buildSummary();
+ rep = new JacksonRepresentation<TrainingRecord>(mRecord);
break;
}
- if (result == null) {
+ if (rep == null) {
setStatus(Status.CLIENT_ERROR_NOT_FOUND);
return null;
- }
- return new StringRepresentation(result);
+ } else {
+ rep.setObjectMapper(JsonEncodedProvider.MAPPER);
+ return rep;
+ }
}
/**
@@ -96,27 +135,37 @@ public class TrainingRecordResource extends ServerResource {
*/
@Override
protected Representation put(Representation entity) {
- boolean success = false;
+ if (mRecord == null) {
+ setStatus(Status.SERVER_ERROR_INTERNAL);
+ return null;
+ }
switch (mRequestType) {
case VIDEO:
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);
- }
+ JacksonRepresentation<VideoRecord> representation =
+ new JacksonRepresentation<>(entity, VideoRecord.class);
+ representation.setObjectMapper(JsonEncodedProvider.MAPPER);
+ VideoRecord update = representation.getObject();
+ VideoRecord video = mRecord.getPlaylist().find(mVideoId);
+
+ if (video == null) {
+ // TODO: Video isn't on their playlist...
+ LOG.warn("Skipping video completion for video missing from playlist.");
+
+ } else if (update.getComplete() && !video.getComplete()) {
+ // Video was newly completed
+ video.complete();
+ mRecord.setLastVideo(mVideoId);
+
+ mTrainingRecordProvider.put(mUserId, mRecord);
}
- success = true;
+ setStatus(Status.SUCCESS_NO_CONTENT);
} catch (Exception e) {
LOG.warn("Caught exception updating training record: " + e.getMessage(), e);
+ setStatus(Status.SERVER_ERROR_INTERNAL);
}
break;
@@ -124,116 +173,7 @@ public class TrainingRecordResource extends ServerResource {
setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED);
}
- if (success) {
- setStatus(Status.SUCCESS_NO_CONTENT);
-
- } else {
- setStatus(Status.SERVER_ERROR_INTERNAL);
- }
-
return null;
}
- /**
- * This method compiles the summary of the training completed.
- */
- private String buildSummary() {
- StringBuilder sb = new StringBuilder("{ ");
-
- // Last watch video
- final String lastVideo = mDb.getKey("training", mUserId, "lastVideo");
- if (lastVideo != null) {
- sb.append("\"lastVideo\": \"" + lastVideo + "\", ");
- }
-
- // 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()) ||
- "playlist".equals(c.getName())) {
- continue;
- }
-
- if (first) {
- sb.append("\"" + c.getName() + "\": ");
- first = false;
- } else {
- sb.append(", \"" + c.getName() + "\": ");
- }
-
- 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) {
- boolean required;
-
- if ("introduction".equals(chapter)) {
- // Introduction chapter is always required
- required = true;
- } else {
- // Chapter required if the floor of the score is <= the chapter's numeric value.
- 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/frontend/ChapterCompletePage.java b/src/com/p4square/grow/frontend/ChapterCompletePage.java
index 2f981ae..b0bfdd7 100644
--- a/src/com/p4square/grow/frontend/ChapterCompletePage.java
+++ b/src/com/p4square/grow/frontend/ChapterCompletePage.java
@@ -23,6 +23,9 @@ import com.p4square.fmfacade.json.JsonResponse;
import com.p4square.fmfacade.json.ClientException;
import com.p4square.grow.config.Config;
+import com.p4square.grow.model.TrainingRecord;
+import com.p4square.grow.provider.TrainingRecordProvider;
+import com.p4square.grow.provider.Provider;
/**
* This resource displays the transitional page between chapters.
@@ -35,6 +38,7 @@ public class ChapterCompletePage extends FreeMarkerPageResource {
private GrowFrontend mGrowFrontend;
private Config mConfig;
private JsonRequestClient mJsonClient;
+ private Provider<String, TrainingRecord> mTrainingRecordProvider;
private String mUserId;
private String mChapter;
@@ -47,6 +51,12 @@ public class ChapterCompletePage extends FreeMarkerPageResource {
mConfig = mGrowFrontend.getConfig();
mJsonClient = new JsonRequestClient(getContext().getClientDispatcher());
+ mTrainingRecordProvider = new TrainingRecordProvider<String>(new JsonRequestProvider<TrainingRecord>(getContext().getClientDispatcher(), TrainingRecord.class)) {
+ @Override
+ public String makeKey(String userid) {
+ return getBackendEndpoint() + "/accounts/" + userid + "/training";
+ }
+ };
mUserId = getRequest().getClientInfo().getUser().getIdentifier();
@@ -69,17 +79,15 @@ public class ChapterCompletePage extends FreeMarkerPageResource {
Map<String, Object> root = getRootObject();
// Get the training summary
- Map<String, Object> trainingRecord = null;
- Map<String, Boolean> chapters = null;
- {
- JsonResponse response = backendGet("/accounts/" + mUserId + "/training");
- if (response.getStatus().isSuccess()) {
- trainingRecord = response.getMap();
- chapters = (Map<String, Boolean>) trainingRecord.get("chapters");
- }
+ TrainingRecord trainingRecord = mTrainingRecordProvider.get(mUserId);
+ if (trainingRecord == null) {
+ // Wait. What? Everyone has a training record...
+ setStatus(Status.SERVER_ERROR_INTERNAL);
+ return new ErrorPage("Could not retrieve your training record.");
}
// Verify they completed the chapter.
+ Map<String, Boolean> chapters = trainingRecord.getPlaylist().getChapterStatuses();
Boolean completed = chapters.get(mChapter);
if (completed == null || !completed) {
// Redirect back to training page...
diff --git a/src/com/p4square/grow/frontend/TrainingPageResource.java b/src/com/p4square/grow/frontend/TrainingPageResource.java
index b27d86e..bf08a1a 100644
--- a/src/com/p4square/grow/frontend/TrainingPageResource.java
+++ b/src/com/p4square/grow/frontend/TrainingPageResource.java
@@ -27,13 +27,18 @@ import com.p4square.fmfacade.json.JsonResponse;
import com.p4square.fmfacade.FreeMarkerPageResource;
import com.p4square.grow.config.Config;
+import com.p4square.grow.model.TrainingRecord;
+import com.p4square.grow.model.VideoRecord;
+import com.p4square.grow.model.Playlist;
+import com.p4square.grow.provider.TrainingRecordProvider;
+import com.p4square.grow.provider.Provider;
/**
* TrainingPageResource handles rendering the training page.
*
* This resource expects the user to be authenticated and the ClientInfo User object
* to be populated.
- *
+ *
* @author Jesse Morgan <jesse@jesterpm.net>
*/
public class TrainingPageResource extends FreeMarkerPageResource {
@@ -43,6 +48,8 @@ public class TrainingPageResource extends FreeMarkerPageResource {
private Template mTrainingTemplate;
private JsonRequestClient mJsonClient;
+ private Provider<String, TrainingRecord> mTrainingRecordProvider;
+
// Fields pertaining to this request.
private String mChapter;
private String mUserId;
@@ -60,6 +67,12 @@ public class TrainingPageResource extends FreeMarkerPageResource {
}
mJsonClient = new JsonRequestClient(getContext().getClientDispatcher());
+ mTrainingRecordProvider = new TrainingRecordProvider<String>(new JsonRequestProvider<TrainingRecord>(getContext().getClientDispatcher(), TrainingRecord.class)) {
+ @Override
+ public String makeKey(String userid) {
+ return getBackendEndpoint() + "/accounts/" + userid + "/training";
+ }
+ };
mChapter = getAttribute("chapter");
mUserId = getRequest().getClientInfo().getUser().getIdentifier();
@@ -72,18 +85,15 @@ public class TrainingPageResource extends FreeMarkerPageResource {
protected Representation get() {
try {
// 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");
- }
+ TrainingRecord trainingRecord = mTrainingRecordProvider.get(mUserId);
+ if (trainingRecord == null) {
+ setStatus(Status.SERVER_ERROR_INTERNAL);
+ return new ErrorPage("Could not retrieve TrainingRecord.");
}
+ Playlist playlist = trainingRecord.getPlaylist();
+ Map<String, Boolean> chapters = playlist.getChapterStatuses();
+
// Get the current chapter (the lowest, incomplete chapter)
if (mChapter == null) {
int min = Integer.MAX_VALUE;
@@ -120,7 +130,13 @@ public class TrainingPageResource extends FreeMarkerPageResource {
// Mark the completed videos as completed
int chapterProgress = 0;
for (Map<String, Object> video : videos) {
- boolean completed = (null != completedVideos.get(video.get("id")));
+ boolean completed = false;
+ VideoRecord record = playlist.find((String) video.get("id"));
+ cLog.info("VideoId: " + video.get("id"));
+ if (record != null) {
+ cLog.info("VideoRecord: " + record.getComplete());
+ completed = record.getComplete();
+ }
video.put("completed", completed);
if (completed) {
@@ -133,7 +149,6 @@ public class TrainingPageResource extends FreeMarkerPageResource {
root.put("chapter", mChapter);
root.put("chapterProgress", chapterProgress);
root.put("videos", videos);
- root.put("completedVideos", completedVideos);
return new TemplateRepresentation(mTrainingTemplate, root, MediaType.TEXT_HTML);
@@ -166,18 +181,6 @@ public class TrainingPageResource extends FreeMarkerPageResource {
return response;
}
- private JsonResponse backendPut(final String uri, final Map data) {
- cLog.debug("Sending backend PUT " + uri);
-
- final JsonResponse response = mJsonClient.put(getBackendEndpoint() + uri, data);
- final Status status = response.getStatus();
- if (!status.isSuccess() && !Status.CLIENT_ERROR_NOT_FOUND.equals(status)) {
- cLog.warn("Error making backend request for '" + uri + "'. status = " + response.getStatus().toString());
- }
-
- return response;
- }
-
int chapterIndex(String chapter) {
if ("teacher".equals(chapter)) {
return 4;
diff --git a/src/com/p4square/grow/frontend/VideosResource.java b/src/com/p4square/grow/frontend/VideosResource.java
index caf8dc1..2099a77 100644
--- a/src/com/p4square/grow/frontend/VideosResource.java
+++ b/src/com/p4square/grow/frontend/VideosResource.java
@@ -83,7 +83,7 @@ public class VideosResource extends ServerResource {
@Override
protected Representation post(Representation entity) {
Map<String, Object> data = new HashMap<String, Object>();
- data.put("completed", "t");
+ data.put("complete", "true");
JsonResponse response = backendPut("/accounts/" + mUserId + "/training/videos/" + mVideoId, data);
if (!response.getStatus().isSuccess()) {
diff --git a/src/com/p4square/grow/model/Chapter.java b/src/com/p4square/grow/model/Chapter.java
new file mode 100644
index 0000000..4d59983
--- /dev/null
+++ b/src/com/p4square/grow/model/Chapter.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonAnyGetter;
+import com.fasterxml.jackson.annotation.JsonAnySetter;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+/**
+ * Chapter is a list of VideoRecords in a Playlist.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class Chapter implements Cloneable {
+ private Map<String, VideoRecord> mVideos;
+
+ public Chapter() {
+ mVideos = new HashMap<String, VideoRecord>();
+ }
+
+ /**
+ * @return The VideoRecord for videoid or null if videoid is not in the chapter.
+ */
+ public VideoRecord getVideoRecord(String videoid) {
+ return mVideos.get(videoid);
+ }
+
+ /**
+ * @return A map of video ids to VideoRecords.
+ */
+ @JsonAnyGetter
+ public Map<String, VideoRecord> getVideos() {
+ return mVideos;
+ }
+
+ /**
+ * Set the VideoRecord for a video id.
+ * @param videoId the video id.
+ * @param video the VideoRecord.
+ */
+ @JsonAnySetter
+ public void setVideoRecord(String videoId, VideoRecord video) {
+ mVideos.put(videoId, video);
+ }
+
+ /**
+ * @return true if every required video has been completed.
+ */
+ @JsonIgnore
+ public boolean isComplete() {
+ boolean complete = true;
+
+ for (VideoRecord r : mVideos.values()) {
+ if (r.getRequired() && !r.getComplete()) {
+ return false;
+ }
+ }
+
+ return complete;
+ }
+
+ /**
+ * Deeply clone a chapter.
+ *
+ * @return a new Chapter object identical but independent of this one.
+ */
+ public Chapter clone() throws CloneNotSupportedException {
+ Chapter c = new Chapter();
+ for (Map.Entry<String, VideoRecord> videoEntry : mVideos.entrySet()) {
+ c.setVideoRecord(videoEntry.getKey(), videoEntry.getValue().clone());
+ }
+ return c;
+ }
+}
diff --git a/src/com/p4square/grow/model/Playlist.java b/src/com/p4square/grow/model/Playlist.java
new file mode 100644
index 0000000..79ce68e
--- /dev/null
+++ b/src/com/p4square/grow/model/Playlist.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonAnyGetter;
+import com.fasterxml.jackson.annotation.JsonAnySetter;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+/**
+ * Representation of a user's playlist.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class Playlist {
+ /**
+ * Map of Chapter ID to map of Video ID to VideoRecord.
+ */
+ private Map<String, Chapter> mPlaylist;
+
+ private Date mLastUpdated;
+
+ /**
+ * Construct an empty playlist.
+ */
+ public Playlist() {
+ mPlaylist = new HashMap<String, Chapter>();
+ mLastUpdated = new Date(0); // Default to a prehistoric date if we don't have one.
+ }
+
+ /**
+ * Find the VideoRecord for a video id.
+ */
+ public VideoRecord find(String videoId) {
+ for (Chapter chapter : mPlaylist.values()) {
+ VideoRecord r = chapter.getVideoRecord(videoId);
+
+ if (r != null) {
+ return r;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @return The last modified date of the source playlist.
+ */
+ public Date getLastUpdated() {
+ return mLastUpdated;
+ }
+
+ /**
+ * Set the last updated date.
+ * @param date the new last updated date.
+ */
+ public void setLastUpdated(Date date) {
+ mLastUpdated = date;
+ }
+
+ /**
+ * Add a video to the playlist.
+ */
+ public VideoRecord add(String chapterId, String videoId) {
+ Chapter chapter = mPlaylist.get(chapterId);
+
+ if (chapter == null) {
+ chapter = new Chapter();
+ mPlaylist.put(chapterId, chapter);
+ }
+
+ VideoRecord r = new VideoRecord();
+ chapter.setVideoRecord(videoId, r);
+ return r;
+ }
+
+ /**
+ * Add a Chapter to the Playlist.
+ * @param chapterId The name of the chapter.
+ * @param chapter The Chapter object to add.
+ */
+ @JsonAnySetter
+ public void addChapter(String chapterId, Chapter chapter) {
+ mPlaylist.put(chapterId, chapter);
+ }
+
+ /**
+ * @return a map of chapter id to chapter.
+ */
+ @JsonAnyGetter
+ public Map<String, Chapter> getChaptersMap() {
+ return mPlaylist;
+ }
+
+ /**
+ * @return The last chapter to be completed.
+ */
+ @JsonIgnore
+ public Map<String, Boolean> getChapterStatuses() {
+ Map<String, Boolean> completed = new HashMap<String, Boolean>();
+
+ for (Map.Entry<String, Chapter> entry : mPlaylist.entrySet()) {
+ completed.put(entry.getKey(), entry.getValue().isComplete());
+ }
+
+ return completed;
+ }
+
+ /**
+ * @return true if all required videos in the chapter have been watched.
+ */
+ public boolean isChapterComplete(String chapterId) {
+ Chapter chapter = mPlaylist.get(chapterId);
+ if (chapter != null) {
+ return chapter.isComplete();
+ }
+
+ return false;
+ }
+
+ /**
+ * Merge a playlist into this playlist.
+ *
+ * Merge is accomplished by adding all missing Chapters and VideoRecords to
+ * this playlist.
+ */
+ public void merge(Playlist source) {
+ if (source.getLastUpdated().before(mLastUpdated)) {
+ // Already up to date.
+ return;
+ }
+
+ for (Map.Entry<String, Chapter> entry : source.getChaptersMap().entrySet()) {
+ String chapterName = entry.getKey();
+ Chapter theirChapter = entry.getValue();
+ Chapter myChapter = mPlaylist.get(entry.getKey());
+
+ if (myChapter == null) {
+ // Add entire chapter
+ try {
+ mPlaylist.put(chapterName, theirChapter.clone());
+ } catch (CloneNotSupportedException e) {
+ throw new RuntimeException(e); // Unexpected...
+ }
+
+ } else {
+ // Check chapter for missing videos
+ for (Map.Entry<String, VideoRecord> videoEntry : theirChapter.getVideos().entrySet()) {
+ String videoId = videoEntry.getKey();
+ VideoRecord myVideo = myChapter.getVideoRecord(videoId);
+
+ if (myVideo == null) {
+ try {
+ myVideo = videoEntry.getValue().clone();
+ myChapter.setVideoRecord(videoId, myVideo);
+ } catch (CloneNotSupportedException e) {
+ throw new RuntimeException(e); // Unexpected...
+ }
+ }
+ }
+ }
+ }
+
+ mLastUpdated = source.getLastUpdated();
+ }
+}
diff --git a/src/com/p4square/grow/model/Question.java b/src/com/p4square/grow/model/Question.java
index 37deffa..f4b9458 100644
--- a/src/com/p4square/grow/model/Question.java
+++ b/src/com/p4square/grow/model/Question.java
@@ -7,9 +7,9 @@ package com.p4square.grow.model;
import java.util.HashMap;
import java.util.Map;
-import org.codehaus.jackson.annotate.JsonSubTypes;
-import org.codehaus.jackson.annotate.JsonSubTypes.Type;
-import org.codehaus.jackson.annotate.JsonTypeInfo;
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
/**
* Model of an assessment question.
diff --git a/src/com/p4square/grow/model/TrainingRecord.java b/src/com/p4square/grow/model/TrainingRecord.java
new file mode 100644
index 0000000..bc3ffa9
--- /dev/null
+++ b/src/com/p4square/grow/model/TrainingRecord.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+/**
+ * Representation of a user's training record.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class TrainingRecord {
+ private String mLastVideo;
+ private Playlist mPlaylist;
+
+ public TrainingRecord() {
+ mPlaylist = new Playlist();
+ }
+
+ /**
+ * @return Video id of the last video watched.
+ */
+ public String getLastVideo() {
+ return mLastVideo;
+ }
+
+ /**
+ * Set the video id for the last video watched.
+ * @param video The new video id.
+ */
+ public void setLastVideo(String video) {
+ mLastVideo = video;
+ }
+
+ /**
+ * @return the user's Playlist.
+ */
+ public Playlist getPlaylist() {
+ return mPlaylist;
+ }
+
+ /**
+ * Set the user's playlist.
+ * @param playlist The new playlist.
+ */
+ public void setPlaylist(Playlist playlist) {
+ mPlaylist = playlist;
+ }
+}
diff --git a/src/com/p4square/grow/backend/resources/VideoRecord.java b/src/com/p4square/grow/model/VideoRecord.java
index 2ba28c3..ec99d0d 100644
--- a/src/com/p4square/grow/backend/resources/VideoRecord.java
+++ b/src/com/p4square/grow/model/VideoRecord.java
@@ -2,27 +2,32 @@
* Copyright 2013 Jesse Morgan
*/
-package com.p4square.grow.backend.resources;
+package com.p4square.grow.model;
import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
/**
* Simple bean containing video completion data.
*
* @author Jesse Morgan <jesse@jesterpm.net>
*/
-class VideoRecord {
- private boolean mComplete;
- private boolean mRequired;
+public class VideoRecord implements Cloneable {
+ private Boolean mComplete;
+ private Boolean mRequired;
private Date mCompletionDate;
public VideoRecord() {
- mComplete = false;
- mRequired = true;
+ mComplete = null;
+ mRequired = null;
mCompletionDate = null;
}
public boolean getComplete() {
+ if (mComplete == null) {
+ return false;
+ }
return mComplete;
}
@@ -30,7 +35,15 @@ class VideoRecord {
mComplete = complete;
}
+ @JsonIgnore
+ public boolean isCompleteSet() {
+ return mComplete != null;
+ }
+
public boolean getRequired() {
+ if (mRequired == null) {
+ return true;
+ }
return mRequired;
}
@@ -38,6 +51,11 @@ class VideoRecord {
mRequired = complete;
}
+ @JsonIgnore
+ public boolean isRequiredSet() {
+ return mRequired != null;
+ }
+
public Date getCompletionDate() {
return mCompletionDate;
}
@@ -53,4 +71,15 @@ class VideoRecord {
mComplete = true;
mCompletionDate = new Date();
}
+
+ /**
+ * @return an identical clone of this record.
+ */
+ public VideoRecord clone() throws CloneNotSupportedException {
+ VideoRecord r = (VideoRecord) super.clone();
+ r.mComplete = mComplete;
+ r.mRequired = mRequired;
+ r.mCompletionDate = mCompletionDate;
+ return r;
+ }
}
diff --git a/src/com/p4square/grow/provider/JsonEncodedProvider.java b/src/com/p4square/grow/provider/JsonEncodedProvider.java
index 605b18c..655b531 100644
--- a/src/com/p4square/grow/provider/JsonEncodedProvider.java
+++ b/src/com/p4square/grow/provider/JsonEncodedProvider.java
@@ -6,9 +6,9 @@ package com.p4square.grow.provider;
import java.io.IOException;
-import org.codehaus.jackson.map.DeserializationConfig;
-import org.codehaus.jackson.map.ObjectMapper;
-import org.codehaus.jackson.map.SerializationConfig;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
/**
* Provider provides a simple interface for loading and persisting
@@ -19,9 +19,9 @@ import org.codehaus.jackson.map.SerializationConfig;
public abstract class JsonEncodedProvider<K, V> implements Provider<K, V> {
public static final ObjectMapper MAPPER = new ObjectMapper();
static {
- MAPPER.configure(SerializationConfig.Feature.WRITE_ENUMS_USING_TO_STRING, true);
- MAPPER.configure(DeserializationConfig.Feature.READ_ENUMS_USING_TO_STRING, true);
- MAPPER.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+ MAPPER.configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, true);
+ MAPPER.configure(DeserializationFeature.READ_ENUMS_USING_TO_STRING, true);
+ MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
private final Class<V> mClazz;
diff --git a/src/com/p4square/grow/provider/ProvidesQuestions.java b/src/com/p4square/grow/provider/ProvidesQuestions.java
new file mode 100644
index 0000000..b43f649
--- /dev/null
+++ b/src/com/p4square/grow/provider/ProvidesQuestions.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.provider;
+
+import com.p4square.grow.model.Question;
+
+/**
+ * Indicates the ability to provide a Question Provider.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public interface ProvidesQuestions {
+ /**
+ * @return A Provider of Questions keyed by question id.
+ */
+ Provider<String, Question> getQuestionProvider();
+}
diff --git a/src/com/p4square/grow/provider/ProvidesTrainingRecords.java b/src/com/p4square/grow/provider/ProvidesTrainingRecords.java
new file mode 100644
index 0000000..27ffa3e
--- /dev/null
+++ b/src/com/p4square/grow/provider/ProvidesTrainingRecords.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.provider;
+
+import com.p4square.grow.model.TrainingRecord;
+
+/**
+ * Indicates the ability to provide a TrainingRecord Provider.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public interface ProvidesTrainingRecords {
+ /**
+ * @return A Provider of Questions keyed by question id.
+ */
+ Provider<String, TrainingRecord> getTrainingRecordProvider();
+}
diff --git a/src/com/p4square/grow/provider/TrainingRecordProvider.java b/src/com/p4square/grow/provider/TrainingRecordProvider.java
new file mode 100644
index 0000000..44dba87
--- /dev/null
+++ b/src/com/p4square/grow/provider/TrainingRecordProvider.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.provider;
+
+import java.io.IOException;
+
+import com.p4square.grow.model.TrainingRecord;
+
+/**
+ * TrainingRecordProvider wraps an existing Provider to get and put TrainingRecords.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public abstract class TrainingRecordProvider<K> implements Provider<String, TrainingRecord> {
+
+ private Provider<K, TrainingRecord> mProvider;
+
+ public TrainingRecordProvider(Provider<K, TrainingRecord> provider) {
+ mProvider = provider;
+ }
+
+ @Override
+ public TrainingRecord get(String key) throws IOException {
+ return mProvider.get(makeKey(key));
+ }
+
+ @Override
+ public void put(String key, TrainingRecord obj) throws IOException {
+ mProvider.put(makeKey(key), obj);
+ }
+
+ /**
+ * Make a Key for a TrainingRecord..
+ *
+ * @param userId The user id.
+ * @return a key for the TrainingRecord of userid.
+ */
+ protected abstract K makeKey(String userId);
+}