diff options
Diffstat (limited to 'src/com/p4square/grow/backend/resources')
8 files changed, 780 insertions, 0 deletions
diff --git a/src/com/p4square/grow/backend/resources/Answer.java b/src/com/p4square/grow/backend/resources/Answer.java new file mode 100644 index 0000000..5ba1bce --- /dev/null +++ b/src/com/p4square/grow/backend/resources/Answer.java @@ -0,0 +1,57 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.backend.resources; + +import java.util.Map; + +/** + * This is the model of an assessment question's answer. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +class Answer { + public static enum ScoreType { + NONE, AVERAGE, TRUMP; + } + + private final String mAnswerId; + private final String mAnswerText; + private final ScoreType mType; + private final float mScoreFactor; + + public Answer(final String id, final Map<String, Object> answer) { + mAnswerId = id; + mAnswerText = (String) answer.get("text"); + final String typeStr = (String) answer.get("type"); + if (typeStr == null) { + mType = ScoreType.AVERAGE; + } else { + mType = ScoreType.valueOf(typeStr.toUpperCase()); + } + + if (mType != ScoreType.NONE) { + mScoreFactor = Float.valueOf((String) answer.get("score")); + } else { + mScoreFactor = 0; + } + + } + + public String getId() { + return mAnswerId; + } + + public String getText() { + return mAnswerText; + } + + public ScoreType getType() { + return mType; + } + + public float getScoreFactor() { + return mScoreFactor; + } +} diff --git a/src/com/p4square/grow/backend/resources/Point.java b/src/com/p4square/grow/backend/resources/Point.java new file mode 100644 index 0000000..e1b15a8 --- /dev/null +++ b/src/com/p4square/grow/backend/resources/Point.java @@ -0,0 +1,52 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.backend.resources; + +/** + * Simple double based point class. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +class Point { + public static Point valueOf(String str) { + final int comma = str.indexOf(','); + if (comma == -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; + + public Point(double x, double y) { + mX = x; + mY = y; + } + + public double distance(Point other) { + final double dx = mX - other.mX; + final double dy = mY - other.mY; + + return Math.sqrt(dx*dx + dy*dy); + } + + public double getX() { + return mX; + } + + public double getY() { + return mY; + } + + @Override + public String toString() { + return String.format("%.2f,%.2f", mX, mY); + } +} diff --git a/src/com/p4square/grow/backend/resources/Question.java b/src/com/p4square/grow/backend/resources/Question.java new file mode 100644 index 0000000..c53883c --- /dev/null +++ b/src/com/p4square/grow/backend/resources/Question.java @@ -0,0 +1,72 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.backend.resources; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Model of an assessment question. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +class Question { + public static enum QuestionType { + TEXT, IMAGE, SLIDER, QUAD; + } + + private final String mQuestionId; + private final QuestionType mType; + private final String mQuestionText; + private Map<String, Answer> mAnswers; + + private final String mPreviousQuestionId; + private final String mNextQuestionId; + + public Question(final Map<String, Object> map) { + mQuestionId = (String) map.get("id"); + mType = QuestionType.valueOf(((String) map.get("type")).toUpperCase()); + + mQuestionText = (String) map.get("text"); + + mPreviousQuestionId = (String) map.get("previousQuestion"); + mNextQuestionId = (String) map.get("nextQuestion"); + + mAnswers = new HashMap<String, Answer>(); + for (Map.Entry<String, Object> answer : + ((Map<String, Object>) map.get("answers")).entrySet()) { + + final String id = answer.getKey(); + final Map<String, Object> answerMap = (Map<String, Object>) answer.getValue(); + final Answer answerObj = new Answer(id, answerMap); + mAnswers.put(id, answerObj); + } + } + + public String getId() { + return mQuestionId; + } + + public QuestionType getType() { + return mType; + } + + public String getText() { + return mQuestionText; + } + + public String getPrevious() { + return mPreviousQuestionId; + } + + public String getNext() { + return mNextQuestionId; + } + + public Map<String, Answer> getAnswers() { + return Collections.unmodifiableMap(mAnswers); + } +} diff --git a/src/com/p4square/grow/backend/resources/Score.java b/src/com/p4square/grow/backend/resources/Score.java new file mode 100644 index 0000000..c7e5ecc --- /dev/null +++ b/src/com/p4square/grow/backend/resources/Score.java @@ -0,0 +1,34 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.backend.resources; + +/** + * Simple structure containing a score's sum and count. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +class Score { + double sum; + int count; + + @Override + public String toString() { + final double score = sum / count; + + if (score >= 4) { + return "teacher"; + + } else if (score >= 3) { + return "disciple"; + + } else if (score >= 2) { + return "believer"; + + } else { + return "seeker"; + } + } + +} diff --git a/src/com/p4square/grow/backend/resources/SurveyResource.java b/src/com/p4square/grow/backend/resources/SurveyResource.java new file mode 100644 index 0000000..d22d763 --- /dev/null +++ b/src/com/p4square/grow/backend/resources/SurveyResource.java @@ -0,0 +1,70 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.backend.resources; + +import java.util.Map; +import java.util.HashMap; + +import org.restlet.data.MediaType; +import org.restlet.data.Status; +import org.restlet.resource.ServerResource; +import org.restlet.representation.Representation; +import org.restlet.representation.StringRepresentation; + +import org.apache.log4j.Logger; + +import com.p4square.grow.backend.GrowBackend; +import com.p4square.grow.backend.db.CassandraDatabase; + +/** + * This resource manages assessment questions. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class SurveyResource extends ServerResource { + private final static Logger cLog = Logger.getLogger(SurveyResource.class); + + private CassandraDatabase mDb; + + private String mQuestionId; + + @Override + public void doInit() { + super.doInit(); + + final GrowBackend backend = (GrowBackend) getApplication(); + mDb = backend.getDatabase(); + + mQuestionId = getAttribute("questionId"); + } + + /** + * Handle GET Requests. + */ + @Override + protected Representation get() { + String result = ""; + + if (mQuestionId == null) { + // TODO: List all question ids + + } else if (mQuestionId.equals("first")) { + // TODO: Get the first question id from db? + result = "1"; + + } else { + // Get a question by id + result = mDb.getKey("strings", "/questions/" + mQuestionId); + + if (result == null) { + // 404 + setStatus(Status.CLIENT_ERROR_NOT_FOUND); + return null; + } + } + + return new StringRepresentation(result); + } +} diff --git a/src/com/p4square/grow/backend/resources/SurveyResultsResource.java b/src/com/p4square/grow/backend/resources/SurveyResultsResource.java new file mode 100644 index 0000000..db7dad0 --- /dev/null +++ b/src/com/p4square/grow/backend/resources/SurveyResultsResource.java @@ -0,0 +1,252 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.backend.resources; + +import java.util.Map; +import java.util.HashMap; + +import com.netflix.astyanax.model.Column; +import com.netflix.astyanax.model.ColumnList; + +import org.codehaus.jackson.map.ObjectMapper; + +import org.restlet.data.MediaType; +import org.restlet.data.Status; +import org.restlet.resource.ServerResource; +import org.restlet.representation.Representation; +import org.restlet.representation.StringRepresentation; + +import org.apache.log4j.Logger; + +import com.p4square.grow.backend.GrowBackend; +import com.p4square.grow.backend.db.CassandraDatabase; + +/** + * Store the user's answers to the assessment and generate their score. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class SurveyResultsResource extends ServerResource { + private final static Logger cLog = Logger.getLogger(SurveyResultsResource.class); + + private final static ObjectMapper cMapper = new ObjectMapper(); + + static enum RequestType { + ASSESSMENT, ANSWER + } + + private CassandraDatabase mDb; + + private RequestType mRequestType; + private String mUserId; + private String mQuestionId; + + @Override + public void doInit() { + super.doInit(); + + final GrowBackend backend = (GrowBackend) getApplication(); + mDb = backend.getDatabase(); + + mUserId = getAttribute("userId"); + mQuestionId = getAttribute("questionId"); + + mRequestType = RequestType.ASSESSMENT; + if (mQuestionId != null) { + mRequestType = RequestType.ANSWER; + } + } + + /** + * Handle GET Requests. + */ + @Override + protected Representation get() { + String result = null; + + switch (mRequestType) { + case ANSWER: + result = mDb.getKey("assessments", mUserId, mQuestionId); + break; + + case ASSESSMENT: + result = buildAssessment(); + break; + } + + if (result == null) { + setStatus(Status.CLIENT_ERROR_NOT_FOUND); + return null; + } + + return new StringRepresentation(result); + } + + /** + * Handle PUT requests + */ + @Override + protected Representation put(Representation entity) { + boolean success = false; + + switch (mRequestType) { + case ANSWER: + try { + mDb.putKey("assessments", mUserId, mQuestionId, entity.getText()); + mDb.putKey("assessments", mUserId, "lastAnswered", mQuestionId); + mDb.deleteKey("assessments", mUserId, "summary"); + success = true; + + } catch (Exception e) { + cLog.warn("Caught exception putting answer: " + e.getMessage(), e); + } + break; + + default: + 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 assessment results. + */ + private String buildAssessment() { + StringBuilder sb = new StringBuilder("{ "); + + // Last question answered + final String lastAnswered = mDb.getKey("assessments", mUserId, "lastAnswered"); + if (lastAnswered != null) { + sb.append("\"lastAnswered\": \"" + lastAnswered + "\""); + } + + // Compute score + ColumnList<String> row = mDb.getRow("assessments", mUserId); + if (!row.isEmpty()) { + Score score = new Score(); + for (Column<String> c : row) { + if (c.getName().equals("lastAnswered")) { + continue; + } + + final String questionId = c.getName(); + final String answerId = c.getStringValue(); + if (!scoreQuestion(score, questionId, answerId)) { + break; + } + } + + sb.append(", \"score\":" + score.sum / score.count); + sb.append(", \"sum\":" + score.sum); + sb.append(", \"count\":" + score.count); + sb.append(", \"result\":\"" + score.toString() + "\""); + } + + sb.append(" }"); + return sb.toString(); + } + + private boolean scoreQuestion(final Score score, final String questionId, + final String answerJson) { + + final String data = mDb.getKey("strings", "/questions/" + questionId); + + try { + final Map<?,?> questionMap = cMapper.readValue(data, Map.class); + final Map<?,?> answerMap = cMapper.readValue(answerJson, Map.class); + final Question question = new Question((Map<String, Object>) questionMap); + final String answerId = (String) answerMap.get("answerId"); + + switch (question.getType()) { + case TEXT: + case IMAGE: + final Answer answer = question.getAnswers().get(answerId); + if (answer == null) { + cLog.warn("Got unknown answer " + answerId + + " for question " + questionId); + } else { + if (!scoreAnswer(score, answer)) { + return false; // Quit scoring + } + } + break; + + case SLIDER: + score.sum += Double.valueOf(answerId) * 4 + 1; + score.count++; + break; + + case QUAD: + scoreQuad(score, question, answerId); + break; + } + + } catch (Exception e) { + cLog.error("Exception parsing question id " + questionId, e); + } + + return true; + } + + private boolean scoreAnswer(final Score score, final Answer answer) { + switch (answer.getType()) { + case TRUMP: + score.sum = answer.getScoreFactor(); + score.count = 1; + return false; // Quit scoring. + + case AVERAGE: + score.sum += answer.getScoreFactor(); + score.count++; + break; + + case NONE: + break; + } + + return true; // Continue scoring + } + + private boolean scoreQuad(final Score score, final Question question, + final String answerId) { + + Point[] answers = new Point[question.getAnswers().size()]; + { + int i = 0; + for (String answer : question.getAnswers().keySet()) { + answers[i++] = Point.valueOf(answer); + } + } + + Point userAnswer = Point.valueOf(answerId); + + double minDistance = Double.MAX_VALUE; + int answerIndex = 0; + for (int i = 0; i < answers.length; i++) { + final double distance = userAnswer.distance(answers[i]); + if (distance < minDistance) { + minDistance = distance; + answerIndex = i; + } + } + + cLog.debug("Quad " + question.getId() + ": Got answer " + + answers[answerIndex].toString() + " for user point " + answerId); + + final Answer answer = question.getAnswers().get(answers[answerIndex].toString()); + score.sum += answer.getScoreFactor(); + score.count++; + + return true; // Continue scoring + } +} diff --git a/src/com/p4square/grow/backend/resources/TrainingRecordResource.java b/src/com/p4square/grow/backend/resources/TrainingRecordResource.java new file mode 100644 index 0000000..93f4fbc --- /dev/null +++ b/src/com/p4square/grow/backend/resources/TrainingRecordResource.java @@ -0,0 +1,155 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.backend.resources; + +import java.util.Map; +import java.util.HashMap; + +import com.netflix.astyanax.model.Column; +import com.netflix.astyanax.model.ColumnList; + +import org.restlet.data.MediaType; +import org.restlet.data.Status; +import org.restlet.resource.ServerResource; +import org.restlet.representation.Representation; +import org.restlet.representation.StringRepresentation; + +import org.apache.log4j.Logger; + +import com.p4square.grow.backend.GrowBackend; +import com.p4square.grow.backend.db.CassandraDatabase; + +/** + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class TrainingRecordResource extends ServerResource { + private final static Logger cLog = Logger.getLogger(TrainingRecordResource.class); + + static enum RequestType { + SUMMARY, VIDEO + } + + private GrowBackend mBackend; + private CassandraDatabase mDb; + + private RequestType mRequestType; + private String mUserId; + private String mVideoId; + + @Override + public void doInit() { + super.doInit(); + + mBackend = (GrowBackend) getApplication(); + mDb = mBackend.getDatabase(); + + mUserId = getAttribute("userId"); + mVideoId = getAttribute("videoId"); + + mRequestType = RequestType.SUMMARY; + if (mVideoId != null) { + mRequestType = RequestType.VIDEO; + } + } + + /** + * Handle GET Requests. + */ + @Override + protected Representation get() { + String result = null; + + switch (mRequestType) { + case VIDEO: + result = mDb.getKey("training", mUserId, mVideoId); + break; + + case SUMMARY: + result = buildSummary(); + break; + } + + if (result == null) { + setStatus(Status.CLIENT_ERROR_NOT_FOUND); + return null; + } + + return new StringRepresentation(result); + } + + /** + * Handle PUT requests + */ + @Override + protected Representation put(Representation entity) { + boolean success = false; + + switch (mRequestType) { + case VIDEO: + try { + mDb.putKey("training", mUserId, mVideoId, entity.getText()); + mDb.putKey("training", mUserId, "lastVideo", mVideoId); + success = true; + + } catch (Exception e) { + cLog.warn("Caught exception updating training record: " + e.getMessage(), e); + } + break; + + default: + 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 question answered + final String lastVideo = mDb.getKey("training", mUserId, "lastVideo"); + if (lastVideo != null) { + sb.append("\"lastVideo\": \"" + lastVideo + "\", "); + } + + // List of videos watched + sb.append("\"videos\": { "); + ColumnList<String> row = mDb.getRow("training", mUserId); + if (!row.isEmpty()) { + boolean first = true; + for (Column<String> c : row) { + if ("lastVideo".equals(c.getName())) { + continue; + } + + if (first) { + sb.append("\"" + c.getName() + "\": "); + first = false; + } else { + sb.append(", \"" + c.getName() + "\": "); + } + + sb.append(c.getStringValue()); + } + } + sb.append(" }"); + + + sb.append(" }"); + return sb.toString(); + } + +} diff --git a/src/com/p4square/grow/backend/resources/TrainingResource.java b/src/com/p4square/grow/backend/resources/TrainingResource.java new file mode 100644 index 0000000..85d08c1 --- /dev/null +++ b/src/com/p4square/grow/backend/resources/TrainingResource.java @@ -0,0 +1,88 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.backend.resources; + +import com.netflix.astyanax.model.Column; +import com.netflix.astyanax.model.ColumnList; + +import org.restlet.data.Status; +import org.restlet.resource.ServerResource; +import org.restlet.representation.Representation; +import org.restlet.representation.StringRepresentation; + +import org.apache.log4j.Logger; + +import com.p4square.grow.backend.GrowBackend; +import com.p4square.grow.backend.db.CassandraDatabase; + +/** + * This resource returns a listing of training items for a particular level. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class TrainingResource extends ServerResource { + private final static Logger cLog = Logger.getLogger(TrainingResource.class); + + private GrowBackend mBackend; + private CassandraDatabase mDb; + + private String mLevel; + private String mVideoId; + + @Override + public void doInit() { + super.doInit(); + + mBackend = (GrowBackend) getApplication(); + mDb = mBackend.getDatabase(); + + mLevel = getAttribute("level"); + mVideoId = getAttribute("videoId"); + } + + /** + * Handle GET Requests. + */ + @Override + protected Representation get() { + String result = null; + + if (mLevel == null) { + setStatus(Status.CLIENT_ERROR_NOT_FOUND); + return null; + } + + if (mVideoId == null) { + // Get all videos + ColumnList<String> row = mDb.getRow("strings", "/training/" + mLevel); + if (!row.isEmpty()) { + StringBuilder sb = new StringBuilder("{ \"level\": \"" + mLevel + "\""); + sb.append(", \"videos\": ["); + boolean first = true; + for (Column<String> c : row) { + if (!first) { + sb.append(", "); + } + sb.append(c.getStringValue()); + first = false; + } + sb.append("] }"); + result = sb.toString(); + } + + } else { + // Get single video + result = mDb.getKey("strings", "/training/" + mLevel, mVideoId); + } + + if (result == null) { + // 404 + setStatus(Status.CLIENT_ERROR_NOT_FOUND); + return null; + } + + return new StringRepresentation(result); + } +} |