diff --git a/src/main/java/de/thm/arsnova/entities/AnswerStatistics.java b/src/main/java/de/thm/arsnova/entities/AnswerStatistics.java index bbc71a6fcb6cc4a025d50a55a4ce36a1402cf349..336b39ab7a529c73b76929ce5b213c380714d838 100644 --- a/src/main/java/de/thm/arsnova/entities/AnswerStatistics.java +++ b/src/main/java/de/thm/arsnova/entities/AnswerStatistics.java @@ -2,19 +2,44 @@ package de.thm.arsnova.entities; import com.fasterxml.jackson.annotation.JsonView; import de.thm.arsnova.entities.serialization.View; +import org.springframework.core.style.ToStringCreator; +import java.util.Collection; import java.util.List; public class AnswerStatistics { public static class RoundStatistics { public static class Combination { - private int[] selectedChoiceIndexes; + private List<Integer> selectedChoiceIndexes; private int count; + + public Combination(final List<Integer> selectedChoiceIndexes, final int count) { + this.selectedChoiceIndexes = selectedChoiceIndexes; + this.count = count; + } + + @JsonView(View.Public.class) + public List<Integer> getSelectedChoiceIndexes() { + return selectedChoiceIndexes; + } + + @JsonView(View.Public.class) + public int getCount() { + return count; + } + + @Override + public String toString() { + return new ToStringCreator(this) + .append("selectedChoiceIndexes", selectedChoiceIndexes) + .append("count", count) + .toString(); + } } private int round; - private int[] independentCounts; - private List<Combination> combinatedCounts; + private List<Integer> independentCounts; + private Collection<Combination> combinatedCounts; private int abstentionCount; @JsonView(View.Public.class) @@ -27,20 +52,20 @@ public class AnswerStatistics { } @JsonView(View.Public.class) - public int[] getIndependentCounts() { + public List<Integer> getIndependentCounts() { return independentCounts; } - public void setIndependentCounts(final int[] independentCounts) { + public void setIndependentCounts(final List<Integer> independentCounts) { this.independentCounts = independentCounts; } @JsonView(View.Public.class) - public List<Combination> getCombinatedCounts() { + public Collection<Combination> getCombinatedCounts() { return combinatedCounts; } - public void setCombinatedCounts(List<Combination> combinatedCounts) { + public void setCombinatedCounts(Collection<Combination> combinatedCounts) { this.combinatedCounts = combinatedCounts; } @@ -52,14 +77,69 @@ public class AnswerStatistics { public void setAbstentionCount(int abstentionCount) { this.abstentionCount = abstentionCount; } + + @Override + public String toString() { + return new ToStringCreator(this) + .append("round", round) + .append("independentCounts", independentCounts) + .append("combinatedCounts", combinatedCounts) + .append("abstentionCount", abstentionCount) + .toString(); + } } public static class RoundTransition { private int roundA; private int roundB; - private int[] selectedChoiceIndexesA; - private int[] selectedChoiceIndexesB; + private List<Integer> selectedChoiceIndexesA; + private List<Integer> selectedChoiceIndexesB; private int count; + + public RoundTransition(final int roundA, final List<Integer> selectedChoiceIndexesA, + final int roundB, final List<Integer> selectedChoiceIndexesB, final int count) { + this.roundA = roundA; + this.roundB = roundB; + this.selectedChoiceIndexesA = selectedChoiceIndexesA; + this.selectedChoiceIndexesB = selectedChoiceIndexesB; + this.count = count; + } + + @JsonView(View.Public.class) + public int getRoundA() { + return roundA; + } + + @JsonView(View.Public.class) + public int getRoundB() { + return roundB; + } + + @JsonView(View.Public.class) + public List<Integer> getSelectedChoiceIndexesA() { + return selectedChoiceIndexesA; + } + + @JsonView(View.Public.class) + public List<Integer> getSelectedChoiceIndexesB() { + return selectedChoiceIndexesB; + } + + @JsonView(View.Public.class) + public int getCount() { + return count; + } + + @Override + public String toString() { + return new ToStringCreator(this) + .append("roundA", roundA) + .append("selectedChoiceIndexesA", selectedChoiceIndexesA) + .append("roundB", roundB) + .append("selectedChoiceIndexesB", selectedChoiceIndexesB) + .append("count", count) + .toString(); + } } private String contentId; @@ -92,4 +172,13 @@ public class AnswerStatistics { public void setRoundTransitions(List<RoundTransition> roundTransitions) { this.roundTransitions = roundTransitions; } + + @Override + public String toString() { + return new ToStringCreator(this) + .append("contentId", contentId) + .append("roundStatistics", roundStatistics) + .append("roundTransitions", roundTransitions) + .toString(); + } } diff --git a/src/main/java/de/thm/arsnova/entities/migration/ToV2Migrator.java b/src/main/java/de/thm/arsnova/entities/migration/ToV2Migrator.java index 6109f0a41d9efaff51659accf2f0963598c78a16..59d3469f4e3d773c3edf27c5bb11015e772f59d3 100644 --- a/src/main/java/de/thm/arsnova/entities/migration/ToV2Migrator.java +++ b/src/main/java/de/thm/arsnova/entities/migration/ToV2Migrator.java @@ -23,7 +23,9 @@ import de.thm.arsnova.entities.UserProfile; import de.thm.arsnova.entities.migration.v2.*; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; @@ -329,16 +331,31 @@ public class ToV2Migrator { to.add(abstention); } - int i = 0; - for (ChoiceQuestionContent.AnswerOption option : content.getOptions()) { + Map<String, Integer> choices; + if (content.isMultiple()) { + /* Map selected choice indexes -> answer count */ + choices = stats.getCombinatedCounts().stream().collect(Collectors.toMap( + c -> migrateChoice(c.getSelectedChoiceIndexes(), content.getOptions()), + c -> c.getCount(), + (u, v) -> { throw new IllegalStateException(String.format("Duplicate key %s", u)); }, + LinkedHashMap::new)); + } else { + choices = new LinkedHashMap<>(); + int i = 0; + for (ChoiceQuestionContent.AnswerOption option : content.getOptions()) { + choices.put(option.getLabel(), stats.getIndependentCounts().get(i)); + i++; + } + } + + for (Map.Entry<String, Integer> choice : choices.entrySet()) { Answer answer = new Answer(); answer.setQuestionId(content.getId()); answer.setPiRound(round); - answer.setAnswerCount(stats.getIndependentCounts()[i]); + answer.setAnswerCount(choice.getValue()); answer.setAbstentionCount(stats.getAbstentionCount()); - answer.setAnswerText(option.getLabel()); + answer.setAnswerText(choice.getKey()); to.add(answer); - i++; } return to; diff --git a/src/main/java/de/thm/arsnova/persistance/AnswerRepository.java b/src/main/java/de/thm/arsnova/persistance/AnswerRepository.java index 642e4465fce4436ce528a3e72d6251925ef10f58..82cc505ee9dc2778c0f2f4e99471d6fa13de9861 100644 --- a/src/main/java/de/thm/arsnova/persistance/AnswerRepository.java +++ b/src/main/java/de/thm/arsnova/persistance/AnswerRepository.java @@ -26,7 +26,7 @@ import java.util.List; public interface AnswerRepository extends CrudRepository<Answer, String> { <T extends Answer> T findByContentIdUserPiRound(String contentId, Class<T> type, UserAuthentication user, int piRound); - AnswerStatistics findByContentIdPiRound(String contentId, int piRound); + AnswerStatistics findByContentIdRound(String contentId, int round, final int optionCount); int countByContentIdRound(String contentId, int round); int countByContentId(String contentId); <T extends Answer> List<T> findByContentId(String contentId, Class<T> type, int start, int limit); diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbAnswerRepository.java b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbAnswerRepository.java index e181cf4098ea812468350365bbf10101e5fc7ea6..90b57c9d6e3e88081a06966bff30df412a06a80c 100644 --- a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbAnswerRepository.java +++ b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbAnswerRepository.java @@ -1,5 +1,6 @@ package de.thm.arsnova.persistance.couchdb; +import com.fasterxml.jackson.databind.JsonNode; import com.google.common.collect.Lists; import de.thm.arsnova.entities.Answer; import de.thm.arsnova.entities.AnswerStatistics; @@ -19,7 +20,11 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class CouchDbAnswerRepository extends CouchDbCrudRepository<Answer> implements AnswerRepository, ApplicationEventPublisherAware { private static final int BULK_PARTITION_SIZE = 500; @@ -77,31 +82,45 @@ public class CouchDbAnswerRepository extends CouchDbCrudRepository<Answer> imple } @Override - public AnswerStatistics findByContentIdPiRound(final String contentId, final int piRound) { - final ViewResult result = db.queryView(createQuery("by_contentid_round_body_subject") + public AnswerStatistics findByContentIdRound(final String contentId, final int round, final int optionCount) { + final ViewResult result = db.queryView(createQuery("by_contentid_round_selectedchoiceindexes") .group(true) - .startKey(ComplexKey.of(contentId, piRound)) - .endKey(ComplexKey.of(contentId, piRound, ComplexKey.emptyObject()))); - final int abstentionCount = countByContentId(contentId); - + .startKey(ComplexKey.of(contentId, round)) + .endKey(ComplexKey.of(contentId, round, ComplexKey.emptyObject()))); final AnswerStatistics stats = new AnswerStatistics(); stats.setContentId(contentId); final AnswerStatistics.RoundStatistics roundStats = new AnswerStatistics.RoundStatistics(); - roundStats.setRound(piRound); - roundStats.setAbstentionCount(abstentionCount); - /* FIXME: determine correct array size dynamically */ - final int[] independentCounts = new int[16]; + roundStats.setRound(round); + roundStats.setAbstentionCount(0); + final List<Integer> independentCounts = new ArrayList(Collections.nCopies(optionCount, 0)); + final Map<List<Integer>, AnswerStatistics.RoundStatistics.Combination> combinations = new HashMap(); for (final ViewResult.Row d : result) { - if (d.getKeyAsNode().get(3).asBoolean()) { + if (d.getKeyAsNode().get(2).size() == 0) { + /* Abstentions */ roundStats.setAbstentionCount(d.getValueAsInt()); } else { - int optionIndex = d.getKeyAsNode().get(4).asInt(); - independentCounts[optionIndex] = d.getValueAsInt(); + /* Answers: + * Extract selected indexes from key[2] and count from value */ + final JsonNode jsonIndexes = d.getKeyAsNode().get(2); + Integer[] indexes = new Integer[jsonIndexes.size()]; + /* Count independently */ + for (int i = 0; i < jsonIndexes.size(); i++) { + indexes[i] = jsonIndexes.get(i).asInt(); + independentCounts.set(indexes[i], independentCounts.get(indexes[i]) + d.getValueAsInt()); + } + /* Count option combinations */ + AnswerStatistics.RoundStatistics.Combination combination = + combinations.getOrDefault(Arrays.asList(indexes), + new AnswerStatistics.RoundStatistics.Combination( + Arrays.asList(indexes), d.getValueAsInt())); + combinations.put(Arrays.asList(indexes), combination); + roundStats.setCombinatedCounts(combinations.values()); } } roundStats.setIndependentCounts(independentCounts); - List<AnswerStatistics.RoundStatistics> roundStatisticsList = new ArrayList<>(); - roundStatisticsList.add(roundStats); + /* TODO: Review - might lead easily to IndexOutOfBoundsExceptions - use a Map instead? */ + List<AnswerStatistics.RoundStatistics> roundStatisticsList = new ArrayList(Collections.nCopies(round, null)); + roundStatisticsList.set(round - 1, roundStats); stats.setRoundStatistics(roundStatisticsList); return stats; diff --git a/src/main/java/de/thm/arsnova/services/ContentServiceImpl.java b/src/main/java/de/thm/arsnova/services/ContentServiceImpl.java index 258a06b39dbabaabb3e64ab74e942348f23fdf72..966107a4662b6156e0f007a4a664397d948dcde3 100644 --- a/src/main/java/de/thm/arsnova/services/ContentServiceImpl.java +++ b/src/main/java/de/thm/arsnova/services/ContentServiceImpl.java @@ -19,6 +19,7 @@ package de.thm.arsnova.services; import de.thm.arsnova.entities.Answer; import de.thm.arsnova.entities.AnswerStatistics; +import de.thm.arsnova.entities.ChoiceQuestionContent; import de.thm.arsnova.entities.Content; import de.thm.arsnova.entities.Room; import de.thm.arsnova.entities.TextAnswer; @@ -451,13 +452,20 @@ public class ContentServiceImpl extends DefaultEntityServiceImpl<Content> implem @Override @PreAuthorize("isAuthenticated()") - public AnswerStatistics getStatistics(final String contentId, final int piRound) { - final Content content = contentRepository.findOne(contentId); + public AnswerStatistics getStatistics(final String contentId, final int round) { + final ChoiceQuestionContent content = (ChoiceQuestionContent) contentRepository.findOne(contentId); if (content == null) { throw new NotFoundException(); } + AnswerStatistics stats = answerRepository.findByContentIdRound( + content.getId(), round, content.getOptions().size()); + /* Fill list with zeros to prevent IndexOutOfBoundsExceptions */ + List<Integer> independentCounts = stats.getRoundStatistics().get(round - 1).getIndependentCounts(); + while (independentCounts.size() < content.getOptions().size()) { + independentCounts.add(0); + } - return answerRepository.findByContentIdPiRound(content.getId(), piRound); + return stats; } @Override @@ -478,9 +486,9 @@ public class ContentServiceImpl extends DefaultEntityServiceImpl<Content> implem if (content == null) { throw new NotFoundException(); } - AnswerStatistics stats = answerRepository.findByContentIdPiRound(content.getId(), 1); - AnswerStatistics stats2 = answerRepository.findByContentIdPiRound(content.getId(), 2); - stats.getRoundStatistics().add(stats2.getRoundStatistics().get(0)); + AnswerStatistics stats = getStatistics(content.getId(), 1); + AnswerStatistics stats2 = getStatistics(content.getId(), 2); + stats.getRoundStatistics().add(stats2.getRoundStatistics().get(1)); return stats; } diff --git a/src/main/resources/couchdb/Answer.design.js b/src/main/resources/couchdb/Answer.design.js index d1a91ec29f900dcdf4511290c748e325991e10c8..cb63778f8120a5eac7205f2f055fefae71f328c0 100644 --- a/src/main/resources/couchdb/Answer.design.js +++ b/src/main/resources/couchdb/Answer.design.js @@ -17,6 +17,14 @@ var designDoc = { }, "reduce": "_count" }, + "by_contentid_round_selectedchoiceindexes": { + "map": function (doc) { + if (["Answer", "ChoiceAnswer", "TextAnswer"].indexOf(doc.type) !== -1) { + emit([doc.contentId, doc.round, doc.selectedChoiceIndexes], {_rev: doc._rev}); + } + }, + "reduce": "_count" + }, "by_contentid_creationtimestamp": { "map": function (doc) { if (["Answer", "ChoiceAnswer", "TextAnswer"].indexOf(doc.type) !== -1) { diff --git a/src/test/java/de/thm/arsnova/entities/migration/ToV2MigratorTest.java b/src/test/java/de/thm/arsnova/entities/migration/ToV2MigratorTest.java index 7c8802b901dfd01d4f97c7e51eed679aaeed408f..26a344cfe778de9e240b9799054ffb4adfa642ee 100644 --- a/src/test/java/de/thm/arsnova/entities/migration/ToV2MigratorTest.java +++ b/src/test/java/de/thm/arsnova/entities/migration/ToV2MigratorTest.java @@ -106,11 +106,11 @@ public class ToV2MigratorTest { } @Test - public void testMigrateAnswerStatisticsSingleResponse() { + public void testMigrateAnswerStatisticsSingleChoice() { final AnswerStatistics statsV3 = new AnswerStatistics(); final AnswerStatistics.RoundStatistics roundStatsV3 = new AnswerStatistics.RoundStatistics(); roundStatsV3.setRound(ROUND); - roundStatsV3.setIndependentCounts(ANSWER_COUNTS.stream().mapToInt(Integer::intValue).toArray()); + roundStatsV3.setIndependentCounts(ANSWER_COUNTS); roundStatsV3.setAbstentionCount(7); statsV3.setRoundStatistics(Collections.singletonList(roundStatsV3));