diff --git a/src/main/java/de/thm/arsnova/config/PersistenceConfig.java b/src/main/java/de/thm/arsnova/config/PersistenceConfig.java index 1f831776d279908dbed4613813d590ae813b7b2b..f8e6bdf5501ada9a641c2700ca3bf5e1865fe5cf 100644 --- a/src/main/java/de/thm/arsnova/config/PersistenceConfig.java +++ b/src/main/java/de/thm/arsnova/config/PersistenceConfig.java @@ -32,6 +32,7 @@ import de.thm.arsnova.config.properties.CouchDbProperties; import de.thm.arsnova.model.serialization.CouchDbObjectMapperFactory; import de.thm.arsnova.persistence.AnswerRepository; import de.thm.arsnova.persistence.CommentRepository; +import de.thm.arsnova.persistence.ContentGroupRepository; import de.thm.arsnova.persistence.ContentRepository; import de.thm.arsnova.persistence.LogEntryRepository; import de.thm.arsnova.persistence.MotdRepository; @@ -41,6 +42,7 @@ import de.thm.arsnova.persistence.StatisticsRepository; import de.thm.arsnova.persistence.UserRepository; import de.thm.arsnova.persistence.couchdb.CouchDbAnswerRepository; import de.thm.arsnova.persistence.couchdb.CouchDbCommentRepository; +import de.thm.arsnova.persistence.couchdb.CouchDbContentGroupRepository; import de.thm.arsnova.persistence.couchdb.CouchDbContentRepository; import de.thm.arsnova.persistence.couchdb.CouchDbLogEntryRepository; import de.thm.arsnova.persistence.couchdb.CouchDbMotdRepository; @@ -140,6 +142,11 @@ public class PersistenceConfig { return new CouchDbContentRepository(couchDbConnector(), false); } + @Bean + public ContentGroupRepository contentGroupRepository() throws Exception { + return new CouchDbContentGroupRepository(couchDbConnector(), false); + } + @Bean public AnswerRepository answerRepository() throws Exception { return new CouchDbAnswerRepository(couchDbConnector(), false); diff --git a/src/main/java/de/thm/arsnova/controller/RoomController.java b/src/main/java/de/thm/arsnova/controller/RoomController.java index 67eb68cbaa54c8c0b3fb3d2503487c209c841c50..5357089b80125af0994470f853d91ac869cabe9f 100644 --- a/src/main/java/de/thm/arsnova/controller/RoomController.java +++ b/src/main/java/de/thm/arsnova/controller/RoomController.java @@ -22,12 +22,15 @@ import java.util.Set; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import de.thm.arsnova.model.ContentGroup; import de.thm.arsnova.model.Room; +import de.thm.arsnova.service.ContentGroupService; import de.thm.arsnova.service.RoomService; @RestController @@ -36,12 +39,16 @@ public class RoomController extends AbstractEntityController<Room> { protected static final String REQUEST_MAPPING = "/room"; private static final String GET_MODERATORS_MAPPING = DEFAULT_ID_MAPPING + "/moderator"; private static final String MODERATOR_MAPPING = DEFAULT_ID_MAPPING + "/moderator/{userId}"; + private static final String CONTENTGROUP_MAPPING = DEFAULT_ID_MAPPING + "/contentgroup/{groupName}"; + private static final String CONTENTGROUP_ADD_MAPPING = CONTENTGROUP_MAPPING + "/{contentId}"; private RoomService roomService; + private ContentGroupService contentGroupService; - public RoomController(final RoomService roomService) { + public RoomController(final RoomService roomService, final ContentGroupService contentGroupService) { super(roomService); this.roomService = roomService; + this.contentGroupService = contentGroupService; } @Override @@ -78,4 +85,15 @@ public class RoomController extends AbstractEntityController<Room> { room.getModerators().removeIf(m -> m.getUserId().equals(userId)); roomService.update(room); } + + @GetMapping(CONTENTGROUP_MAPPING) + public ContentGroup getContentGroup(@PathVariable final String id, @PathVariable final String groupName) { + return contentGroupService.getByRoomIdAndName(id, groupName); + } + + @PostMapping(CONTENTGROUP_ADD_MAPPING) + public void addContentToGroup(@PathVariable final String id, @PathVariable final String groupName, + @RequestBody final String contentId) { + contentGroupService.addContentToGroup(id, groupName, contentId); + } } diff --git a/src/main/java/de/thm/arsnova/controller/v2/ContentController.java b/src/main/java/de/thm/arsnova/controller/v2/ContentController.java index f788bcdec499cb6c0b37a10032c7820c882ee4d3..ad1ea3d6f758ae7a00babbdc5b71806fc8d0a08c 100644 --- a/src/main/java/de/thm/arsnova/controller/v2/ContentController.java +++ b/src/main/java/de/thm/arsnova/controller/v2/ContentController.java @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.StreamSupport; import javax.naming.OperationNotSupportedException; @@ -48,12 +49,14 @@ import org.springframework.web.bind.annotation.RestController; import de.thm.arsnova.controller.PaginationController; import de.thm.arsnova.model.ChoiceAnswer; import de.thm.arsnova.model.ChoiceQuestionContent; +import de.thm.arsnova.model.ContentGroup; import de.thm.arsnova.model.TextAnswer; import de.thm.arsnova.model.migration.FromV2Migrator; import de.thm.arsnova.model.migration.ToV2Migrator; import de.thm.arsnova.model.migration.v2.Answer; import de.thm.arsnova.model.migration.v2.Content; import de.thm.arsnova.service.AnswerService; +import de.thm.arsnova.service.ContentGroupService; import de.thm.arsnova.service.ContentService; import de.thm.arsnova.service.RoomService; import de.thm.arsnova.service.TimerService; @@ -76,6 +79,9 @@ public class ContentController extends PaginationController { @Autowired private ContentService contentService; + @Autowired + private ContentGroupService contentGroupService; + @Autowired private AnswerService answerService; @@ -100,7 +106,12 @@ public class ContentController extends PaginationController { public Content getContent(@PathVariable final String contentId) { final de.thm.arsnova.model.Content content = contentService.get(contentId); if (content != null) { - return toV2Migrator.migrate(content); + final Optional<ContentGroup> contentGroup = contentGroupService.getByRoomId(content.getRoomId()).stream() + .filter(cg -> cg.getContentIds().contains(contentId)).findFirst(); + final Content contentV2 = toV2Migrator.migrate(content); + contentGroup.ifPresent(cg -> contentV2.setQuestionVariant(cg.getName())); + + return contentV2; } throw new NotFoundException(); @@ -118,6 +129,7 @@ public class ContentController extends PaginationController { final String roomId = roomService.getIdByShortId(content.getSessionKeyword()); contentV3.setRoomId(roomId); contentService.create(contentV3); + contentGroupService.addContentToGroup(roomId, content.getQuestionVariant(), contentV3.getId()); return toV2Migrator.migrate(contentV3); } @@ -344,14 +356,16 @@ public class ContentController extends PaginationController { nickname = "deleteContents") @DeleteMapping("/") public void deleteContents( - @RequestParam(value = "sessionkey") final String roomShortId, - @RequestParam(value = "lecturequestionsonly", defaultValue = "false") boolean lectureContentsOnly, - @RequestParam(value = "flashcardsonly", defaultValue = "false") boolean flashcardsOnly, - @RequestParam(value = "preparationquestionsonly", defaultValue = "false") boolean preparationContentsOnly, + @RequestParam(value = "sessionkey") + final String roomShortId, + @RequestParam(value = "lecturequestionsonly", defaultValue = "false") + final boolean lectureContentsOnly, + @RequestParam(value = "flashcardsonly", defaultValue = "false") + final boolean flashcardsOnly, + @RequestParam(value = "preparationquestionsonly", defaultValue = "false") + final boolean preparationContentsOnly, final HttpServletResponse response) { final String roomId = roomService.getIdByShortId(roomShortId); - /* FIXME: Content variant is ignored for now */ - lectureContentsOnly = preparationContentsOnly = flashcardsOnly = false; if (lectureContentsOnly) { contentService.deleteLectureContents(roomId); } else if (preparationContentsOnly) { @@ -402,13 +416,14 @@ public class ContentController extends PaginationController { @Deprecated @GetMapping("/unanswered") public List<String> getUnAnsweredContentIds( - @RequestParam(value = "sessionkey") final String roomShortId, - @RequestParam(value = "lecturequestionsonly", defaultValue = "false") boolean lectureContentsOnly, - @RequestParam(value = "preparationquestionsonly", defaultValue = "false") boolean preparationContentsOnly) { + @RequestParam(value = "sessionkey") + final String roomShortId, + @RequestParam(value = "lecturequestionsonly", defaultValue = "false") + final boolean lectureContentsOnly, + @RequestParam(value = "preparationquestionsonly", defaultValue = "false") + final boolean preparationContentsOnly) { final String roomId = roomService.getIdByShortId(roomShortId); final List<String> answers; - /* FIXME: Content variant is ignored for now */ - lectureContentsOnly = preparationContentsOnly = false; if (lectureContentsOnly) { answers = contentService.getUnAnsweredLectureContentIds(roomId); } else if (preparationContentsOnly) { @@ -577,13 +592,14 @@ public class ContentController extends PaginationController { nickname = "deleteAllContentsAnswers") @DeleteMapping("/answers") public void deleteAllContentsAnswers( - @RequestParam(value = "sessionkey") final String roomShortId, - @RequestParam(value = "lecturequestionsonly", defaultValue = "false") boolean lectureContentsOnly, - @RequestParam(value = "preparationquestionsonly", defaultValue = "false") boolean preparationContentsOnly, + @RequestParam(value = "sessionkey") + final String roomShortId, + @RequestParam(value = "lecturequestionsonly", defaultValue = "false") + final boolean lectureContentsOnly, + @RequestParam(value = "preparationquestionsonly", defaultValue = "false") + final boolean preparationContentsOnly, final HttpServletResponse response) { final String roomId = roomService.getIdByShortId(roomShortId); - /* FIXME: Content variant is ignored for now */ - lectureContentsOnly = preparationContentsOnly = false; if (lectureContentsOnly) { contentService.deleteAllLectureAnswers(roomId); } else if (preparationContentsOnly) { @@ -673,13 +689,14 @@ public class ContentController extends PaginationController { @Deprecated @GetMapping(value = "/answercount", produces = MediaType.TEXT_PLAIN_VALUE) public String getTotalAnswerCount( - @RequestParam(value = "sessionkey") final String roomShortId, - @RequestParam(value = "lecturequestionsonly", defaultValue = "false") boolean lectureContentsOnly, - @RequestParam(value = "preparationquestionsonly", defaultValue = "false") boolean preparationContentsOnly) { + @RequestParam(value = "sessionkey") + final String roomShortId, + @RequestParam(value = "lecturequestionsonly", defaultValue = "false") + final boolean lectureContentsOnly, + @RequestParam(value = "preparationquestionsonly", defaultValue = "false") + final boolean preparationContentsOnly) { final String roomId = roomService.getIdByShortId(roomShortId); int count = 0; - /* FIXME: Content variant is ignored for now */ - lectureContentsOnly = preparationContentsOnly = false; if (lectureContentsOnly) { count = answerService.countLectureContentAnswers(roomId); } else if (preparationContentsOnly) { diff --git a/src/main/java/de/thm/arsnova/model/ContentGroup.java b/src/main/java/de/thm/arsnova/model/ContentGroup.java new file mode 100644 index 0000000000000000000000000000000000000000..25ad8733c95c7bff348e3d3b3d1892aedf56c8a9 --- /dev/null +++ b/src/main/java/de/thm/arsnova/model/ContentGroup.java @@ -0,0 +1,122 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2019 The ARSnova Team and Contributors + * + * 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.model; + +import com.fasterxml.jackson.annotation.JsonView; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import org.springframework.core.style.ToStringCreator; + +import de.thm.arsnova.model.serialization.View; + +public class ContentGroup extends Entity { + @NotEmpty + private String roomId; + + @NotBlank + private String name; + + private Set<String> contentIds; + private boolean autoSort; + + public ContentGroup() { + + } + + public ContentGroup(final String roomId, final String name) { + this.roomId = roomId; + this.name = name; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public String getRoomId() { + return roomId; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setRoomId(final String roomId) { + this.roomId = roomId; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public String getName() { + return this.name; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setName(final String name) { + this.name = name; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public Set<String> getContentIds() { + if (contentIds == null) { + contentIds = new HashSet<>(); + } + + return contentIds; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setContentIds(final Set<String> contentIds) { + this.contentIds = contentIds; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public boolean isAutoSort() { + return autoSort; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setAutoSort(final boolean autoSort) { + this.autoSort = autoSort; + } + + @Override + public int hashCode() { + return Objects.hash(name, contentIds, autoSort); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final ContentGroup that = (ContentGroup) o; + + return autoSort == that.autoSort + && Objects.equals(name, that.name) + && Objects.equals(contentIds, that.contentIds); + } + + @Override + public String toString() { + return new ToStringCreator(this) + .append("name", name) + .append("contentIds", contentIds) + .append("autoSort", autoSort) + .toString(); + } +} diff --git a/src/main/java/de/thm/arsnova/model/Room.java b/src/main/java/de/thm/arsnova/model/Room.java index 689eae0a866629bf4ae1dc3fef4a2ee7373baaa3..cf5f69e31f525a78f5f58cc378c76e880547253a 100644 --- a/src/main/java/de/thm/arsnova/model/Room.java +++ b/src/main/java/de/thm/arsnova/model/Room.java @@ -23,8 +23,6 @@ import java.util.HashSet; import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotEmpty; import org.springframework.core.style.ToStringCreator; @@ -32,77 +30,6 @@ import org.springframework.core.style.ToStringCreator; import de.thm.arsnova.model.serialization.View; public class Room extends Entity { - public static class ContentGroup { - @NotBlank - private String name; - - private Set<String> contentIds; - private boolean autoSort; - - @JsonView({View.Persistence.class, View.Public.class}) - public String getName() { - return this.name; - } - - @JsonView({View.Persistence.class, View.Public.class}) - public void setName(final String name) { - this.name = name; - } - - @JsonView({View.Persistence.class, View.Public.class}) - public Set<String> getContentIds() { - if (contentIds == null) { - contentIds = new HashSet<>(); - } - - return contentIds; - } - - @JsonView({View.Persistence.class, View.Public.class}) - public void setContentIds(final Set<String> contentIds) { - this.contentIds = contentIds; - } - - @JsonView({View.Persistence.class, View.Public.class}) - public boolean isAutoSort() { - return autoSort; - } - - @JsonView({View.Persistence.class, View.Public.class}) - public void setAutoSort(final boolean autoSort) { - this.autoSort = autoSort; - } - - @Override - public int hashCode() { - return Objects.hash(name, contentIds, autoSort); - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - final ContentGroup that = (ContentGroup) o; - - return autoSort == that.autoSort - && Objects.equals(name, that.name) - && Objects.equals(contentIds, that.contentIds); - } - - @Override - public String toString() { - return new ToStringCreator(this) - .append("name", name) - .append("contentIds", contentIds) - .append("autoSort", autoSort) - .toString(); - } - } - public static class Moderator { public enum Role { EDITING_MODERATOR, @@ -468,7 +395,6 @@ public class Room extends Entity { private String description; private boolean closed; - private Set<ContentGroup> contentGroups; private Set<Moderator> moderators; private Settings settings; private Author author; @@ -537,28 +463,6 @@ public class Room extends Entity { this.closed = closed; } - @JsonView({View.Persistence.class, View.Public.class}) - public Set<ContentGroup> getContentGroups() { - if (contentGroups == null) { - contentGroups = new HashSet<>(); - } - - return contentGroups; - } - - public Map<String, ContentGroup> getContentGroupsAsMap() { - return getContentGroups().stream().collect(Collectors.toMap(ContentGroup::getName, Function.identity())); - } - - @JsonView({View.Persistence.class, View.Public.class}) - public void setContentGroups(final Set<ContentGroup> contentGroups) { - this.contentGroups = contentGroups; - } - - public void setContentGroupsFromMap(final Map<String, ContentGroup> groups) { - this.contentGroups = new HashSet<>(groups.values()); - } - @JsonView(View.Persistence.class) public Set<Moderator> getModerators() { if (moderators == null) { @@ -641,8 +545,8 @@ public class Room extends Entity { * * <p> * The following fields of <tt>Room</tt> are excluded from equality checks: - * {@link #contentGroups}, {@link #settings}, {@link #author}, {@link #poolProperties}, {@link #extensions}, - * {@link #attachments}, {@link #statistics}. + * {@link #settings}, {@link #author}, {@link #poolProperties}, {@link #extensions}, {@link #attachments}, + * {@link #statistics}. * </p> */ @Override @@ -672,7 +576,6 @@ public class Room extends Entity { .append("abbreviation", abbreviation) .append("description", description) .append("closed", closed) - .append("contentGroups", contentGroups) .append("settings", settings) .append("author", author) .append("poolProperties", poolProperties) diff --git a/src/main/java/de/thm/arsnova/persistence/AnswerRepository.java b/src/main/java/de/thm/arsnova/persistence/AnswerRepository.java index f7260fe5658bc5b780f3f48cbac8aeaea613f08d..eebde69a8e0a8240ae15ebef89adb08bb8374e33 100644 --- a/src/main/java/de/thm/arsnova/persistence/AnswerRepository.java +++ b/src/main/java/de/thm/arsnova/persistence/AnswerRepository.java @@ -18,6 +18,7 @@ package de.thm.arsnova.persistence; +import java.util.Collection; import java.util.List; import de.thm.arsnova.model.Answer; @@ -32,17 +33,15 @@ public interface AnswerRepository extends CrudRepository<Answer, String> { int countByContentId(String contentId); + int countByContentIds(Collection<String> contentIds); + <T extends Answer> List<T> findByContentId(String contentId, Class<T> type, int start, int limit); List<Answer> findByUserIdRoomId(String userId, String roomId); Iterable<Answer> findStubsByContentId(String contentId); - Iterable<Answer> findStubsByContentIds(List<String> contentId); + Iterable<Answer> findStubsByContentIds(Collection<String> contentId); int countByRoomId(String roomId); - - int countByRoomIdOnlyLectureVariant(String roomId); - - int countByRoomIdOnlyPreparationVariant(String roomId); } diff --git a/src/main/java/de/thm/arsnova/persistence/ContentGroupRepository.java b/src/main/java/de/thm/arsnova/persistence/ContentGroupRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..6ee6a87b0b15ebcff3a9d3897f8a538e88587c14 --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistence/ContentGroupRepository.java @@ -0,0 +1,29 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2019 The ARSnova Team and Contributors + * + * 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.persistence; + +import java.util.List; + +import de.thm.arsnova.model.ContentGroup; + +public interface ContentGroupRepository extends CrudRepository<ContentGroup, String> { + ContentGroup findByRoomIdAndName(String roomId, String name); + + List<ContentGroup> findByRoomId(String roomId); +} diff --git a/src/main/java/de/thm/arsnova/persistence/ContentRepository.java b/src/main/java/de/thm/arsnova/persistence/ContentRepository.java index 9b2ca2558e20f75dec3ac454177007133ad3f31f..320d031f0515e48392a451acbe5a1b7bf0742cf8 100644 --- a/src/main/java/de/thm/arsnova/persistence/ContentRepository.java +++ b/src/main/java/de/thm/arsnova/persistence/ContentRepository.java @@ -1,6 +1,7 @@ package de.thm.arsnova.persistence; import java.util.List; +import java.util.Set; import de.thm.arsnova.model.Content; @@ -15,37 +16,13 @@ public interface ContentRepository extends CrudRepository<Content, String> { List<String> findIdsByRoomId(String roomId); - Iterable<Content> findStubsByRoomId(final String roomId); + Iterable<Content> findStubsByIds(Set<String> ids); - Iterable<Content> findStubsByRoomIdAndVariant(String roomId, String variant); + Iterable<Content> findStubsByRoomId(final String roomId); List<String> findUnansweredIdsByRoomIdAndUser(String roomId, String userId); - List<Content> findByRoomIdOnlyLectureVariantAndActive(String roomId); - - List<Content> findByRoomIdOnlyLectureVariant(String roomId); - List<Content> findByRoomIdOnlyFlashcardVariantAndActive(String roomId); - List<Content> findByRoomIdOnlyFlashcardVariant(String roomId); - - List<Content> findByRoomIdOnlyPreparationVariantAndActive(String roomId); - - List<Content> findByRoomIdOnlyPreparationVariant(String roomId); - List<Content> findByRoomId(String roomId); - - int countLectureVariantByRoomId(String roomId); - - int countFlashcardVariantRoomId(String roomId); - - int countPreparationVariantByRoomId(String roomId); - - List<String> findIdsByRoomIdAndVariantAndSubject(String roomId, String questionVariant, String subject); - - List<String> findSubjectsByRoomIdAndVariant(String roomId, String questionVariant); - - List<String> findUnansweredIdsByRoomIdAndUserOnlyLectureVariant(String roomId, String userId); - - List<String> findUnansweredIdsByRoomIdAndUserOnlyPreparationVariant(String roomId, String userId); } diff --git a/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbAnswerRepository.java b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbAnswerRepository.java index a6b70c5d398951eda9d9178365fbd33402a6de4d..17773478d87ee9ed30bc75c97a0a9dff6e743071 100644 --- a/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbAnswerRepository.java +++ b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbAnswerRepository.java @@ -21,6 +21,7 @@ package de.thm.arsnova.persistence.couchdb; import com.fasterxml.jackson.databind.JsonNode; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -67,7 +68,7 @@ public class CouchDbAnswerRepository extends CouchDbCrudRepository<Answer> } @Override - public Iterable<Answer> findStubsByContentIds(final List<String> contentIds) { + public Iterable<Answer> findStubsByContentIds(final Collection<String> contentIds) { return createEntityStubs(db.queryView(createQuery("by_contentid").keys(contentIds))); } @@ -144,6 +145,16 @@ public class CouchDbAnswerRepository extends CouchDbCrudRepository<Answer> return result.isEmpty() ? 0 : result.getRows().get(0).getValueAsInt(); } + @Override + public int countByContentIds(final Collection<String> contentIds) { + final ViewResult result = db.queryView(createQuery("by_contentid") + .reduce(true) + .group(true) + .keys(contentIds)); + + return result.isEmpty() ? 0 : result.getRows().get(0).getValueAsInt(); + } + @Override public <T extends Answer> List<T> findByContentId( final String contentId, final Class<T> type, final int start, final int limit) { @@ -175,21 +186,4 @@ public class CouchDbAnswerRepository extends CouchDbCrudRepository<Answer> return result.isEmpty() ? 0 : result.getRows().get(0).getValueAsInt(); } - - @Override - public int countByRoomIdOnlyLectureVariant(final String roomId) { - return countBySessionIdVariant(roomId, "lecture"); - } - - @Override - public int countByRoomIdOnlyPreparationVariant(final String roomId) { - return countBySessionIdVariant(roomId, "preparation"); - } - - private int countBySessionIdVariant(final String sessionId, final String variant) { - final ViewResult result = db.queryView(createQuery("by_roomid_variant") - .key(ComplexKey.of(sessionId, variant))); - - return result.isEmpty() ? 0 : result.getRows().get(0).getValueAsInt(); - } } diff --git a/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbContentGroupRepository.java b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbContentGroupRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..8edebb3ec0a68e5ca629ac5b9d154cf330227b78 --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbContentGroupRepository.java @@ -0,0 +1,56 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2019 The ARSnova Team and Contributors + * + * 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.persistence.couchdb; + +import java.util.List; +import org.ektorp.ComplexKey; +import org.ektorp.CouchDbConnector; + +import de.thm.arsnova.model.ContentGroup; +import de.thm.arsnova.persistence.ContentGroupRepository; + +public class CouchDbContentGroupRepository extends CouchDbCrudRepository<ContentGroup> + implements ContentGroupRepository { + public CouchDbContentGroupRepository(final CouchDbConnector db, final boolean createIfNotExists) { + super(ContentGroup.class, db, "by_id", createIfNotExists); + } + + @Override + public ContentGroup findByRoomIdAndName(final String roomId, final String name) { + final List<ContentGroup> contentGroupList = db.queryView(createQuery("by_roomid_name") + .key(ComplexKey.of(roomId, name)) + .includeDocs(true) + .reduce(false), + ContentGroup.class); + + return !contentGroupList.isEmpty() ? contentGroupList.get(0) : null; + } + + @Override + public List<ContentGroup> findByRoomId(final String roomId) { + final List<ContentGroup> contentGroups = db.queryView(createQuery("by_roomid_name") + .startKey(ComplexKey.of(roomId)) + .endKey(ComplexKey.of(roomId, ComplexKey.emptyObject())) + .includeDocs(true) + .reduce(false), + ContentGroup.class); + + return contentGroups; + } +} diff --git a/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbContentRepository.java b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbContentRepository.java index b1eabaf7b2a1644e365f4f164db994539e77db62..0fea9e85652fa848940b0edff2b4df2225234856 100644 --- a/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbContentRepository.java +++ b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbContentRepository.java @@ -20,8 +20,6 @@ package de.thm.arsnova.persistence.couchdb; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -66,7 +64,7 @@ public class CouchDbContentRepository extends CouchDbCrudRepository<Content> imp @Override public int countByRoomId(final String roomId) { - final ViewResult result = db.queryView(createQuery("by_roomid_group_locked") + final ViewResult result = db.queryView(createQuery("by_roomid_locked") .startKey(ComplexKey.of(roomId)) .endKey(ComplexKey.of(roomId, ComplexKey.emptyObject())) .reduce(true)); @@ -76,25 +74,24 @@ public class CouchDbContentRepository extends CouchDbCrudRepository<Content> imp @Override public List<String> findIdsByRoomId(final String roomId) { - return collectQuestionIds(db.queryView(createQuery("by_roomid_group_locked") + return collectQuestionIds(db.queryView(createQuery("by_roomid_locked") .startKey(ComplexKey.of(roomId)) .endKey(ComplexKey.of(roomId, ComplexKey.emptyObject())) .reduce(false))); } @Override - public Iterable<Content> findStubsByRoomId(final String roomId) { - return createEntityStubs(db.queryView(createQuery("by_roomid_group_locked") - .startKey(ComplexKey.of(roomId)) - .endKey(ComplexKey.of(roomId, ComplexKey.emptyObject())) + public Iterable<Content> findStubsByIds(final Set<String> ids) { + return createEntityStubs(db.queryView(createQuery("by_id") + .keys(ids) .reduce(false))); } @Override - public Iterable<Content> findStubsByRoomIdAndVariant(final String roomId, final String variant) { - return createEntityStubs(db.queryView(createQuery("by_roomid_group_locked") - .startKey(ComplexKey.of(roomId, variant)) - .endKey(ComplexKey.of(roomId, variant, ComplexKey.emptyObject())) + public Iterable<Content> findStubsByRoomId(final String roomId) { + return createEntityStubs(db.queryView(createQuery("by_roomid_locked") + .startKey(ComplexKey.of(roomId)) + .endKey(ComplexKey.of(roomId, ComplexKey.emptyObject())) .reduce(false))); } @@ -104,7 +101,7 @@ public class CouchDbContentRepository extends CouchDbCrudRepository<Content> imp @Override public List<String> findUnansweredIdsByRoomIdAndUser(final String roomId, final String userId) { - final ViewResult result = db.queryView(createQuery("contentid_by_creatorid_roomid_variant") + final ViewResult result = db.queryView(createQuery("contentid_by_creatorid_roomid") .designDocId("_design/Answer") .startKey(ComplexKey.of(userId, roomId)) .endKey(ComplexKey.of(userId, roomId, ComplexKey.emptyObject()))); @@ -112,45 +109,8 @@ public class CouchDbContentRepository extends CouchDbCrudRepository<Content> imp for (final ViewResult.Row row : result.getRows()) { answeredIds.add(row.getId()); } - return collectUnansweredQuestionIds(findIdsByRoomId(roomId), answeredIds); - } - - @Override - public List<String> findUnansweredIdsByRoomIdAndUserOnlyLectureVariant(final String roomId, final String userId) { - final ViewResult result = db.queryView(createQuery("contentid_round_by_creatorid_roomid_variant") - .designDocId("_design/Answer") - .key(ComplexKey.of(userId, roomId, "lecture"))); - final Map<String, Integer> answeredQuestions = new HashMap<>(); - for (final ViewResult.Row row : result.getRows()) { - answeredQuestions.put(row.getId(), row.getKeyAsNode().get(2).asInt()); - } - - return collectUnansweredQuestionIdsByPiRound(findByRoomIdOnlyLectureVariantAndActive(roomId), answeredQuestions); - } - - @Override - public List<String> findUnansweredIdsByRoomIdAndUserOnlyPreparationVariant( - final String roomId, final String userId) { - final ViewResult result = db.queryView(createQuery("contentid_round_by_creatorid_roomid_variant") - .designDocId("_design/Answer") - .key(ComplexKey.of(userId, roomId, "preparation"))); - final Map<String, Integer> answeredQuestions = new HashMap<>(); - for (final ViewResult.Row row : result.getRows()) { - answeredQuestions.put(row.getId(), row.getKeyAsNode().get(2).asInt()); - } - - return collectUnansweredQuestionIdsByPiRound( - findByRoomIdOnlyPreparationVariantAndActive(roomId), answeredQuestions); - } - @Override - public List<Content> findByRoomIdOnlyLectureVariantAndActive(final String roomId) { - return findByRoomIdAndVariantAndActive(roomId, "lecture", true); - } - - @Override - public List<Content> findByRoomIdOnlyLectureVariant(final String roomId) { - return findByRoomIdAndVariantAndActive(roomId, "lecture"); + return collectUnansweredQuestionIds(findIdsByRoomId(roomId), answeredIds); } @Override @@ -158,21 +118,6 @@ public class CouchDbContentRepository extends CouchDbCrudRepository<Content> imp return findByRoomIdAndVariantAndActive(roomId, "flashcard", true); } - @Override - public List<Content> findByRoomIdOnlyFlashcardVariant(final String roomId) { - return findByRoomIdAndVariantAndActive(roomId, "flashcard"); - } - - @Override - public List<Content> findByRoomIdOnlyPreparationVariantAndActive(final String roomId) { - return findByRoomIdAndVariantAndActive(roomId, "preparation", true); - } - - @Override - public List<Content> findByRoomIdOnlyPreparationVariant(final String roomId) { - return findByRoomIdAndVariantAndActive(roomId, "preparation"); - } - @Override public List<Content> findByRoomId(final String roomId) { return findByRoomIdAndVariantAndActive(roomId); @@ -183,7 +128,7 @@ public class CouchDbContentRepository extends CouchDbCrudRepository<Content> imp final Object[] endKeys = Arrays.copyOf(keys, keys.length + 1); endKeys[keys.length] = ComplexKey.emptyObject(); - return db.queryView(createQuery("by_roomid_group_locked") + return db.queryView(createQuery("by_roomid_locked") .includeDocs(true) .reduce(false) .startKey(ComplexKey.of(keys)) @@ -191,36 +136,6 @@ public class CouchDbContentRepository extends CouchDbCrudRepository<Content> imp Content.class); } - @Override - public int countLectureVariantByRoomId(final String roomId) { - /* TODO: reduce code duplication */ - final ViewResult result = db.queryView(createQuery("by_roomid_group_locked") - .startKey(ComplexKey.of(roomId, "lecture")) - .endKey(ComplexKey.of(roomId, "lecture", ComplexKey.emptyObject()))); - - return result.isEmpty() ? 0 : result.getRows().get(0).getValueAsInt(); - } - - @Override - public int countFlashcardVariantRoomId(final String roomId) { - /* TODO: reduce code duplication */ - final ViewResult result = db.queryView(createQuery("by_roomid_group_locked") - .startKey(ComplexKey.of(roomId, "flashcard")) - .endKey(ComplexKey.of(roomId, "flashcard", ComplexKey.emptyObject()))); - - return result.isEmpty() ? 0 : result.getRows().get(0).getValueAsInt(); - } - - @Override - public int countPreparationVariantByRoomId(final String roomId) { - /* TODO: reduce code duplication */ - final ViewResult result = db.queryView(createQuery("by_roomid_group_locked") - .startKey(ComplexKey.of(roomId, "preparation")) - .endKey(ComplexKey.of(roomId, "preparation", ComplexKey.emptyObject()))); - - return result.isEmpty() ? 0 : result.getRows().get(0).getValueAsInt(); - } - private List<String> collectUnansweredQuestionIds( final List<String> contentIds, final List<String> answeredContentIds) { @@ -257,37 +172,4 @@ public class CouchDbContentRepository extends CouchDbCrudRepository<Content> imp } return ids; } - - /* TODO: remove if this method is no longer used */ - @Override - public List<String> findIdsByRoomIdAndVariantAndSubject( - final String roomId, final String questionVariant, final String subject) { - final ViewResult result = db.queryView(createQuery("by_roomid_group_locked") - .startKey(ComplexKey.of(roomId, questionVariant, false, subject)) - .endKey(ComplexKey.of(roomId, questionVariant, false, subject, ComplexKey.emptyObject()))); - - final List<String> qids = new ArrayList<>(); - - for (final ViewResult.Row row : result.getRows()) { - final String s = row.getId(); - qids.add(s); - } - - return qids; - } - - @Override - public List<String> findSubjectsByRoomIdAndVariant(final String roomId, final String questionVariant) { - final ViewResult result = db.queryView(createQuery("by_roomid_group_locked") - .startKey(ComplexKey.of(roomId, questionVariant)) - .endKey(ComplexKey.of(roomId, questionVariant, ComplexKey.emptyObject()))); - - final Set<String> uniqueSubjects = new HashSet<>(); - - for (final ViewResult.Row row : result.getRows()) { - uniqueSubjects.add(row.getKeyAsNode().get(3).asText()); - } - - return new ArrayList<>(uniqueSubjects); - } } diff --git a/src/main/java/de/thm/arsnova/security/ApplicationPermissionEvaluator.java b/src/main/java/de/thm/arsnova/security/ApplicationPermissionEvaluator.java index d91e4574794b59554251197e74045f4a2e9f4324..66a50e5bef1b31e8f5b13aced1695b98e12eb08c 100644 --- a/src/main/java/de/thm/arsnova/security/ApplicationPermissionEvaluator.java +++ b/src/main/java/de/thm/arsnova/security/ApplicationPermissionEvaluator.java @@ -35,11 +35,13 @@ import de.thm.arsnova.config.properties.SecurityProperties; import de.thm.arsnova.model.Answer; import de.thm.arsnova.model.Comment; import de.thm.arsnova.model.Content; +import de.thm.arsnova.model.ContentGroup; import de.thm.arsnova.model.Motd; import de.thm.arsnova.model.Room; import de.thm.arsnova.model.UserProfile; import de.thm.arsnova.persistence.AnswerRepository; import de.thm.arsnova.persistence.CommentRepository; +import de.thm.arsnova.persistence.ContentGroupRepository; import de.thm.arsnova.persistence.ContentRepository; import de.thm.arsnova.persistence.MotdRepository; import de.thm.arsnova.persistence.RoomRepository; @@ -67,6 +69,9 @@ public class ApplicationPermissionEvaluator implements PermissionEvaluator { @Autowired private ContentRepository contentRepository; + @Autowired + ContentGroupRepository contentGroupRepository; + @Autowired private AnswerRepository answerRepository; @@ -96,6 +101,8 @@ public class ApplicationPermissionEvaluator implements PermissionEvaluator { && hasRoomPermission(userId, ((Room) targetDomainObject), permission.toString())) || (targetDomainObject instanceof Content && hasContentPermission(userId, ((Content) targetDomainObject), permission.toString())) + || (targetDomainObject instanceof ContentGroup + && hasContentGroupPermission(userId, ((ContentGroup) targetDomainObject), permission.toString())) || (targetDomainObject instanceof Answer && hasAnswerPermission(userId, ((Answer) targetDomainObject), permission.toString())) || (targetDomainObject instanceof Comment @@ -132,6 +139,10 @@ public class ApplicationPermissionEvaluator implements PermissionEvaluator { case "content": final Content targetContent = contentRepository.findOne(targetId.toString()); return targetContent != null && hasContentPermission(userId, targetContent, permission.toString()); + case "contentgroup": + final ContentGroup targetContentGroup = contentGroupRepository.findOne(targetId.toString()); + return targetContentGroup != null + && hasContentGroupPermission(userId, targetContentGroup, permission.toString()); case "answer": final Answer targetAnswer = answerRepository.findOne(targetId.toString()); return targetAnswer != null && hasAnswerPermission(userId, targetAnswer, permission.toString()); @@ -211,6 +222,28 @@ public class ApplicationPermissionEvaluator implements PermissionEvaluator { } } + private boolean hasContentGroupPermission( + final String userId, + final ContentGroup targetContentGroup, + final String permission) { + final Room room = roomRepository.findOne(targetContentGroup.getRoomId()); + if (room == null) { + return false; + } + + switch (permission) { + case "read": + return !room.isClosed() || hasUserIdRoomModeratingPermission(room, userId); + case "create": + case "update": + case "delete": + return room.getOwnerId().equals(userId) + || hasUserIdRoomModeratorRole(room, userId, Room.Moderator.Role.EDITING_MODERATOR); + default: + return false; + } + } + private boolean hasAnswerPermission( final String userId, final Answer targetAnswer, diff --git a/src/main/java/de/thm/arsnova/service/AnswerServiceImpl.java b/src/main/java/de/thm/arsnova/service/AnswerServiceImpl.java index c231d311bdf22350cb8e2849427805bedc4b0cbd..db181211c9e33a55eb2b44505ad58671097d9f59 100644 --- a/src/main/java/de/thm/arsnova/service/AnswerServiceImpl.java +++ b/src/main/java/de/thm/arsnova/service/AnswerServiceImpl.java @@ -23,6 +23,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Queue; +import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; import org.ektorp.DbAccessException; import org.slf4j.Logger; @@ -62,6 +63,7 @@ public class AnswerServiceImpl extends DefaultEntityServiceImpl<Answer> implemen private RoomService roomService; private ContentService contentService; + private ContentGroupService contentGroupService; private AnswerRepository answerRepository; private UserService userService; @@ -83,6 +85,11 @@ public class AnswerServiceImpl extends DefaultEntityServiceImpl<Answer> implemen this.contentService = contentService; } + @Autowired + public void setContentGroupService(final ContentGroupService contentGroupService) { + this.contentGroupService = contentGroupService; + } + @Scheduled(fixedDelay = 5000) public void flushAnswerQueue() { if (answerQueue.isEmpty()) { @@ -377,15 +384,6 @@ public class AnswerServiceImpl extends DefaultEntityServiceImpl<Answer> implemen answer.setRoomId(room.getId()); } - /* - * The "internal" suffix means it is called by internal services that have no authentication! - * TODO: Find a better way of doing this... - */ - @Override - public int countLectureQuestionAnswersInternal(final String roomId) { - return answerRepository.countByRoomIdOnlyLectureVariant(roomService.get(roomId).getId()); - } - @Override public Map<String, Object> countAnswersAndAbstentionsInternal(final String contentId) { final Content content = contentService.get(contentId); @@ -414,13 +412,26 @@ public class AnswerServiceImpl extends DefaultEntityServiceImpl<Answer> implemen return this.countPreparationQuestionAnswersInternal(roomId); } + /* + * The "internal" suffix means it is called by internal services that have no authentication! + * TODO: Find a better way of doing this... + */ + @Override + public int countLectureQuestionAnswersInternal(final String roomId) { + final Set<String> contentIds = + contentGroupService.getByRoomIdAndName(roomId, "lecture").getContentIds(); + return answerRepository.countByContentIds(contentIds); + } + /* * The "internal" suffix means it is called by internal services that have no authentication! * TODO: Find a better way of doing this... */ @Override public int countPreparationQuestionAnswersInternal(final String roomId) { - return answerRepository.countByRoomIdOnlyPreparationVariant(roomService.get(roomId).getId()); + final Set<String> contentIds = + contentGroupService.getByRoomIdAndName(roomId, "preparation").getContentIds(); + return answerRepository.countByContentIds(contentIds); } @EventListener diff --git a/src/main/java/de/thm/arsnova/service/ContentFindQueryService.java b/src/main/java/de/thm/arsnova/service/ContentFindQueryService.java index ad668a81f3f7903fe2724dc8e66abfcaaf81f235..c1df6a20ebf2b699ec266c06b4de4b64cf3462ee 100644 --- a/src/main/java/de/thm/arsnova/service/ContentFindQueryService.java +++ b/src/main/java/de/thm/arsnova/service/ContentFindQueryService.java @@ -21,6 +21,7 @@ package de.thm.arsnova.service; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; import org.springframework.stereotype.Service; import de.thm.arsnova.model.Content; @@ -30,15 +31,28 @@ import de.thm.arsnova.model.FindQuery; public class ContentFindQueryService implements FindQueryService<Content> { private RoomService roomService; private ContentService contentService; + private ContentGroupService contentGroupService; - public ContentFindQueryService(final RoomService roomService, final ContentService contentService) { + public ContentFindQueryService(final RoomService roomService, final ContentService contentService, + final ContentGroupService contentGroupService) { this.roomService = roomService; this.contentService = contentService; + this.contentGroupService = contentGroupService; } @Override public Set<String> resolveQuery(final FindQuery<Content> findQuery) { final Set<String> contentIds = new HashSet<>(); + + if (findQuery.getExternalFilters().get("notInContentGroupOfRoomId") instanceof String) { + final String roomId = (String) findQuery.getExternalFilters().get("notInContentGroupOfRoomId"); + final Set<String> idsWithGroup = contentGroupService.getByRoomId(roomId).stream() + .flatMap(cg -> cg.getContentIds().stream()).collect(Collectors.toSet()); + final Set<String> idsWithoutGroup = contentService.getByRoomId(roomId).stream() + .map(Content::getId).filter(id -> !idsWithGroup.contains(id)).collect(Collectors.toSet()); + contentIds.addAll(idsWithoutGroup); + } + if (findQuery.getProperties().getRoomId() != null) { final List<Content> contentList = contentService.getByRoomId(findQuery.getProperties().getRoomId()); for (final Content c : contentList) { diff --git a/src/main/java/de/thm/arsnova/service/ContentGroupService.java b/src/main/java/de/thm/arsnova/service/ContentGroupService.java new file mode 100644 index 0000000000000000000000000000000000000000..74b6e84e857cbc44be193b99a44031f5cd230024 --- /dev/null +++ b/src/main/java/de/thm/arsnova/service/ContentGroupService.java @@ -0,0 +1,96 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2019 The ARSnova Team and Contributors + * + * 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.service; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.event.EventListener; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.security.access.annotation.Secured; +import org.springframework.stereotype.Service; +import org.springframework.validation.Validator; + +import de.thm.arsnova.event.BeforeDeletionEvent; +import de.thm.arsnova.model.Content; +import de.thm.arsnova.model.ContentGroup; +import de.thm.arsnova.model.Room; +import de.thm.arsnova.persistence.ContentGroupRepository; + +@Service +public class ContentGroupService extends DefaultEntityServiceImpl<ContentGroup> { + private ContentGroupRepository contentGroupRepository; + private ContentService contentService; + + public ContentGroupService( + final ContentGroupRepository repository, + @Qualifier("defaultJsonMessageConverter") + final MappingJackson2HttpMessageConverter jackson2HttpMessageConverter, + final Validator validator) { + super(ContentGroup.class, repository, jackson2HttpMessageConverter.getObjectMapper(), validator); + this.contentGroupRepository = repository; + } + + @Autowired + public void setContentService(final ContentService contentService) { + this.contentService = contentService; + } + + public ContentGroup getByRoomIdAndName(final String roomId, final String name) { + return contentGroupRepository.findByRoomIdAndName(roomId, name); + } + + public List<ContentGroup> getByRoomId(final String roomId) { + return contentGroupRepository.findByRoomId(roomId); + } + + public void addContentToGroup(final String roomId, final String groupName, final String contentId) { + ContentGroup contentGroup = getByRoomIdAndName(roomId, groupName); + if (contentGroup == null) { + contentGroup = new ContentGroup(roomId, groupName); + contentGroup.getContentIds().add(contentId); + create(contentGroup); + } else { + contentGroup.getContentIds().add(contentId); + update(contentGroup); + } + } + + public void updateContentGroup(final ContentGroup contentGroup) { + if (contentGroup.getContentIds().isEmpty()) { + delete(contentGroup); + } else { + final Set<String> contentIds = StreamSupport.stream( + contentService.get(contentGroup.getContentIds()).spliterator(), false) + .filter(c -> c.getRoomId().equals(contentGroup.getRoomId())) + .map(Content::getId).collect(Collectors.toSet()); + update(contentGroup); + } + } + + @EventListener + @Secured({"ROLE_USER", "RUN_AS_SYSTEM"}) + public void handleRoomDeletion(final BeforeDeletionEvent<Room> event) { + final Iterable<ContentGroup> contentGroups = contentGroupRepository.findByRoomId(event.getEntity().getId()); + delete(contentGroups); + } +} diff --git a/src/main/java/de/thm/arsnova/service/ContentServiceImpl.java b/src/main/java/de/thm/arsnova/service/ContentServiceImpl.java index 4321993f857e4ae52f833db4449a0190795e0d6d..898cc636cbe03196912339f59a260d666b1fe67e 100644 --- a/src/main/java/de/thm/arsnova/service/ContentServiceImpl.java +++ b/src/main/java/de/thm/arsnova/service/ContentServiceImpl.java @@ -22,7 +22,6 @@ import java.io.IOException; import java.util.Collections; import java.util.Date; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -42,8 +41,8 @@ import org.springframework.validation.Validator; import de.thm.arsnova.event.BeforeDeletionEvent; import de.thm.arsnova.model.Content; +import de.thm.arsnova.model.ContentGroup; import de.thm.arsnova.model.Room; -import de.thm.arsnova.model.Room.ContentGroup; import de.thm.arsnova.persistence.AnswerRepository; import de.thm.arsnova.persistence.ContentRepository; import de.thm.arsnova.persistence.LogEntryRepository; @@ -64,6 +63,8 @@ public class ContentServiceImpl extends DefaultEntityServiceImpl<Content> implem private ContentRepository contentRepository; + private ContentGroupService contentGroupService; + private AnswerService answerService; private AnswerRepository answerRepository; @@ -92,16 +93,17 @@ public class ContentServiceImpl extends DefaultEntityServiceImpl<Content> implem this.answerService = answerService; } + @Autowired + public void setContentGroupService(final ContentGroupService contentGroupService) { + this.contentGroupService = contentGroupService; + } + @Override protected void modifyRetrieved(final Content content) { if (content.getFormat() != Content.Format.TEXT && 0 == content.getState().getRound()) { /* needed for legacy questions whose piRound property has not been set */ content.getState().setRound(1); } - - final Room room = roomService.get(content.getRoomId()); - content.setGroups(room.getContentGroups().stream() - .map(Room.ContentGroup::getName).filter(g -> !g.isEmpty()).collect(Collectors.toSet())); } /* FIXME: caching */ @@ -120,13 +122,8 @@ public class ContentServiceImpl extends DefaultEntityServiceImpl<Content> implem @Override public Iterable<Content> getByRoomIdAndGroup(final String roomId, final String group) { - final Room room = roomService.get(roomId); - Room.ContentGroup contentGroup = null; - for (final Room.ContentGroup cg : room.getContentGroups()) { - if (cg.getName().equals(group)) { - contentGroup = cg; - } - } + final ContentGroup contentGroup = contentGroupService.getByRoomIdAndName(roomId, group); + if (contentGroup == null) { throw new NotFoundException("Content group does not exist."); } @@ -142,13 +139,8 @@ public class ContentServiceImpl extends DefaultEntityServiceImpl<Content> implem @Override public int countByRoomIdAndGroup(final String roomId, final String group) { - final Room room = roomService.get(roomId); - Room.ContentGroup contentGroup = null; - for (final Room.ContentGroup cg : room.getContentGroups()) { - if (cg.getName().equals(group)) { - contentGroup = cg; - } - } + final ContentGroup contentGroup = contentGroupService.getByRoomIdAndName(roomId, group); + if (contentGroup == null) { throw new NotFoundException("Content group does not exist."); } @@ -179,22 +171,6 @@ public class ContentServiceImpl extends DefaultEntityServiceImpl<Content> implem */ } - @Override - protected void finalizeCreate(final Content content) { - /* Update content groups of room */ - final Room room = roomService.get(content.getRoomId()); - final Map<String, Room.ContentGroup> groups = room.getContentGroupsAsMap(); - for (final String groupName : content.getGroups()) { - final Room.ContentGroup group = groups.getOrDefault(groupName, new Room.ContentGroup()); - groups.put(groupName, group); - group.getContentIds().add(content.getId()); - group.setName(groupName); - group.setAutoSort(true); - } - room.setContentGroupsFromMap(groups); - roomService.update(room); - } - @Override protected void prepareUpdate(final Content content) { final User user = userService.getCurrentUser(); @@ -220,25 +196,6 @@ public class ContentServiceImpl extends DefaultEntityServiceImpl<Content> implem @Override protected void finalizeUpdate(final Content content) { - /* Update content groups of room */ - final Room room = roomService.get(content.getRoomId()); - final Set<String> contentsGroupNames = content.getGroups(); - final Set<String> allGroupNames = new HashSet<>(contentsGroupNames); - final Map<String, Room.ContentGroup> groups = room.getContentGroupsAsMap(); - allGroupNames.addAll(groups.keySet()); - for (final String groupName : allGroupNames) { - final Room.ContentGroup group = groups.getOrDefault(groupName, new Room.ContentGroup()); - if (contentsGroupNames.contains(groupName)) { - group.getContentIds().add(content.getId()); - group.setName(groupName); - group.setAutoSort(true); - } else { - group.getContentIds().remove(content.getId()); - } - } - room.setContentGroupsFromMap(groups); - roomService.update(room); - /* TODO: not sure yet how to refactor this code - we need access to the old and new entity if (!oldContent.getState().isVisible() && content.getState().isVisible()) { final UnlockQuestionEvent event = new UnlockQuestionEvent(this, room, content); @@ -252,11 +209,18 @@ public class ContentServiceImpl extends DefaultEntityServiceImpl<Content> implem @Override protected void prepareDelete(final Content content) { - final Room room = roomService.get(content.getRoomId()); - for (final ContentGroup group : room.getContentGroups()) { - group.getContentIds().remove(content.getId()); + final List<ContentGroup> contentGroups = contentGroupService.getByRoomId(content.getRoomId()); + for (final ContentGroup contentGroup : contentGroups) { + final Set<String> ids = contentGroup.getContentIds(); + if (ids.contains(content.getId())) { + ids.remove(content.getId()); + if (!ids.isEmpty()) { + contentGroupService.update(contentGroup); + } else { + contentGroupService.delete(contentGroup); + } + } } - roomService.update(room); } @Override @@ -275,43 +239,42 @@ public class ContentServiceImpl extends DefaultEntityServiceImpl<Content> implem } @PreAuthorize("isAuthenticated()") - private void deleteBySessionAndVariant(final Room room, final String variant) { + private void deleteByRoomAndGroupName(final Room room, final String groupName) { final Iterable<Content> contents; - if ("all".equals(variant)) { - contents = contentRepository.findStubsByRoomId(room.getId()); + if ("all".equals(groupName)) { + delete(contentRepository.findStubsByRoomId(room.getId())); } else { - contents = contentRepository.findStubsByRoomIdAndVariant(room.getId(), variant); + final Set<String> ids = contentGroupService.getByRoomIdAndName(room.getId(), groupName).getContentIds(); + delete(contentRepository.findStubsByIds(ids)); } - - delete(contents); } @Override @PreAuthorize("isAuthenticated()") public void deleteAllContents(final String roomId) { final Room room = getRoomWithAuthCheck(roomId); - deleteBySessionAndVariant(room, "all"); + deleteByRoomAndGroupName(room, "all"); } @Override @PreAuthorize("isAuthenticated()") public void deleteLectureContents(final String roomId) { final Room room = getRoomWithAuthCheck(roomId); - deleteBySessionAndVariant(room, "lecture"); + deleteByRoomAndGroupName(room, "lecture"); } @Override @PreAuthorize("isAuthenticated()") public void deletePreparationContents(final String roomId) { final Room room = getRoomWithAuthCheck(roomId); - deleteBySessionAndVariant(room, "preparation"); + deleteByRoomAndGroupName(room, "preparation"); } @Override @PreAuthorize("isAuthenticated()") public void deleteFlashcards(final String roomId) { final Room room = getRoomWithAuthCheck(roomId); - deleteBySessionAndVariant(room, "flashcard"); + deleteByRoomAndGroupName(room, "flashcard"); } @Override @@ -394,7 +357,10 @@ public class ContentServiceImpl extends DefaultEntityServiceImpl<Content> implem @Override public List<String> getUnAnsweredLectureContentIds(final String roomId, final String userId) { - return contentRepository.findUnansweredIdsByRoomIdAndUserOnlyLectureVariant(roomId, userId); + final List<String> ids = contentRepository.findUnansweredIdsByRoomIdAndUser(roomId, userId); + ids.retainAll(contentGroupService.getByRoomIdAndName(roomId, "lecture").getContentIds()); + + return ids; } @Override @@ -406,7 +372,10 @@ public class ContentServiceImpl extends DefaultEntityServiceImpl<Content> implem @Override public List<String> getUnAnsweredPreparationContentIds(final String roomId, final String userId) { - return contentRepository.findUnansweredIdsByRoomIdAndUserOnlyPreparationVariant(roomId, userId); + final List<String> ids = contentRepository.findUnansweredIdsByRoomIdAndUser(roomId, userId); + ids.retainAll(contentGroupService.getByRoomIdAndName(roomId, "preparation").getContentIds()); + + return ids; } @Override @@ -458,10 +427,9 @@ public class ContentServiceImpl extends DefaultEntityServiceImpl<Content> implem @PreAuthorize("isAuthenticated()") public void deleteAllPreparationAnswers(final String roomId) { final Room room = roomService.get(roomId); - - final List<Content> contents = contentRepository.findByRoomIdAndVariantAndActive(room.getId(), "preparation"); - resetContentsRoundState(room.getId(), contents); - final List<String> contentIds = contents.stream().map(Content::getId).collect(Collectors.toList()); + contentGroupService.getByRoomIdAndName(roomId, "preparation").getContentIds(); + final Set<String> contentIds = contentGroupService.getByRoomIdAndName(roomId, "preparation").getContentIds(); + resetContentsRoundState(room.getId(), get(contentIds)); answerService.delete(answerRepository.findStubsByContentIds(contentIds)); } @@ -469,10 +437,9 @@ public class ContentServiceImpl extends DefaultEntityServiceImpl<Content> implem @PreAuthorize("isAuthenticated()") public void deleteAllLectureAnswers(final String roomId) { final Room room = roomService.get(roomId); - - final List<Content> contents = contentRepository.findByRoomIdAndVariantAndActive(room.getId(), "lecture"); - resetContentsRoundState(room.getId(), contents); - final List<String> contentIds = contents.stream().map(Content::getId).collect(Collectors.toList()); + contentGroupService.getByRoomIdAndName(roomId, "lecture").getContentIds(); + final Set<String> contentIds = contentGroupService.getByRoomIdAndName(roomId, "lecture").getContentIds(); + resetContentsRoundState(room.getId(), get(contentIds)); answerService.delete(answerRepository.findStubsByContentIds(contentIds)); } @@ -482,7 +449,7 @@ public class ContentServiceImpl extends DefaultEntityServiceImpl<Content> implem @CacheEvict(value = "lecturecontentlists", key = "#roomId"), @CacheEvict(value = "preparationcontentlists", key = "#roomId"), @CacheEvict(value = "flashcardcontentlists", key = "#roomId") }) - private void resetContentsRoundState(final String roomId, final List<Content> contents) { + private void resetContentsRoundState(final String roomId, final Iterable<Content> contents) { for (final Content q : contents) { /* TODO: Check if setting the sessionId is necessary. */ q.setRoomId(roomId); diff --git a/src/main/java/de/thm/arsnova/service/RoomServiceImpl.java b/src/main/java/de/thm/arsnova/service/RoomServiceImpl.java index 0ddfa42fb2372025da2f94d915b35e1b9b7dcd2a..2ea4ae30ee8cbeec857bb80c8dc5393c4750fab0 100644 --- a/src/main/java/de/thm/arsnova/service/RoomServiceImpl.java +++ b/src/main/java/de/thm/arsnova/service/RoomServiceImpl.java @@ -23,9 +23,7 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; -import java.util.HashSet; import java.util.List; -import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; import org.slf4j.Logger; @@ -167,32 +165,6 @@ public class RoomServiceImpl extends DefaultEntityServiceImpl<Room> implements R } } - /** - * Adds a default content group with contents that have no other content group assigned. - * - * @param room The Room to be modified - */ - @Override - protected void modifyRetrieved(final Room room) { - // creates a set from all room content groups - final Set<String> cidsWithGroup = room.getContentGroups().stream() - .map(Room.ContentGroup::getContentIds) - .flatMap(ids -> ids.stream()) - .collect(Collectors.toSet()); - - final Set<String> cids = new HashSet<>(contentRepository.findIdsByRoomId(room.getId())); - cids.removeAll(cidsWithGroup); - - if (!cids.isEmpty()) { - final Set<Room.ContentGroup> cgs = room.getContentGroups(); - final Room.ContentGroup defaultGroup = new Room.ContentGroup(); - defaultGroup.setContentIds(cids); - defaultGroup.setAutoSort(true); - defaultGroup.setName(""); - cgs.add(defaultGroup); - } - } - @Override public Room join(final String id, final UUID socketId) { final Room room = null != id ? get(id) : null; diff --git a/src/main/resources/couchdb/Answer.design.js b/src/main/resources/couchdb/Answer.design.js index 3e89d47b19c6d1875d7b4d134198f78259ad42c4..ba5814d6a4f8bcbb205408a2394dffb3d608622d 100644 --- a/src/main/resources/couchdb/Answer.design.js +++ b/src/main/resources/couchdb/Answer.design.js @@ -15,7 +15,8 @@ var designDoc = { if (doc.type === "Answer") { emit(doc.contentId, {_rev: doc._rev}); } - } + }, + "reduce": "_count" }, "by_contentid_round_body_subject": { "map": function (doc) { @@ -55,34 +56,12 @@ var designDoc = { }, "reduce": "_count" }, - "by_roomid_variant": { - "map": function (doc) { - if (doc.type === "Answer") { - emit([doc.roomId, doc.questionVariant], {_rev: doc._rev}); - } - }, - "reduce": "_count" - }, "by_creatorid_roomid": { "map": function (doc) { if (doc.type === "Answer") { emit([doc.creatorId, doc.roomId], {_rev: doc._rev}); } } - }, - "contentid_by_creatorid_roomid_variant": { - "map": function (doc) { - if (doc.type === "Answer") { - emit([doc.user, doc.roomId, doc.questionVariant], doc.contentId); - } - } - }, - "contentid_round_by_creatorid_roomid_variant": { - "map": function (doc) { - if (doc.type === "Answer") { - emit([doc.creatorId, doc.roomId, doc.questionVariant], [doc.contentId, doc.round]); - } - } } } }; diff --git a/src/main/resources/couchdb/Content.design.js b/src/main/resources/couchdb/Content.design.js index 5b09c23f6fe15cb03a481f2c737ee23d54f8a0d3..82b773ef68c20dbd1c153c19bc20b6ce0c3c812c 100644 --- a/src/main/resources/couchdb/Content.design.js +++ b/src/main/resources/couchdb/Content.design.js @@ -18,10 +18,10 @@ var designDoc = { }, "reduce": "_count" }, - "by_roomid_group_locked": { + "by_roomid_locked": { "map": function (doc) { if (doc.type === "Content") { - emit([doc.roomId, doc.group, doc.locked, doc.subject, doc.body.substr(0, 16)], {_rev: doc._rev}); + emit([doc.roomId, doc.locked, doc.subject, doc.body.substr(0, 16)], {_rev: doc._rev}); } }, "reduce": "_count" diff --git a/src/main/resources/couchdb/ContentGroup.design.js b/src/main/resources/couchdb/ContentGroup.design.js new file mode 100644 index 0000000000000000000000000000000000000000..8592b2926ba3da698b9aa6325bf68dcc79a6e1b4 --- /dev/null +++ b/src/main/resources/couchdb/ContentGroup.design.js @@ -0,0 +1,22 @@ +var designDoc = { + "_id": "_design/ContentGroup", + "language": "javascript", + "views": { + "by_id": { + "map": function (doc) { + if (doc.type === "ContentGroup") { + emit(doc._id, {_rev: doc._rev}); + } + }, + "reduce": "_count" + }, + "by_roomid_name": { + "map": function (doc) { + if (doc.type === "ContentGroup") { + emit([doc.roomId, doc.name], {_rev: doc._rev}); + } + }, + "reduce": "_count" + } + } +}; diff --git a/src/test/java/de/thm/arsnova/config/TestPersistanceConfig.java b/src/test/java/de/thm/arsnova/config/TestPersistanceConfig.java index 92e5af312794a0aefe659063d20e5be2425739bf..89974031aa94c8e7172f0a4b168cbf546f2ef9a4 100644 --- a/src/test/java/de/thm/arsnova/config/TestPersistanceConfig.java +++ b/src/test/java/de/thm/arsnova/config/TestPersistanceConfig.java @@ -26,6 +26,7 @@ import org.springframework.context.annotation.Profile; import de.thm.arsnova.persistence.AnswerRepository; import de.thm.arsnova.persistence.AttachmentRepository; import de.thm.arsnova.persistence.CommentRepository; +import de.thm.arsnova.persistence.ContentGroupRepository; import de.thm.arsnova.persistence.ContentRepository; import de.thm.arsnova.persistence.LogEntryRepository; import de.thm.arsnova.persistence.MotdRepository; @@ -62,6 +63,11 @@ public class TestPersistanceConfig { return Mockito.mock(ContentRepository.class); } + @Bean + public ContentGroupRepository contentGroupRepository() { + return Mockito.mock(ContentGroupRepository.class); + } + @Bean public AnswerRepository answerRepository() { return Mockito.mock(AnswerRepository.class);