summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJesse Morgan <jesse@jesterpm.net>2013-08-04 16:09:29 -0700
committerJesse Morgan <jesse@jesterpm.net>2013-08-04 16:09:29 -0700
commit52539d7aaba96b7997a3c5a07e4a1ad234af7d04 (patch)
tree2686f56bc37656c0824a05e28472f7334ed39028 /src
parent69e2512750dd75fce43a21226979996c3cd7da1d (diff)
Committing everything since its long overdue.
Diffstat (limited to 'src')
-rw-r--r--src/com/p4square/grow/backend/GrowBackend.java138
-rw-r--r--src/com/p4square/grow/backend/db/CassandraDatabase.java192
-rw-r--r--src/com/p4square/grow/backend/resources/Answer.java57
-rw-r--r--src/com/p4square/grow/backend/resources/Point.java52
-rw-r--r--src/com/p4square/grow/backend/resources/Question.java72
-rw-r--r--src/com/p4square/grow/backend/resources/Score.java34
-rw-r--r--src/com/p4square/grow/backend/resources/SurveyResource.java70
-rw-r--r--src/com/p4square/grow/backend/resources/SurveyResultsResource.java252
-rw-r--r--src/com/p4square/grow/backend/resources/TrainingRecordResource.java155
-rw-r--r--src/com/p4square/grow/backend/resources/TrainingResource.java88
-rw-r--r--src/com/p4square/grow/frontend/GrowFrontend.java32
-rw-r--r--src/com/p4square/grow/frontend/SurveyPageResource.java22
-rw-r--r--src/com/p4square/grow/frontend/TrainingPageResource.java228
-rw-r--r--src/grow.properties12
-rw-r--r--src/log4j.properties15
-rw-r--r--src/templates/macros/common-page.ftl1
-rw-r--r--src/templates/macros/common.ftl3
-rw-r--r--src/templates/pages/contact.html.ftl0
-rw-r--r--src/templates/pages/video.html.ftl0
-rw-r--r--src/templates/templates/question-image.ftl4
-rw-r--r--src/templates/templates/question-quad.ftl16
-rw-r--r--src/templates/templates/question-slider.ftl16
-rw-r--r--src/templates/templates/survey.ftl9
-rw-r--r--src/templates/templates/training.ftl71
24 files changed, 1534 insertions, 5 deletions
diff --git a/src/com/p4square/grow/backend/GrowBackend.java b/src/com/p4square/grow/backend/GrowBackend.java
new file mode 100644
index 0000000..515cd1b
--- /dev/null
+++ b/src/com/p4square/grow/backend/GrowBackend.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2012 Jesse Morgan
+ */
+
+package com.p4square.grow.backend;
+
+import org.apache.log4j.Logger;
+
+import org.restlet.Application;
+import org.restlet.Component;
+import org.restlet.data.Protocol;
+import org.restlet.Restlet;
+import org.restlet.routing.Router;
+
+import com.p4square.grow.config.Config;
+
+import com.p4square.grow.backend.db.CassandraDatabase;
+import com.p4square.grow.backend.resources.SurveyResource;
+import com.p4square.grow.backend.resources.SurveyResultsResource;
+import com.p4square.grow.backend.resources.TrainingResource;
+import com.p4square.grow.backend.resources.TrainingRecordResource;
+
+/**
+ * Main class for the backend application.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class GrowBackend extends Application {
+ private final static Logger cLog = Logger.getLogger(GrowBackend.class);
+
+ private final Config mConfig;
+ private final CassandraDatabase mDatabase;
+
+ public GrowBackend() {
+ mConfig = new Config();
+ mDatabase = new CassandraDatabase();
+ }
+
+ @Override
+ public Restlet createInboundRoot() {
+ Router router = new Router(getContext());
+
+ // Survey API
+ router.attach("/assessment/question/{questionId}", SurveyResource.class);
+
+ router.attach("/accounts/{userId}/assessment", SurveyResultsResource.class);
+ router.attach("/accounts/{userId}/assessment/answers/{questionId}",
+ SurveyResultsResource.class);
+
+ // Training API
+ router.attach("/training/{level}", TrainingResource.class);
+ router.attach("/training/{level}/videos/{videoId}", TrainingResource.class);
+
+ router.attach("/accounts/{userId}/training", TrainingRecordResource.class);
+ router.attach("/accounts/{userId}/training/videos/{videoId}",
+ TrainingRecordResource.class);
+
+
+ return router;
+ }
+
+ /**
+ * Open the database.
+ */
+ @Override
+ public void start() throws Exception {
+ super.start();
+
+ // Load config
+ final String configDomain =
+ getContext().getParameters().getFirstValue("configDomain");
+ if (configDomain != null) {
+ mConfig.setDomain(configDomain);
+ }
+
+ mConfig.updateConfig(this.getClass().getResourceAsStream("/grow.properties"));
+
+ final String configFilename =
+ getContext().getParameters().getFirstValue("configFile");
+
+ if (configFilename != null) {
+ mConfig.updateConfig(configFilename);
+ }
+
+ // Setup database
+ mDatabase.setClusterName(mConfig.getString("clusterName", "Dev Cluster"));
+ mDatabase.setKeyspaceName(mConfig.getString("keyspace", "GROW"));
+ mDatabase.init();
+ }
+
+ /**
+ * Close the database.
+ */
+ @Override
+ public void stop() throws Exception {
+ cLog.info("Shutting down...");
+ mDatabase.close();
+
+ super.stop();
+ }
+
+ /**
+ * @return the current database.
+ */
+ public CassandraDatabase getDatabase() {
+ return mDatabase;
+ }
+
+ /**
+ * Stand-alone main for testing.
+ */
+ public static void main(String[] args) throws Exception {
+ // Start the HTTP Server
+ final Component component = new Component();
+ component.getServers().add(Protocol.HTTP, 9095);
+ component.getClients().add(Protocol.HTTP);
+ component.getDefaultHost().attach(new GrowBackend());
+
+ // Setup shutdown hook
+ Runtime.getRuntime().addShutdownHook(new Thread() {
+ public void run() {
+ try {
+ component.stop();
+ } catch (Exception e) {
+ cLog.error("Exception during cleanup", e);
+ }
+ }
+ });
+
+ cLog.info("Starting server...");
+
+ try {
+ component.start();
+ } catch (Exception e) {
+ cLog.fatal("Could not start: " + e.getMessage(), e);
+ }
+ }
+}
diff --git a/src/com/p4square/grow/backend/db/CassandraDatabase.java b/src/com/p4square/grow/backend/db/CassandraDatabase.java
new file mode 100644
index 0000000..e40c251
--- /dev/null
+++ b/src/com/p4square/grow/backend/db/CassandraDatabase.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.backend.db;
+
+import com.netflix.astyanax.AstyanaxContext;
+import com.netflix.astyanax.connectionpool.exceptions.ConnectionException;
+import com.netflix.astyanax.connectionpool.impl.ConnectionPoolConfigurationImpl;
+import com.netflix.astyanax.connectionpool.impl.CountingConnectionPoolMonitor;
+import com.netflix.astyanax.connectionpool.NodeDiscoveryType;
+import com.netflix.astyanax.connectionpool.OperationResult;
+import com.netflix.astyanax.impl.AstyanaxConfigurationImpl;
+import com.netflix.astyanax.Keyspace;
+import com.netflix.astyanax.ColumnMutation;
+import com.netflix.astyanax.model.Column;
+import com.netflix.astyanax.model.ColumnFamily;
+import com.netflix.astyanax.model.ColumnList;
+import com.netflix.astyanax.MutationBatch;
+import com.netflix.astyanax.serializers.StringSerializer;
+import com.netflix.astyanax.thrift.ThriftFamilyFactory;
+
+import org.apache.log4j.Logger;
+
+/**
+ * Cassandra Database Abstraction for the Backend.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class CassandraDatabase {
+ private static Logger cLog = Logger.getLogger(CassandraDatabase.class);
+
+ // Configuration fields.
+ private String mClusterName;
+ private String mKeyspaceName;
+ private String mSeedEndpoint = "127.0.0.1:9160";
+ private int mPort = 9160;
+
+ private AstyanaxContext<Keyspace> mContext;
+ private Keyspace mKeyspace;
+
+ /**
+ * Connect to Cassandra.
+ *
+ * Cluster and Keyspace must be set before calling init().
+ */
+ public void init() {
+ mContext = new AstyanaxContext.Builder()
+ .forCluster(mClusterName)
+ .forKeyspace(mKeyspaceName)
+ .withAstyanaxConfiguration(new AstyanaxConfigurationImpl()
+ .setDiscoveryType(NodeDiscoveryType.RING_DESCRIBE)
+ )
+ .withConnectionPoolConfiguration(new ConnectionPoolConfigurationImpl("MyConnectionPool")
+ .setPort(mPort)
+ .setMaxConnsPerHost(1)
+ .setSeeds(mSeedEndpoint)
+ )
+ .withConnectionPoolMonitor(new CountingConnectionPoolMonitor())
+ .buildKeyspace(ThriftFamilyFactory.getInstance());
+
+ mContext.start();
+ mKeyspace = mContext.getClient();
+ }
+
+ /**
+ * Close the database connection.
+ */
+ public void close() {
+ mContext.shutdown();
+ }
+
+ /**
+ * Set the cluster name to connect to.
+ */
+ public void setClusterName(final String cluster) {
+ mClusterName = cluster;
+ }
+
+ /**
+ * Set the name of the keyspace to open.
+ */
+ public void setKeyspaceName(final String keyspace) {
+ mKeyspaceName = keyspace;
+ }
+
+ /**
+ * Change the seed endpoint.
+ * The default is 127.0.0.1:9160.
+ */
+ public void setSeedEndpoint(final String endpoint) {
+ mSeedEndpoint = endpoint;
+ }
+
+ /**
+ * Change the port to connect to.
+ * The default is 9160.
+ */
+ public void setPort(final int port) {
+ mPort = port;
+ }
+
+ /**
+ * @return The entire row associated with this key.
+ */
+ public ColumnList<String> getRow(final String cfName, final String key) {
+ try {
+ ColumnFamily<String, String> cf = new ColumnFamily(cfName,
+ StringSerializer.get(),
+ StringSerializer.get());
+
+ OperationResult<ColumnList<String>> result =
+ mKeyspace.prepareQuery(cf)
+ .getKey(key)
+ .execute();
+
+ return result.getResult();
+
+ } catch (ConnectionException e) {
+ cLog.error("getRow failed due to Connection Exception", e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * @return The value associated with the given key.
+ */
+ public String getKey(final String cfName, final String key) {
+ return getKey(cfName, key, "value");
+ }
+
+ /**
+ * @return The value associated with the given key, column pair.
+ */
+ public String getKey(final String cfName, final String key, final String column) {
+ final ColumnList<String> row = getRow(cfName, key);
+
+ if (row != null) {
+ final Column rowColumn = row.getColumnByName(column);
+ if (rowColumn != null) {
+ return rowColumn.getStringValue();
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Assign value to key.
+ */
+ public void putKey(final String cfName, final String key, final String value) {
+ putKey(cfName, key, "value", value);
+ }
+
+ /**
+ * Assign value to the key, column pair.
+ */
+ public void putKey(final String cfName, final String key,
+ final String column, final String value) {
+
+ ColumnFamily<String, String> cf = new ColumnFamily(cfName,
+ StringSerializer.get(),
+ StringSerializer.get());
+
+ MutationBatch m = mKeyspace.prepareMutationBatch();
+ m.withRow(cf, key).putColumn(column, value);
+
+ try {
+ m.execute();
+ } catch (ConnectionException e) {
+ cLog.error("putKey failed due to Connection Exception", e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Remove a key, column pair.
+ */
+ public void deleteKey(final String cfName, final String key, final String column) {
+ ColumnFamily<String, String> cf = new ColumnFamily(cfName,
+ StringSerializer.get(),
+ StringSerializer.get());
+
+ try {
+ ColumnMutation m = mKeyspace.prepareColumnMutation(cf, key, column);
+ m.deleteColumn().execute();
+ } catch (ConnectionException e) {
+ cLog.error("deleteKey failed due to Connection Exception", e);
+ throw new RuntimeException(e);
+ }
+ }
+}
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);
+ }
+}
diff --git a/src/com/p4square/grow/frontend/GrowFrontend.java b/src/com/p4square/grow/frontend/GrowFrontend.java
index 226929d..74cd704 100644
--- a/src/com/p4square/grow/frontend/GrowFrontend.java
+++ b/src/com/p4square/grow/frontend/GrowFrontend.java
@@ -4,8 +4,14 @@
package com.p4square.grow.frontend;
+import java.io.File;
+import java.io.IOException;
+
+import org.restlet.Application;
import org.restlet.Component;
+import org.restlet.Restlet;
import org.restlet.data.Protocol;
+import org.restlet.resource.Directory;
import org.restlet.routing.Router;
import org.apache.log4j.Logger;
@@ -71,6 +77,8 @@ public class GrowFrontend extends FMFacade {
final Router accountRouter = new Router(getContext());
accountRouter.attach("/assessment/question/{questionId}", SurveyPageResource.class);
accountRouter.attach("/assessment", SurveyPageResource.class);
+ accountRouter.attach("/training/{chapter}", TrainingPageResource.class);
+ accountRouter.attach("/training", TrainingPageResource.class);
final LoginAuthenticator accountGuard =
new LoginAuthenticator(getContext(), false, loginPage);
@@ -88,6 +96,17 @@ public class GrowFrontend extends FMFacade {
final Component component = new Component();
component.getServers().add(Protocol.HTTP, 8085);
component.getClients().add(Protocol.HTTP);
+ component.getClients().add(Protocol.FILE);
+
+ // Static content
+ try {
+ component.getDefaultHost().attach("/images/", new FileServingApp("./build/images/"));
+ component.getDefaultHost().attach("/scripts", new FileServingApp("./build/scripts"));
+ component.getDefaultHost().attach("/style.css", new FileServingApp("./build/style.css"));
+ } catch (IOException e) {
+ cLog.error("Could not create directory for static resources: "
+ + e.getMessage(), e);
+ }
// Setup App
GrowFrontend app = new GrowFrontend();
@@ -119,4 +138,17 @@ public class GrowFrontend extends FMFacade {
cLog.fatal("Could not start: " + e.getMessage(), e);
}
}
+
+ private static class FileServingApp extends Application {
+ private final String mPath;
+
+ public FileServingApp(String path) throws IOException {
+ mPath = new File(path).getAbsolutePath();
+ }
+
+ @Override
+ public Restlet createInboundRoot() {
+ return new Directory(getContext(), "file://" + mPath);
+ }
+ }
}
diff --git a/src/com/p4square/grow/frontend/SurveyPageResource.java b/src/com/p4square/grow/frontend/SurveyPageResource.java
index 280184b..8145c0d 100644
--- a/src/com/p4square/grow/frontend/SurveyPageResource.java
+++ b/src/com/p4square/grow/frontend/SurveyPageResource.java
@@ -115,6 +115,7 @@ public class SurveyPageResource extends FreeMarkerPageResource {
protected Representation post(Representation entity) {
final Form form = new Form(entity);
final String answerId = form.getFirstValue("answer");
+ final String direction = form.getFirstValue("direction");
if (mQuestionId == null || answerId == null || answerId.length() == 0) {
// Something is wrong.
@@ -154,9 +155,26 @@ public class SurveyPageResource extends FreeMarkerPageResource {
// Find the next question or finish the assessment.
String nextPage = mConfig.getString("dynamicRoot", "");
{
- String nextQuestionId = (String) questionData.get("nextQuestion");
+ String nextQuestionId = null;
+ if ("previous".equals(direction)) {
+ nextQuestionId = (String) questionData.get("previousQuestion");
+ } else {
+ nextQuestionId = (String) questionData.get("nextQuestion");
+ }
+
if (nextQuestionId == null) {
- nextPage += "/account/assessment/results";
+ //nextPage += "/account/assessment/results";
+ // TODO: Remove this hack:
+ JsonResponse response = backendGet("/accounts/" + mUserId + "/assessment");
+ if (!response.getStatus().isSuccess()) {
+ nextPage += "/account/assessment/results";
+ } else {
+ final String score = (String) response.getMap().get("result");
+ if (score != null) {
+ nextPage += "/account/training/" + score;
+ }
+ }
+
} else {
nextPage += "/account/assessment/question/" + nextQuestionId;
}
diff --git a/src/com/p4square/grow/frontend/TrainingPageResource.java b/src/com/p4square/grow/frontend/TrainingPageResource.java
new file mode 100644
index 0000000..6493638
--- /dev/null
+++ b/src/com/p4square/grow/frontend/TrainingPageResource.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.HashMap;
+
+import freemarker.template.Template;
+
+import org.restlet.data.Form;
+import org.restlet.data.MediaType;
+import org.restlet.data.Status;
+import org.restlet.ext.freemarker.TemplateRepresentation;
+import org.restlet.representation.Representation;
+import org.restlet.resource.ServerResource;
+
+import org.apache.log4j.Logger;
+
+import net.jesterpm.fmfacade.json.JsonRequestClient;
+import net.jesterpm.fmfacade.json.JsonResponse;
+
+import net.jesterpm.fmfacade.FreeMarkerPageResource;
+
+import com.p4square.grow.config.Config;
+
+/**
+ * SurveyPageResource handles rendering the survey and processing user's answers.
+ *
+ * This resource expects the user to be authenticated and the ClientInfo User object
+ * to be populated. Each question is requested from the backend along with the
+ * user's previous answer. Each answer is sent to the backend and the user is redirected
+ * to the next question. After the last question the user is sent to his results.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class TrainingPageResource extends FreeMarkerPageResource {
+ private static Logger cLog = Logger.getLogger(TrainingPageResource.class);
+
+ private Config mConfig;
+ private Template mTrainingTemplate;
+ private JsonRequestClient mJsonClient;
+
+ // Fields pertaining to this request.
+ private String mChapter;
+ private String mUserId;
+
+ @Override
+ public void doInit() {
+ super.doInit();
+
+ GrowFrontend growFrontend = (GrowFrontend) getApplication();
+ mConfig = growFrontend.getConfig();
+ mTrainingTemplate = growFrontend.getTemplate("templates/training.ftl");
+ if (mTrainingTemplate == null) {
+ cLog.fatal("Could not find training template.");
+ setStatus(Status.SERVER_ERROR_INTERNAL);
+ }
+
+ mJsonClient = new JsonRequestClient(getContext().getClientDispatcher());
+
+ mChapter = getAttribute("chapter");
+ mUserId = getRequest().getClientInfo().getUser().getIdentifier();
+ }
+
+ /**
+ * Return a page with a survey question.
+ */
+ @Override
+ protected Representation get() {
+ try {
+ // Get the current chapter.
+ if (mChapter == null) {
+ // TODO: Get user's current question
+ mChapter = "seeker";
+ }
+
+ // Get videos for the chapter.
+ List<Map<String, Object>> videos = null;
+ {
+ JsonResponse response = backendGet("/training/" + mChapter);
+ if (!response.getStatus().isSuccess()) {
+ setStatus(Status.CLIENT_ERROR_NOT_FOUND);
+ return null;
+ }
+ videos = (List<Map<String, Object>>) response.getMap().get("videos");
+ }
+
+ // Get list of completed videos
+ Map<String, Object> trainingRecord = null;
+ Map<String, Object> completedVideos = new HashMap<String, Object>();
+ {
+ JsonResponse response = backendGet("/accounts/" + mUserId + "/training");
+ if (response.getStatus().isSuccess()) {
+ trainingRecord = response.getMap();
+ completedVideos = (Map<String, Object>) trainingRecord.get("videos");
+ }
+ }
+
+ // Mark the completed videos as completed
+ int chapterProgress = 0;
+ for (Map<String, Object> video : videos) {
+ boolean completed = (null != completedVideos.get(video.get("id")));
+ video.put("completed", completed);
+
+ if (completed) {
+ chapterProgress++;
+ }
+ }
+ chapterProgress = chapterProgress * 100 / videos.size();
+
+ Map root = getRootObject();
+ root.put("chapter", mChapter);
+ root.put("chapterProgress", chapterProgress);
+ root.put("videos", videos);
+ root.put("completedVideos", completedVideos);
+
+ return new TemplateRepresentation(mTrainingTemplate, root, MediaType.TEXT_HTML);
+
+ } catch (Exception e) {
+ cLog.fatal("Could not render page: " + e.getMessage(), e);
+ setStatus(Status.SERVER_ERROR_INTERNAL);
+ return null;
+ }
+ }
+
+ /**
+ * Record a survey answer and redirect to the next question.
+ */
+ @Override
+ protected Representation post(Representation entity) {
+ return null;
+ /*final Form form = new Form(entity);
+ final String answerId = form.getFirstValue("answer");
+
+ if (mQuestionId == null || answerId == null || answerId.length() == 0) {
+ // Something is wrong.
+ setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
+ return null;
+ }
+
+ try {
+ // Find the question
+ Map questionData = null;
+ {
+ JsonResponse response = backendGet("/assessment/question/" + mQuestionId);
+ if (!response.getStatus().isSuccess()) {
+ // User is answering a question which doesn't exist
+ setStatus(Status.CLIENT_ERROR_NOT_FOUND);
+ return null;
+ }
+
+ questionData = response.getMap();
+ }
+
+ // Store answer
+ {
+ Map<String, String> answer = new HashMap<String, String>();
+ answer.put("answerId", answerId);
+ JsonResponse response = backendPut("/accounts/" + mUserId +
+ "/assessment/answers/" + mQuestionId, answer);
+
+ if (!response.getStatus().isSuccess()) {
+ // Something went wrong talking to the backend, error out.
+ cLog.fatal("Error recording survey answer " + response.getStatus());
+ setStatus(Status.SERVER_ERROR_INTERNAL);
+ return null;
+ }
+ }
+
+ // Find the next question or finish the assessment.
+ String nextPage = mConfig.getString("dynamicRoot", "");
+ {
+ String nextQuestionId = (String) questionData.get("nextQuestion");
+ if (nextQuestionId == null) {
+ nextPage += "/account/assessment/results";
+ } else {
+ nextPage += "/account/assessment/question/" + nextQuestionId;
+ }
+ }
+
+ getResponse().redirectSeeOther(nextPage);
+ return null;
+
+ } catch (Exception e) {
+ cLog.fatal("Could not render page: " + e.getMessage(), e);
+ setStatus(Status.SERVER_ERROR_INTERNAL);
+ return null;
+ }*/
+ }
+
+ /**
+ * @return The backend endpoint URI
+ */
+ private String getBackendEndpoint() {
+ return mConfig.getString("backendUri", "riap://component/backend");
+ }
+
+ /**
+ * Helper method to send a GET to the backend.
+ */
+ private JsonResponse backendGet(final String uri) {
+ cLog.debug("Sending backend GET " + uri);
+
+ final JsonResponse response = mJsonClient.get(getBackendEndpoint() + uri);
+ 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;
+ }
+
+ 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;
+ }
+}
diff --git a/src/grow.properties b/src/grow.properties
index b1bd989..1f76237 100644
--- a/src/grow.properties
+++ b/src/grow.properties
@@ -1 +1,13 @@
+# Frontend Settings
dev.backendUri = http://localhost:9095
+dev.staticRoot =
+dev.dynamicRoot =
+
+*.backendUri = riap://component/backend
+*.staticRoot = /grow-frontend
+*.dynamicRoot = /grow-frontend
+
+# Backend Settings
+dev.clusterName = Dev Cluster
+
+prod.clusterName = Prod Cluster
diff --git a/src/log4j.properties b/src/log4j.properties
new file mode 100644
index 0000000..c1e2ecb
--- /dev/null
+++ b/src/log4j.properties
@@ -0,0 +1,15 @@
+# Set root logger level to DEBUG and its only appender to A1.
+log4j.rootLogger=DEBUG, stdout, logfile
+
+log4j.loggger.org.eclipse = WARN
+
+# stdout appender
+log4j.appender.stdout=org.apache.log4j.ConsoleAppender
+log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
+log4j.appender.stdout.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n
+
+# service.log appender
+log4j.appender.logfile=org.apache.log4j.FileAppender
+log4j.appender.logfile.File=service.log
+log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
+log4j.appender.logfile.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n
diff --git a/src/templates/macros/common-page.ftl b/src/templates/macros/common-page.ftl
index 5fa2740..9b4323d 100644
--- a/src/templates/macros/common-page.ftl
+++ b/src/templates/macros/common-page.ftl
@@ -6,6 +6,7 @@
<link rel="stylesheet" href="${staticRoot}/style.css" />
<script src="${staticRoot}/scripts/jquery.min.js"></script>
+ <script src="${staticRoot}/scripts/jquery-ui.js"></script>
<script src="${staticRoot}/scripts/growth.js"></script>
</head>
<body>
diff --git a/src/templates/macros/common.ftl b/src/templates/macros/common.ftl
index ab0d769..513fc57 100644
--- a/src/templates/macros/common.ftl
+++ b/src/templates/macros/common.ftl
@@ -1,4 +1,5 @@
<#include "content.ftl">
<#include "noticebox.ftl">
-<#assign contentroot = "http://localhost/~jesterpm/growcontent">
+<#assign dynamicRoot = "">
+<#assign staticRoot = "">
diff --git a/src/templates/pages/contact.html.ftl b/src/templates/pages/contact.html.ftl
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/templates/pages/contact.html.ftl
diff --git a/src/templates/pages/video.html.ftl b/src/templates/pages/video.html.ftl
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/templates/pages/video.html.ftl
diff --git a/src/templates/templates/question-image.ftl b/src/templates/templates/question-image.ftl
index f117256..0cf815c 100644
--- a/src/templates/templates/question-image.ftl
+++ b/src/templates/templates/question-image.ftl
@@ -1,9 +1,9 @@
<div class="imageQuestion question">
<#list question.answers?keys as answerid>
<#if selectedAnswerId?? && answerid == selectedAnswerId>
- <a href="#" class="answer" id="${answerid}" onclick="selectAnswer(this)" class="selected"><img src="${staticRoot}/images/${question.id}-${answerid}.png" alt="${question.answers[answerid]!}" /></a>
+ <img class="answer selected" id="${answerid}" onclick="selectAnswer(this)" src="${staticRoot}/images/${question.id}-${answerid}-hover.jpg" />
<#else>
- <a href="#" class="answer" id="${answerid}" onclick="selectAnswer(this)" class="answer"><img src="${staticRoot}/images/${question.id}-${answerid}.png" alt="${question.answers[answerid]!}" /></a>
+ <img class="answer" id="${answerid}" onclick="selectAnswer(this)" src="${staticRoot}/images/${question.id}-${answerid}.jpg" />
</#if>
</#list>
</div>
diff --git a/src/templates/templates/question-quad.ftl b/src/templates/templates/question-quad.ftl
new file mode 100644
index 0000000..90a7381
--- /dev/null
+++ b/src/templates/templates/question-quad.ftl
@@ -0,0 +1,16 @@
+<div class="quadQuestion question">
+ <div class="top">${question.top}</div>
+ <div class="middle">
+ <div class="left">${question.left}</div>
+ <div class="quad"><img src="${staticRoot}/images/quadselector.png" class="selector" /></div>
+ <div class="right">${question.right}</div>
+ </div>
+ <div class="bottom">${question.bottom}</div>
+</div>
+
+<h1>${question.question}</h1>
+<#if question.description??>
+<p>
+ ${question.description}
+</p>
+</#if>
diff --git a/src/templates/templates/question-slider.ftl b/src/templates/templates/question-slider.ftl
new file mode 100644
index 0000000..faded36
--- /dev/null
+++ b/src/templates/templates/question-slider.ftl
@@ -0,0 +1,16 @@
+<h1>${question.question}</h1>
+<#if question.description??>
+<p>
+ ${question.description}
+</p>
+</#if>
+
+<div class="sliderQuestion question">
+ <div class="sliderbar"><img src="${staticRoot}/images/slider.png" class="slider" /></div>
+ <div class="answers">
+ <#list question.answers?keys as answerid>
+ <div id="${answerid}" class="label">${question.answers[answerid].text}</div>
+ </#list>
+ <span class="stretch"></span>
+ </div>
+</div>
diff --git a/src/templates/templates/survey.ftl b/src/templates/templates/survey.ftl
index 9ea47d5..da4e0b3 100644
--- a/src/templates/templates/survey.ftl
+++ b/src/templates/templates/survey.ftl
@@ -14,11 +14,14 @@
<div id="content">
<form id="questionForm" action="${dynamicRoot}/account/assessment/question/${question.id}" method="post">
+ <input id="direction" type="hidden" name="direction" value="next" />
<input id="answerField" type="hidden" name="answer" value="${selectedAnswerId!}" />
<div id="previous">
+ <#if question.previousQuestion??>
<a href="#" onclick="previousQuestion();return false;">
<img src="${staticRoot}/images/previous.png" alt="Previous Question" />
</a>
+ </#if>
</div>
<div id="next">
<a href="#" onclick="nextQuestion();return false;">
@@ -33,6 +36,12 @@
<#case "image">
<#include "/templates/question-image.ftl">
<#break>
+ <#case "slider">
+ <#include "/templates/question-slider.ftl">
+ <#break>
+ <#case "quad">
+ <#include "/templates/question-quad.ftl">
+ <#break>
</#switch>
</article>
</form>
diff --git a/src/templates/templates/training.ftl b/src/templates/templates/training.ftl
new file mode 100644
index 0000000..3207c33
--- /dev/null
+++ b/src/templates/templates/training.ftl
@@ -0,0 +1,71 @@
+<#include "/macros/common.ftl">
+<#include "/macros/common-page.ftl">
+
+<@commonpage>
+ <@noticebox>
+ The Grow Process focuses on the topic that you want to learn
+ about. Our 'Assessment' test will give you the right courses
+ fit for your level.
+ </@noticebox>
+
+ <div id="progressbar">
+ <#switch chapter>
+ <#case "seeker"><#assign overallProgress = 0><#break>
+ <#case "believer"><#assign overallProgress = 25><#break>
+ <#case "disciple"><#assign overallProgress = 50><#break>
+ <#case "teacher"><#assign overallProgress = 75><#break>
+ </#switch>
+ <div class="progress" style="width: ${overallProgress}%"></div>
+ </div>
+
+ <div id="content">
+ <nav>
+ <#assign chapters = ["seeker", "believer", "disciple", "teacher"]>
+ <#list chapters as x>
+ <a href="${dynamicRoot}/account/training/${x}" <#if x == chapter>class="current"</#if>>${x?capitalize}</a>
+ <#if x_has_next> - </#if>
+ </#list>
+ </nav>
+
+ <div id="chapterprogress">
+ "${chapter?capitalize} Chapter Progress:"
+ <div class="progressbar"><div class="progress" style="width: ${chapterProgress}%"></div></div>
+ <div class="progresslabel" style="left:${chapterProgress}%">${chapterProgress}%</div>
+ </div>
+
+ <div id="videos">
+ <#list videos as video>
+ <article>
+ <div class="image <#if video.completed>completed</#if>"><a href="#"><img src="${video.image!staticRoot+"/images/videoimage.jpg"}" alt="${video.title}" /></a></div>
+ <h2>${video.title}</h2> <span class="duration">${video.length}</span>
+ </article>
+ </#list>
+ </div>
+
+ <div id="thefeed">
+ <h2>The Feed</h2>
+ <article>
+ <div class="question">
+ Q: How has God worked in your life...
+ <div><a class="reply" href="#">Answer</a></div>
+ </div>
+ <div class="answer">
+ A: God has worked in an amazing way in my own life. I constantly
+ <a class="readmore" href="#">(continue)</a>
+ </div>
+ </article>
+ <article>
+ <div class="question">
+ Q: How has God worked in your life...
+ <div><a class="reply" href="#">Answer</a></div>
+ </div>
+ <div class="answer">
+ A: God has worked in an amazing way in my own life. I constantly
+ <a class="readmore" href="#">(continue)</a>
+ </div>
+ </article>
+ </div>
+ </div>
+</@commonpage>
+
+