From 0d90da39f77ac3cfa607a68bc59336bf0bdff240 Mon Sep 17 00:00:00 2001 From: Jesse Morgan Date: Sat, 9 Nov 2013 15:24:56 -0800 Subject: 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. --- devfiles/scripts/compile-videos.sh | 18 +- devfiles/videos/playlist.json | 46 +++++ src/com/p4square/grow/backend/GrowBackend.java | 30 ++- src/com/p4square/grow/backend/db/CassandraKey.java | 8 +- .../grow/backend/db/CassandraProviderImpl.java | 12 +- .../db/CassandraTrainingRecordProvider.java | 71 ++++++++ .../p4square/grow/backend/resources/Playlist.java | 134 -------------- .../grow/backend/resources/SurveyResource.java | 2 +- .../backend/resources/SurveyResultsResource.java | 2 +- .../backend/resources/TrainingRecordResource.java | 202 ++++++++------------- .../grow/backend/resources/VideoRecord.java | 56 ------ .../grow/frontend/ChapterCompletePage.java | 24 ++- .../grow/frontend/TrainingPageResource.java | 53 +++--- src/com/p4square/grow/frontend/VideosResource.java | 2 +- src/com/p4square/grow/model/Chapter.java | 79 ++++++++ src/com/p4square/grow/model/Playlist.java | 171 +++++++++++++++++ src/com/p4square/grow/model/Question.java | 6 +- src/com/p4square/grow/model/TrainingRecord.java | 49 +++++ src/com/p4square/grow/model/VideoRecord.java | 85 +++++++++ .../grow/provider/JsonEncodedProvider.java | 12 +- .../p4square/grow/provider/ProvidesQuestions.java | 19 ++ .../grow/provider/ProvidesTrainingRecords.java | 19 ++ .../grow/provider/TrainingRecordProvider.java | 41 +++++ .../p4square/grow/model/CircleQuestionTest.java | 92 ++++++++++ tst/com/p4square/grow/model/ImageQuestionTest.java | 74 ++++++++ tst/com/p4square/grow/model/QuadQuestionTest.java | 92 ++++++++++ tst/com/p4square/grow/model/ScoreTest.java | 111 +++++++++++ .../p4square/grow/model/SliderQuestionTest.java | 58 ++++++ tst/com/p4square/grow/model/TextQuestionTest.java | 74 ++++++++ .../p4square/grow/model/TrainingRecordTest.java | 178 ++++++++++++++++++ tst/com/p4square/grow/model/trainingrecord.json | 18 ++ 31 files changed, 1453 insertions(+), 385 deletions(-) create mode 100644 devfiles/videos/playlist.json create mode 100644 src/com/p4square/grow/backend/db/CassandraTrainingRecordProvider.java delete mode 100644 src/com/p4square/grow/backend/resources/Playlist.java delete mode 100644 src/com/p4square/grow/backend/resources/VideoRecord.java create mode 100644 src/com/p4square/grow/model/Chapter.java create mode 100644 src/com/p4square/grow/model/Playlist.java create mode 100644 src/com/p4square/grow/model/TrainingRecord.java create mode 100644 src/com/p4square/grow/model/VideoRecord.java create mode 100644 src/com/p4square/grow/provider/ProvidesQuestions.java create mode 100644 src/com/p4square/grow/provider/ProvidesTrainingRecords.java create mode 100644 src/com/p4square/grow/provider/TrainingRecordProvider.java create mode 100644 tst/com/p4square/grow/model/CircleQuestionTest.java create mode 100644 tst/com/p4square/grow/model/ImageQuestionTest.java create mode 100644 tst/com/p4square/grow/model/QuadQuestionTest.java create mode 100644 tst/com/p4square/grow/model/ScoreTest.java create mode 100644 tst/com/p4square/grow/model/SliderQuestionTest.java create mode 100644 tst/com/p4square/grow/model/TextQuestionTest.java create mode 100644 tst/com/p4square/grow/model/TrainingRecordTest.java create mode 100644 tst/com/p4square/grow/model/trainingrecord.json 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 */ -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 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(new CassandraProviderImpl(mDatabase, "strings", Question.class)) { + mQuestionProvider = new QuestionProvider(new CassandraProviderImpl(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 getQuestionProvider() { return mQuestionProvider; } + @Override + public Provider 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 */ 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 extends JsonEncodedProvider { private final CassandraDatabase mDb; - private final String mColumnFamily; - public CassandraProviderImpl(CassandraDatabase db, String columnFamily, Class clazz) { + public CassandraProviderImpl(CassandraDatabase db, Class 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 + */ +public class CassandraTrainingRecordProvider implements Provider { + 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 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 - */ -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> playlist = - MAPPER.readValue(playlistString, new TypeReference>>() { }); - - 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> mPlaylist; - - /** - * Construct an empty playlist. - */ - public Playlist() { - mPlaylist = new HashMap>(); - } - - /** - * Constructor for database initialization. - */ - private Playlist(Map> playlist) { - mPlaylist = playlist; - } - - public VideoRecord find(String videoId) { - for (Map 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 chapterMap = mPlaylist.get(chapter); - - if (chapterMap == null) { - chapterMap = new HashMap(); - mPlaylist.put(chapter, chapterMap); - } - - VideoRecord r = new VideoRecord(); - chapterMap.put(videoId, r); - return r; - } - - /** - * @return The last chapter to be completed. - */ - public Map getChapterStatuses() { - Map completed = new HashMap(); - - for (String chapter : mPlaylist.keySet()) { - completed.put(chapter, isChapterComplete(chapter)); - } - - return completed; - } - - public boolean isChapterComplete(String chapterId) { - boolean complete = true; - - Map 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 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(video); break; case SUMMARY: - result = buildSummary(); + rep = new JacksonRepresentation(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 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 row = mDb.getRow("training", mUserId); - if (!row.isEmpty()) { - boolean first = true; - for (Column 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 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 row = mDb.getRow("strings", "/training/" + chapter); - if (!row.isEmpty()) { - for (Column c : row) { - VideoRecord r = playlist.add(chapter, c.getName()); - r.setRequired(required); - } - } - } - - Playlist.save(mDb, mUserId, playlist); - - return playlist; - } } diff --git a/src/com/p4square/grow/backend/resources/VideoRecord.java b/src/com/p4square/grow/backend/resources/VideoRecord.java deleted file mode 100644 index 2ba28c3..0000000 --- a/src/com/p4square/grow/backend/resources/VideoRecord.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.backend.resources; - -import java.util.Date; - -/** - * Simple bean containing video completion data. - * - * @author Jesse Morgan - */ -class VideoRecord { - private boolean mComplete; - private boolean mRequired; - private Date mCompletionDate; - - public VideoRecord() { - mComplete = false; - mRequired = true; - mCompletionDate = null; - } - - public boolean getComplete() { - return mComplete; - } - - public void setComplete(boolean complete) { - mComplete = complete; - } - - public boolean getRequired() { - return mRequired; - } - - public void setRequired(boolean complete) { - mRequired = complete; - } - - public Date getCompletionDate() { - return mCompletionDate; - } - - public void setCompletionDate(Date date) { - mCompletionDate = date; - } - - /** - * Convenience method to mark a video complete. - */ - public void complete() { - mComplete = true; - mCompletionDate = new Date(); - } -} diff --git a/src/com/p4square/grow/frontend/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 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(new JsonRequestProvider(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 root = getRootObject(); // Get the training summary - Map trainingRecord = null; - Map chapters = null; - { - JsonResponse response = backendGet("/accounts/" + mUserId + "/training"); - if (response.getStatus().isSuccess()) { - trainingRecord = response.getMap(); - chapters = (Map) 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 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 */ public class TrainingPageResource extends FreeMarkerPageResource { @@ -43,6 +48,8 @@ public class TrainingPageResource extends FreeMarkerPageResource { private Template mTrainingTemplate; private JsonRequestClient mJsonClient; + private Provider 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(new JsonRequestProvider(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 trainingRecord = null; - Map completedVideos = new HashMap(); - Map chapters = null; - { - JsonResponse response = backendGet("/accounts/" + mUserId + "/training"); - if (response.getStatus().isSuccess()) { - trainingRecord = response.getMap(); - completedVideos = (Map) trainingRecord.get("videos"); - chapters = (Map) 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 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 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 data = new HashMap(); - 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 + */ +public class Chapter implements Cloneable { + private Map mVideos; + + public Chapter() { + mVideos = new HashMap(); + } + + /** + * @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 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 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 + */ +public class Playlist { + /** + * Map of Chapter ID to map of Video ID to VideoRecord. + */ + private Map mPlaylist; + + private Date mLastUpdated; + + /** + * Construct an empty playlist. + */ + public Playlist() { + mPlaylist = new HashMap(); + 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 getChaptersMap() { + return mPlaylist; + } + + /** + * @return The last chapter to be completed. + */ + @JsonIgnore + public Map getChapterStatuses() { + Map completed = new HashMap(); + + for (Map.Entry 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 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 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 + */ +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/model/VideoRecord.java b/src/com/p4square/grow/model/VideoRecord.java new file mode 100644 index 0000000..ec99d0d --- /dev/null +++ b/src/com/p4square/grow/model/VideoRecord.java @@ -0,0 +1,85 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +import java.util.Date; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * Simple bean containing video completion data. + * + * @author Jesse Morgan + */ +public class VideoRecord implements Cloneable { + private Boolean mComplete; + private Boolean mRequired; + private Date mCompletionDate; + + public VideoRecord() { + mComplete = null; + mRequired = null; + mCompletionDate = null; + } + + public boolean getComplete() { + if (mComplete == null) { + return false; + } + return mComplete; + } + + public void setComplete(boolean complete) { + mComplete = complete; + } + + @JsonIgnore + public boolean isCompleteSet() { + return mComplete != null; + } + + public boolean getRequired() { + if (mRequired == null) { + return true; + } + return mRequired; + } + + public void setRequired(boolean complete) { + mRequired = complete; + } + + @JsonIgnore + public boolean isRequiredSet() { + return mRequired != null; + } + + public Date getCompletionDate() { + return mCompletionDate; + } + + public void setCompletionDate(Date date) { + mCompletionDate = date; + } + + /** + * Convenience method to mark a video complete. + */ + public void complete() { + mComplete = true; + mCompletionDate = new Date(); + } + + /** + * @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 implements Provider { 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 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 + */ +public interface ProvidesQuestions { + /** + * @return A Provider of Questions keyed by question id. + */ + Provider 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 + */ +public interface ProvidesTrainingRecords { + /** + * @return A Provider of Questions keyed by question id. + */ + Provider 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 + */ +public abstract class TrainingRecordProvider implements Provider { + + private Provider mProvider; + + public TrainingRecordProvider(Provider 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 + */ +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 + */ +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 + */ +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 + */ +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 + */ +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 + */ +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 + */ +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 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 chapterMap = playlist.getChaptersMap(); + Chapter chapter1 = chapterMap.get("chapter1"); + assertTrue(null != chapter1); + + // We should find the videos in the map. + Map 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 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} + } + } +} -- cgit v1.2.3