From d47c41ff14ac5c0229c61b18f79bc3a5c5fea533 Mon Sep 17 00:00:00 2001
From: Daniel Gerhardt <code@dgerhardt.net>
Date: Tue, 4 Jul 2017 18:27:09 +0200
Subject: [PATCH] Add CouchDB design docs as JavaScript

Functions need to be stringified to create valid JSON which is accepted
by CouchDB.
---
 src/main/resources/couchdb/answer.design.js   | 73 ++++++++++++++++++
 src/main/resources/couchdb/comment.design.js  | 45 +++++++++++
 src/main/resources/couchdb/content.design.js  | 31 ++++++++
 .../couchdb/lerning_progress.design.js        | 55 +++++++++++++
 .../resources/couchdb/logged_in.design.js     | 27 +++++++
 src/main/resources/couchdb/motd.design.js     | 27 +++++++
 src/main/resources/couchdb/motdlist.design.js | 13 ++++
 src/main/resources/couchdb/session.design.js  | 52 +++++++++++++
 .../resources/couchdb/statistics.design.js    | 77 +++++++++++++++++++
 src/main/resources/couchdb/user.design.js     | 20 +++++
 10 files changed, 420 insertions(+)
 create mode 100644 src/main/resources/couchdb/answer.design.js
 create mode 100644 src/main/resources/couchdb/comment.design.js
 create mode 100644 src/main/resources/couchdb/content.design.js
 create mode 100644 src/main/resources/couchdb/lerning_progress.design.js
 create mode 100644 src/main/resources/couchdb/logged_in.design.js
 create mode 100644 src/main/resources/couchdb/motd.design.js
 create mode 100644 src/main/resources/couchdb/motdlist.design.js
 create mode 100644 src/main/resources/couchdb/session.design.js
 create mode 100644 src/main/resources/couchdb/statistics.design.js
 create mode 100644 src/main/resources/couchdb/user.design.js

diff --git a/src/main/resources/couchdb/answer.design.js b/src/main/resources/couchdb/answer.design.js
new file mode 100644
index 00000000..a79ac0a4
--- /dev/null
+++ b/src/main/resources/couchdb/answer.design.js
@@ -0,0 +1,73 @@
+var designDoc = {
+	"_id": "_design/answer",
+	"language": "javascript",
+	"views": {
+		"doc_by_questionid_user_piround": {
+			"map": function (doc) {
+				if (doc.type === "skill_question_answer") {
+					emit([doc.questionId, doc.user, doc.piRound], doc);
+				}
+			}
+		},
+		"doc_by_questionid_timestamp": {
+			"map": function (doc) {
+				if (doc.type === "skill_question_answer") {
+					emit([doc.questionId, doc.timestamp], doc);
+				}
+			}
+		},
+		"doc_by_user_sessionid": {
+			"map": function (doc) {
+				if (doc.type === "skill_question_answer") {
+					emit([doc.user, doc.sessionId], doc);
+				}
+			}
+		},
+		"by_questionid": {
+			"map": function (doc) {
+				if (doc.type === "skill_question_answer") {
+					emit(doc.questionId, null);
+				}
+			}
+		},
+		"by_questionid_piround_text_subject": {
+			"map": function (doc) {
+				if (doc.type === "skill_question_answer") {
+					emit([doc.questionId, doc.piRound, doc.abstention, doc.answerText, doc.answerSubject, doc.successfulFreeTextAnswer], null);
+				}
+			},
+			"reduce": "_count"
+		},
+		"by_sessionid": {
+			/* Redundant view but kept for now to allow simpler queries. */
+			"map": function (doc) {
+				if (doc.type === "skill_question_answer") {
+					emit(doc.sessionId, null);
+				}
+			},
+			"reduce": "_count"
+		},
+		"by_sessionid_variant": {
+			"map": function (doc) {
+				if (doc.type === "skill_question_answer") {
+					emit([doc.sessionId, doc.questionVariant], null);
+				}
+			},
+			"reduce": "_count"
+		},
+		"questionid_by_user_sessionid_variant": {
+			"map": function (doc) {
+				if (doc.type === "skill_question_answer") {
+					emit([doc.user, doc.sessionId, doc.questionVariant], doc.questionId);
+				}
+			}
+		},
+		"questionid_piround_by_user_sessionid_variant": {
+			"map": function (doc) {
+				if (doc.type === "skill_question_answer") {
+					emit([doc.user, doc.sessionId, doc.questionVariant], [doc.questionId, doc.piRound]);
+				}
+			}
+		}
+	}
+};
diff --git a/src/main/resources/couchdb/comment.design.js b/src/main/resources/couchdb/comment.design.js
new file mode 100644
index 00000000..23936b94
--- /dev/null
+++ b/src/main/resources/couchdb/comment.design.js
@@ -0,0 +1,45 @@
+var designDoc = {
+	"_id": "_design/comment",
+	"language": "javascript",
+	"views": {
+		"doc_by_sessionid_creator_timestamp": {
+			"map": function (doc) {
+				if (doc.type === "interposed_question") {
+					emit([doc.sessionId, doc.creator, doc.timestamp], doc);
+				}
+			}
+		},
+		"doc_by_sessionid_timestamp": {
+			"map": function (doc) {
+				if (doc.type === "interposed_question") {
+					emit([doc.sessionId, doc.timestamp], doc);
+				}
+			}
+		},
+		"by_sessionid": {
+			/* Redundant view but kept for now to allow simpler queries. */
+			"map": function (doc) {
+				if (doc.type === "interposed_question") {
+					emit(doc.sessionId, null);
+				}
+			},
+			"reduce": "_count"
+		},
+		"by_sessionid_read": {
+			"map": function (doc) {
+				if (doc.type === "interposed_question") {
+					emit([doc.sessionId, doc.read], null);
+				}
+			},
+			"reduce": "_count"
+		},
+		"by_sessionid_creator_read": {
+			"map": function (doc) {
+				if (doc.type === "interposed_question") {
+					emit([doc.sessionId, doc.creator, doc.read], null);
+				}
+			},
+			"reduce": "_count"
+		}
+	}
+};
diff --git a/src/main/resources/couchdb/content.design.js b/src/main/resources/couchdb/content.design.js
new file mode 100644
index 00000000..74518a47
--- /dev/null
+++ b/src/main/resources/couchdb/content.design.js
@@ -0,0 +1,31 @@
+var designDoc = {
+	"_id": "_design/content",
+	"language": "javascript",
+	"views": {
+		"doc_by_sessionid_variant_active": {
+			"map": function (doc) {
+				if (doc.type === "skill_question") {
+					emit([doc.sessionId, doc.questionVariant, doc.active, doc.subject, doc.text.substr(0, 16)], doc);
+				}
+			},
+			"reduce": "_count"
+		},
+		"by_sessionid": {
+			/* Redundant view but kept for now to allow simpler queries. */
+			"map": function (doc) {
+				if (doc.type === "skill_question") {
+					emit(doc.sessionId, null);
+				}
+			},
+			"reduce": "_count"
+		},
+		"by_sessionid_variant_active": {
+			"map": function (doc) {
+				if (doc.type === "skill_question") {
+					emit([doc.sessionId, doc.questionVariant, doc.active, doc.subject, doc.text.substr(0, 16)], null);
+				}
+			},
+			"reduce": "_count"
+		}
+	}
+};
diff --git a/src/main/resources/couchdb/lerning_progress.design.js b/src/main/resources/couchdb/lerning_progress.design.js
new file mode 100644
index 00000000..2b35eec8
--- /dev/null
+++ b/src/main/resources/couchdb/lerning_progress.design.js
@@ -0,0 +1,55 @@
+var designDoc = {
+	"_id": "_design/learning_progress",
+	"language": "javascript",
+	"views": {
+		"question_value_achieved_for_user": {
+			"comment": "This view returns the points users scored for answered questions.",
+			"map": function (doc) {
+				if (doc.type === "skill_question_answer" && !doc.abstention) {
+					/* The 'questionValue' contains the points scored with this answer,
+					 * and this could be negative if a wrong answer was given.
+					 * However, we do not want negative values, so we set the lower bound to 0.*/
+					score = Math.max(doc.questionValue || 0, 0);
+					emit([doc.sessionId, doc.user], {
+						questionId: doc.questionId,
+						score: score, piRound: doc.piRound
+					});
+				}
+			}
+		},
+		"maximum_value_of_question": {
+			"comment": "This view returns the maximum number that can be achieved when answering this question.",
+			"map": function (doc) {
+				/* The question's value is determined by the maximum of all possibleAnswer values.
+				 * We assume that a correct answer is assigned a positive value,
+				 * while a negative value suggests a wrong answer.
+				 * The goal then is to get the highest possible value.
+				 * This leaves us with two cases:
+				 * 1) On any single choice question, the value is the maximum of all possibleAnswer values.
+				 * 2) On a multiple choice question, we add up all positive values. */
+				 var value = 0, answers = [], positiveAnswers = [], score = 0;
+				 if (doc.type === "skill_question" && ["school", "flashcard"].indexOf(doc.questionType) === -1) {
+				 	if ("freetext" === doc.questionType && !doc.fixedAnswer) { return; }
+					answers = doc.possibleAnswers.map(function(answer) { return answer.value || 0; });
+					/* find the maximum value */
+					if (doc.fixedAnswer) { value = doc.rating; }
+					else { value = Math.max.apply(null, [0].concat(answers)); }
+					/* ignore likert ('vote') questions without any points */
+					if (doc.questionType === "vote" && value === 0) { return; }
+					/* special case for mc and grid questions: add up all positive answers. */
+					if (["grid", "mc"].indexOf(doc.questionType) !== -1) {
+						positiveAnswers = answers.filter(function(val) { return val >= 0; });
+						if (positiveAnswers.length > 0) {
+							value = positiveAnswers.reduce(function(prev, cur) { return prev + cur; }, 0);
+						}
+					}
+					emit([doc.sessionId, doc._id], {
+						value: value,
+						questionVariant: doc.questionVariant,
+						piRound: doc.piRound
+					});
+				}
+			}
+		}
+	}
+};
diff --git a/src/main/resources/couchdb/logged_in.design.js b/src/main/resources/couchdb/logged_in.design.js
new file mode 100644
index 00000000..1b196b4f
--- /dev/null
+++ b/src/main/resources/couchdb/logged_in.design.js
@@ -0,0 +1,27 @@
+var designDoc = {
+	"_id": "_design/logged_in",
+	"language": "javascript",
+	"views": {
+		"visited_sessions_by_user": {
+			"map": function (doc) {
+				if (doc.type === "logged_in") {
+					emit(doc.user, doc.visitedSessions);
+				}
+			}
+		},
+		"all": {
+			"map": function (doc) {
+				if (doc.type === "logged_in"){
+					emit(doc.user, doc);
+				}
+			}
+		},
+		"by_last_activity_for_guests": {
+			"map": function (doc) {
+				if (doc.type === "logged_in" && doc.user.indexOf("Guest") === 0) {
+					emit(doc.timestamp || 0, {_rev: doc._rev});
+				}
+			}
+		}
+	}
+};
diff --git a/src/main/resources/couchdb/motd.design.js b/src/main/resources/couchdb/motd.design.js
new file mode 100644
index 00000000..33b111ee
--- /dev/null
+++ b/src/main/resources/couchdb/motd.design.js
@@ -0,0 +1,27 @@
+var designDoc = {
+	"_id": "_design/motd",
+	"language": "javascript",
+	"views": {
+		"doc_by_sessionkey": {
+			"map": function (doc) {
+				if (doc.type === "motd" && doc.audience === "session") {
+					emit(doc.sessionkey, doc);
+				}
+			}
+		},
+		"doc_by_audience_for_global": {
+			"map": function (doc) {
+				if (doc.type === "motd" && doc.audience !== "session") {
+					emit(doc.audience, doc);
+				}
+			}
+		},
+		"by_motdkey": {
+			"map": function (doc) {
+				if (doc.type === "motd") {
+					emit(doc.motdkey, doc);
+				}
+			}
+		}
+	}
+};
diff --git a/src/main/resources/couchdb/motdlist.design.js b/src/main/resources/couchdb/motdlist.design.js
new file mode 100644
index 00000000..37712e4e
--- /dev/null
+++ b/src/main/resources/couchdb/motdlist.design.js
@@ -0,0 +1,13 @@
+var designDoc = {
+	"_id": "_design/motdlist",
+	"language": "javascript",
+	"views": {
+		"doc_by_username": {
+			"map": function (doc) {
+				if (doc.type === "motdlist") {
+					emit(doc.username, doc);
+				}
+			}
+		}
+	}
+};
diff --git a/src/main/resources/couchdb/session.design.js b/src/main/resources/couchdb/session.design.js
new file mode 100644
index 00000000..b8b1346b
--- /dev/null
+++ b/src/main/resources/couchdb/session.design.js
@@ -0,0 +1,52 @@
+var designDoc = {
+	"_id": "_design/session",
+	"language": "javascript",
+	"views": {
+		"by_courseid": {
+			"map": function (doc) {
+				if (doc.type === "session" && doc.courseId && doc.sessionType !== "public_pool") {
+					emit(doc.courseId, null);
+				}
+			}
+		},
+		"by_keyword": {
+			"map": function (doc) {
+				if (doc.type === "session") {
+					emit(doc.keyword, null);
+				}
+			}
+		},
+		"partial_by_sessiontype_creator_name": {
+			"map": function (doc) {
+				if (doc.type === "session") {
+					emit([doc.sessionType, doc.creator, doc.name], {
+						shortName: doc.shortName,
+						keyword: doc.keyword,
+						active: doc.active,
+						courseType: doc.courseType,
+						creationTime: doc.creationTime
+					});
+				}
+			}
+		},
+		"partial_by_ppsubject_name_for_publicpool": {
+			"map": function (doc) {
+				if (doc.type === "session" && doc.sessionType === "public_pool") {
+					emit([doc.ppSubject, doc.name], {
+						ppSubject: doc.ppSubject,
+						name: doc.name,
+						keyword: doc.keyword,
+						ppLevel: doc.ppLevel
+					});
+				}
+			}
+		},
+		"by_lastactivity_for_guests": {
+			"map": function (doc) {
+				if (doc.type === "session" && doc.sessionType !== "public_pool" && doc.creator.indexOf("Guest") === 0) {
+					emit(doc.lastOwnerActivity || doc.creationTime, {_rev: doc._rev});
+				}
+			}
+		}
+	}
+};
diff --git a/src/main/resources/couchdb/statistics.design.js b/src/main/resources/couchdb/statistics.design.js
new file mode 100644
index 00000000..c9004fd5
--- /dev/null
+++ b/src/main/resources/couchdb/statistics.design.js
@@ -0,0 +1,77 @@
+var designDoc = {
+	"_id": "_design/statistics",
+	"language": "javascript",
+	"views": {
+		"active_student_users": {
+			"map": function (doc) {
+				if (doc.type === "skill_question_answer") {
+					emit(doc.user, 1);
+				}
+			},
+			"reduce": "_count"
+		},
+		"statistics": {
+			"map": function (doc) {
+				switch (doc.type) {
+				case "session":
+					if (doc.active) {
+						emit("openSessions", 1);
+					} else {
+						emit("closedSessions", 1);
+					}
+					break;
+				case "skill_question":
+					if (doc.questionType === "flashcard") {
+						emit("flashcards", 1);
+					} else {
+						if (doc.questionVariant === "lecture") {
+							emit("lectureQuestions", 1);
+						} else if (doc.questionVariant === "preparation") {
+							emit("preparationQuestions", 1);
+						}
+						if (doc.piRound === 2) {
+							emit("conceptQuestions", 1);
+						}
+					}
+					break;
+				case "skill_question_answer":
+					emit("answers", 1);
+					break;
+				case "interposed_question":
+					emit ("interposedQuestions", 1);
+					break;
+				case "log":
+					if (doc.event === "delete") {
+						switch (doc.payload.type) {
+						case "session":
+							emit("deletedSessions", doc.payload.sessionCount || 1);
+							break;
+						case "question":
+							emit("deletedQuestions", doc.payload.questionCount || 1);
+							break;
+						case "answer":
+							emit("deletedAnswers", doc.payload.answerCount || 1);
+							break;
+						case "comment":
+							emit("deletedComments", doc.payload.commentCount || 1);
+							break;
+						case "user":
+							emit("deletedUsers", 1);
+							break;
+						}
+					}
+					break;
+				}
+			},
+			"reduce": "_sum"
+		},
+		"unique_session_creators": {
+			"map": function (doc) {
+				if (doc.type === "session") {
+					emit(doc.creator, 1);
+				}
+			},
+			"reduce": "_count"
+		}
+	}
+};
diff --git a/src/main/resources/couchdb/user.design.js b/src/main/resources/couchdb/user.design.js
new file mode 100644
index 00000000..9299516f
--- /dev/null
+++ b/src/main/resources/couchdb/user.design.js
@@ -0,0 +1,20 @@
+var designDoc = {
+	"_id": "_design/user",
+	"language": "javascript",
+	"views": {
+		"doc_by_username": {
+			"map": function (doc) {
+				if (doc.type === "userdetails") {
+					emit(doc.username, doc);
+				}
+			}
+		},
+		"by_creation_for_inactive": {
+			"map": function (doc) {
+				if (doc.type === "userdetails" && doc.activationKey) {
+					emit(doc.creation, {_rev: doc._rev});
+				}
+			}
+		}
+	}
+};
-- 
GitLab