diff --git a/src/main/java/de/thm/arsnova/controller/AudienceQuestionController.java b/src/main/java/de/thm/arsnova/controller/AudienceQuestionController.java index 3f42d627ea2b0e22d214a206d0695875313b3d26..a674b6b24476ea1b2e44635bc98366a02ddb37cf 100644 --- a/src/main/java/de/thm/arsnova/controller/AudienceQuestionController.java +++ b/src/main/java/de/thm/arsnova/controller/AudienceQuestionController.java @@ -32,10 +32,9 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; -import de.thm.arsnova.entities.InterposedQuestion; +import de.thm.arsnova.entities.transport.InterposedQuestion; import de.thm.arsnova.entities.InterposedReadingCount; import de.thm.arsnova.exceptions.BadRequestException; -import de.thm.arsnova.exceptions.PreconditionFailedException; import de.thm.arsnova.services.IQuestionService; import de.thm.arsnova.web.DeprecatedApi; @@ -62,24 +61,20 @@ public class AudienceQuestionController extends AbstractController { @RequestMapping(value = "/", method = RequestMethod.GET) public final List<InterposedQuestion> getInterposedQuestions(@RequestParam final String sessionkey) { - return questionService.getInterposedQuestions(sessionkey); + return InterposedQuestion.fromList(questionService.getInterposedQuestions(sessionkey)); } @RequestMapping(value = "/{questionId}", method = RequestMethod.GET) public final InterposedQuestion getInterposedQuestion(@PathVariable final String questionId) { - return questionService.readInterposedQuestion(questionId); + return new InterposedQuestion(questionService.readInterposedQuestion(questionId)); } @RequestMapping(value = "/", method = RequestMethod.POST) @ResponseStatus(HttpStatus.CREATED) public final void postInterposedQuestion( @RequestParam final String sessionkey, - @RequestBody final InterposedQuestion question + @RequestBody final de.thm.arsnova.entities.InterposedQuestion question ) { - if (!sessionkey.equals(question.getSessionId())) { - throw new PreconditionFailedException(); - } - if (questionService.saveQuestion(question)) { return; } diff --git a/src/main/java/de/thm/arsnova/controller/LecturerQuestionController.java b/src/main/java/de/thm/arsnova/controller/LecturerQuestionController.java index e4c7e2d526ec421507a3204656cf91788f935941..964f1005eacfa6ea0e0a8d66831fbba57559264f 100644 --- a/src/main/java/de/thm/arsnova/controller/LecturerQuestionController.java +++ b/src/main/java/de/thm/arsnova/controller/LecturerQuestionController.java @@ -38,6 +38,7 @@ import org.springframework.web.bind.annotation.RestController; import de.thm.arsnova.entities.Answer; import de.thm.arsnova.entities.Question; import de.thm.arsnova.exceptions.BadRequestException; +import de.thm.arsnova.exceptions.ForbiddenException; import de.thm.arsnova.exceptions.NoContentException; import de.thm.arsnova.exceptions.NotFoundException; import de.thm.arsnova.services.IQuestionService; @@ -332,9 +333,17 @@ public class LecturerQuestionController extends AbstractController { @RequestMapping(value = "/answers", method = RequestMethod.DELETE) public final void deleteAllQuestionsAnswers( @RequestParam final String sessionkey, + @RequestParam(value = "lecturequestionsonly", defaultValue = "false") final boolean lectureQuestionsOnly, + @RequestParam(value = "preparationquestionsonly", defaultValue = "false") final boolean preparationQuestionsOnly, final HttpServletResponse response ) { - questionService.deleteAllQuestionsAnswers(sessionkey); + if (lectureQuestionsOnly) { + questionService.deleteAllLectureAnswers(sessionkey); + } else if (preparationQuestionsOnly) { + questionService.deleteAllPreparationAnswers(sessionkey); + } else { + questionService.deleteAllQuestionsAnswers(sessionkey); + } } /** diff --git a/src/main/java/de/thm/arsnova/dao/CouchDBDao.java b/src/main/java/de/thm/arsnova/dao/CouchDBDao.java index 2785a2494fee974c144f9c8e3bbdefdede5e0989..3df9c25a8bd7de2375afe265690ccfda4d8c2a9a 100644 --- a/src/main/java/de/thm/arsnova/dao/CouchDBDao.java +++ b/src/main/java/de/thm/arsnova/dao/CouchDBDao.java @@ -425,7 +425,7 @@ public class CouchDBDao implements IDatabaseDao { q.put("gridType", question.getGridType()); q.put("scaleFactor", question.getScaleFactor()); q.put("gridScaleFactor", question.getGridScaleFactor()); - + return q; } @@ -464,7 +464,7 @@ public class CouchDBDao implements IDatabaseDao { q.put("gridType", question.getGridType()); q.put("scaleFactor", question.getScaleFactor()); q.put("gridScaleFactor", question.getGridScaleFactor()); - + database.saveDocument(q); question.set_rev(q.getRev()); @@ -877,7 +877,7 @@ public class CouchDBDao implements IDatabaseDao { @Override public List<InterposedQuestion> getInterposedQuestions(final Session session) { - final NovaView view = new NovaView("interposed_question/by_session"); + final NovaView view = new NovaView("interposed_question/by_session_full"); view.setKey(session.get_id()); final ViewResults questions = getDatabase().view(view); if (questions == null || questions.isEmpty()) { @@ -1462,6 +1462,22 @@ public class CouchDBDao implements IDatabaseDao { } } + @Override + public void deleteAllPreparationAnswers(final Session session) { + final List<Question> questions = getQuestions(new NovaView("skill_question/preparation_question_by_session"), session); + for (final Question q : questions) { + deleteAnswers(q); + } + } + + @Override + public void deleteAllLectureAnswers(final Session session) { + final List<Question> questions = getQuestions(new NovaView("skill_question/lecture_question_by_session"), session); + for (final Question q : questions) { + deleteAnswers(q); + } + } + @Override public int getLearningProgress(final Session session) { // Note: we have to use this many views because our CouchDB version does not support @@ -1492,7 +1508,7 @@ public class CouchDBDao implements IDatabaseDao { } final double courseAverageValue = userTotalValue / numUsers; final double courseProgress = courseAverageValue / courseMaximumValue; - return (int)Math.round(courseProgress * 100); + return (int)Math.min(100, Math.round(courseProgress * 100)); } @Override @@ -1518,8 +1534,9 @@ public class CouchDBDao implements IDatabaseDao { 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>((int)Math.round(myProgress*100), courseProgress); + return new AbstractMap.SimpleEntry<Integer, Integer>(myLearningProgress, courseProgress); } @Override diff --git a/src/main/java/de/thm/arsnova/dao/IDatabaseDao.java b/src/main/java/de/thm/arsnova/dao/IDatabaseDao.java index 37e1a2115e5701672fb6504d616c963723786fe0..7a1c205f9d70e80088fa542a091fc812876f9634 100644 --- a/src/main/java/de/thm/arsnova/dao/IDatabaseDao.java +++ b/src/main/java/de/thm/arsnova/dao/IDatabaseDao.java @@ -173,4 +173,8 @@ public interface IDatabaseDao { List<SessionInfo> getMySessionsInfo(User user); List<SessionInfo> getMyVisitedSessionsInfo(User currentUser); + + void deleteAllPreparationAnswers(Session session); + + void deleteAllLectureAnswers(Session session); } diff --git a/src/main/java/de/thm/arsnova/entities/InterposedQuestion.java b/src/main/java/de/thm/arsnova/entities/InterposedQuestion.java index a08b136ae2b43be9ca73e7cb3bd0a97b3a6323a1..116535266e958aefde1dedbf3ab09084fe5f650f 100644 --- a/src/main/java/de/thm/arsnova/entities/InterposedQuestion.java +++ b/src/main/java/de/thm/arsnova/entities/InterposedQuestion.java @@ -98,4 +98,8 @@ public class InterposedQuestion { public void setCreator(String creator) { this.creator = creator; } + + public boolean isCreator(User user) { + return user.getUsername().equals(creator); + } } diff --git a/src/main/java/de/thm/arsnova/entities/transport/InterposedQuestion.java b/src/main/java/de/thm/arsnova/entities/transport/InterposedQuestion.java new file mode 100644 index 0000000000000000000000000000000000000000..702836ccc5f836edf8ddfc9c9e9ad09d2e088c4f --- /dev/null +++ b/src/main/java/de/thm/arsnova/entities/transport/InterposedQuestion.java @@ -0,0 +1,71 @@ +package de.thm.arsnova.entities.transport; + +import java.util.ArrayList; +import java.util.List; + +public class InterposedQuestion { + + private String id; + private String subject; + private String text; + private long timestamp; + private boolean read; + + public static List<InterposedQuestion> fromList(List<de.thm.arsnova.entities.InterposedQuestion> questions) { + ArrayList<InterposedQuestion> interposedQuestions = new ArrayList<InterposedQuestion>(); + for (de.thm.arsnova.entities.InterposedQuestion question : questions) { + interposedQuestions.add(new InterposedQuestion(question)); + } + return interposedQuestions; + } + + public InterposedQuestion(de.thm.arsnova.entities.InterposedQuestion question) { + this.id = question.get_id(); + this.subject = question.getSubject(); + this.text = question.getText(); + this.timestamp = question.getTimestamp(); + this.read = question.isRead(); + } + + public InterposedQuestion() {} + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getSubject() { + return subject; + } + + public void setSubject(String subject) { + this.subject = subject; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + public boolean isRead() { + return read; + } + + public void setRead(boolean read) { + this.read = read; + } +} diff --git a/src/main/java/de/thm/arsnova/events/DeleteAnswerEvent.java b/src/main/java/de/thm/arsnova/events/DeleteAnswerEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..1f5c564f9936ea82a8cb13aa91c723dd304fb4ba --- /dev/null +++ b/src/main/java/de/thm/arsnova/events/DeleteAnswerEvent.java @@ -0,0 +1,33 @@ +package de.thm.arsnova.events; + +import de.thm.arsnova.entities.Question; +import de.thm.arsnova.entities.Session; + +public class DeleteAnswerEvent extends NovaEvent { + + private static final long serialVersionUID = 1L; + + private final Question question; + + private final Session session; + + public DeleteAnswerEvent(Object source, Question question, Session session) { + super(source); + this.question = question; + this.session = session; + } + + @Override + public void accept(NovaEventVisitor visitor) { + visitor.visit(this); + } + + public Question getQuestion() { + return question; + } + + public Session getSession() { + return session; + } + +} diff --git a/src/main/java/de/thm/arsnova/events/DeleteInterposedQuestionEvent.java b/src/main/java/de/thm/arsnova/events/DeleteInterposedQuestionEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..b8aea746a91cd56939a1be1e1fe354a7f4be1cfa --- /dev/null +++ b/src/main/java/de/thm/arsnova/events/DeleteInterposedQuestionEvent.java @@ -0,0 +1,26 @@ +package de.thm.arsnova.events; + +import de.thm.arsnova.entities.InterposedQuestion; +import de.thm.arsnova.entities.Session; + +public class DeleteInterposedQuestionEvent extends NovaEvent { + + private static final long serialVersionUID = 1L; + + private final Session session; + + private final InterposedQuestion question; + + public DeleteInterposedQuestionEvent(Object source, Session session, InterposedQuestion question) { + super(source); + this.session = session; + this.question = question; + } + + @Override + public void accept(NovaEventVisitor visitor) { + // TODO Auto-generated method stub + + } + +} diff --git a/src/main/java/de/thm/arsnova/events/NewAnswerEvent.java b/src/main/java/de/thm/arsnova/events/NewAnswerEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..256d569db2665b380988576c5e16319ddcfeb69b --- /dev/null +++ b/src/main/java/de/thm/arsnova/events/NewAnswerEvent.java @@ -0,0 +1,49 @@ +package de.thm.arsnova.events; + +import de.thm.arsnova.entities.Answer; +import de.thm.arsnova.entities.Question; +import de.thm.arsnova.entities.Session; +import de.thm.arsnova.entities.User; + +public class NewAnswerEvent extends NovaEvent { + + private static final long serialVersionUID = 1L; + + private final Answer answer; + + private final User user; + + private final Question question; + + private final Session session; + + public NewAnswerEvent(Object source, Answer answer, User user, Question question, Session session) { + super(source); + this.answer = answer; + this.user = user; + this.question = question; + this.session = session; + } + + @Override + public void accept(NovaEventVisitor visitor) { + visitor.visit(this); + } + + public Answer getAnswer() { + return answer; + } + + public User getUser() { + return user; + } + + public Question getQuestion() { + return question; + } + + public Session getSession() { + return session; + } + +} diff --git a/src/main/java/de/thm/arsnova/events/NovaEventVisitor.java b/src/main/java/de/thm/arsnova/events/NovaEventVisitor.java index 4664d21d26c63cf0ecd5e0dc0ddcb04f5e0c5fe8..6f1ec04a56e65fcf23036f41dff141b4dabf05c7 100644 --- a/src/main/java/de/thm/arsnova/events/NovaEventVisitor.java +++ b/src/main/java/de/thm/arsnova/events/NovaEventVisitor.java @@ -6,4 +6,8 @@ public interface NovaEventVisitor { void visit(NewQuestionEvent newQuestionEvent); + void visit(NewAnswerEvent newAnswerEvent); + + void visit(DeleteAnswerEvent deleteAnswerEvent); + } diff --git a/src/main/java/de/thm/arsnova/services/IQuestionService.java b/src/main/java/de/thm/arsnova/services/IQuestionService.java index 596d1ffd40bc9986f29f060f281bc22a597306d8..633478fd05377f74204b4f9c6e4f1287af036ea7 100644 --- a/src/main/java/de/thm/arsnova/services/IQuestionService.java +++ b/src/main/java/de/thm/arsnova/services/IQuestionService.java @@ -25,6 +25,7 @@ import de.thm.arsnova.entities.Answer; import de.thm.arsnova.entities.InterposedQuestion; import de.thm.arsnova.entities.InterposedReadingCount; import de.thm.arsnova.entities.Question; +import de.thm.arsnova.entities.User; public interface IQuestionService { Question saveQuestion(Question question); @@ -65,6 +66,8 @@ public interface IQuestionService { InterposedQuestion readInterposedQuestion(String questionId); + InterposedQuestion readInterposedQuestionInternal(String questionId, User user); + Question update(Question question); void deleteAnswers(String questionId); @@ -91,8 +94,12 @@ public interface IQuestionService { int countLectureQuestionAnswers(String sessionkey); + int countLectureQuestionAnswersInternal(String sessionkey); + int countPreparationQuestionAnswers(String sessionkey); + int countPreparationQuestionAnswersInternal(String sessionkey); + void deleteLectureQuestions(String sessionkey); void deleteFlashcards(String sessionkey); @@ -101,12 +108,20 @@ public interface IQuestionService { List<String> getUnAnsweredLectureQuestionIds(String sessionkey); + List<String> getUnAnsweredLectureQuestionIds(String sessionKey, User user); + List<String> getUnAnsweredPreparationQuestionIds(String sessionkey); + List<String> getUnAnsweredPreparationQuestionIds(String sessionKey, User user); + void deleteAllInterposedQuestions(String sessionKeyword); void publishAll(String sessionkey, boolean publish); void deleteAllQuestionsAnswers(String sessionkey); + void deleteAllPreparationAnswers(String sessionkey); + + void deleteAllLectureAnswers(String sessionkey); + } diff --git a/src/main/java/de/thm/arsnova/services/QuestionService.java b/src/main/java/de/thm/arsnova/services/QuestionService.java index ee1d5e827fd8f72582341e482cdee32401182358..9484450063b0acec0519561ea9acffa7bfcbfd38 100644 --- a/src/main/java/de/thm/arsnova/services/QuestionService.java +++ b/src/main/java/de/thm/arsnova/services/QuestionService.java @@ -41,6 +41,9 @@ import de.thm.arsnova.entities.InterposedReadingCount; import de.thm.arsnova.entities.Question; import de.thm.arsnova.entities.Session; import de.thm.arsnova.entities.User; +import de.thm.arsnova.events.DeleteAnswerEvent; +import de.thm.arsnova.events.DeleteInterposedQuestionEvent; +import de.thm.arsnova.events.NewAnswerEvent; import de.thm.arsnova.events.NewInterposedQuestionEvent; import de.thm.arsnova.events.NewQuestionEvent; import de.thm.arsnova.exceptions.BadRequestException; @@ -189,6 +192,10 @@ public class QuestionService implements IQuestionService, ApplicationEventPublis throw new NotFoundException(); } databaseDao.deleteInterposedQuestion(question); + + final Session session = databaseDao.getSessionFromKeyword(question.getSessionId()); + final DeleteInterposedQuestionEvent event = new DeleteInterposedQuestionEvent(this, session, question); + this.publisher.publishEvent(event); } @Override @@ -207,21 +214,13 @@ public class QuestionService implements IQuestionService, ApplicationEventPublis } @Override - @PreAuthorize("isAuthenticated()") + @PreAuthorize("isAuthenticated() and hasPermission(#questionId, 'question', 'owner')") public void deleteAnswers(final String questionId) { final Question question = databaseDao.getQuestion(questionId); - if (question == null) { - throw new NotFoundException(); - } - - final User user = userService.getCurrentUser(); - final Session session = databaseDao.getSession(question.getSessionKeyword()); - if (user == null || session == null || !session.isCreator(user)) { - throw new UnauthorizedException(); - } databaseDao.deleteAnswers(question); } + @Override @PreAuthorize("isAuthenticated()") public List<String> getUnAnsweredQuestionIds(final String sessionKey) { @@ -268,7 +267,9 @@ public class QuestionService implements IQuestionService, ApplicationEventPublis @PreAuthorize("isAuthenticated()") public int getAnswerCount(final String questionId) { final Question question = getQuestion(questionId); - + if (question == null) { + return 0; + } return databaseDao.getAnswerCount(question, question.getPiRound()); } @@ -354,13 +355,24 @@ public class QuestionService implements IQuestionService, ApplicationEventPublis @Override @PreAuthorize("isAuthenticated()") public InterposedQuestion readInterposedQuestion(final String questionId) { + final User user = userService.getCurrentUser(); + return this.readInterposedQuestionInternal(questionId, user); + } + + /* + * The "internal" suffix means it is called by internal services that have no authentication! + * TODO: Find a better way of doing this... + */ + @Override + public InterposedQuestion readInterposedQuestionInternal(final String questionId, User user) { final InterposedQuestion question = databaseDao.getInterposedQuestion(questionId); if (question == null) { throw new NotFoundException(); } final Session session = databaseDao.getSessionFromKeyword(question.getSessionId()); - - final User user = userService.getCurrentUser(); + if (!question.isCreator(user) && !session.isCreator(user)) { + throw new UnauthorizedException(); + } if (session.isCreator(user)) { databaseDao.markInterposedQuestionAsRead(question); } @@ -406,7 +418,8 @@ public class QuestionService implements IQuestionService, ApplicationEventPublis } final Answer result = databaseDao.saveAnswer(answer, user); - socketIoServer.reportAnswersToLecturerQuestionAvailable(question.getSessionKeyword(), question.get_id()); + final Session session = databaseDao.getSessionFromKeyword(question.getSessionKeyword()); + this.publisher.publishEvent(new NewAnswerEvent(this, result, user, question, session)); return result; } @@ -415,13 +428,15 @@ public class QuestionService implements IQuestionService, ApplicationEventPublis @PreAuthorize("isAuthenticated()") public Answer updateAnswer(final Answer answer) { final User user = userService.getCurrentUser(); - if (user == null || !user.getUsername().equals(answer.getUser())) { + final Answer realAnswer = this.getMyAnswer(answer.getQuestionId()); + if (user == null || realAnswer == null || !user.getUsername().equals(realAnswer.getUser())) { throw new UnauthorizedException(); } final Question question = getQuestion(answer.getQuestionId()); - final Answer result = databaseDao.updateAnswer(answer); - socketIoServer.reportAnswersToLecturerQuestionAvailable(question.getSessionKeyword(), question.get_id()); + final Answer result = databaseDao.updateAnswer(realAnswer); + final Session session = databaseDao.getSessionFromKeyword(question.getSessionKeyword()); + this.publisher.publishEvent(new NewAnswerEvent(this, result, user, question, session)); return result; } @@ -440,7 +455,7 @@ public class QuestionService implements IQuestionService, ApplicationEventPublis } databaseDao.deleteAnswer(answerId); - socketIoServer.reportAnswersToLecturerQuestionAvailable(question.getSessionKeyword(), question.get_id()); + this.publisher.publishEvent(new DeleteAnswerEvent(this, question, session)); } @Override @@ -490,12 +505,30 @@ public class QuestionService implements IQuestionService, ApplicationEventPublis @Override @PreAuthorize("isAuthenticated()") public int countLectureQuestionAnswers(final String sessionkey) { + return this.countLectureQuestionAnswersInternal(sessionkey); + } + + /* + * 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 sessionkey) { return databaseDao.countLectureQuestionAnswers(getSession(sessionkey)); } @Override @PreAuthorize("isAuthenticated()") public int countPreparationQuestionAnswers(final String sessionkey) { + return this.countPreparationQuestionAnswersInternal(sessionkey); + } + + /* + * 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 sessionkey) { return databaseDao.countPreparationQuestionAnswers(getSession(sessionkey)); } @@ -524,6 +557,11 @@ public class QuestionService implements IQuestionService, ApplicationEventPublis @PreAuthorize("isAuthenticated()") public List<String> getUnAnsweredLectureQuestionIds(final String sessionkey) { final User user = getCurrentUser(); + return this.getUnAnsweredLectureQuestionIds(sessionkey, user); + } + + @Override + public List<String> getUnAnsweredLectureQuestionIds(final String sessionkey, final User user) { final Session session = getSession(sessionkey); return databaseDao.getUnAnsweredLectureQuestionIds(session, user); } @@ -532,6 +570,11 @@ public class QuestionService implements IQuestionService, ApplicationEventPublis @PreAuthorize("isAuthenticated()") public List<String> getUnAnsweredPreparationQuestionIds(final String sessionkey) { final User user = getCurrentUser(); + return this.getUnAnsweredPreparationQuestionIds(sessionkey, user); + } + + @Override + public List<String> getUnAnsweredPreparationQuestionIds(final String sessionkey, final User user) { final Session session = getSession(sessionkey); return databaseDao.getUnAnsweredPreparationQuestionIds(session, user); } @@ -558,6 +601,20 @@ public class QuestionService implements IQuestionService, ApplicationEventPublis databaseDao.deleteAllQuestionsAnswers(session); } + @Override + @PreAuthorize("isAuthenticated() and hasPermission(#sessionkey, 'session', 'owner')") + public void deleteAllPreparationAnswers(String sessionkey) { + final Session session = getSession(sessionkey); + databaseDao.deleteAllPreparationAnswers(session); + } + + @Override + @PreAuthorize("isAuthenticated() and hasPermission(#sessionkey, 'session', 'owner')") + public void deleteAllLectureAnswers(String sessionkey) { + final Session session = getSession(sessionkey); + databaseDao.deleteAllLectureAnswers(session); + } + @Override public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { this.publisher = publisher; diff --git a/src/main/java/de/thm/arsnova/socket/ARSnovaSocketIOServer.java b/src/main/java/de/thm/arsnova/socket/ARSnovaSocketIOServer.java index c663b35137a257d03463374989034ecb5fc706f0..b7dbfd3c364bec9593f153efa5bdd74e2489535f 100644 --- a/src/main/java/de/thm/arsnova/socket/ARSnovaSocketIOServer.java +++ b/src/main/java/de/thm/arsnova/socket/ARSnovaSocketIOServer.java @@ -30,17 +30,22 @@ import com.corundumstudio.socketio.protocol.Packet; import com.corundumstudio.socketio.protocol.PacketType; import de.thm.arsnova.entities.InterposedQuestion; -import de.thm.arsnova.entities.Question; import de.thm.arsnova.entities.User; +import de.thm.arsnova.events.DeleteAnswerEvent; +import de.thm.arsnova.events.NewAnswerEvent; import de.thm.arsnova.events.NewInterposedQuestionEvent; import de.thm.arsnova.events.NewQuestionEvent; import de.thm.arsnova.events.NovaEvent; import de.thm.arsnova.events.NovaEventVisitor; +import de.thm.arsnova.exceptions.UnauthorizedException; import de.thm.arsnova.exceptions.NoContentException; +import de.thm.arsnova.exceptions.NotFoundException; import de.thm.arsnova.services.IFeedbackService; +import de.thm.arsnova.services.IQuestionService; import de.thm.arsnova.services.ISessionService; import de.thm.arsnova.services.IUserService; import de.thm.arsnova.socket.message.Feedback; +import de.thm.arsnova.socket.message.Question; import de.thm.arsnova.socket.message.Session; @Component @@ -55,6 +60,9 @@ public class ARSnovaSocketIOServer implements ApplicationListener<NovaEvent>, No @Autowired private ISessionService sessionService; + @Autowired + private IQuestionService questionService; + private static final Logger LOGGER = LoggerFactory.getLogger(ARSnovaSocketIOServer.class); private int portNumber; @@ -140,7 +148,25 @@ public class ARSnovaSocketIOServer implements ApplicationListener<NovaEvent>, No /* active user count has to be sent to the client since the broadcast is * not always sent as long as the polling solution is active simultaneously */ reportActiveUserCountForSession(session.getKeyword()); - reportSessionDataToClient(session.getKeyword(), client); + reportSessionDataToClient(session.getKeyword(), u, client); + } + } + }); + + server.addEventListener( + "readInterposedQuestion", + de.thm.arsnova.entities.transport.InterposedQuestion.class, + new DataListener<de.thm.arsnova.entities.transport.InterposedQuestion>() { + @Override + public void onData( + SocketIOClient client, + de.thm.arsnova.entities.transport.InterposedQuestion question, + AckRequest ackRequest) { + final User user = userService.getUser2SocketId(client.getSessionId()); + try { + questionService.readInterposedQuestionInternal(question.getId(), user); + } catch (NotFoundException | UnauthorizedException e) { + LOGGER.error("Loading of question {} failed for user {} with exception {}", question.getId(), user, e.getMessage()); } } }); @@ -233,22 +259,11 @@ public class ARSnovaSocketIOServer implements ApplicationListener<NovaEvent>, No } public void reportDeletedFeedback(final User user, final Set<de.thm.arsnova.entities.Session> arsSessions) { - final List<UUID> connectionIds = findConnectionIdForUser(user); - if (connectionIds.isEmpty()) { - return; - } final List<String> keywords = new ArrayList<String>(); for (final de.thm.arsnova.entities.Session session : arsSessions) { keywords.add(session.getKeyword()); } - - for (final SocketIOClient client : server.getAllClients()) { - // Find the client whose feedback has been deleted and send a - // message. - if (connectionIds.contains(client.getSessionId())) { - client.sendEvent("feedbackReset", keywords); - } - } + this.sendToUser(user, "feedbackReset", keywords); } private List<UUID> findConnectionIdForUser(final User user) { @@ -263,14 +278,31 @@ public class ARSnovaSocketIOServer implements ApplicationListener<NovaEvent>, No return result; } + private void sendToUser(final User user, final String event, Object data) { + final List<UUID> connectionIds = findConnectionIdForUser(user); + if (connectionIds.isEmpty()) { + return; + } + for (final SocketIOClient client : server.getAllClients()) { + if (connectionIds.contains(client.getSessionId())) { + client.sendEvent(event, data); + } + } + } + /** * Currently only sends the feedback data to the client. Should be used for all * relevant Socket.IO data, the client needs to know after joining a session. * * @param sessionKey + * @param user * @param client */ - public void reportSessionDataToClient(final String sessionKey, final SocketIOClient client) { + public void reportSessionDataToClient(final String sessionKey, final User user, final SocketIOClient client) { + client.sendEvent("unansweredLecturerQuestions", questionService.getUnAnsweredLectureQuestionIds(sessionKey, user)); + client.sendEvent("unansweredPreparationQuestions", questionService.getUnAnsweredPreparationQuestionIds(sessionKey, user)); + client.sendEvent("countLectureQuestionAnswers", questionService.countLectureQuestionAnswersInternal(sessionKey)); + client.sendEvent("countPreparationQuestionAnswers", questionService.countPreparationQuestionAnswersInternal(sessionKey)); client.sendEvent("activeUserCountData", sessionService.activeUsers(sessionKey)); final de.thm.arsnova.entities.Feedback fb = feedbackService.getFeedback(sessionKey); client.sendEvent("feedbackData", fb.getValues()); @@ -322,8 +354,8 @@ public class ARSnovaSocketIOServer implements ApplicationListener<NovaEvent>, No broadcastInSession(sessionKey, "activeUserCountData", count); } - public void reportAnswersToLecturerQuestionAvailable(final String sessionKey, final String lecturerQuestionId) { - broadcastInSession(sessionKey, "answersToLecQuestionAvail", lecturerQuestionId); + public void reportAnswersToLecturerQuestionAvailable(final de.thm.arsnova.entities.Session session, final Question lecturerQuestion) { + broadcastInSession(session.getKeyword(), "answersToLecQuestionAvail", lecturerQuestion.get_id()); } public void reportAudienceQuestionAvailable(final de.thm.arsnova.entities.Session session, final InterposedQuestion audienceQuestion) { @@ -333,7 +365,8 @@ public class ARSnovaSocketIOServer implements ApplicationListener<NovaEvent>, No public void reportLecturerQuestionAvailable(final de.thm.arsnova.entities.Session session, final Question lecturerQuestion) { /* TODO role handling implementation, send this only to users with role audience */ - broadcastInSession(session.getKeyword(), "lecQuestionAvail", lecturerQuestion.get_id()); + broadcastInSession(session.getKeyword(), "lecQuestionAvail", lecturerQuestion.get_id()); // deprecated! + broadcastInSession(session.getKeyword(), "lecturerQuestionAvailable", lecturerQuestion); } public void reportSessionStatus(final String sessionKey, final boolean active) { @@ -358,7 +391,7 @@ public class ARSnovaSocketIOServer implements ApplicationListener<NovaEvent>, No @Override public void visit(NewQuestionEvent event) { - this.reportLecturerQuestionAvailable(event.getSession(), event.getQuestion()); + this.reportLecturerQuestionAvailable(event.getSession(), new Question(event.getQuestion())); } @Override @@ -366,6 +399,25 @@ public class ARSnovaSocketIOServer implements ApplicationListener<NovaEvent>, No this.reportAudienceQuestionAvailable(event.getSession(), event.getQuestion()); } + @Override + public void visit(NewAnswerEvent event) { + final String sessionKey = event.getSession().getKeyword(); + this.reportAnswersToLecturerQuestionAvailable(event.getSession(), new Question(event.getQuestion())); + broadcastInSession(sessionKey, "countLectureQuestionAnswers", questionService.countLectureQuestionAnswersInternal(sessionKey)); + broadcastInSession(sessionKey, "countPreparationQuestionAnswers", questionService.countPreparationQuestionAnswersInternal(sessionKey)); + sendToUser(event.getUser(), "unansweredLecturerQuestions", questionService.getUnAnsweredLectureQuestionIds(sessionKey, event.getUser())); + sendToUser(event.getUser(), "unansweredPreparationQuestions", questionService.getUnAnsweredPreparationQuestionIds(sessionKey, event.getUser())); + } + + @Override + public void visit(DeleteAnswerEvent event) { + final String sessionKey = event.getSession().getKeyword(); + this.reportAnswersToLecturerQuestionAvailable(event.getSession(), new Question(event.getQuestion())); + // We do not know which user's answer was deleted, so we can't update his 'unanswered' list of questions... + broadcastInSession(sessionKey, "countLectureQuestionAnswers", questionService.countLectureQuestionAnswersInternal(sessionKey)); + broadcastInSession(sessionKey, "countPreparationQuestionAnswers", questionService.countPreparationQuestionAnswersInternal(sessionKey)); + } + @Override public void onApplicationEvent(NovaEvent event) { event.accept(this); diff --git a/src/main/java/de/thm/arsnova/socket/message/Question.java b/src/main/java/de/thm/arsnova/socket/message/Question.java new file mode 100644 index 0000000000000000000000000000000000000000..8b6947a116755f9ac7948812f3e3230d70fbc1d6 --- /dev/null +++ b/src/main/java/de/thm/arsnova/socket/message/Question.java @@ -0,0 +1,20 @@ +package de.thm.arsnova.socket.message; + +public class Question { + + private final String _id; + private final String variant; + + public Question(de.thm.arsnova.entities.Question question) { + this._id = question.get_id(); + this.variant = question.getQuestionVariant(); + } + + public String get_id() { + return _id; + } + + public String getVariant() { + return variant; + } +} diff --git a/src/test/java/de/thm/arsnova/dao/StubDatabaseDao.java b/src/test/java/de/thm/arsnova/dao/StubDatabaseDao.java index 4a48df93f774a2c1f0515f77465962f603eea5ca..956d495664e41c99b1f2ad73b1b983cd463d0468 100644 --- a/src/test/java/de/thm/arsnova/dao/StubDatabaseDao.java +++ b/src/test/java/de/thm/arsnova/dao/StubDatabaseDao.java @@ -511,7 +511,7 @@ public class StubDatabaseDao implements IDatabaseDao { @Override public void deleteAllInterposedQuestions(Session session, User user) { // TODO Auto-generated method stub - + } @Override @@ -531,4 +531,16 @@ public class StubDatabaseDao implements IDatabaseDao { // TODO Auto-generated method stub return null; } + + @Override + public void deleteAllPreparationAnswers(Session session) { + // TODO Auto-generated method stub + + } + + @Override + public void deleteAllLectureAnswers(Session session) { + // TODO Auto-generated method stub + + } } diff --git a/src/test/java/de/thm/arsnova/services/QuestionServiceTest.java b/src/test/java/de/thm/arsnova/services/QuestionServiceTest.java index c8b86acdb8e7f3bc047ebf6633d45610746eb41b..25c7642966f4a95108eb6c4ebf616bf000a5806a 100644 --- a/src/test/java/de/thm/arsnova/services/QuestionServiceTest.java +++ b/src/test/java/de/thm/arsnova/services/QuestionServiceTest.java @@ -122,6 +122,7 @@ public class QuestionServiceTest { theQ.setRead(false); theQ.set_id("the internal id"); theQ.setSessionId("12345678"); + theQ.setCreator("regular user"); databaseDao.interposedQuestion = theQ; questionService.readInterposedQuestion(theQ.get_id());