diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/RespondToPollReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/RespondToPollReqMsgHdlr.scala index acecb6d013..a7763e9220 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/RespondToPollReqMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/RespondToPollReqMsgHdlr.scala @@ -34,12 +34,12 @@ trait RespondToPollReqMsgHdlr { bus.outGW.send(msgEvent) } - def broadcastUserRespondedToPollRespMsg(msg: RespondToPollReqMsg, pollId: String, answerId: Int, sendToId: String): Unit = { + def broadcastUserRespondedToPollRespMsg(msg: RespondToPollReqMsg, pollId: String, answerIds: Seq[Int], sendToId: String): Unit = { val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, liveMeeting.props.meetingProp.intId, sendToId) val envelope = BbbCoreEnvelope(UserRespondedToPollRespMsg.NAME, routing) val header = BbbClientMsgHeader(UserRespondedToPollRespMsg.NAME, liveMeeting.props.meetingProp.intId, sendToId) - val body = UserRespondedToPollRespMsgBody(pollId, msg.header.userId, answerId) + val body = UserRespondedToPollRespMsgBody(pollId, msg.header.userId, answerIds) val event = UserRespondedToPollRespMsg(header, body) val msgEvent = BbbCommonEnvCoreMsg(envelope, event) bus.outGW.send(msgEvent) @@ -47,20 +47,24 @@ trait RespondToPollReqMsgHdlr { for { (pollId: String, updatedPoll: SimplePollResultOutVO) <- Polls.handleRespondToPollReqMsg(msg.header.userId, msg.body.pollId, - msg.body.questionId, msg.body.answerId, liveMeeting) + msg.body.questionId, msg.body.answerIds, liveMeeting) } yield { broadcastPollUpdatedEvent(msg, pollId, updatedPoll) for { poll <- Polls.getPoll(pollId, liveMeeting.polls) } yield { - val answerText = poll.questions(0).answers.get(msg.body.answerId).key - broadcastUserRespondedToPollRecordMsg(msg, pollId, msg.body.answerId, answerText, poll.isSecret) + for { + answerId <- msg.body.answerIds + } yield { + val answerText = poll.questions(0).answers.get(answerId).key + broadcastUserRespondedToPollRecordMsg(msg, pollId, answerId, answerText, poll.isSecret) + } } for { presenter <- Users2x.findPresenter(liveMeeting.users2x) } yield { - broadcastUserRespondedToPollRespMsg(msg, pollId, msg.body.answerId, presenter.intId) + broadcastUserRespondedToPollRespMsg(msg, pollId, msg.body.answerIds, presenter.intId) } } } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/StartCustomPollReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/StartCustomPollReqMsgHdlr.scala index 770bf71775..cf355079f0 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/StartCustomPollReqMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/StartCustomPollReqMsgHdlr.scala @@ -29,7 +29,7 @@ trait StartCustomPollReqMsgHdlr extends RightsManagementTrait { PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting) } else { for { - pvo <- Polls.handleStartCustomPollReqMsg(state, msg.header.userId, msg.body.pollId, msg.body.pollType, msg.body.secretPoll, msg.body.answers, msg.body.question, liveMeeting) + pvo <- Polls.handleStartCustomPollReqMsg(state, msg.header.userId, msg.body.pollId, msg.body.pollType, msg.body.secretPoll, msg.body.isMultipleResponse, msg.body.answers, msg.body.question, liveMeeting) } yield { broadcastEvent(msg, pvo) } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/StartPollReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/StartPollReqMsgHdlr.scala index 71c960ed76..6f6749a95c 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/StartPollReqMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/StartPollReqMsgHdlr.scala @@ -30,7 +30,7 @@ trait StartPollReqMsgHdlr extends RightsManagementTrait { PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting) } else { for { - pvo <- Polls.handleStartPollReqMsg(state, msg.header.userId, msg.body.pollId, msg.body.pollType, msg.body.secretPoll, msg.body.question, liveMeeting) + pvo <- Polls.handleStartPollReqMsg(state, msg.header.userId, msg.body.pollId, msg.body.pollType, msg.body.secretPoll, msg.body.question, msg.body.isMultipleResponse, liveMeeting) } yield { broadcastEvent(msg, pvo) } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Polls.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Polls.scala index f6ee722633..db49411f0d 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Polls.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Polls.scala @@ -12,13 +12,13 @@ import org.bigbluebutton.core.running.LiveMeeting object Polls { def handleStartPollReqMsg(state: MeetingState2x, userId: String, pollId: String, pollType: String, secretPoll: Boolean, questionText: String, - lm: LiveMeeting): Option[SimplePollOutVO] = { + multiResponse: Boolean, lm: LiveMeeting): Option[SimplePollOutVO] = { def createPoll(stampedPollId: String): Option[Poll] = { val numRespondents: Int = Users2x.numUsers(lm.users2x) - 1 // subtract the presenter for { - poll <- PollFactory.createPoll(stampedPollId, pollType, numRespondents, None, Some(questionText), secretPoll) + poll <- PollFactory.createPoll(stampedPollId, pollType, multiResponse, numRespondents, None, Some(questionText), secretPoll) } yield { lm.polls.save(poll) poll @@ -146,12 +146,12 @@ object Polls { } } - def handleRespondToPollReqMsg(requesterId: String, pollId: String, questionId: Int, answerId: Int, + def handleRespondToPollReqMsg(requesterId: String, pollId: String, questionId: Int, answerIds: Seq[Int], lm: LiveMeeting): Option[(String, SimplePollResultOutVO)] = { for { poll <- getSimplePollResult(pollId, lm.polls) - pvo <- handleRespondToPoll(poll, requesterId, pollId, questionId, answerId, lm) + pvo <- handleRespondToPoll(poll, requesterId, pollId, questionId, answerIds, lm) } yield { (pollId, pvo) } @@ -169,12 +169,12 @@ object Polls { } def handleStartCustomPollReqMsg(state: MeetingState2x, requesterId: String, pollId: String, pollType: String, secretPoll: Boolean, - answers: Seq[String], questionText: String, lm: LiveMeeting): Option[SimplePollOutVO] = { + multiResponse: Boolean, answers: Seq[String], questionText: String, lm: LiveMeeting): Option[SimplePollOutVO] = { def createPoll(stampedPollId: String): Option[Poll] = { val numRespondents: Int = Users2x.numUsers(lm.users2x) - 1 // subtract the presenter for { - poll <- PollFactory.createPoll(stampedPollId, pollType, numRespondents, Some(answers), Some(questionText), secretPoll) + poll <- PollFactory.createPoll(stampedPollId, pollType, multiResponse, numRespondents, Some(answers), Some(questionText), secretPoll) } yield { lm.polls.save(poll) poll @@ -215,7 +215,7 @@ object Polls { // Helper methods: // private def handleRespondToPoll(poll: SimplePollResultOutVO, requesterId: String, pollId: String, questionId: Int, - answerId: Int, lm: LiveMeeting): Option[SimplePollResultOutVO] = { + answerIds: Seq[Int], lm: LiveMeeting): Option[SimplePollResultOutVO] = { /* * Hardcode to zero as we are assuming the poll has only one question. * Our data model supports multiple question polls but for this @@ -225,7 +225,7 @@ object Polls { val questionId = 0 def storePollResult(responder: Responder): Option[SimplePollResultOutVO] = { - respondToQuestion(poll.id, questionId, answerId, responder, lm.polls) + respondToQuestion(poll.id, questionId, answerIds, responder, lm.polls) for { updatedPoll <- getSimplePollResult(poll.id, lm.polls) } yield updatedPoll @@ -415,12 +415,12 @@ object Polls { } } - def respondToQuestion(pollId: String, questionID: Int, responseID: Int, responder: Responder, polls: Polls) { + def respondToQuestion(pollId: String, questionID: Int, responseIDs: Seq[Int], responder: Responder, polls: Polls) { polls.polls.get(pollId) match { case Some(p) => { - if (!p.getResponders().exists(_ == responder)) { + if (!p.getResponders().contains(responder)) { p.addResponder(responder) - p.respondToQuestion(questionID, responseID, responder) + p.respondToQuestion(questionID, responseIDs, responder) } } case None => @@ -452,32 +452,32 @@ object PollFactory { val LetterArray = Array("A", "B", "C", "D", "E", "F") val NumberArray = Array("1", "2", "3", "4", "5", "6") - private def processYesNoPollType(qType: String, text: Option[String]): Question = { + private def processYesNoPollType(qType: String, multiResponse: Boolean, text: Option[String]): Question = { val answers = new ArrayBuffer[Answer]; answers += new Answer(0, "Yes", Some("Yes")) answers += new Answer(1, "No", Some("No")) - new Question(0, PollType.YesNoPollType, false, text, answers) + new Question(0, PollType.YesNoPollType, multiResponse, text, answers) } - private def processYesNoAbstentionPollType(qType: String, text: Option[String]): Question = { + private def processYesNoAbstentionPollType(qType: String, multiResponse: Boolean, text: Option[String]): Question = { val answers = new ArrayBuffer[Answer] answers += new Answer(0, "Yes", Some("Yes")) answers += new Answer(1, "No", Some("No")) answers += new Answer(2, "Abstention", Some("Abstention")) - new Question(0, PollType.YesNoAbstentionPollType, false, text, answers) + new Question(0, PollType.YesNoAbstentionPollType, multiResponse, text, answers) } - private def processTrueFalsePollType(qType: String, text: Option[String]): Question = { + private def processTrueFalsePollType(qType: String, multiResponse: Boolean, text: Option[String]): Question = { val answers = new ArrayBuffer[Answer]; answers += new Answer(0, "True", Some("True")) answers += new Answer(1, "False", Some("False")) - new Question(0, PollType.TrueFalsePollType, false, text, answers) + new Question(0, PollType.TrueFalsePollType, multiResponse, text, answers) } private def processLetterPollType(qType: String, multiResponse: Boolean, text: Option[String]): Option[Question] = { @@ -546,23 +546,23 @@ object PollFactory { questionOption } - private def createQuestion(qType: String, answers: Option[Seq[String]], text: Option[String]): Option[Question] = { + private def createQuestion(qType: String, multiResponse: Boolean, answers: Option[Seq[String]], text: Option[String]): Option[Question] = { val qt = qType.toUpperCase() var questionOption: Option[Question] = None if (qt.matches(PollType.YesNoPollType)) { - questionOption = Some(processYesNoPollType(qt, text)) + questionOption = Some(processYesNoPollType(qt, multiResponse, text)) } else if (qt.matches(PollType.YesNoAbstentionPollType)) { - questionOption = Some(processYesNoAbstentionPollType(qt, text)) + questionOption = Some(processYesNoAbstentionPollType(qt, multiResponse, text)) } else if (qt.matches(PollType.TrueFalsePollType)) { - questionOption = Some(processTrueFalsePollType(qt, text)) + questionOption = Some(processTrueFalsePollType(qt, multiResponse, text)) } else if (qt.matches(PollType.CustomPollType)) { - questionOption = processCustomPollType(qt, false, text, answers) + questionOption = processCustomPollType(qt, multiResponse, text, answers) } else if (qt.startsWith(PollType.LetterPollType)) { - questionOption = processLetterPollType(qt, false, text) + questionOption = processLetterPollType(qt, multiResponse, text) } else if (qt.startsWith(PollType.NumberPollType)) { - questionOption = processNumberPollType(qt, false, text) + questionOption = processNumberPollType(qt, multiResponse, text) } else if (qt.startsWith(PollType.ResponsePollType)) { questionOption = processResponsePollType(qt, text) } @@ -570,10 +570,10 @@ object PollFactory { questionOption } - def createPoll(id: String, pollType: String, numRespondents: Int, answers: Option[Seq[String]], questionText: Option[String], isSecret: Boolean): Option[Poll] = { + def createPoll(id: String, pollType: String, multiResponse: Boolean, numRespondents: Int, answers: Option[Seq[String]], questionText: Option[String], isSecret: Boolean): Option[Poll] = { var poll: Option[Poll] = None - createQuestion(pollType, answers, questionText) match { + createQuestion(pollType, multiResponse, answers, questionText) match { case Some(question) => { poll = Some(new Poll(id, Array(question), numRespondents, None, isSecret)) } @@ -622,10 +622,10 @@ class Poll(val id: String, val questions: Array[Question], val numRespondents: I return false } - def respondToQuestion(questionID: Int, responseID: Int, responder: Responder) { + def respondToQuestion(questionID: Int, responseIDs: Seq[Int], responder: Responder) { questions.foreach(q => { if (q.id == questionID) { - q.respondToQuestion(responseID, responder) + q.respondToQuestion(responseIDs, responder) _numResponders += 1 } }) @@ -649,7 +649,7 @@ class Poll(val id: String, val questions: Array[Question], val numRespondents: I } def toSimplePollOutVO(): SimplePollOutVO = { - new SimplePollOutVO(id, questions(0).toSimpleAnswerOutVO()) + new SimplePollOutVO(id, questions(0).multiResponse, questions(0).toSimpleAnswerOutVO()) } def toSimplePollResultOutVO(): SimplePollResultOutVO = { @@ -674,9 +674,9 @@ class Question(val id: Int, val questionType: String, val multiResponse: Boolean return false } - def respondToQuestion(id: Int, responder: Responder) { + def respondToQuestion(ids: Seq[Int], responder: Responder) { answers.foreach(r => { - if (r.id == id) r.addResponder(responder) + if (ids.contains(r.id)) r.addResponder(responder) }) } diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Meeting2x.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Meeting2x.scala index 0e9bb0f867..a81362697e 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Meeting2x.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Meeting2x.scala @@ -75,7 +75,7 @@ case class MeetingStatus(startEndTimeStatus: StartEndTimeStatus, recordingStatus case class Meeting2x(defaultProps: DefaultProps, meetingStatus: MeetingStatus) case class SimpleAnswerOutVO(id: Int, key: String) -case class SimplePollOutVO(id: String, answers: Array[SimpleAnswerOutVO]) +case class SimplePollOutVO(id: String, isMultipleResponse: Boolean, answers: Array[SimpleAnswerOutVO]) case class SimpleVoteOutVO(id: Int, key: String, numVotes: Int) case class SimplePollResultOutVO(id: String, questionType: String, questionText: Option[String], answers: Array[SimpleVoteOutVO], numRespondents: Int, numResponders: Int) case class Responder(userId: String, name: String) diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PollsMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PollsMsgs.scala index 1a69c05fa0..53a31a7986 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PollsMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PollsMsgs.scala @@ -1,63 +1,63 @@ -package org.bigbluebutton.common2.msgs - -import org.bigbluebutton.common2.domain.{ PollVO, SimplePollOutVO, SimplePollResultOutVO } - -object GetCurrentPollReqMsg { val NAME = "GetCurrentPollReqMsg" } -case class GetCurrentPollReqMsg(header: BbbClientMsgHeader, body: GetCurrentPollReqMsgBody) extends StandardMsg -case class GetCurrentPollReqMsgBody(requesterId: String) - -object GetCurrentPollRespMsg { val NAME = "GetCurrentPollRespMsg" } -case class GetCurrentPollRespMsg(header: BbbClientMsgHeader, body: GetCurrentPollRespMsgBody) extends BbbCoreMsg -case class GetCurrentPollRespMsgBody(userId: String, hasPoll: Boolean, poll: Option[PollVO]) - -object PollShowResultEvtMsg { val NAME = "PollShowResultEvtMsg" } -case class PollShowResultEvtMsg(header: BbbClientMsgHeader, body: PollShowResultEvtMsgBody) extends BbbCoreMsg -case class PollShowResultEvtMsgBody(userId: String, pollId: String, poll: SimplePollResultOutVO) - -object PollStartedEvtMsg { val NAME = "PollStartedEvtMsg" } -case class PollStartedEvtMsg(header: BbbClientMsgHeader, body: PollStartedEvtMsgBody) extends BbbCoreMsg -case class PollStartedEvtMsgBody(userId: String, pollId: String, pollType: String, secretPoll: Boolean, question: String, poll: SimplePollOutVO) - -object PollStoppedEvtMsg { val NAME = "PollStoppedEvtMsg" } -case class PollStoppedEvtMsg(header: BbbClientMsgHeader, body: PollStoppedEvtMsgBody) extends BbbCoreMsg -case class PollStoppedEvtMsgBody(userId: String, pollId: String) - -object PollUpdatedEvtMsg { val NAME = "PollUpdatedEvtMsg" } -case class PollUpdatedEvtMsg(header: BbbClientMsgHeader, body: PollUpdatedEvtMsgBody) extends BbbCoreMsg -case class PollUpdatedEvtMsgBody(pollId: String, poll: SimplePollResultOutVO) - -object UserRespondedToPollRecordMsg { val NAME = "UserRespondedToPollRecordMsg" } -case class UserRespondedToPollRecordMsg(header: BbbClientMsgHeader, body: UserRespondedToPollRecordMsgBody) extends BbbCoreMsg -case class UserRespondedToPollRecordMsgBody(pollId: String, answerId: Int, answer: String, isSecret: Boolean) - -object RespondToPollReqMsg { val NAME = "RespondToPollReqMsg" } -case class RespondToPollReqMsg(header: BbbClientMsgHeader, body: RespondToPollReqMsgBody) extends StandardMsg -case class RespondToPollReqMsgBody(requesterId: String, pollId: String, questionId: Int, answerId: Int) - -object RespondToTypedPollReqMsg { val NAME = "RespondToTypedPollReqMsg" } -case class RespondToTypedPollReqMsg(header: BbbClientMsgHeader, body: RespondToTypedPollReqMsgBody) extends StandardMsg -case class RespondToTypedPollReqMsgBody(requesterId: String, pollId: String, questionId: Int, answer: String) - -object UserRespondedToPollRespMsg { val NAME = "UserRespondedToPollRespMsg" } -case class UserRespondedToPollRespMsg(header: BbbClientMsgHeader, body: UserRespondedToPollRespMsgBody) extends BbbCoreMsg -case class UserRespondedToPollRespMsgBody(pollId: String, userId: String, answerId: Int) - -object UserRespondedToTypedPollRespMsg { val NAME = "UserRespondedToTypedPollRespMsg" } -case class UserRespondedToTypedPollRespMsg(header: BbbClientMsgHeader, body: UserRespondedToTypedPollRespMsgBody) extends BbbCoreMsg -case class UserRespondedToTypedPollRespMsgBody(pollId: String, userId: String, answer: String) - -object ShowPollResultReqMsg { val NAME = "ShowPollResultReqMsg" } -case class ShowPollResultReqMsg(header: BbbClientMsgHeader, body: ShowPollResultReqMsgBody) extends StandardMsg -case class ShowPollResultReqMsgBody(requesterId: String, pollId: String) - -object StartCustomPollReqMsg { val NAME = "StartCustomPollReqMsg" } -case class StartCustomPollReqMsg(header: BbbClientMsgHeader, body: StartCustomPollReqMsgBody) extends StandardMsg -case class StartCustomPollReqMsgBody(requesterId: String, pollId: String, pollType: String, secretPoll: Boolean, answers: Seq[String], question: String) - -object StartPollReqMsg { val NAME = "StartPollReqMsg" } -case class StartPollReqMsg(header: BbbClientMsgHeader, body: StartPollReqMsgBody) extends StandardMsg -case class StartPollReqMsgBody(requesterId: String, pollId: String, pollType: String, secretPoll: Boolean, question: String) - -object StopPollReqMsg { val NAME = "StopPollReqMsg" } -case class StopPollReqMsg(header: BbbClientMsgHeader, body: StopPollReqMsgBody) extends StandardMsg -case class StopPollReqMsgBody(requesterId: String) +package org.bigbluebutton.common2.msgs + +import org.bigbluebutton.common2.domain.{ PollVO, SimplePollOutVO, SimplePollResultOutVO } + +object GetCurrentPollReqMsg { val NAME = "GetCurrentPollReqMsg" } +case class GetCurrentPollReqMsg(header: BbbClientMsgHeader, body: GetCurrentPollReqMsgBody) extends StandardMsg +case class GetCurrentPollReqMsgBody(requesterId: String) + +object GetCurrentPollRespMsg { val NAME = "GetCurrentPollRespMsg" } +case class GetCurrentPollRespMsg(header: BbbClientMsgHeader, body: GetCurrentPollRespMsgBody) extends BbbCoreMsg +case class GetCurrentPollRespMsgBody(userId: String, hasPoll: Boolean, poll: Option[PollVO]) + +object PollShowResultEvtMsg { val NAME = "PollShowResultEvtMsg" } +case class PollShowResultEvtMsg(header: BbbClientMsgHeader, body: PollShowResultEvtMsgBody) extends BbbCoreMsg +case class PollShowResultEvtMsgBody(userId: String, pollId: String, poll: SimplePollResultOutVO) + +object PollStartedEvtMsg { val NAME = "PollStartedEvtMsg" } +case class PollStartedEvtMsg(header: BbbClientMsgHeader, body: PollStartedEvtMsgBody) extends BbbCoreMsg +case class PollStartedEvtMsgBody(userId: String, pollId: String, pollType: String, secretPoll: Boolean, question: String, poll: SimplePollOutVO) + +object PollStoppedEvtMsg { val NAME = "PollStoppedEvtMsg" } +case class PollStoppedEvtMsg(header: BbbClientMsgHeader, body: PollStoppedEvtMsgBody) extends BbbCoreMsg +case class PollStoppedEvtMsgBody(userId: String, pollId: String) + +object PollUpdatedEvtMsg { val NAME = "PollUpdatedEvtMsg" } +case class PollUpdatedEvtMsg(header: BbbClientMsgHeader, body: PollUpdatedEvtMsgBody) extends BbbCoreMsg +case class PollUpdatedEvtMsgBody(pollId: String, poll: SimplePollResultOutVO) + +object UserRespondedToPollRecordMsg { val NAME = "UserRespondedToPollRecordMsg" } +case class UserRespondedToPollRecordMsg(header: BbbClientMsgHeader, body: UserRespondedToPollRecordMsgBody) extends BbbCoreMsg +case class UserRespondedToPollRecordMsgBody(pollId: String, answerId: Int, answer: String, isSecret: Boolean) + +object RespondToPollReqMsg { val NAME = "RespondToPollReqMsg" } +case class RespondToPollReqMsg(header: BbbClientMsgHeader, body: RespondToPollReqMsgBody) extends StandardMsg +case class RespondToPollReqMsgBody(requesterId: String, pollId: String, questionId: Int, answerIds: Seq[Int]) + +object RespondToTypedPollReqMsg { val NAME = "RespondToTypedPollReqMsg" } +case class RespondToTypedPollReqMsg(header: BbbClientMsgHeader, body: RespondToTypedPollReqMsgBody) extends StandardMsg +case class RespondToTypedPollReqMsgBody(requesterId: String, pollId: String, questionId: Int, answer: String) + +object UserRespondedToPollRespMsg { val NAME = "UserRespondedToPollRespMsg" } +case class UserRespondedToPollRespMsg(header: BbbClientMsgHeader, body: UserRespondedToPollRespMsgBody) extends BbbCoreMsg +case class UserRespondedToPollRespMsgBody(pollId: String, userId: String, answerIds: Seq[Int]) + +object UserRespondedToTypedPollRespMsg { val NAME = "UserRespondedToTypedPollRespMsg" } +case class UserRespondedToTypedPollRespMsg(header: BbbClientMsgHeader, body: UserRespondedToTypedPollRespMsgBody) extends BbbCoreMsg +case class UserRespondedToTypedPollRespMsgBody(pollId: String, userId: String, answer: String) + +object ShowPollResultReqMsg { val NAME = "ShowPollResultReqMsg" } +case class ShowPollResultReqMsg(header: BbbClientMsgHeader, body: ShowPollResultReqMsgBody) extends StandardMsg +case class ShowPollResultReqMsgBody(requesterId: String, pollId: String) + +object StartCustomPollReqMsg { val NAME = "StartCustomPollReqMsg" } +case class StartCustomPollReqMsg(header: BbbClientMsgHeader, body: StartCustomPollReqMsgBody) extends StandardMsg +case class StartCustomPollReqMsgBody(requesterId: String, pollId: String, pollType: String, secretPoll: Boolean, isMultipleResponse: Boolean, answers: Seq[String], question: String) + +object StartPollReqMsg { val NAME = "StartPollReqMsg" } +case class StartPollReqMsg(header: BbbClientMsgHeader, body: StartPollReqMsgBody) extends StandardMsg +case class StartPollReqMsgBody(requesterId: String, pollId: String, pollType: String, secretPoll: Boolean, question: String, isMultipleResponse: Boolean) + +object StopPollReqMsg { val NAME = "StopPollReqMsg" } +case class StopPollReqMsg(header: BbbClientMsgHeader, body: StopPollReqMsgBody) extends StandardMsg +case class StopPollReqMsgBody(requesterId: String) diff --git a/bigbluebutton-html5/imports/api/polls/server/handlers/userResponded.js b/bigbluebutton-html5/imports/api/polls/server/handlers/userResponded.js index 6c1b835ec0..7973bf73ed 100644 --- a/bigbluebutton-html5/imports/api/polls/server/handlers/userResponded.js +++ b/bigbluebutton-html5/imports/api/polls/server/handlers/userResponded.js @@ -3,11 +3,11 @@ import Polls from '/imports/api/polls'; import Logger from '/imports/startup/server/logger'; export default function userResponded({ body }) { - const { pollId, userId, answerId } = body; + const { pollId, userId, answerIds } = body; check(pollId, String); check(userId, String); - check(answerId, Number); + check(answerIds, Array); const selector = { id: pollId, @@ -18,7 +18,7 @@ export default function userResponded({ body }) { users: userId, }, $push: { - responses: { userId, answerId }, + responses: { userId, answerIds }, }, }; @@ -26,7 +26,7 @@ export default function userResponded({ body }) { const numberAffected = Polls.update(selector, modifier); if (numberAffected) { - Logger.info(`Updating Poll response (userId: ${userId}, response: ${answerId}, pollId: ${pollId})`); + Logger.info(`Updating Poll response (userId: ${userId}, response: ${JSON.stringify(answerIds)}, pollId: ${pollId})`); } } catch (err) { Logger.error(`Updating Poll responses: ${err}`); diff --git a/bigbluebutton-html5/imports/api/polls/server/handlers/userTypedResponse.js b/bigbluebutton-html5/imports/api/polls/server/handlers/userTypedResponse.js index eceb10dfbf..7e904f00f2 100644 --- a/bigbluebutton-html5/imports/api/polls/server/handlers/userTypedResponse.js +++ b/bigbluebutton-html5/imports/api/polls/server/handlers/userTypedResponse.js @@ -27,7 +27,7 @@ export default function userTypedResponse({ header, body }) { requesterId: userId, pollId, questionId: 0, - answerId, + answerIds: [answerId], }; return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, userId, payload); diff --git a/bigbluebutton-html5/imports/api/polls/server/methods/publishTypedVote.js b/bigbluebutton-html5/imports/api/polls/server/methods/publishTypedVote.js index 1d195b5729..bb7b7dd805 100644 --- a/bigbluebutton-html5/imports/api/polls/server/methods/publishTypedVote.js +++ b/bigbluebutton-html5/imports/api/polls/server/methods/publishTypedVote.js @@ -28,7 +28,7 @@ export default function publishTypedVote(id, pollAnswer) { activePoll.answers.forEach((a) => { if (a.key === pollAnswer) existingAnsId = a.id; }); - + if (existingAnsId !== null) { check(existingAnsId, Number); EVENT_NAME = 'RespondToPollReqMsg'; @@ -42,11 +42,11 @@ export default function publishTypedVote(id, pollAnswer) { requesterId: requesterUserId, pollId: id, questionId: 0, - answerId: existingAnsId, + answerIds: [existingAnsId], }, ); } - + const payload = { requesterId: requesterUserId, pollId: id, diff --git a/bigbluebutton-html5/imports/api/polls/server/methods/publishVote.js b/bigbluebutton-html5/imports/api/polls/server/methods/publishVote.js index 51dd6f29cc..0c7b31cb1e 100644 --- a/bigbluebutton-html5/imports/api/polls/server/methods/publishVote.js +++ b/bigbluebutton-html5/imports/api/polls/server/methods/publishVote.js @@ -4,7 +4,7 @@ import Polls from '/imports/api/polls'; import Logger from '/imports/startup/server/logger'; import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function publishVote(pollId, pollAnswerId) { +export default function publishVote(pollId, pollAnswerIds) { try { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; @@ -13,7 +13,7 @@ export default function publishVote(pollId, pollAnswerId) { check(meetingId, String); check(requesterUserId, String); - check(pollAnswerId, Number); + check(pollAnswerIds, Array); check(pollId, String); const allowedToVote = Polls.findOne({ @@ -34,14 +34,14 @@ export default function publishVote(pollId, pollAnswerId) { const selector = { users: requesterUserId, meetingId, - 'answers.id': pollAnswerId, + 'answers.id': { $all: pollAnswerIds }, }; const payload = { requesterId: requesterUserId, pollId, questionId: 0, - answerId: pollAnswerId, + answerIds: pollAnswerIds, }; /* diff --git a/bigbluebutton-html5/imports/api/polls/server/methods/startPoll.js b/bigbluebutton-html5/imports/api/polls/server/methods/startPoll.js index 4c35ee2056..6aacb99784 100644 --- a/bigbluebutton-html5/imports/api/polls/server/methods/startPoll.js +++ b/bigbluebutton-html5/imports/api/polls/server/methods/startPoll.js @@ -3,7 +3,7 @@ import { check } from 'meteor/check'; import { extractCredentials } from '/imports/api/common/server/helpers'; import Logger from '/imports/startup/server/logger'; -export default function startPoll(pollTypes, pollType, pollId, secretPoll, question, answers) { +export default function startPoll(pollTypes, pollType, pollId, secretPoll, question, isMultipleResponse, answers) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; let EVENT_NAME = 'StartPollReqMsg'; @@ -23,6 +23,7 @@ export default function startPoll(pollTypes, pollType, pollId, secretPoll, quest pollType, secretPoll, question, + isMultipleResponse, }; if (pollType === pollTypes.Custom) { diff --git a/bigbluebutton-html5/imports/api/polls/server/modifiers/addPoll.js b/bigbluebutton-html5/imports/api/polls/server/modifiers/addPoll.js index 3a498c86ea..e1e424b906 100644 --- a/bigbluebutton-html5/imports/api/polls/server/modifiers/addPoll.js +++ b/bigbluebutton-html5/imports/api/polls/server/modifiers/addPoll.js @@ -15,6 +15,7 @@ export default function addPoll(meetingId, requesterId, poll, pollType, secretPo key: String, }, ], + isMultipleResponse: Boolean, }); const userSelector = { diff --git a/bigbluebutton-html5/imports/ui/components/poll/component.jsx b/bigbluebutton-html5/imports/ui/components/poll/component.jsx index 457e881837..cba8686564 100644 --- a/bigbluebutton-html5/imports/ui/components/poll/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/poll/component.jsx @@ -6,6 +6,7 @@ import _ from 'lodash'; import { Session } from 'meteor/session'; import cx from 'classnames'; import Button from '/imports/ui/components/button/component'; +import Checkbox from '/imports/ui/components/checkbox/component'; import Toggle from '/imports/ui/components/switch/component'; import LiveResult from './live-result/component'; import { styles } from './styles.scss'; @@ -154,6 +155,10 @@ const intlMessages = defineMessages({ id: 'app.poll.abstention', description: '', }, + enableMultipleResponseLabel: { + id: 'app.poll.enableMultipleResponseLabel', + description: 'label for checkbox to enable multiple choice', + }, startPollDesc: { id: 'app.poll.startPollDesc', description: '', @@ -210,6 +215,7 @@ class Poll extends Component { question: '', optList: [], error: null, + isMultipleResponse: false, secretPoll: false, }; @@ -218,6 +224,7 @@ class Poll extends Component { this.handleRemoveOption = this.handleRemoveOption.bind(this); this.handleTextareaChange = this.handleTextareaChange.bind(this); this.handleInputChange = this.handleInputChange.bind(this); + this.toggleIsMultipleResponse = this.toggleIsMultipleResponse.bind(this); this.displayToggleStatus = this.displayToggleStatus.bind(this); } @@ -281,6 +288,11 @@ class Poll extends Component { this.setState({ optList: list, error: clearError ? null : error }); } + toggleIsMultipleResponse() { + const { isMultipleResponse } = this.state; + return this.setState({ isMultipleResponse: !isMultipleResponse }); + } + handleTextareaChange(e) { const { type, error } = this.state; const { pollTypes } = this.props; @@ -452,7 +464,7 @@ class Poll extends Component { renderPollOptions() { const { - type, secretPoll, optList, question, error, + type, secretPoll, optList, question, error, isMultipleResponse } = this.state; const { startPoll, @@ -582,18 +594,32 @@ class Poll extends Component { ) } - { - (defaultPoll || type === pollTypes.Response) - && ( -
- {defaultPoll && this.renderInputs()} - {defaultPoll - && ( + { + (defaultPoll || type === pollTypes.Response) + && ( +
+ {defaultPoll + && ( +
+ + +
+ )} + {defaultPoll && this.renderInputs()} + {defaultPoll + && (
+ ); + })} +
+ + ) + } + { + poll.pollType === pollTypes.Response + && ( +
+ { + this.handleUpdateResponseInput(e); + }} + onKeyDown={(e) => { + this.handleMessageKeyDown(e); + }} + type="text" + className={styles.typedResponseInput} + placeholder={intl.formatMessage(intlMessages.responsePlaceholder)} + maxLength={MAX_INPUT_CHARS} + ref={(r) => { this.responseInput = r; }} + /> +
+ ) + } +
+ {intl.formatMessage(poll.secretPoll ? intlMessages.responseIsSecret : intlMessages.responseNotSecret)} +
+ + ); + } + + renderCheckboxAnswers() { + const { + isMeteorConnected, + intl, + poll, + pollAnswerIds, + } = this.props; + const { checkedAnswers } = this.state; + const { question } = poll; + return ( +
+ {question.length === 0 + && ( +
+ {intl.formatMessage(intlMessages.pollingTitleLabel)} +
+ )} + + {poll.answers.map((pollAnswer) => { + const formattedMessageIndex = pollAnswer.key.toLowerCase(); + let label = pollAnswer.key; + if (pollAnswerIds[formattedMessageIndex]) { + label = intl.formatMessage(pollAnswerIds[formattedMessageIndex]); + } + + return ( + + + + + ); + })} +
+ this.handleCheckboxChange(poll.pollId, pollAnswer.id)} + checked={checkedAnswers.includes(pollAnswer.id)} + className={styles.checkbox} + ariaLabelledBy={`pollAnswerLabel${pollAnswer.key}`} + ariaDescribedBy={`pollAnswerDesc${pollAnswer.key}`} + /> + + +
+ {intl.formatMessage(intlMessages.pollAnswerDesc, { 0: label })} +
+
+
+
+
+ ); + } + + render() { + const { + intl, + poll, + } = this.props; + + if (!poll) return null; + + const { stackOptions, answers, question } = poll; const pollAnswerStyles = { [styles.pollingAnswers]: true, [styles.removeColumns]: answers.length === 1, @@ -137,96 +336,7 @@ class Polling extends Component { ) } - { - poll.pollType !== pollTypes.Response && ( - - { - question.length === 0 && ( -
- {intl.formatMessage(intlMessages.pollingTitleLabel)} -
- ) - } -
- {poll.answers.map((pollAnswer) => { - const formattedMessageIndex = pollAnswer.key.toLowerCase(); - let label = pollAnswer.key; - if (defaultPoll && pollAnswerIds[formattedMessageIndex]) { - label = intl.formatMessage(pollAnswerIds[formattedMessageIndex]); - } - - return ( -
-
- ); - })} -
-
- ) - } - { - poll.pollType === pollTypes.Response - && ( -
- { - this.handleUpdateResponseInput(e); - }} - onKeyDown={(e) => { - this.handleMessageKeyDown(e); - }} - type="text" - className={styles.typedResponseInput} - placeholder={intl.formatMessage(intlMessages.responsePlaceholder)} - maxLength={MAX_INPUT_CHARS} - ref={(r) => { this.responseInput = r; }} - /> -
- ) - } -
- {intl.formatMessage(poll.secretPoll ? intlMessages.responseIsSecret : intlMessages.responseNotSecret)} -
+ {poll.isMultipleResponse ? this.renderCheckboxAnswers(pollAnswerStyles) : this.renderButtonAnswers(pollAnswerStyles)} ); diff --git a/bigbluebutton-html5/imports/ui/components/polling/service.js b/bigbluebutton-html5/imports/ui/components/polling/service.js index bfe2bbe64c..5c5f7ab344 100644 --- a/bigbluebutton-html5/imports/ui/components/polling/service.js +++ b/bigbluebutton-html5/imports/ui/components/polling/service.js @@ -4,8 +4,8 @@ import { debounce } from 'lodash'; const MAX_CHAR_LENGTH = 5; -const handleVote = (pollId, answerId) => { - makeCall('publishVote', pollId, answerId.id); +const handleVote = (pollId, answerIds) => { + makeCall('publishVote', pollId, answerIds); }; const handleTypedVote = (pollId, answer) => { @@ -35,6 +35,7 @@ const mapPolls = () => { poll: { answers: poll.answers, pollId: poll.id, + isMultipleResponse: poll.isMultipleResponse, pollType: poll.pollType, stackOptions, question: poll.question, diff --git a/bigbluebutton-html5/imports/ui/components/polling/styles.scss b/bigbluebutton-html5/imports/ui/components/polling/styles.scss index 9e43d59105..b79791e6bb 100644 --- a/bigbluebutton-html5/imports/ui/components/polling/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/polling/styles.scss @@ -121,9 +121,18 @@ display: none; } +.checkbox { + display: inline-block; + margin-right: var(--poll-sm-margin); +} + +.checkboxContainer { + margin-bottom: var(--poll-sm-margin); +} + .qHeader { text-align: left; - position: relative; + position: relative; left: var(--sm-padding-y); } @@ -162,6 +171,14 @@ margin-bottom: 1rem; } +.multipleResponseAnswersTable { + margin-left: auto; + margin-right: auto; +} + +.multipleResponseAnswersTableAnswerText { + text-align: left; +} .pollingSecret { font-size: var(--font-size-small); max-width: var(--poll-width); diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx index 923b722150..8142766f6c 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx @@ -40,7 +40,7 @@ export default withTracker((params) => { Session.set('forcePollOpen', true); window.dispatchEvent(new Event('panelChanged')); - makeCall('startPoll', PollService.pollTypes, type, id, false, '', answers); + makeCall('startPoll', PollService.pollTypes, type, id, false, '', false, answers); }; return { diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/annotations/poll/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/annotations/poll/component.jsx index c7e70008b3..98d6bba592 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/annotations/poll/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/annotations/poll/component.jsx @@ -211,9 +211,8 @@ class PollDrawComponent extends Component { // calculating only the parts which have to be done just once and don't require // rendering / rerendering the text objects - // if (!state.initialState) return; const { annotation } = this.props; - const { points, result, pollType } = annotation; + const { points, result, numResponders, pollType } = annotation; const { slideWidth, slideHeight, intl } = this.props; // group duplicated responses and keep track of the number of removed items @@ -277,11 +276,11 @@ class PollDrawComponent extends Component { } } _tempArray.push(_result.key, `${_result.numVotes}`); - if (votesTotal === 0) { + if (numResponders === 0) { _tempArray.push('0%'); _tempArray.push(i); } else { - const percResult = (_result.numVotes / votesTotal) * 100; + const percResult = (_result.numVotes / numResponders) * 100; _tempArray.push(`${Math.round(percResult)}%`); _tempArray.push(i); } @@ -475,7 +474,7 @@ class PollDrawComponent extends Component { fill={backgroundColor} strokeWidth={thickness} /> - {extendedTextArray.map(line => ( + {extendedTextArray.map((line) => ( ))} - {extendedTextArray.map(line => ( + {extendedTextArray.map((line) => ( - {extendedTextArray.map(line => ( + {extendedTextArray.map((line) => ( - {extendedTextArray.map(line => ( + {extendedTextArray.map((line) => ( - {textArray.map(line => this.renderLine(line))} + {textArray.map((line) => this.renderLine(line))} { + textArray.forEach((t, idx) => { const pollLine = t.slice(0, -1); ariaResultLabel += `${idx > 0 ? ' |' : ''} ${pollLine.join(' | ')}`; }); @@ -632,8 +631,7 @@ class PollDrawComponent extends Component { {prepareToDisplay ? this.renderTestStrings() - : this.renderPoll() - } + : this.renderPoll()} ); } diff --git a/bigbluebutton-html5/public/locales/de.json b/bigbluebutton-html5/public/locales/de.json index 8382a8ad67..d902df40ff 100644 --- a/bigbluebutton-html5/public/locales/de.json +++ b/bigbluebutton-html5/public/locales/de.json @@ -235,6 +235,7 @@ "app.presentationUploder.clearErrorsDesc": "Löscht fehlgeschlagene Präsentationsuploads", "app.presentationUploder.uploadViewTitle": "Präsentation hochladen", "app.poll.pollPaneTitle": "Umfrage", + "app.poll.enableMultipleResponseLabel": "Mehrere Antworten pro Teilnehmer zulassen?", "app.poll.quickPollTitle": "Schnellumfrage", "app.poll.hidePollDesc": "Versteckt das Umfragemenü", "app.poll.quickPollInstruction": "Wählen Sie eine der unten stehenden Optionen, um die Umfrage zu starten.", diff --git a/bigbluebutton-html5/public/locales/en.json b/bigbluebutton-html5/public/locales/en.json index 0dfa1cb16b..bc2be614a4 100755 --- a/bigbluebutton-html5/public/locales/en.json +++ b/bigbluebutton-html5/public/locales/en.json @@ -235,6 +235,7 @@ "app.presentationUploder.clearErrorsDesc": "Clears failed presentation uploads", "app.presentationUploder.uploadViewTitle": "Upload Presentation", "app.poll.pollPaneTitle": "Polling", + "app.poll.enableMultipleResponseLabel": "Allow multiple answers per respondent?", "app.poll.quickPollTitle": "Quick Poll", "app.poll.hidePollDesc": "Hides the poll menu pane", "app.poll.quickPollInstruction": "Select an option below to start your poll.",