From 3cb9fa6f64af3a3c4d33c1068eb2f222349c4743 Mon Sep 17 00:00:00 2001
From: Christoph Thelen <christoph.thelen@mni.thm.de>
Date: Tue, 27 Jan 2015 16:49:48 +0100
Subject: [PATCH] Fix for #14067: Two ways of calculating the learning progress

---
 .../arsnova/controller/SessionController.java |  18 +--
 .../java/de/thm/arsnova/dao/CouchDBDao.java   | 122 ++++--------------
 .../java/de/thm/arsnova/dao/IDatabaseDao.java |  19 +--
 .../de/thm/arsnova/domain/CourseScore.java    |  80 ++++++++++++
 .../thm/arsnova/domain/LearningProgress.java  |  30 +++++
 .../domain/LearningProgressFactory.java       |  22 ++++
 .../domain/PointBasedLearningProgress.java    |  70 ++++++++++
 .../domain/QuestionBasedLearningProgress.java | 100 ++++++++++++++
 .../de/thm/arsnova/domain/QuestionScore.java  |  68 ++++++++++
 .../java/de/thm/arsnova/domain/UserScore.java |  48 +++++++
 .../de/thm/arsnova/domain/package-info.java   |   1 +
 .../thm/arsnova/services/ISessionService.java |  10 +-
 .../thm/arsnova/services/SessionService.java  |  28 ++--
 .../webapp/WEB-INF/spring/spring-main.xml     |   2 +-
 .../de/thm/arsnova/dao/StubDatabaseDao.java   |  18 +--
 15 files changed, 486 insertions(+), 150 deletions(-)
 create mode 100644 src/main/java/de/thm/arsnova/domain/CourseScore.java
 create mode 100644 src/main/java/de/thm/arsnova/domain/LearningProgress.java
 create mode 100644 src/main/java/de/thm/arsnova/domain/LearningProgressFactory.java
 create mode 100644 src/main/java/de/thm/arsnova/domain/PointBasedLearningProgress.java
 create mode 100644 src/main/java/de/thm/arsnova/domain/QuestionBasedLearningProgress.java
 create mode 100644 src/main/java/de/thm/arsnova/domain/QuestionScore.java
 create mode 100644 src/main/java/de/thm/arsnova/domain/UserScore.java
 create mode 100644 src/main/java/de/thm/arsnova/domain/package-info.java

diff --git a/src/main/java/de/thm/arsnova/controller/SessionController.java b/src/main/java/de/thm/arsnova/controller/SessionController.java
index 3383c290..890e9f33 100644
--- a/src/main/java/de/thm/arsnova/controller/SessionController.java
+++ b/src/main/java/de/thm/arsnova/controller/SessionController.java
@@ -21,8 +21,6 @@ import java.util.AbstractMap.SimpleEntry;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
-
 import javax.servlet.http.HttpServletResponse;
 
 import net.sf.json.JSONObject;
@@ -101,8 +99,8 @@ public class SessionController extends AbstractController {
 				session.setName(session.getName() + appendix);
 				session.setShortName(session.getShortName() + appendix);
 			}
-		}		
-		
+		}
+
 		final Session newSession = sessionService.saveSession(session);
 
 		if (newSession == null) {
@@ -195,7 +193,7 @@ public class SessionController extends AbstractController {
 			final HttpServletResponse response
 			) {
 		List<SessionInfo> sessions = sessionService.getMyPublicPoolSessionsInfo();
-			
+
 		if (sessions == null || sessions.isEmpty()) {
 			response.setStatus(HttpServletResponse.SC_NO_CONTENT);
 			return null;
@@ -203,13 +201,13 @@ public class SessionController extends AbstractController {
 
 		return sessions;
 	}
-	
+
 	@RequestMapping(value = "/publicpool", method = RequestMethod.GET)
 	public final List<Session> getPublicPoolSessions(
 			final HttpServletResponse response
 			) {
 		List<Session> sessions = sessionService.getPublicPoolSessions();
-		
+
 		if (sessions == null || sessions.isEmpty()) {
 			response.setStatus(HttpServletResponse.SC_NO_CONTENT);
 			return null;
@@ -234,17 +232,19 @@ public class SessionController extends AbstractController {
 	@RequestMapping(value = "/{sessionkey}/learningprogress", method = RequestMethod.GET)
 	public final int learningProgress(
 			@PathVariable final String sessionkey,
+			@RequestParam(value = "type", defaultValue = "questions") final String progressType,
 			final HttpServletResponse response
 			) {
-		return sessionService.getLearningProgress(sessionkey);
+		return sessionService.getLearningProgress(sessionkey, progressType);
 	}
 
 	@RequestMapping(value = "/{sessionkey}/mylearningprogress", method = RequestMethod.GET)
 	public final JSONObject myLearningProgress(
 			@PathVariable final String sessionkey,
+			@RequestParam(value = "type", defaultValue = "questions") final String progressType,
 			final HttpServletResponse response
 			) {
-		final SimpleEntry<Integer, Integer> result = sessionService.getMyLearningProgress(sessionkey);
+		final SimpleEntry<Integer, Integer> result = sessionService.getMyLearningProgress(sessionkey, progressType);
 		final JSONObject json = new JSONObject();
 		json.put("myprogress", result.getKey());
 		json.put("courseprogress", result.getValue());
diff --git a/src/main/java/de/thm/arsnova/dao/CouchDBDao.java b/src/main/java/de/thm/arsnova/dao/CouchDBDao.java
index c2eb003d..3f2a8b3b 100644
--- a/src/main/java/de/thm/arsnova/dao/CouchDBDao.java
+++ b/src/main/java/de/thm/arsnova/dao/CouchDBDao.java
@@ -18,15 +18,12 @@
 package de.thm.arsnova.dao;
 
 import java.io.IOException;
-import java.util.AbstractMap;
-import java.util.AbstractMap.SimpleEntry;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
 import java.util.Set;
 
 import net.sf.ezmorph.Morpher;
@@ -50,6 +47,7 @@ import com.fourspaces.couchdb.View;
 import com.fourspaces.couchdb.ViewResults;
 
 import de.thm.arsnova.connector.model.Course;
+import de.thm.arsnova.domain.CourseScore;
 import de.thm.arsnova.entities.Answer;
 import de.thm.arsnova.entities.DbUser;
 import de.thm.arsnova.entities.InterposedQuestion;
@@ -127,7 +125,7 @@ public class CouchDBDao implements IDatabaseDao {
 		}
 		return result;
 	}
-	
+
 	@Override
 	public final List<Session> getPublicPoolSessions() {
 		final NovaView view = new NovaView("session/public_pool_by_subject");
@@ -135,7 +133,7 @@ public class CouchDBDao implements IDatabaseDao {
 		final ViewResults sessions = getDatabase().view(view);
 
 		final List<Session> result = new ArrayList<Session>();
-		
+
 		for (final Document d : sessions.getResults()) {
 			final Session session = (Session) JSONObject.toBean(
 					d.getJSONObject().getJSONObject("value"),
@@ -146,13 +144,13 @@ public class CouchDBDao implements IDatabaseDao {
 		}
 		return result;
 	}
-	
+
 	@Override
 	public final List<Session> getMyPublicPoolSessions(final User user) {
 		final NovaView view = new NovaView("session/public_pool_by_creator");
 		view.setStartKeyArray(user.getUsername());
 		view.setEndKeyArray(user.getUsername(), "{}");
-		
+
 		final ViewResults sessions = getDatabase().view(view);
 
 		final List<Session> result = new ArrayList<Session>();
@@ -168,13 +166,13 @@ public class CouchDBDao implements IDatabaseDao {
 		}
 		return result;
 	}
-	
+
 	@Override
 	public final List<SessionInfo> getMyPublicPoolSessionsInfo(final User user) {
 		final List<Session> sessions = this.getMyPublicPoolSessions(user);
 		return getInfosForSessions(sessions);
 	}
-	
+
 	@Override
 	public final List<SessionInfo> getMySessionsInfo(final User user) {
 		final List<Session> sessions = this.getMySessions(user);
@@ -186,7 +184,7 @@ public class CouchDBDao implements IDatabaseDao {
 		final ExtendedView answerCountView = new ExtendedView("skill_question/count_answers_by_session");
 		final ExtendedView interposedCountView = new ExtendedView("interposed_question/count_by_session");
 		final ExtendedView unredInterposedCountView = new ExtendedView("interposed_question/count_by_session_reading");
-		
+
 		interposedCountView.setSessionIdKeys(sessions);
 		interposedCountView.setGroup(true);
 		questionCountView.setSessionIdKeys(sessions);
@@ -278,7 +276,7 @@ public class CouchDBDao implements IDatabaseDao {
 	private List<SessionInfo> getSessionInfoData(final List<Session> sessions,
 			final ExtendedView questionCountView,
 			final ExtendedView answerCountView,
-			final ExtendedView interposedCountView, 
+			final ExtendedView interposedCountView,
 			final ExtendedView unredInterposedCountView) {
 		final ViewResults questionCountViewResults = getDatabase().view(questionCountView);
 		final ViewResults answerCountViewResults = getDatabase().view(answerCountView);
@@ -301,7 +299,7 @@ public class CouchDBDao implements IDatabaseDao {
 		for (final Document d : unredInterposedCountViewResults.getResults()) {
 			unredInterposedCountMap.put(d.getJSONArray("key").getString(0), d.getInt("value"));
 		}
-		
+
 		List<SessionInfo> sessionInfos = new ArrayList<SessionInfo>();
 		for (Session session : sessions) {
 			int numQuestions = 0;
@@ -320,7 +318,7 @@ public class CouchDBDao implements IDatabaseDao {
 			if (unredInterposedCountMap.containsKey(session.get_id())) {
 				numUnredInterposed = unredInterposedCountMap.get(session.get_id());
 			}
-			
+
 			SessionInfo info = new SessionInfo(session);
 			info.setNumQuestions(numQuestions);
 			info.setNumAnswers(numAnswers);
@@ -811,7 +809,7 @@ public class CouchDBDao implements IDatabaseDao {
 		if (results.getResults().size() == 0) {
 			return 0;
 		}
-		
+
 		return results.getJSONArray("rows").optJSONObject(0).optInt("value");
 	}
 
@@ -1565,9 +1563,7 @@ public class CouchDBDao implements IDatabaseDao {
 	}
 
 	@Override
-	public int getLearningProgress(final Session session) {
-		// Note: we have to use this many views because our CouchDB version does not support
-		// advanced features like summing over lists. Thus, we have to do it all by ourselves...
+	public CourseScore getLearningProgress(final Session session) {
 		final NovaView maximumValueView = new NovaView("learning_progress/maximum_value_of_question");
 		final NovaView answerSumView = new NovaView("learning_progress/question_value_achieved_for_user");
 		maximumValueView.setStartKeyArray(session.get_id());
@@ -1578,98 +1574,28 @@ public class CouchDBDao implements IDatabaseDao {
 		final List<Document> maximumValueResult = getDatabase().view(maximumValueView).getResults();
 		final List<Document> answerSumResult = getDatabase().view(answerSumView).getResults();
 
+		CourseScore courseScore = new CourseScore();
+
 		// no results found
 		if (maximumValueResult.isEmpty() || answerSumResult.isEmpty()) {
-			return 0;
+			return courseScore;
 		}
 
-		// collect mapping (questionId -> value)
-		Map<String, Integer> questionValues = new HashMap<String, Integer>();
+		// collect mapping (questionId -> max value)
 		for (Document d : maximumValueResult) {
-			questionValues.put(d.getJSONArray("key").getString(1), d.getInt("value"));
+			String questionId = d.getJSONArray("key").getString(1);
+			int questionScore = d.getInt("value");
+			courseScore.add(questionId, questionScore);
 		}
 		// collect mapping (questionId -> (user -> value))
-		Map<String, Map<String, Integer>> answerValues = new HashMap<String, Map<String, Integer>>();
 		for (Document d : answerSumResult) {
+			String username = d.getJSONArray("key").getString(1);
 			JSONObject value = d.getJSONObject("value");
 			String questionId = value.getString("questionId");
-			if (!answerValues.containsKey(questionId)) {
-				answerValues.put(questionId, new HashMap<String, Integer>());
-			}
-			Map<String, Integer> userValues = answerValues.get(questionId);
-			userValues.put(d.getJSONArray("key").getString(1), value.getInt("score"));
-			answerValues.put(questionId, userValues);
-		}
-
-		// a question is seen as "correct" if and only if all participants have answered it correctly
-		int numQuestionsCorrect = 0;
-		for (Entry<String, Integer> entry : questionValues.entrySet()) {
-			if (answerValues.containsKey(entry.getKey())) {
-				Map<String, Integer> userValues = answerValues.get(entry.getKey());
-				int requiredValue = entry.getValue();
-				boolean allCorrect = true;
-				for (Entry<String, Integer> userEntry : userValues.entrySet()) {
-					// did this particular participant answer it correctly, i.e., has the required points?
-					if (userEntry.getValue() != requiredValue) {
-						allCorrect = false;
-						break;
-					}
-				}
-				if (allCorrect) {
-					numQuestionsCorrect++;
-				}
-			}
+			int userscore = value.getInt("score");
+			courseScore.add(questionId, username, userscore);
 		}
-
-		final double myLearningProgress = (double)numQuestionsCorrect / (questionValues.size());
-		// calculate percent, cap results to 100
-		return (int) Math.min(100, Math.round(myLearningProgress*100));
-	}
-
-	@Override
-	public SimpleEntry<Integer,Integer> getMyLearningProgress(final Session session, final User user) {
-		final int courseProgress = getLearningProgress(session);
-
-		final NovaView maximumValueView = new NovaView("learning_progress/maximum_value_of_question");
-		final NovaView answerSumView = new NovaView("learning_progress/question_value_achieved_for_user");
-		maximumValueView.setStartKeyArray(session.get_id());
-		maximumValueView.setEndKeyArray(session.get_id(), "{}");
-		answerSumView.setKey(session.get_id(), user.getUsername());
-
-		final List<Document> maximumValueResult = getDatabase().view(maximumValueView).getResults();
-		final List<Document> answerSumResult = getDatabase().view(answerSumView).getResults();
-
-		// no results found
-		if (maximumValueResult.isEmpty() || answerSumResult.isEmpty()) {
-			return new AbstractMap.SimpleEntry<Integer, Integer>(0, courseProgress);
-		}
-
-		// collect mappings (questionId -> value)
-		Map<String, Integer> questionValues = new HashMap<String, Integer>();
-		for (Document d : maximumValueResult) {
-			questionValues.put(d.getJSONArray("key").getString(1), d.getInt("value"));
-		}
-		Map<String, Integer> answerValues = new HashMap<String, Integer>();
-		for (Document d : answerSumResult) {
-			JSONObject value = d.getJSONObject("value");
-			answerValues.put(value.getString("questionId"), value.getInt("score"));
-		}
-		// compare user's values to the maximum number for each question to determine the answers' correctness
-		// mapping (questionId -> 1 if correct, 0 if incorrect)
-		int numQuestionsCorrect = 0;
-		for (Entry<String, Integer> entry : questionValues.entrySet()) {
-			if (answerValues.containsKey(entry.getKey())) {
-				int answeredValue = answerValues.get(entry.getKey());
-				if (answeredValue == entry.getValue()) {
-					numQuestionsCorrect++;
-				}
-			}
-		}
-
-		final double myLearningProgress = (double)numQuestionsCorrect / (double)(questionValues.size());
-		// calculate percent, cap results to 100
-		final int percentage = (int) Math.min(100, Math.round(myLearningProgress*100));
-		return new AbstractMap.SimpleEntry<Integer, Integer>(percentage, courseProgress);
+		return courseScore;
 	}
 
 	@Override
diff --git a/src/main/java/de/thm/arsnova/dao/IDatabaseDao.java b/src/main/java/de/thm/arsnova/dao/IDatabaseDao.java
index 80224015..3807436a 100644
--- a/src/main/java/de/thm/arsnova/dao/IDatabaseDao.java
+++ b/src/main/java/de/thm/arsnova/dao/IDatabaseDao.java
@@ -17,12 +17,9 @@
  */
 package de.thm.arsnova.dao;
 
-import java.util.AbstractMap.SimpleEntry;
-import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
-
 import de.thm.arsnova.connector.model.Course;
+import de.thm.arsnova.domain.CourseScore;
 import de.thm.arsnova.entities.Answer;
 import de.thm.arsnova.entities.DbUser;
 import de.thm.arsnova.entities.InterposedQuestion;
@@ -39,9 +36,9 @@ public interface IDatabaseDao {
 	Session getSession(String keyword);
 
 	List<Session> getMySessions(User user);
-	
+
 	List<Session> getPublicPoolSessions();
-	
+
 	List<Session> getMyPublicPoolSessions(User user);
 
 	Session saveSession(User user, Session session);
@@ -75,7 +72,7 @@ public interface IDatabaseDao {
 	List<Answer> getAnswers(String questionId, int piRound);
 
 	int getAnswerCount(Question question, int piRound);
-	
+
 	int getAbstentionAnswerCount(String questionId);
 
 	List<Answer> getFreetextAnswers(String questionId);
@@ -161,7 +158,7 @@ public interface IDatabaseDao {
 	void deleteAllInterposedQuestions(Session session);
 
 	void deleteAllInterposedQuestions(Session session, User user);
-	
+
 	void publishQuestions(Session session, boolean publish, List<Question> questions);
 
 	void publishAllQuestions(Session session, boolean publish);
@@ -174,14 +171,12 @@ public interface IDatabaseDao {
 
 	boolean deleteUser(DbUser dbUser);
 
-	int getLearningProgress(Session session);
-
-	SimpleEntry<Integer, Integer> getMyLearningProgress(Session session, User user);
+	CourseScore getLearningProgress(Session session);
 
 	List<SessionInfo> getMySessionsInfo(User user);
 
 	List<SessionInfo> getMyPublicPoolSessionsInfo(final User user);
-	
+
 	List<SessionInfo> getMyVisitedSessionsInfo(User currentUser);
 
 	void deleteAllPreparationAnswers(Session session);
diff --git a/src/main/java/de/thm/arsnova/domain/CourseScore.java b/src/main/java/de/thm/arsnova/domain/CourseScore.java
new file mode 100644
index 00000000..13730285
--- /dev/null
+++ b/src/main/java/de/thm/arsnova/domain/CourseScore.java
@@ -0,0 +1,80 @@
+/*
+ * This file is part of ARSnova Backend.
+ * Copyright (C) 2012-2015 The ARSnova Team
+ *
+ * ARSnova Backend is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * ARSnova Backend is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package de.thm.arsnova.domain;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+public class CourseScore implements Iterable<QuestionScore> {
+
+	private final Map<String, QuestionScore> scores = new HashMap<String, QuestionScore>();
+
+	public void add(String questionId, int questionScore) {
+		scores.put(questionId, new QuestionScore(questionId, questionScore));
+	}
+
+	/**
+	 * @pre questionId has been added before.
+	 * @param questionId
+	 * @param username
+	 * @param userscore
+	 */
+	public void add(String questionId, String username, int userscore) {
+		if (!scores.containsKey(questionId)) {
+			throw new IllegalArgumentException("Invalid argument questionId");
+		}
+		QuestionScore questionScore = scores.get(questionId);
+		questionScore.add(username, userscore);
+	}
+
+	public int getMaximumScore() {
+		int score = 0;
+		for (QuestionScore questionScore : this) {
+			score += questionScore.getMaximum();
+		}
+		return score;
+	}
+
+	public int getTotalUserScore() {
+		int score = 0;
+		for (QuestionScore questionScore : this) {
+			score += questionScore.getTotalUserScore();
+		}
+		return score;
+	}
+
+	public int getTotalUserCount() {
+		Set<String> users = new HashSet<String>();
+		for (QuestionScore questionScore : this) {
+			questionScore.collectUsers(users);
+		}
+		return users.size();
+	}
+
+	public int getQuestionCount() {
+		return scores.size();
+	}
+
+	@Override
+	public Iterator<QuestionScore> iterator() {
+		return this.scores.values().iterator();
+	}
+}
diff --git a/src/main/java/de/thm/arsnova/domain/LearningProgress.java b/src/main/java/de/thm/arsnova/domain/LearningProgress.java
new file mode 100644
index 00000000..e8ccfbfe
--- /dev/null
+++ b/src/main/java/de/thm/arsnova/domain/LearningProgress.java
@@ -0,0 +1,30 @@
+/*
+ * This file is part of ARSnova Backend.
+ * Copyright (C) 2012-2015 The ARSnova Team
+ *
+ * ARSnova Backend is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * ARSnova Backend is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package de.thm.arsnova.domain;
+
+import java.util.AbstractMap.SimpleEntry;
+
+import de.thm.arsnova.entities.Session;
+import de.thm.arsnova.entities.User;
+
+public interface LearningProgress {
+
+	public int getCourseProgress(Session session);
+
+	public SimpleEntry<Integer,Integer> getMyProgress(Session session, User user);
+}
diff --git a/src/main/java/de/thm/arsnova/domain/LearningProgressFactory.java b/src/main/java/de/thm/arsnova/domain/LearningProgressFactory.java
new file mode 100644
index 00000000..0f819d41
--- /dev/null
+++ b/src/main/java/de/thm/arsnova/domain/LearningProgressFactory.java
@@ -0,0 +1,22 @@
+package de.thm.arsnova.domain;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import de.thm.arsnova.dao.IDatabaseDao;
+
+@Component
+public class LearningProgressFactory {
+
+	@Autowired
+	private IDatabaseDao databaseDao;
+
+	public LearningProgress createFromType(String progressType) {
+		if (progressType.equals("questions")) {
+			return new QuestionBasedLearningProgress(databaseDao);
+		} else {
+			return new PointBasedLearningProgress(databaseDao);
+		}
+	}
+
+}
diff --git a/src/main/java/de/thm/arsnova/domain/PointBasedLearningProgress.java b/src/main/java/de/thm/arsnova/domain/PointBasedLearningProgress.java
new file mode 100644
index 00000000..be968bff
--- /dev/null
+++ b/src/main/java/de/thm/arsnova/domain/PointBasedLearningProgress.java
@@ -0,0 +1,70 @@
+/*
+ * This file is part of ARSnova Backend.
+ * Copyright (C) 2012-2015 The ARSnova Team
+ *
+ * ARSnova Backend is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * ARSnova Backend is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package de.thm.arsnova.domain;
+
+import java.util.AbstractMap;
+import java.util.AbstractMap.SimpleEntry;
+
+import de.thm.arsnova.dao.IDatabaseDao;
+import de.thm.arsnova.entities.Session;
+import de.thm.arsnova.entities.User;
+
+public class PointBasedLearningProgress implements LearningProgress {
+
+	private IDatabaseDao databaseDao;
+
+	public PointBasedLearningProgress(IDatabaseDao dao) {
+		this.databaseDao = dao;
+	}
+
+	@Override
+	public int getCourseProgress(Session session) {
+		CourseScore courseScore = databaseDao.getLearningProgress(session);
+		return calculateCourseScore(courseScore);
+	}
+
+	private int calculateCourseScore(CourseScore courseScore) {
+		final double courseMaximumValue = courseScore.getMaximumScore();
+		final double userTotalValue = courseScore.getTotalUserScore();
+		final double numUsers = courseScore.getTotalUserCount();
+		if (courseMaximumValue == 0 || numUsers == 0) {
+			return 0;
+		}
+		final double courseAverageValue = userTotalValue / numUsers;
+		final double courseProgress = courseAverageValue / courseMaximumValue;
+		return (int)Math.min(100, Math.round(courseProgress * 100));
+	}
+
+	@Override
+	public SimpleEntry<Integer, Integer> getMyProgress(Session session, User user) {
+		CourseScore courseScore = databaseDao.getLearningProgress(session);
+		int courseProgress = calculateCourseScore(courseScore);
+
+		final double courseMaximumValue = courseScore.getMaximumScore();
+		final double userTotalValue = courseScore.getTotalUserScore();
+
+		if (courseMaximumValue == 0) {
+			return new AbstractMap.SimpleEntry<Integer, Integer>(0, courseProgress);
+		}
+		final double myProgress = userTotalValue / courseMaximumValue;
+		final int myLearningProgress = (int)Math.min(100, Math.round(myProgress*100));
+
+		return new AbstractMap.SimpleEntry<Integer, Integer>(myLearningProgress, courseProgress);
+	}
+
+}
diff --git a/src/main/java/de/thm/arsnova/domain/QuestionBasedLearningProgress.java b/src/main/java/de/thm/arsnova/domain/QuestionBasedLearningProgress.java
new file mode 100644
index 00000000..9fcc56ce
--- /dev/null
+++ b/src/main/java/de/thm/arsnova/domain/QuestionBasedLearningProgress.java
@@ -0,0 +1,100 @@
+/*
+ * This file is part of ARSnova Backend.
+ * Copyright (C) 2012-2015 The ARSnova Team
+ *
+ * ARSnova Backend is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * ARSnova Backend is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package de.thm.arsnova.domain;
+
+import java.util.AbstractMap;
+import java.util.AbstractMap.SimpleEntry;
+
+import de.thm.arsnova.dao.IDatabaseDao;
+import de.thm.arsnova.entities.Session;
+import de.thm.arsnova.entities.User;
+
+public class QuestionBasedLearningProgress implements LearningProgress {
+
+	private IDatabaseDao databaseDao;
+
+	public QuestionBasedLearningProgress(IDatabaseDao dao) {
+		this.databaseDao = dao;
+	}
+
+	@Override
+	public int getCourseProgress(Session session) {
+		CourseScore courseScore = databaseDao.getLearningProgress(session);
+		return calculateCourseProgress(courseScore);
+	}
+
+	private int calculateCourseProgress(CourseScore courseScore) {
+		int numQuestionsCorrect = numQuestionsCorrectForCourse(courseScore);
+		int numUsers = courseScore.getTotalUserCount();
+		final double correctQuestionsOnAverage = (double)numQuestionsCorrect / (double)numUsers;
+		final double myLearningProgress = correctQuestionsOnAverage / courseScore.getQuestionCount();
+		// calculate percent, cap results to 100
+		return (int) Math.min(100, Math.round(myLearningProgress*100));
+	}
+
+	private int numQuestionsCorrectForCourse(CourseScore courseScore) {
+		// a question is seen as "correct" if and only if all participants have answered it correctly
+		int numQuestionsCorrect = 0;
+		for (QuestionScore questionScore : courseScore) {
+			int requiredScore = questionScore.getMaximum();
+			boolean allCorrect = true;
+			for (UserScore userScore : questionScore) {
+				if (!userScore.hasScore(requiredScore)) {
+					allCorrect = false;
+					break;
+				}
+			}
+			if (allCorrect) {
+				numQuestionsCorrect++;
+			}
+		}
+		return numQuestionsCorrect;
+	}
+
+	@Override
+	public SimpleEntry<Integer, Integer> getMyProgress(Session session, User user) {
+		CourseScore courseScore = databaseDao.getLearningProgress(session);
+
+		int courseProgress = calculateCourseProgress(courseScore);
+
+		int numQuestionsCorrect = numQuestionsCorrectForUser(user, courseScore);
+		final double myLearningProgress = numQuestionsCorrect / (double)(courseScore.getQuestionCount());
+		// calculate percent, cap results to 100
+		final int percentage = (int) Math.min(100, Math.round(myLearningProgress*100));
+		return new AbstractMap.SimpleEntry<Integer, Integer>(percentage, courseProgress);
+	}
+
+	private int numQuestionsCorrectForUser(User user, CourseScore courseScore) {
+		// compare user's values to the maximum number for each question to determine the answers' correctness
+		// mapping (questionId -> 1 if correct, 0 if incorrect)
+		int numQuestionsCorrect = 0;
+		for (QuestionScore questionScore : courseScore) {
+			int requiredScore = questionScore.getMaximum();
+			for (UserScore userScore : questionScore) {
+				if (!userScore.isUser(user)) {
+					continue;
+				}
+				if (userScore.hasScore(requiredScore)) {
+					numQuestionsCorrect++;
+				}
+			}
+		}
+		return numQuestionsCorrect;
+	}
+
+}
diff --git a/src/main/java/de/thm/arsnova/domain/QuestionScore.java b/src/main/java/de/thm/arsnova/domain/QuestionScore.java
new file mode 100644
index 00000000..3aceb3e9
--- /dev/null
+++ b/src/main/java/de/thm/arsnova/domain/QuestionScore.java
@@ -0,0 +1,68 @@
+/*
+ * This file is part of ARSnova Backend.
+ * Copyright (C) 2012-2015 The ARSnova Team
+ *
+ * ARSnova Backend is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * ARSnova Backend is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package de.thm.arsnova.domain;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+public class QuestionScore implements Iterable<UserScore> {
+
+	private String questionId;
+
+	private int maximumScore;
+
+	private List<UserScore> userScores = new ArrayList<UserScore>();
+
+	public QuestionScore(String questionId, int maximumScore) {
+		this.questionId = questionId;
+		this.maximumScore = maximumScore;
+	}
+
+	public int getMaximum() {
+		return this.maximumScore;
+	}
+
+	@Override
+	public Iterator<UserScore> iterator() {
+		return this.userScores.iterator();
+	}
+
+	public void add(String username, int userscore) {
+		userScores.add(new UserScore(username, userscore));
+	}
+
+	public int getTotalUserScore() {
+		int totalScore = 0;
+		for (UserScore score : userScores) {
+			totalScore += score.getScore();
+		}
+		return totalScore;
+	}
+
+	public int getUserCount() {
+		return userScores.size();
+	}
+
+	public void collectUsers(Set<String> users) {
+		for (UserScore score : userScores) {
+			users.add(score.getUsername());
+		}
+	}
+}
diff --git a/src/main/java/de/thm/arsnova/domain/UserScore.java b/src/main/java/de/thm/arsnova/domain/UserScore.java
new file mode 100644
index 00000000..282fd06e
--- /dev/null
+++ b/src/main/java/de/thm/arsnova/domain/UserScore.java
@@ -0,0 +1,48 @@
+/*
+ * This file is part of ARSnova Backend.
+ * Copyright (C) 2012-2015 The ARSnova Team
+ *
+ * ARSnova Backend is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * ARSnova Backend is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package de.thm.arsnova.domain;
+
+import de.thm.arsnova.entities.User;
+
+public class UserScore {
+
+	private String username;
+
+	private int score;
+
+	public UserScore(String username, int score) {
+		this.username = username;
+		this.score = score;
+	}
+
+	public boolean hasScore(int score) {
+		return this.score == score;
+	}
+
+	public int getScore() {
+		return score;
+	}
+
+	public boolean isUser(User user) {
+		return user.getUsername().equals(username);
+	}
+
+	public String getUsername() {
+		return username;
+	}
+}
diff --git a/src/main/java/de/thm/arsnova/domain/package-info.java b/src/main/java/de/thm/arsnova/domain/package-info.java
new file mode 100644
index 00000000..ee2d2f1d
--- /dev/null
+++ b/src/main/java/de/thm/arsnova/domain/package-info.java
@@ -0,0 +1 @@
+package de.thm.arsnova.domain;
\ No newline at end of file
diff --git a/src/main/java/de/thm/arsnova/services/ISessionService.java b/src/main/java/de/thm/arsnova/services/ISessionService.java
index 0ded048f..8d223c64 100644
--- a/src/main/java/de/thm/arsnova/services/ISessionService.java
+++ b/src/main/java/de/thm/arsnova/services/ISessionService.java
@@ -18,9 +18,7 @@
 package de.thm.arsnova.services;
 
 import java.util.AbstractMap.SimpleEntry;
-import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
 import java.util.UUID;
 
 import de.thm.arsnova.connector.model.Course;
@@ -37,7 +35,7 @@ public interface ISessionService {
 	String generateKeyword();
 
 	List<Session> getMySessions();
-	
+
 	List<Session> getPublicPoolSessions();
 
 	List<Session> getMyVisitedSessions();
@@ -54,12 +52,12 @@ public interface ISessionService {
 
 	void deleteSession(String sessionkey);
 
-	int getLearningProgress(String sessionkey);
+	int getLearningProgress(String sessionkey, String progressType);
 
-	SimpleEntry<Integer, Integer> getMyLearningProgress(String sessionkey);
+	SimpleEntry<Integer, Integer> getMyLearningProgress(String sessionkey, String progressType);
 
 	List<SessionInfo> getMySessionsInfo();
-	
+
 	List<SessionInfo> getMyPublicPoolSessionsInfo();
 
 	List<SessionInfo> getMyVisitedSessionsInfo();
diff --git a/src/main/java/de/thm/arsnova/services/SessionService.java b/src/main/java/de/thm/arsnova/services/SessionService.java
index 37b9f8b4..1f29a67c 100644
--- a/src/main/java/de/thm/arsnova/services/SessionService.java
+++ b/src/main/java/de/thm/arsnova/services/SessionService.java
@@ -19,17 +19,14 @@ package de.thm.arsnova.services;
 
 import java.io.Serializable;
 import java.util.AbstractMap.SimpleEntry;
-import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.List;
-import java.util.Map;
 import java.util.UUID;
 
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.stereotype.Service;
-
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -37,6 +34,8 @@ import de.thm.arsnova.ImageUtils;
 import de.thm.arsnova.connector.client.ConnectorClient;
 import de.thm.arsnova.connector.model.Course;
 import de.thm.arsnova.dao.IDatabaseDao;
+import de.thm.arsnova.domain.LearningProgress;
+import de.thm.arsnova.domain.LearningProgressFactory;
 import de.thm.arsnova.entities.Question;
 import de.thm.arsnova.entities.Session;
 import de.thm.arsnova.entities.SessionInfo;
@@ -94,12 +93,15 @@ public class SessionService implements ISessionService {
 	@Autowired
 	private ARSnovaSocketIOServer socketIoServer;
 
+	@Autowired
+	private LearningProgressFactory learningProgressFactory;
+
 	@Autowired(required = false)
 	private ConnectorClient connectorClient;
-	
+
 	@Value("${pp.logofilesize_b}")
 	private int uploadFileSizeByte;
-	
+
 	public static final Logger LOGGER = LoggerFactory.getLogger(SessionService.class);
 
 	public void setDatabaseDao(final IDatabaseDao newDatabaseDao) {
@@ -164,13 +166,13 @@ public class SessionService implements ISessionService {
 	public final List<Session> getMySessions() {
 		return databaseDao.getMySessions(userService.getCurrentUser());
 	}
-	
+
 	@Override
 	@PreAuthorize("isAuthenticated()")
 	public final List<Session> getPublicPoolSessions() {
 		return databaseDao.getPublicPoolSessions();
 	}
-	
+
 	@Override
 	@PreAuthorize("isAuthenticated()")
 	public final List<SessionInfo> getMyPublicPoolSessionsInfo() {
@@ -221,7 +223,7 @@ public class SessionService implements ISessionService {
 				throw new BadRequestException();
 			}
 		}
-		
+
 		return databaseDao.saveSession(userService.getCurrentUser(), session);
 	}
 
@@ -286,16 +288,18 @@ public class SessionService implements ISessionService {
 
 	@Override
 	@PreAuthorize("isAuthenticated()")
-	public int getLearningProgress(final String sessionkey) {
+	public int getLearningProgress(final String sessionkey, final String progressType) {
 		final Session session = databaseDao.getSession(sessionkey);
-		return databaseDao.getLearningProgress(session);
+		LearningProgress learningProgress = learningProgressFactory.createFromType(progressType);
+		return learningProgress.getCourseProgress(session);
 	}
 
 	@Override
 	@PreAuthorize("isAuthenticated()")
-	public SimpleEntry<Integer, Integer> getMyLearningProgress(final String sessionkey) {
+	public SimpleEntry<Integer, Integer> getMyLearningProgress(final String sessionkey, final String progressType) {
 		final Session session = databaseDao.getSession(sessionkey);
 		final User user = userService.getCurrentUser();
-		return databaseDao.getMyLearningProgress(session, user);
+		LearningProgress learningProgress = learningProgressFactory.createFromType(progressType);
+		return learningProgress.getMyProgress(session, user);
 	}
 }
diff --git a/src/main/webapp/WEB-INF/spring/spring-main.xml b/src/main/webapp/WEB-INF/spring/spring-main.xml
index b9bedd60..e5141f75 100644
--- a/src/main/webapp/WEB-INF/spring/spring-main.xml
+++ b/src/main/webapp/WEB-INF/spring/spring-main.xml
@@ -23,7 +23,7 @@
 		<property name="fileEncoding" value="UTF-8" />
 	</bean>
 
-	<context:component-scan base-package="de.thm.arsnova.dao,de.thm.arsnova.events,de.thm.arsnova.security,de.thm.arsnova.services,de.thm.arsnova.config" />
+	<context:component-scan base-package="de.thm.arsnova.dao,de.thm.arsnova.events,de.thm.arsnova.security,de.thm.arsnova.services,de.thm.arsnova.config,de.thm.arsnova.domain" />
 
 	<context:annotation-config />
 
diff --git a/src/test/java/de/thm/arsnova/dao/StubDatabaseDao.java b/src/test/java/de/thm/arsnova/dao/StubDatabaseDao.java
index 8a09d652..6d21ab41 100644
--- a/src/test/java/de/thm/arsnova/dao/StubDatabaseDao.java
+++ b/src/test/java/de/thm/arsnova/dao/StubDatabaseDao.java
@@ -18,13 +18,13 @@
  */
 package de.thm.arsnova.dao;
 
-import java.util.AbstractMap.SimpleEntry;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
 import de.thm.arsnova.connector.model.Course;
+import de.thm.arsnova.domain.CourseScore;
 import de.thm.arsnova.entities.Answer;
 import de.thm.arsnova.entities.DbUser;
 import de.thm.arsnova.entities.Feedback;
@@ -193,19 +193,19 @@ public class StubDatabaseDao implements IDatabaseDao {
 		// TODO Auto-generated method stub
 		return null;
 	}
-	
+
 	@Override
 	public List<Session> getPublicPoolSessions() {
 		// TODO Auto-generated method stub
 		return null;
 	}
-	
+
 	@Override
 	public List<Session> getMyPublicPoolSessions(User user) {
 		// TODO Auto-generated method stub
 		return null;
 	}
-	
+
 	@Override
 	public List<SessionInfo> getMyPublicPoolSessionsInfo(final User user) {
 		// TODO Auto-generated method stub
@@ -477,7 +477,7 @@ public class StubDatabaseDao implements IDatabaseDao {
 	public void deleteAllInterposedQuestions(Session session) {
 		// TODO Auto-generated method stub
 	}
-	
+
 	@Override
 	public void publishQuestions(Session session, boolean publish, List<Question> questions) {
 		// TODO Auto-generated method stub
@@ -509,13 +509,7 @@ public class StubDatabaseDao implements IDatabaseDao {
 	}
 
 	@Override
-	public int getLearningProgress(Session session) {
-		// TODO Auto-generated method stub
-		return 0;
-	}
-
-	@Override
-	public SimpleEntry<Integer, Integer> getMyLearningProgress(Session session, User user) {
+	public CourseScore getLearningProgress(Session session) {
 		// TODO Auto-generated method stub
 		return null;
 	}
-- 
GitLab