summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJesse Morgan <jesse@jesterpm.net>2013-11-09 15:24:56 -0800
committerJesse Morgan <jesse@jesterpm.net>2013-11-09 15:24:56 -0800
commitd94643c217b6b93eb6c539c60b00fe0cf68272b7 (patch)
treeff27dd65b1580c5f498091684d32403577165426
parent5d7828e929a57042e3a0e6f6f2beafc60c748368 (diff)
Refactored TrainingResource to use the Provider interface.
Playlists are now generated from a default playlist and regularly merged with the default playlist to get updates. Also adding the Question tests that got left out of a previous commit.
-rwxr-xr-xdevfiles/scripts/compile-videos.sh18
-rw-r--r--devfiles/videos/playlist.json46
-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
-rw-r--r--tst/com/p4square/grow/model/CircleQuestionTest.java92
-rw-r--r--tst/com/p4square/grow/model/ImageQuestionTest.java74
-rw-r--r--tst/com/p4square/grow/model/QuadQuestionTest.java92
-rw-r--r--tst/com/p4square/grow/model/ScoreTest.java111
-rw-r--r--tst/com/p4square/grow/model/SliderQuestionTest.java58
-rw-r--r--tst/com/p4square/grow/model/TextQuestionTest.java74
-rw-r--r--tst/com/p4square/grow/model/TrainingRecordTest.java178
-rw-r--r--tst/com/p4square/grow/model/trainingrecord.json18
30 files changed, 1403 insertions, 335 deletions
diff --git a/devfiles/scripts/compile-videos.sh b/devfiles/scripts/compile-videos.sh
index 27ac838..918b592 100755
--- a/devfiles/scripts/compile-videos.sh
+++ b/devfiles/scripts/compile-videos.sh
@@ -2,11 +2,17 @@
for i in $DEVFILES/videos/*; do
level=`basename $i`
- for j in $i/*.json; do
- id=`basename $j .json`
- echo "set strings['/training/${level}']['${id}'] = '"
- cat $j|sed "s/'/\\\'/g"
- echo "';"
- done
+ if [ "$level" != "playlist.json" ]; then
+ for j in $i/*.json; do
+ id=`basename $j .json`
+ echo "set strings['/training/${level}']['${id}'] = '"
+ cat $j|sed "s/'/\\\'/g"
+ echo "';"
+ done
+ fi
done
+# Default Playlist
+echo "set strings['defaultPlaylist']['value'] = '"
+cat $DEVFILES/videos/playlist.json
+echo "';"
diff --git a/devfiles/videos/playlist.json b/devfiles/videos/playlist.json
new file mode 100644
index 0000000..1d8fc8d
--- /dev/null
+++ b/devfiles/videos/playlist.json
@@ -0,0 +1,46 @@
+{
+ "introduction":{
+ "introduction-1":{ "required": true }
+ },
+ "seeker":{
+ "seeker-1":{ "required": true },
+ "seeker-2":{ "required": true },
+ "seeker-3":{ "required": true },
+ "seeker-4":{ "required": true },
+ "seeker-5":{ "required": true }
+ },
+ "believer":{
+ "believer-1":{ "required": true },
+ "believer-2":{ "required": true },
+ "believer-3":{ "required": true },
+ "believer-4":{ "required": true },
+ "believer-5":{ "required": true },
+ "believer-6":{ "required": true },
+ "believer-7":{ "required": true },
+ "believer-8":{ "required": true },
+ "believer-9":{ "required": true },
+ "believer-10":{ "required": true }
+ },
+ "disciple":{
+ "disciple-1":{ "required": true },
+ "disciple-2":{ "required": true },
+ "disciple-3":{ "required": true },
+ "disciple-4":{ "required": true },
+ "disciple-5":{ "required": true },
+ "disciple-6":{ "required": true },
+ "disciple-7":{ "required": true },
+ "disciple-8":{ "required": true },
+ "disciple-9":{ "required": true },
+ "disciple-10":{ "required": true }
+ },
+ "teacher":{
+ "teacher-1":{ "required": true },
+ "teacher-2":{ "required": true },
+ "teacher-3":{ "required": true },
+ "teacher-4":{ "required": true },
+ "teacher-5":{ "required": true },
+ "teacher-6":{ "required": true },
+ "teacher-7":{ "required": true }
+ },
+ "lastUpdated": 1383798629000
+}
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 efcde38..f1871b9 100644
--- a/src/com/p4square/grow/frontend/ChapterCompletePage.java
+++ b/src/com/p4square/grow/frontend/ChapterCompletePage.java
@@ -23,6 +23,9 @@ import net.jesterpm.fmfacade.json.JsonResponse;
import net.jesterpm.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 6a0fc10..f71f784 100644
--- a/src/com/p4square/grow/frontend/TrainingPageResource.java
+++ b/src/com/p4square/grow/frontend/TrainingPageResource.java
@@ -27,13 +27,18 @@ import net.jesterpm.fmfacade.json.JsonResponse;
import net.jesterpm.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 e85ecd4..957445d 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);
+}
diff --git a/tst/com/p4square/grow/model/CircleQuestionTest.java b/tst/com/p4square/grow/model/CircleQuestionTest.java
new file mode 100644
index 0000000..222cda5
--- /dev/null
+++ b/tst/com/p4square/grow/model/CircleQuestionTest.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Tests for CircleQuestion.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class CircleQuestionTest {
+ private static final double DELTA = 1e-4;
+
+ public static void main(String... args) {
+ org.junit.runner.JUnitCore.main(CircleQuestionTest.class.getName());
+ }
+
+ private CircleQuestion mQuestion;
+
+ @Before
+ public void setUp() {
+ mQuestion = new CircleQuestion();
+
+ Answer a1 = new Answer();
+ a1.setScore(2);
+
+ Answer a2 = new Answer();
+ a2.setScore(4);
+
+ mQuestion.getAnswers().put("1.00,0.00", a1);
+ mQuestion.getAnswers().put("-1.00,0.00", a2);
+ }
+
+ /**
+ * Verify the getters and setters function correctly.
+ */
+ @Test
+ public void testGetAndSet() {
+ mQuestion.setTopLeft("TopLeft String");
+ assertEquals("TopLeft String", mQuestion.getTopLeft());
+
+ mQuestion.setTopRight("TopRight String");
+ assertEquals("TopRight String", mQuestion.getTopRight());
+
+ mQuestion.setBottomRight("BottomRight String");
+ assertEquals("BottomRight String", mQuestion.getBottomRight());
+
+ mQuestion.setBottomLeft("BottomLeft String");
+ assertEquals("BottomLeft String", mQuestion.getBottomLeft());
+ }
+
+ /**
+ * The ScoringEngines are tested extensively independently, so simply
+ * verify that we get the expected results for our input.
+ */
+ @Test
+ public void testScoreAnswer() {
+ Score score = new Score();
+ RecordedAnswer answer = new RecordedAnswer();
+
+ answer.setAnswerId("0.5,0.5");
+ assertTrue(mQuestion.scoreAnswer(score, answer));
+ assertEquals(2, score.sum, DELTA);
+ assertEquals(1, score.count);
+
+ answer.setAnswerId("-0.5,-0.5");
+ assertTrue(mQuestion.scoreAnswer(score, answer));
+ assertEquals(6, score.sum, DELTA);
+ assertEquals(2, score.count);
+
+ try {
+ answer.setAnswerId("notAPoint");
+ assertTrue(mQuestion.scoreAnswer(score, answer));
+ fail("Should have thrown exception.");
+ } catch (IllegalArgumentException e) {
+ }
+ }
+
+ /**
+ * Verify the correct type string is returned.
+ */
+ @Test
+ public void testType() {
+ assertEquals("circle", mQuestion.getType().toString());
+ }
+}
diff --git a/tst/com/p4square/grow/model/ImageQuestionTest.java b/tst/com/p4square/grow/model/ImageQuestionTest.java
new file mode 100644
index 0000000..28ccdb2
--- /dev/null
+++ b/tst/com/p4square/grow/model/ImageQuestionTest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Test for ImageQuestion.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class ImageQuestionTest {
+ private static final double DELTA = 1e-4;
+
+ public static void main(String... args) {
+ org.junit.runner.JUnitCore.main(ImageQuestionTest.class.getName());
+ }
+
+ private Question mQuestion;
+
+ @Before
+ public void setUp() {
+ mQuestion = new ImageQuestion();
+
+ Answer a1 = new Answer();
+ a1.setScore(2);
+
+ Answer a2 = new Answer();
+ a2.setScore(4);
+
+ mQuestion.getAnswers().put("a1", a1);
+ mQuestion.getAnswers().put("a2", a2);
+ }
+
+ /**
+ * The ScoringEngines are tested extensively independently, so simply
+ * verify that we get the expected results for our input.
+ */
+ @Test
+ public void testScoreAnswer() {
+ Score score = new Score();
+ RecordedAnswer answer = new RecordedAnswer();
+
+ answer.setAnswerId("a1");
+ assertTrue(mQuestion.scoreAnswer(score, answer));
+ assertEquals(2, score.sum, DELTA);
+ assertEquals(1, score.count);
+
+ answer.setAnswerId("a2");
+ assertTrue(mQuestion.scoreAnswer(score, answer));
+ assertEquals(6, score.sum, DELTA);
+ assertEquals(2, score.count);
+
+ try {
+ answer.setAnswerId("unknown");
+ assertTrue(mQuestion.scoreAnswer(score, answer));
+ fail("Should have thrown exception.");
+ } catch (IllegalArgumentException e) {
+ }
+ }
+
+ /**
+ * Verify the correct type string is returned.
+ */
+ @Test
+ public void testType() {
+ assertEquals("image", mQuestion.getType().toString());
+ }
+}
diff --git a/tst/com/p4square/grow/model/QuadQuestionTest.java b/tst/com/p4square/grow/model/QuadQuestionTest.java
new file mode 100644
index 0000000..389148a
--- /dev/null
+++ b/tst/com/p4square/grow/model/QuadQuestionTest.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Test for QuadQuestion.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class QuadQuestionTest {
+ private static final double DELTA = 1e-4;
+
+ public static void main(String... args) {
+ org.junit.runner.JUnitCore.main(QuadQuestionTest.class.getName());
+ }
+
+ private QuadQuestion mQuestion;
+
+ @Before
+ public void setUp() {
+ mQuestion = new QuadQuestion();
+
+ Answer a1 = new Answer();
+ a1.setScore(2);
+
+ Answer a2 = new Answer();
+ a2.setScore(4);
+
+ mQuestion.getAnswers().put("1.00,0.00", a1);
+ mQuestion.getAnswers().put("-1.00,0.00", a2);
+ }
+
+ /**
+ * Verify the getters and setters function correctly.
+ */
+ @Test
+ public void testGetAndSet() {
+ mQuestion.setTop("Top String");
+ assertEquals("Top String", mQuestion.getTop());
+
+ mQuestion.setBottom("Bottom String");
+ assertEquals("Bottom String", mQuestion.getBottom());
+
+ mQuestion.setLeft("Left String");
+ assertEquals("Left String", mQuestion.getLeft());
+
+ mQuestion.setRight("Right String");
+ assertEquals("Right String", mQuestion.getRight());
+ }
+
+ /**
+ * The ScoringEngines are tested extensively independently, so simply
+ * verify that we get the expected results for our input.
+ */
+ @Test
+ public void testScoreAnswer() {
+ Score score = new Score();
+ RecordedAnswer answer = new RecordedAnswer();
+
+ answer.setAnswerId("0.5,0.5");
+ assertTrue(mQuestion.scoreAnswer(score, answer));
+ assertEquals(2, score.sum, DELTA);
+ assertEquals(1, score.count);
+
+ answer.setAnswerId("-0.5,-0.5");
+ assertTrue(mQuestion.scoreAnswer(score, answer));
+ assertEquals(6, score.sum, DELTA);
+ assertEquals(2, score.count);
+
+ try {
+ answer.setAnswerId("notAPoint");
+ assertTrue(mQuestion.scoreAnswer(score, answer));
+ fail("Should have thrown exception.");
+ } catch (IllegalArgumentException e) {
+ }
+ }
+
+ /**
+ * Verify the correct type string is returned.
+ */
+ @Test
+ public void testType() {
+ assertEquals("quad", mQuestion.getType().toString());
+ }
+}
diff --git a/tst/com/p4square/grow/model/ScoreTest.java b/tst/com/p4square/grow/model/ScoreTest.java
new file mode 100644
index 0000000..dd3522a
--- /dev/null
+++ b/tst/com/p4square/grow/model/ScoreTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Test for the Score class.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class ScoreTest {
+ private static final double DELTA = 1e-4;
+
+ public static void main(String... args) {
+ org.junit.runner.JUnitCore.main(ScoreTest.class.getName());
+ }
+
+ private Score mScore;
+
+ @Before
+ public void setUp() {
+ mScore = new Score();
+ }
+
+ /**
+ * Verify getters and setters function.
+ */
+ @Test
+ public void testGetAndSet() {
+ // getSum()
+ mScore.sum = 1.1;
+ assertEquals(1.1, mScore.getSum(), DELTA);
+
+ // getCount()
+ mScore.count = 5;
+ assertEquals(5, mScore.getCount());
+ }
+
+ /**
+ * Verify that the average is computed by getScore().
+ */
+ @Test
+ public void testGetScore() {
+ mScore.sum = 7;
+ mScore.count = 2;
+ assertEquals(3.5, mScore.getScore(), DELTA);
+ }
+
+ /**
+ * Verify that numericScore() returns the correct mappings.
+ */
+ @Test
+ public void testNumericScore() {
+ assertEquals(4, Score.numericScore("teacher"));
+ assertEquals(3, Score.numericScore("disciple"));
+ assertEquals(2, Score.numericScore("believer"));
+ assertEquals(1, Score.numericScore("seeker"));
+ }
+
+ /**
+ * Verify that toString() returns the correct mappings.
+ */
+ @Test
+ public void testToString() {
+ mScore.count = 1;
+
+ // Seeker is defined as score < 2
+ mScore.sum = 0;
+ assertEquals("seeker", mScore.toString());
+ mScore.sum = 0.5;
+ assertEquals("seeker", mScore.toString());
+ mScore.sum = 1;
+ assertEquals("seeker", mScore.toString());
+ mScore.sum = 1.5;
+ assertEquals("seeker", mScore.toString());
+ mScore.sum = 1.99;
+ assertEquals("seeker", mScore.toString());
+
+ // Believer is defined as 2 <= score < 3
+ mScore.sum = 2;
+ assertEquals("believer", mScore.toString());
+ mScore.sum = 2.5;
+ assertEquals("believer", mScore.toString());
+ mScore.sum = 2.99;
+ assertEquals("believer", mScore.toString());
+
+ // Disciple is defined as 3 <= score < 4
+ mScore.sum = 3;
+ assertEquals("disciple", mScore.toString());
+ mScore.sum = 3.5;
+ assertEquals("disciple", mScore.toString());
+ mScore.sum = 3.99;
+ assertEquals("disciple", mScore.toString());
+
+ // Teacher is defined as 4 <= score
+ mScore.sum = 4;
+ assertEquals("teacher", mScore.toString());
+ mScore.sum = 4.5;
+ assertEquals("teacher", mScore.toString());
+ mScore.sum = 4.99;
+ assertEquals("teacher", mScore.toString());
+ mScore.sum = 5;
+ assertEquals("teacher", mScore.toString());
+ }
+}
diff --git a/tst/com/p4square/grow/model/SliderQuestionTest.java b/tst/com/p4square/grow/model/SliderQuestionTest.java
new file mode 100644
index 0000000..eaa67b5
--- /dev/null
+++ b/tst/com/p4square/grow/model/SliderQuestionTest.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Tests for SliderQuestion.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class SliderQuestionTest {
+ private static final double DELTA = 1e-4;
+
+ public static void main(String... args) {
+ org.junit.runner.JUnitCore.main(SliderQuestionTest.class.getName());
+ }
+
+ private Question mQuestion;
+
+ @Before
+ public void setUp() {
+ mQuestion = new SliderQuestion();
+ }
+
+ /**
+ * The ScoringEngines are tested extensively independently, so simply
+ * verify that we get the expected results for our input.
+ */
+ @Test
+ public void testScoreAnswer() {
+ Score score = new Score();
+ RecordedAnswer answer = new RecordedAnswer();
+
+ answer.setAnswerId("0.66666");
+ assertTrue(mQuestion.scoreAnswer(score, answer));
+ assertEquals(3, score.sum, DELTA);
+ assertEquals(1, score.count);
+
+ answer.setAnswerId("1");
+ assertTrue(mQuestion.scoreAnswer(score, answer));
+ assertEquals(7, score.sum, DELTA);
+ assertEquals(2, score.count);
+ }
+
+ /**
+ * Verify the correct type string is returned.
+ */
+ @Test
+ public void testType() {
+ assertEquals("slider", mQuestion.getType().toString());
+ }
+}
diff --git a/tst/com/p4square/grow/model/TextQuestionTest.java b/tst/com/p4square/grow/model/TextQuestionTest.java
new file mode 100644
index 0000000..d85ed86
--- /dev/null
+++ b/tst/com/p4square/grow/model/TextQuestionTest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Tests for TextQuestion.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class TextQuestionTest {
+ private static final double DELTA = 1e-4;
+
+ public static void main(String... args) {
+ org.junit.runner.JUnitCore.main(TextQuestionTest.class.getName());
+ }
+
+ private Question mQuestion;
+
+ @Before
+ public void setUp() {
+ mQuestion = new TextQuestion();
+
+ Answer a1 = new Answer();
+ a1.setScore(2);
+
+ Answer a2 = new Answer();
+ a2.setScore(4);
+
+ mQuestion.getAnswers().put("a1", a1);
+ mQuestion.getAnswers().put("a2", a2);
+ }
+
+ /**
+ * The ScoringEngines are tested extensively independently, so simply
+ * verify that we get the expected results for our input.
+ */
+ @Test
+ public void testScoreAnswer() {
+ Score score = new Score();
+ RecordedAnswer answer = new RecordedAnswer();
+
+ answer.setAnswerId("a1");
+ assertTrue(mQuestion.scoreAnswer(score, answer));
+ assertEquals(2, score.sum, DELTA);
+ assertEquals(1, score.count);
+
+ answer.setAnswerId("a2");
+ assertTrue(mQuestion.scoreAnswer(score, answer));
+ assertEquals(6, score.sum, DELTA);
+ assertEquals(2, score.count);
+
+ try {
+ answer.setAnswerId("unknown");
+ assertTrue(mQuestion.scoreAnswer(score, answer));
+ fail("Should have thrown exception.");
+ } catch (IllegalArgumentException e) {
+ }
+ }
+
+ /**
+ * Verify the correct type string is returned.
+ */
+ @Test
+ public void testType() {
+ assertEquals("text", mQuestion.getType().toString());
+ }
+}
diff --git a/tst/com/p4square/grow/model/TrainingRecordTest.java b/tst/com/p4square/grow/model/TrainingRecordTest.java
new file mode 100644
index 0000000..246d6ff
--- /dev/null
+++ b/tst/com/p4square/grow/model/TrainingRecordTest.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.model;
+
+import java.io.InputStream;
+import java.util.Date;
+import java.util.Map;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Test TrainingRecord, Playlist, and Chapter.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class TrainingRecordTest {
+ public static void main(String... args) {
+ org.junit.runner.JUnitCore.main(TrainingRecordTest.class.getName());
+ }
+
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ /**
+ * Test deserialization of a JSON Training record.
+ */
+ @Test
+ public void testDeserialization() throws Exception {
+ InputStream in = getClass().getResourceAsStream("trainingrecord.json");
+ TrainingRecord record = MAPPER.readValue(in, TrainingRecord.class);
+
+ // Last Video
+ assertEquals("teacher-1", record.getLastVideo());
+
+ // Playlist
+ Playlist playlist = record.getPlaylist();
+
+ // Find video successfully
+ VideoRecord r = playlist.find("teacher-1");
+ assertEquals(true, r.getRequired());
+ assertEquals(true, r.getComplete());
+ assertEquals(new Date(1379288806266L), r.getCompletionDate());
+
+ // Find non-existent video
+ r = playlist.find("not-a-video");
+ assertEquals(null, r);
+
+ // isChapterComplete
+ assertTrue(playlist.isChapterComplete("seeker")); // Complete because not required.
+ assertTrue(playlist.isChapterComplete("disciple")); // Required and completed.
+ assertFalse(playlist.isChapterComplete("teacher")); // Not complete.
+
+ // getChapterStatuses
+ Map<String, Boolean> statuses = playlist.getChapterStatuses();
+ assertTrue(statuses.get("seeker")); // Complete because not required.
+ assertTrue(statuses.get("disciple")); // Required and completed.
+ assertFalse(statuses.get("teacher")); // Not complete.
+ }
+
+ /**
+ * Tests for VideoRecord.
+ */
+ @Test
+ public void testVideoRecord() {
+ VideoRecord record = new VideoRecord();
+
+ // Verify defaults
+ assertTrue(record.getRequired());
+ assertFalse(record.getComplete());
+ assertEquals(null, record.getCompletionDate());
+
+ // Verify completion
+ long now = System.currentTimeMillis();
+ record.complete();
+ assertTrue(record.getRequired());
+ assertTrue(record.getComplete());
+ assertTrue(now <= record.getCompletionDate().getTime());
+ }
+
+ /**
+ * Tests for Playlist and Chapter methods not covered in the deserialization test.
+ */
+ @Test
+ public void testPlaylistAndChapter() {
+ // Create a playlist for the test
+ Playlist playlist = new Playlist();
+ playlist.add("chapter1", "video1");
+ playlist.add("chapter1", "video2");
+
+ // Chapter should not be complete
+ assertFalse(playlist.isChapterComplete("chapter1"));
+
+ // We should find the chapter in the map
+ Map<String, Chapter> chapterMap = playlist.getChaptersMap();
+ Chapter chapter1 = chapterMap.get("chapter1");
+ assertTrue(null != chapter1);
+
+ // We should find the videos in the map.
+ Map<String, VideoRecord> videoMap = chapter1.getVideos();
+ assertTrue(null != videoMap.get("video1"));
+ assertTrue(null != videoMap.get("video2"));
+ assertTrue(null == videoMap.get("video3"));
+
+ // Mark the videos as complete
+ VideoRecord video1 = videoMap.get("video1");
+ VideoRecord video2 = videoMap.get("video2");
+ video1.complete();
+ video2.complete();
+
+ // Chapter should be complete now.
+ assertTrue(playlist.isChapterComplete("chapter1"));
+ assertFalse(playlist.isChapterComplete("bogusChapter"));
+ }
+
+ /**
+ * Tests for Playlist default values.
+ */
+ @Test
+ public void testPlaylistDefaults() {
+ Date before = new Date();
+ Playlist p = new Playlist();
+
+ // Verify that a playlist without an explicit lastUpdated date is older than now.
+ assertTrue(p.getLastUpdated().before(before));
+ }
+
+ /**
+ * Tests for the Playlist merge method.
+ */
+ @Test
+ public void testMergePlaylist() {
+ Playlist oldList = new Playlist();
+ oldList.add("chapter1", "video1").setRequired(true);
+ oldList.add("chapter2", "video2").setRequired(false);
+ oldList.add("chapter2", "video3").complete();
+ oldList.setLastUpdated(new Date(100));
+
+ Playlist newList = new Playlist();
+ newList.add("chapter1", "video4").setRequired(true);
+ newList.add("chapter2", "video5").setRequired(false);
+ newList.add("chapter3", "video6").setRequired(false);
+ newList.setLastUpdated(new Date(500));
+
+ // Verify that you can't merge the old into the new
+ newList.merge(oldList);
+ assertTrue(null == newList.find("video2"));
+
+ // Merge the new list into the old and verify results
+ oldList.merge(newList);
+
+ // All Videos Present
+ assertTrue(oldList.find("video1").getRequired());
+ assertFalse(oldList.find("video2").getRequired());
+ assertTrue(oldList.find("video3").getComplete());
+ assertTrue(oldList.find("video4").getRequired());
+ assertFalse(oldList.find("video5").getRequired());
+ assertFalse(oldList.find("video6").getRequired());
+
+ // New Chapter added
+ Map<String, Chapter> chapters = oldList.getChaptersMap();
+ assertEquals(3, chapters.size());
+ assertTrue(null != chapters.get("chapter3"));
+
+ // Date updated
+ assertEquals(newList.getLastUpdated(), oldList.getLastUpdated());
+
+ // Video objects are actually independent
+ VideoRecord oldVideo4 = oldList.find("video4");
+ VideoRecord newVideo4 = newList.find("video4");
+ assertTrue(oldVideo4 != newVideo4);
+ }
+}
diff --git a/tst/com/p4square/grow/model/trainingrecord.json b/tst/com/p4square/grow/model/trainingrecord.json
new file mode 100644
index 0000000..ea214f3
--- /dev/null
+++ b/tst/com/p4square/grow/model/trainingrecord.json
@@ -0,0 +1,18 @@
+{
+ "lastVideo": "teacher-1",
+ "playlist": {
+ "seeker": {
+ "seeker-2":{"required":false,"complete":false,"completionDate":null},
+ "seeker-1":{"required":false,"complete":false,"completionDate":null}
+ },
+ "disciple":{
+ "disciple-8":{"required":true,"complete":true,"completionDate":1379288805010},
+ "disciple-9":{"required":true,"complete":true,"completionDate":1379288805220},
+ "disciple-1":{"required":true,"complete":true,"completionDate":1379288805266}
+ },
+ "teacher":{
+ "teacher-2":{"required":true,"complete":false,"completionDate":null},
+ "teacher-1":{"required":true,"complete":true,"completionDate":1379288806266}
+ }
+ }
+}