From 3102d8bce3426d9cf41aeaf201c360d342677770 Mon Sep 17 00:00:00 2001 From: Jesse Morgan Date: Sat, 9 Apr 2016 14:22:20 -0700 Subject: Switching from Ivy+Ant to Maven. --- src/main/java/com/p4square/grow/model/Answer.java | 142 +++++++++++++++ src/main/java/com/p4square/grow/model/Banner.java | 20 +++ src/main/java/com/p4square/grow/model/Chapter.java | 112 ++++++++++++ .../com/p4square/grow/model/CircleQuestion.java | 89 ++++++++++ .../com/p4square/grow/model/ImageQuestion.java | 24 +++ src/main/java/com/p4square/grow/model/Message.java | 103 +++++++++++ .../com/p4square/grow/model/MessageThread.java | 60 +++++++ .../java/com/p4square/grow/model/Playlist.java | 192 +++++++++++++++++++++ src/main/java/com/p4square/grow/model/Point.java | 79 +++++++++ .../java/com/p4square/grow/model/QuadQuestion.java | 89 ++++++++++ .../com/p4square/grow/model/QuadScoringEngine.java | 49 ++++++ .../java/com/p4square/grow/model/Question.java | 165 ++++++++++++++++++ .../com/p4square/grow/model/RecordedAnswer.java | 34 ++++ src/main/java/com/p4square/grow/model/Score.java | 119 +++++++++++++ .../com/p4square/grow/model/ScoringEngine.java | 26 +++ .../p4square/grow/model/SimpleScoringEngine.java | 26 +++ .../com/p4square/grow/model/SliderQuestion.java | 24 +++ .../p4square/grow/model/SliderScoringEngine.java | 35 ++++ .../java/com/p4square/grow/model/TextQuestion.java | 24 +++ .../com/p4square/grow/model/TrainingRecord.java | 49 ++++++ .../java/com/p4square/grow/model/UserRecord.java | 183 ++++++++++++++++++++ .../java/com/p4square/grow/model/VideoRecord.java | 85 +++++++++ 22 files changed, 1729 insertions(+) create mode 100644 src/main/java/com/p4square/grow/model/Answer.java create mode 100644 src/main/java/com/p4square/grow/model/Banner.java create mode 100644 src/main/java/com/p4square/grow/model/Chapter.java create mode 100644 src/main/java/com/p4square/grow/model/CircleQuestion.java create mode 100644 src/main/java/com/p4square/grow/model/ImageQuestion.java create mode 100644 src/main/java/com/p4square/grow/model/Message.java create mode 100644 src/main/java/com/p4square/grow/model/MessageThread.java create mode 100644 src/main/java/com/p4square/grow/model/Playlist.java create mode 100644 src/main/java/com/p4square/grow/model/Point.java create mode 100644 src/main/java/com/p4square/grow/model/QuadQuestion.java create mode 100644 src/main/java/com/p4square/grow/model/QuadScoringEngine.java create mode 100644 src/main/java/com/p4square/grow/model/Question.java create mode 100644 src/main/java/com/p4square/grow/model/RecordedAnswer.java create mode 100644 src/main/java/com/p4square/grow/model/Score.java create mode 100644 src/main/java/com/p4square/grow/model/ScoringEngine.java create mode 100644 src/main/java/com/p4square/grow/model/SimpleScoringEngine.java create mode 100644 src/main/java/com/p4square/grow/model/SliderQuestion.java create mode 100644 src/main/java/com/p4square/grow/model/SliderScoringEngine.java create mode 100644 src/main/java/com/p4square/grow/model/TextQuestion.java create mode 100644 src/main/java/com/p4square/grow/model/TrainingRecord.java create mode 100644 src/main/java/com/p4square/grow/model/UserRecord.java create mode 100644 src/main/java/com/p4square/grow/model/VideoRecord.java (limited to 'src/main/java/com/p4square/grow/model') diff --git a/src/main/java/com/p4square/grow/model/Answer.java b/src/main/java/com/p4square/grow/model/Answer.java new file mode 100644 index 0000000..a818365 --- /dev/null +++ b/src/main/java/com/p4square/grow/model/Answer.java @@ -0,0 +1,142 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +import org.apache.log4j.Logger; + +/** + * This is the model of an assessment question's answer. + * + * @author Jesse Morgan + */ +public class Answer { + private static final Logger LOG = Logger.getLogger(Answer.class); + + /** + * ScoreType determines how the answer will be scored. + * + */ + public static enum ScoreType { + /** + * This question has no effect on the score. + */ + NONE, + + /** + * The score of this question is part of the average. + */ + AVERAGE, + + /** + * The score of this question is the total score, no other questions + * matter after this point. + */ + TRUMP; + + @Override + public String toString() { + return name().toLowerCase(); + } + } + + private String mAnswerText; + private ScoreType mType; + private float mScoreFactor; + private String mNextQuestionId; + + public Answer() { + mType = ScoreType.AVERAGE; + } + + /** + * @return The text associated with the answer. + */ + public String getText() { + return mAnswerText; + } + + /** + * Set the text associated with the answer. + * @param text The new text. + */ + public void setText(String text) { + mAnswerText = text; + } + + /** + * @return the ScoreType for the Answer. + */ + public ScoreType getType() { + return mType; + } + + /** + * Set the ScoreType for the answer. + * @param type The new ScoreType. + */ + public void setType(ScoreType type) { + mType = type; + } + + /** + * @return the delta of the score if this answer is selected. + */ + public float getScore() { + if (mType == ScoreType.NONE) { + return 0; + } + + return mScoreFactor; + } + + /** + * Set the score delta for this answer. + * @param score The new delta. + */ + public void setScore(float score) { + mScoreFactor = score; + } + + /** + * @return the id of the next question if this answer is selected, or null + * if selecting this answer has no effect. + */ + public String getNextQuestion() { + return mNextQuestionId; + } + + /** + * Set the id of the next question when this answer is selected. + * @param id The next question id or null to proceed as usual. + */ + public void setNextQuestion(String id) { + mNextQuestionId = id; + } + + /** + * Adjust the running score for the selection of this answer. + * @param score The running score to adjust. + * @return true if scoring should continue, false if this answer trumps all. + */ + public boolean score(final Score score) { + switch (getType()) { + case TRUMP: + score.sum = getScore(); + score.count = 1; + return false; // Quit scoring. + + case AVERAGE: + LOG.debug("ScoreType.AVERAGE: { delta: \"" + getScore() + "\" }"); + score.sum += getScore(); + score.count++; + break; + + case NONE: + break; + } + + return true; // Continue scoring + } +} diff --git a/src/main/java/com/p4square/grow/model/Banner.java b/src/main/java/com/p4square/grow/model/Banner.java new file mode 100644 index 0000000..b786b36 --- /dev/null +++ b/src/main/java/com/p4square/grow/model/Banner.java @@ -0,0 +1,20 @@ +/* + * Copyright 2014 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * Page Banner Data. + */ +public class Banner { + private String mHtml; + + public String getHtml() { + return mHtml; + } + + public void setHtml(final String html) { + mHtml = html; + } +} diff --git a/src/main/java/com/p4square/grow/model/Chapter.java b/src/main/java/com/p4square/grow/model/Chapter.java new file mode 100644 index 0000000..3a08e4c --- /dev/null +++ b/src/main/java/com/p4square/grow/model/Chapter.java @@ -0,0 +1,112 @@ +/* + * 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 String mName; + private Map mVideos; + + public Chapter(String name) { + mName = name; + mVideos = new HashMap(); + } + + /** + * Private constructor for JSON decoding. + */ + private Chapter() { + this(null); + } + + /** + * @return The Chapter name. + */ + public String getName() { + return mName; + } + + /** + * Set the chapter name. + * + * @param name The name of the chapter. + */ + public void setName(final String name) { + mName = name; + } + + /** + * @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); + } + + /** + * Remove the VideoRecord for a video id. + * @param videoId The id to remove. + */ + public void removeVideoRecord(String videoId) { + mVideos.remove(videoId); + } + + /** + * @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(mName); + for (Map.Entry videoEntry : mVideos.entrySet()) { + c.setVideoRecord(videoEntry.getKey(), videoEntry.getValue().clone()); + } + return c; + } +} diff --git a/src/main/java/com/p4square/grow/model/CircleQuestion.java b/src/main/java/com/p4square/grow/model/CircleQuestion.java new file mode 100644 index 0000000..71acc14 --- /dev/null +++ b/src/main/java/com/p4square/grow/model/CircleQuestion.java @@ -0,0 +1,89 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * Circle Question. + * + * @author Jesse Morgan + */ +public class CircleQuestion extends Question { + private static final ScoringEngine ENGINE = new QuadScoringEngine(); + + private String mTopLeft; + private String mTopRight; + private String mBottomLeft; + private String mBottomRight; + + /** + * @return the Top Left label. + */ + public String getTopLeft() { + return mTopLeft; + } + + /** + * Set the Top Left label. + * @param s The new top left label. + */ + public void setTopLeft(String s) { + mTopLeft = s; + } + + /** + * @return the Top Right label. + */ + public String getTopRight() { + return mTopRight; + } + + /** + * Set the Top Right label. + * @param s The new top left label. + */ + public void setTopRight(String s) { + mTopRight = s; + } + + /** + * @return the Bottom Left label. + */ + public String getBottomLeft() { + return mBottomLeft; + } + + /** + * Set the Bottom Left label. + * @param s The new top left label. + */ + public void setBottomLeft(String s) { + mBottomLeft = s; + } + + /** + * @return the Bottom Right label. + */ + public String getBottomRight() { + return mBottomRight; + } + + /** + * Set the Bottom Right label. + * @param s The new top left label. + */ + public void setBottomRight(String s) { + mBottomRight = s; + } + + @Override + public boolean scoreAnswer(Score score, RecordedAnswer answer) { + return ENGINE.scoreAnswer(score, this, answer); + } + + @Override + public QuestionType getType() { + return QuestionType.CIRCLE; + } +} diff --git a/src/main/java/com/p4square/grow/model/ImageQuestion.java b/src/main/java/com/p4square/grow/model/ImageQuestion.java new file mode 100644 index 0000000..d94c32c --- /dev/null +++ b/src/main/java/com/p4square/grow/model/ImageQuestion.java @@ -0,0 +1,24 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * Image Question. + * + * @author Jesse Morgan + */ +public class ImageQuestion extends Question { + private static final ScoringEngine ENGINE = new SimpleScoringEngine(); + + @Override + public boolean scoreAnswer(Score score, RecordedAnswer answer) { + return ENGINE.scoreAnswer(score, this, answer); + } + + @Override + public QuestionType getType() { + return QuestionType.IMAGE; + } +} diff --git a/src/main/java/com/p4square/grow/model/Message.java b/src/main/java/com/p4square/grow/model/Message.java new file mode 100644 index 0000000..9d33320 --- /dev/null +++ b/src/main/java/com/p4square/grow/model/Message.java @@ -0,0 +1,103 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +import java.util.Date; +import java.util.UUID; + +/** + * A feed message. + * + * @author Jesse Morgan + */ +public class Message { + private String mThreadId; + private String mId; + private UserRecord mAuthor; + private Date mCreated; + private String mMessage; + + /** + * @return a new message id. + */ + public static String generateId() { + return String.format("%x-%s", System.currentTimeMillis(), UUID.randomUUID().toString()); + } + + /** + * @return The id of the thread that the message belongs to. + */ + public String getThreadId() { + return mThreadId; + } + + /** + * Set the id of the thread that the message belongs to. + * @param id The new thread id. + */ + public void setThreadId(String id) { + mThreadId = id; + } + + /** + * @return The id the message. + */ + public String getId() { + return mId; + } + + /** + * Set the id of the message. + * @param id The new message id. + */ + public void setId(String id) { + mId = id; + } + + /** + * @return The author of the message. + */ + public UserRecord getAuthor() { + return mAuthor; + } + + /** + * Set the author of the message. + * @param author The new author. + */ + public void setAuthor(UserRecord author) { + mAuthor = author; + } + + /** + * @return The Date the message was created. + */ + public Date getCreated() { + return mCreated; + } + + /** + * Set the Date the message was created. + * @param date The new creation date. + */ + public void setCreated(Date date) { + mCreated = date; + } + + /** + * @return The message text. + */ + public String getMessage() { + return mMessage; + } + + /** + * Set the message text. + * @param text The message text. + */ + public void setMessage(String text) { + mMessage = text; + } +} diff --git a/src/main/java/com/p4square/grow/model/MessageThread.java b/src/main/java/com/p4square/grow/model/MessageThread.java new file mode 100644 index 0000000..9542a18 --- /dev/null +++ b/src/main/java/com/p4square/grow/model/MessageThread.java @@ -0,0 +1,60 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +import java.util.UUID; + +/** + * + * @author Jesse Morgan + */ +public class MessageThread { + private String mId; + private Message mMessage; + + /** + * Create a new thread with a probably unique id. + * + * @return the new thread. + */ + public static MessageThread createNew() { + MessageThread t = new MessageThread(); + // IDs are keyed to sort lexicographically from latest to oldest. + t.setId(String.format("%016x-%s", Long.MAX_VALUE - System.currentTimeMillis(), + UUID.randomUUID().toString())); + + return t; + } + + /** + * @return The id the message. + */ + public String getId() { + return mId; + } + + /** + * Set the id of the message. + * @param id The new message id. + */ + public void setId(String id) { + mId = id; + } + + /** + * @return The original message. + */ + public Message getMessage() { + return mMessage; + } + + /** + * Set the original message. + * @param id The new message. + */ + public void setMessage(Message message) { + mMessage = message; + } +} diff --git a/src/main/java/com/p4square/grow/model/Playlist.java b/src/main/java/com/p4square/grow/model/Playlist.java new file mode 100644 index 0000000..3e77ada --- /dev/null +++ b/src/main/java/com/p4square/grow/model/Playlist.java @@ -0,0 +1,192 @@ +/* + * 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; + } + + /** + * @param videoId The video to search for. + * @return the Chapter containing videoId. + */ + private Chapter findChapter(String videoId) { + for (Chapter chapter : mPlaylist.values()) { + VideoRecord r = chapter.getVideoRecord(videoId); + + if (r != null) { + return chapter; + } + } + + 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(chapterId); + 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) { + chapter.setName(chapterId); + 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 new chapter + myChapter = new Chapter(chapterName); + addChapter(chapterName, myChapter); + } + + // Check chapter for missing videos + for (Map.Entry videoEntry : theirChapter.getVideos().entrySet()) { + String videoId = videoEntry.getKey(); + VideoRecord myVideo = myChapter.getVideoRecord(videoId); + + if (myVideo == null) { + myVideo = find(videoId); + if (myVideo == null) { + // New Video + try { + myVideo = videoEntry.getValue().clone(); + myChapter.setVideoRecord(videoId, myVideo); + } catch (CloneNotSupportedException e) { + throw new RuntimeException(e); // Unexpected... + } + } else { + // Video moved + findChapter(videoId).removeVideoRecord(videoId); + myChapter.setVideoRecord(videoId, myVideo); + } + } + } + } + + mLastUpdated = source.getLastUpdated(); + } +} diff --git a/src/main/java/com/p4square/grow/model/Point.java b/src/main/java/com/p4square/grow/model/Point.java new file mode 100644 index 0000000..e9fc0ca --- /dev/null +++ b/src/main/java/com/p4square/grow/model/Point.java @@ -0,0 +1,79 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * Simple double based point class. + * + * @author Jesse Morgan + */ +public class Point { + /** + * Parse a comma separated x,y pair into a point. + * + * @return The point represented by the string. + * @throws IllegalArgumentException if the input is malformed. + */ + public static Point valueOf(String str) { + final int comma = str.indexOf(','); + if (comma == -1 || comma == 0 || comma == str.length() - 1) { + throw new IllegalArgumentException("Malformed point string"); + } + + final String sX = str.substring(0, comma); + final String sY = str.substring(comma + 1); + + return new Point(Double.valueOf(sX), Double.valueOf(sY)); + } + + private final double mX; + private final double mY; + + /** + * Create a new point with the given coordinates. + * + * @param x The x coordinate. + * @param y The y coordinate. + */ + public Point(double x, double y) { + mX = x; + mY = y; + } + + /** + * Compute the distance between this point and another. + * + * @param other The other point. + * @return The distance between this point and other. + */ + public double distance(Point other) { + final double dx = mX - other.mX; + final double dy = mY - other.mY; + + return Math.sqrt(dx*dx + dy*dy); + } + + /** + * @return The x coordinate. + */ + public double getX() { + return mX; + } + + /** + * @return The y coordinate. + */ + public double getY() { + return mY; + } + + /** + * @return The point represented as a comma separated pair. + */ + @Override + public String toString() { + return String.format("%.2f,%.2f", mX, mY); + } +} diff --git a/src/main/java/com/p4square/grow/model/QuadQuestion.java b/src/main/java/com/p4square/grow/model/QuadQuestion.java new file mode 100644 index 0000000..a7b4179 --- /dev/null +++ b/src/main/java/com/p4square/grow/model/QuadQuestion.java @@ -0,0 +1,89 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * Two-dimensional Question. + * + * @author Jesse Morgan + */ +public class QuadQuestion extends Question { + private static final ScoringEngine ENGINE = new QuadScoringEngine(); + + private String mTop; + private String mRight; + private String mBottom; + private String mLeft; + + /** + * @return the top label. + */ + public String getTop() { + return mTop; + } + + /** + * Set the top label. + * @param s The new top label. + */ + public void setTop(String s) { + mTop = s; + } + + /** + * @return the right label. + */ + public String getRight() { + return mRight; + } + + /** + * Set the right label. + * @param s The new right label. + */ + public void setRight(String s) { + mRight = s; + } + + /** + * @return the bottom label. + */ + public String getBottom() { + return mBottom; + } + + /** + * Set the bottom label. + * @param s The new bottom label. + */ + public void setBottom(String s) { + mBottom = s; + } + + /** + * @return the left label. + */ + public String getLeft() { + return mLeft; + } + + /** + * Set the left label. + * @param s The new left label. + */ + public void setLeft(String s) { + mLeft = s; + } + + @Override + public boolean scoreAnswer(Score score, RecordedAnswer answer) { + return ENGINE.scoreAnswer(score, this, answer); + } + + @Override + public QuestionType getType() { + return QuestionType.QUAD; + } +} diff --git a/src/main/java/com/p4square/grow/model/QuadScoringEngine.java b/src/main/java/com/p4square/grow/model/QuadScoringEngine.java new file mode 100644 index 0000000..33403b5 --- /dev/null +++ b/src/main/java/com/p4square/grow/model/QuadScoringEngine.java @@ -0,0 +1,49 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +import com.p4square.grow.model.Point; + +/** + * QuadScoringEngine expects the user's answer to be a Point string. We find + * the closest answer Point to the user's answer and treat that as the answer. + * + * @author Jesse Morgan + */ +public class QuadScoringEngine extends ScoringEngine { + + @Override + public boolean scoreAnswer(Score score, Question question, RecordedAnswer userAnswer) { + // Find all of the answer points. + Point[] answers = new Point[question.getAnswers().size()]; + { + int i = 0; + for (String answerStr : question.getAnswers().keySet()) { + answers[i++] = Point.valueOf(answerStr); + } + } + + // Parse the user's answer. + Point userPoint = Point.valueOf(userAnswer.getAnswerId()); + + // Find the closest answer point to the user's answer. + double minDistance = Double.MAX_VALUE; + int answerIndex = 0; + for (int i = 0; i < answers.length; i++) { + final double distance = userPoint.distance(answers[i]); + if (distance < minDistance) { + minDistance = distance; + answerIndex = i; + } + } + + LOG.debug("Quad " + question.getId() + ": Got answer " + + answers[answerIndex].toString() + " for user point " + userAnswer); + + // Get the answer and update the score. + final Answer answer = question.getAnswers().get(answers[answerIndex].toString()); + return answer.score(score); + } +} diff --git a/src/main/java/com/p4square/grow/model/Question.java b/src/main/java/com/p4square/grow/model/Question.java new file mode 100644 index 0000000..f4b9458 --- /dev/null +++ b/src/main/java/com/p4square/grow/model/Question.java @@ -0,0 +1,165 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +/** + * Model of an assessment question. + * + * @author Jesse Morgan + */ +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type") +@JsonSubTypes({ + @Type(value = TextQuestion.class, name = "text"), + @Type(value = ImageQuestion.class, name = "image"), + @Type(value = SliderQuestion.class, name = "slider"), + @Type(value = QuadQuestion.class, name = "quad"), + @Type(value = CircleQuestion.class, name = "circle"), +}) +public abstract class Question { + /** + * QuestionType indicates the type of Question. + * + * @author Jesse Morgan + */ + public enum QuestionType { + TEXT, + IMAGE, + SLIDER, + QUAD, + CIRCLE; + + @Override + public String toString() { + return name().toLowerCase(); + } + } + + private String mQuestionId; + private QuestionType mType; + private String mQuestionText; + private Map mAnswers; + + private String mPreviousQuestionId; + private String mNextQuestionId; + + public Question() { + mAnswers = new HashMap(); + } + + /** + * @return the id String for this question. + */ + public String getId() { + return mQuestionId; + } + + /** + * Set the id String for this question. + * @param id New id + */ + public void setId(String id) { + mQuestionId = id; + } + + /** + * @return The Question text. + */ + public String getQuestion() { + return mQuestionText; + } + + /** + * Set the question text. + * @param value The new question text. + */ + public void setQuestion(String value) { + mQuestionText = value; + } + + /** + * @return The id String of the previous question or null if no previous question exists. + */ + public String getPreviousQuestion() { + return mPreviousQuestionId; + } + + /** + * Set the id string of the previous question. + * @param id Previous question id or null if there is no previous question. + */ + public void setPreviousQuestion(String id) { + mPreviousQuestionId = id; + } + + /** + * @return The id String of the next question or null if no next question exists. + */ + public String getNextQuestion() { + return mNextQuestionId; + } + + /** + * Set the id string of the next question. + * @param id next question id or null if there is no next question. + */ + public void setNextQuestion(String id) { + mNextQuestionId = id; + } + + /** + * @return a map of Answer id Strings to Answer objects. + */ + public Map getAnswers() { + return mAnswers; + } + + /** + * Determine the id of the next question based on the answer to this + * question. + * + * @param answerid + * The id of the selected answer. + * @return a question id or null if this is the last question. + */ + public String getNextQuestion(String answerid) { + String nextQuestion = null; + + Answer a = mAnswers.get(answerid); + if (a != null) { + nextQuestion = a.getNextQuestion(); + } + + if (nextQuestion == null) { + nextQuestion = mNextQuestionId; + } + + return nextQuestion; + } + + /** + * Update the score based on the answer to this question. + * + * @param score The running score to update. + * @param answer The answer give to this question. + * @return true if scoring should continue, false if this answer trumps everything else. + */ + public abstract boolean scoreAnswer(Score score, RecordedAnswer answer); + + /** + * @return the QuestionType of this question. + */ + public abstract QuestionType getType(); + +} diff --git a/src/main/java/com/p4square/grow/model/RecordedAnswer.java b/src/main/java/com/p4square/grow/model/RecordedAnswer.java new file mode 100644 index 0000000..7d9905d --- /dev/null +++ b/src/main/java/com/p4square/grow/model/RecordedAnswer.java @@ -0,0 +1,34 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * Simple model for a user's assessment answer. + * + * @author Jesse Morgan + */ +public class RecordedAnswer { + private String mAnswerId; + + /** + * @return The user's answer. + */ + public String getAnswerId() { + return mAnswerId; + } + + /** + * Set the answer id field. + * @param id The new id. + */ + public void setAnswerId(String id) { + mAnswerId = id; + } + + @Override + public String toString() { + return mAnswerId; + } +} diff --git a/src/main/java/com/p4square/grow/model/Score.java b/src/main/java/com/p4square/grow/model/Score.java new file mode 100644 index 0000000..031c309 --- /dev/null +++ b/src/main/java/com/p4square/grow/model/Score.java @@ -0,0 +1,119 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * Simple structure containing a score's sum and count. + * + * @author Jesse Morgan + */ +public class Score { + /** + * Return the decimal value for the given Score String. + * + * This method satisfies the invariant for Score x: + * numericScore(x.toString()) <= x.getScore() + */ + public static double numericScore(String score) { + score = score.toLowerCase(); + + if ("teacher".equals(score)) { + return 3.5; + } else if ("disciple".equals(score)) { + return 2.5; + } else if ("believer".equals(score)) { + return 1.5; + } else if ("seeker".equals(score)) { + return 0; + } else { + return Integer.MAX_VALUE; + } + } + + double sum; + int count; + + public Score() { + sum = 0; + count = 0; + } + + public Score(double sum, int count) { + this.sum = sum; + this.count = count; + } + + /** + * Copy Constructor. + */ + public Score(Score other) { + sum = other.sum; + count = other.count; + } + + /** + * @return The sum of all the points. + */ + public double getSum() { + return sum; + } + + /** + * @return The number of questions included in the score. + */ + public int getCount() { + return count; + } + + /** + * @return The final score. + */ + public double getScore() { + if (count == 0) { + return 0; + } + + return sum / count; + } + + /** + * @return the lowest score in the same category as this score. + */ + public double floor() { + final double score = getScore(); + + if (score >= 3.5) { + return 3.5; // teacher + + } else if (score >= 2.5) { + return 2.5; // disciple + + } else if (score >= 1.5) { + return 1.5; // believer + + } else { + return 0; // seeker + } + } + + @Override + public String toString() { + final double score = getScore(); + + if (score >= 3.5) { + return "teacher"; + + } else if (score >= 2.5) { + return "disciple"; + + } else if (score >= 1.5) { + return "believer"; + + } else { + return "seeker"; + } + } + +} diff --git a/src/main/java/com/p4square/grow/model/ScoringEngine.java b/src/main/java/com/p4square/grow/model/ScoringEngine.java new file mode 100644 index 0000000..8ff18b3 --- /dev/null +++ b/src/main/java/com/p4square/grow/model/ScoringEngine.java @@ -0,0 +1,26 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +import org.apache.log4j.Logger; + +/** + * ScoringEngine computes the score for a question and a given answer. + * + * @author Jesse Morgan + */ +public abstract class ScoringEngine { + protected static final Logger LOG = Logger.getLogger(ScoringEngine.class); + + /** + * Update the score based on the given question and answer. + * + * @param score The running score to update. + * @param question The question to compute the score for. + * @param answer The answer give to this question. + * @return true if scoring should continue, false if this answer trumps everything else. + */ + public abstract boolean scoreAnswer(Score score, Question question, RecordedAnswer answer); +} diff --git a/src/main/java/com/p4square/grow/model/SimpleScoringEngine.java b/src/main/java/com/p4square/grow/model/SimpleScoringEngine.java new file mode 100644 index 0000000..6ef2dbb --- /dev/null +++ b/src/main/java/com/p4square/grow/model/SimpleScoringEngine.java @@ -0,0 +1,26 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * SimpleScoringEngine expects the user's answer to a valid answer id and + * scores accordingly. + * + * If the answer id is not valid an Exception is thrown. + * + * @author Jesse Morgan + */ +public class SimpleScoringEngine extends ScoringEngine { + + @Override + public boolean scoreAnswer(Score score, Question question, RecordedAnswer userAnswer) { + final Answer answer = question.getAnswers().get(userAnswer.getAnswerId()); + if (answer == null) { + throw new IllegalArgumentException("Not a valid answer."); + } + + return answer.score(score); + } +} diff --git a/src/main/java/com/p4square/grow/model/SliderQuestion.java b/src/main/java/com/p4square/grow/model/SliderQuestion.java new file mode 100644 index 0000000..f0861e3 --- /dev/null +++ b/src/main/java/com/p4square/grow/model/SliderQuestion.java @@ -0,0 +1,24 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * Slider Question. + * + * @author Jesse Morgan + */ +public class SliderQuestion extends Question { + private static final ScoringEngine ENGINE = new SliderScoringEngine(); + + @Override + public boolean scoreAnswer(Score score, RecordedAnswer answer) { + return ENGINE.scoreAnswer(score, this, answer); + } + + @Override + public QuestionType getType() { + return QuestionType.SLIDER; + } +} diff --git a/src/main/java/com/p4square/grow/model/SliderScoringEngine.java b/src/main/java/com/p4square/grow/model/SliderScoringEngine.java new file mode 100644 index 0000000..2961e95 --- /dev/null +++ b/src/main/java/com/p4square/grow/model/SliderScoringEngine.java @@ -0,0 +1,35 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * SliderScoringEngine expects the user's answer to be a decimal value in the + * range [0, 1]. The value is scaled to the range [1, 4] and added to the + * score. + * + * @author Jesse Morgan + */ +public class SliderScoringEngine extends ScoringEngine { + + @Override + public boolean scoreAnswer(Score score, Question question, RecordedAnswer userAnswer) { + int numberOfAnswers = question.getAnswers().size(); + if (numberOfAnswers == 0) { + throw new IllegalArgumentException("Question has no answers."); + } + + double answer = Double.valueOf(userAnswer.getAnswerId()); + if (answer < 0 || answer > 1) { + throw new IllegalArgumentException("Answer out of bounds."); + } + + double delta = Math.max(1, Math.ceil(answer * numberOfAnswers) / numberOfAnswers * 4); + + score.sum += delta; + score.count++; + + return true; + } +} diff --git a/src/main/java/com/p4square/grow/model/TextQuestion.java b/src/main/java/com/p4square/grow/model/TextQuestion.java new file mode 100644 index 0000000..88c2a34 --- /dev/null +++ b/src/main/java/com/p4square/grow/model/TextQuestion.java @@ -0,0 +1,24 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * Text Question. + * + * @author Jesse Morgan + */ +public class TextQuestion extends Question { + private static final ScoringEngine ENGINE = new SimpleScoringEngine(); + + @Override + public boolean scoreAnswer(Score score, RecordedAnswer answer) { + return ENGINE.scoreAnswer(score, this, answer); + } + + @Override + public QuestionType getType() { + return QuestionType.TEXT; + } +} diff --git a/src/main/java/com/p4square/grow/model/TrainingRecord.java b/src/main/java/com/p4square/grow/model/TrainingRecord.java new file mode 100644 index 0000000..bc3ffa9 --- /dev/null +++ b/src/main/java/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/main/java/com/p4square/grow/model/UserRecord.java b/src/main/java/com/p4square/grow/model/UserRecord.java new file mode 100644 index 0000000..4399282 --- /dev/null +++ b/src/main/java/com/p4square/grow/model/UserRecord.java @@ -0,0 +1,183 @@ +/* + * Copyright 2014 Jesse Morgan + */ + +package com.p4square.grow.model; + +import java.io.IOException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import org.apache.commons.codec.binary.Hex; + +import org.restlet.security.User; + +/** + * A simple user representation without any secrets. + */ +public class UserRecord { + private String mId; + private String mFirstName; + private String mLastName; + private String mEmail; + private String mLanding; + private boolean mNewBeliever; + + // Backend Access + private String mBackendPasswordHash; + + /** + * Create an empty UserRecord. + */ + public UserRecord() { + } + + /** + * Create a new UserRecord with the information from a User. + */ + public UserRecord(final User user) { + mId = user.getIdentifier(); + mFirstName = user.getFirstName(); + mLastName = user.getLastName(); + mEmail = user.getEmail(); + } + + /** + * @return The user's identifier. + */ + public String getId() { + return mId; + } + + /** + * Set the user's identifier. + * @param value The new id. + */ + public void setId(final String value) { + mId = value; + } + + /** + * @return The user's email. + */ + public String getEmail() { + return mEmail; + } + + /** + * Set the user's email. + * @param value The new email. + */ + public void setEmail(final String value) { + mEmail = value; + } + + /** + * @return The user's first name. + */ + public String getFirstName() { + return mFirstName; + } + + /** + * Set the user's first name. + * @param value The new first name. + */ + public void setFirstName(final String value) { + mFirstName = value; + } + + /** + * @return The user's last name. + */ + public String getLastName() { + return mLastName; + } + + /** + * Set the user's last name. + * @param value The new last name. + */ + public void setLastName(final String value) { + mLastName = value; + } + + /** + * @return The user's landing page. + */ + public String getLanding() { + return mLanding; + } + + /** + * Set the user's landing page. + * @param value The new landing page. + */ + public void setLanding(final String value) { + mLanding = value; + } + + /** + * @return true if the user came from the New Believer's landing. + */ + public boolean getNewBeliever() { + return mNewBeliever; + } + + /** + * Set the user's new believer flag. + * @param value The new flag. + */ + public void setNewBeliever(final boolean value) { + mNewBeliever = value; + } + + /** + * @return The user's backend password hash, null if he doesn't have + * access. + */ + public String getBackendPasswordHash() { + return mBackendPasswordHash; + } + + /** + * Set the user's backend password hash. + * @param value The new backend password hash or null to remove + * access. + */ + public void setBackendPasswordHash(final String value) { + mBackendPasswordHash = value; + } + + /** + * Set the user's backend password to the clear-text value given. + * @param value The new backend password. + */ + public void setBackendPassword(final String value) { + try { + mBackendPasswordHash = hashPassword(value); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + /** + * Hash the given secret. + */ + public static String hashPassword(final String secret) throws NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance("SHA-1"); + + // Convert the char[] to byte[] + // FIXME This approach is incorrectly truncating multibyte + // characters. + byte[] b = new byte[secret.length()]; + for (int i = 0; i < secret.length(); i++) { + b[i] = (byte) secret.charAt(i); + } + + md.update(b); + + byte[] hash = md.digest(); + return new String(Hex.encodeHex(hash)); + } +} diff --git a/src/main/java/com/p4square/grow/model/VideoRecord.java b/src/main/java/com/p4square/grow/model/VideoRecord.java new file mode 100644 index 0000000..ec99d0d --- /dev/null +++ b/src/main/java/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; + } +} -- cgit v1.2.3