Merge pull request #13253 from ramonlsouza/PR-11359
feat: multiple choice poll
This commit is contained in:
commit
c60ef3c179
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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}`);
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
};
|
||||
|
||||
/*
|
||||
|
@ -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) {
|
||||
|
@ -15,6 +15,7 @@ export default function addPoll(meetingId, requesterId, poll, pollType, secretPo
|
||||
key: String,
|
||||
},
|
||||
],
|
||||
isMultipleResponse: Boolean,
|
||||
});
|
||||
|
||||
const userSelector = {
|
||||
|
@ -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 {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
(defaultPoll || type === pollTypes.Response)
|
||||
&& (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexFlow: 'wrap',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
{defaultPoll && this.renderInputs()}
|
||||
{defaultPoll
|
||||
&& (
|
||||
{
|
||||
(defaultPoll || type === pollTypes.Response)
|
||||
&& (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexFlow: 'wrap',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
{defaultPoll
|
||||
&& (
|
||||
<div>
|
||||
<Checkbox
|
||||
onChange={this.toggleIsMultipleResponse}
|
||||
checked={isMultipleResponse}
|
||||
className={styles.checkbox}
|
||||
ariaLabelledBy="multipleResponseCheckboxLabel"
|
||||
/>
|
||||
<label id="multipleResponseCheckboxLabel" className={styles.instructions}>
|
||||
{intl.formatMessage(intlMessages.enableMultipleResponseLabel)}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
{defaultPoll && this.renderInputs()}
|
||||
{defaultPoll
|
||||
&& (
|
||||
<Button
|
||||
className={styles.addItemBtn}
|
||||
data-test="addItem"
|
||||
@ -670,10 +696,11 @@ class Poll extends Component {
|
||||
verifiedPollType,
|
||||
secretPoll,
|
||||
question,
|
||||
isMultipleResponse,
|
||||
_.compact(verifiedOptions),
|
||||
);
|
||||
} else {
|
||||
startPoll(verifiedPollType, secretPoll, question);
|
||||
startPoll(verifiedPollType, secretPoll, question, isMultipleResponse);
|
||||
}
|
||||
});
|
||||
}}
|
||||
|
@ -42,9 +42,9 @@ export default withTracker(() => {
|
||||
|
||||
const { pollTypes } = Service;
|
||||
|
||||
const startPoll = (type, secretPoll, question = '') => makeCall('startPoll', pollTypes, type, pollId, secretPoll, question);
|
||||
const startPoll = (type, secretPoll, question = '', isMultipleResponse) => makeCall('startPoll', pollTypes, type, pollId, secretPoll, question, isMultipleResponse);
|
||||
|
||||
const startCustomPoll = (type, secretPoll, question = '', answers) => makeCall('startPoll', pollTypes, type, pollId, secretPoll, question, answers);
|
||||
const startCustomPoll = (type, secretPoll, question = '', isMultipleResponse, answers) => makeCall('startPoll', pollTypes, type, pollId, secretPoll, question, isMultipleResponse, answers);
|
||||
|
||||
const stopPoll = () => makeCall('stopPoll');
|
||||
|
||||
|
@ -73,7 +73,13 @@ class LiveResult extends PureComponent {
|
||||
|
||||
if (responses) {
|
||||
const response = responses.find(r => r.userId === user.userId);
|
||||
if (response) answer = answers[response.answerId].key;
|
||||
if (response) {
|
||||
const answerKeys = [];
|
||||
response.answerIds.forEach((answerId) => {
|
||||
answerKeys.push(answers[answerId].key);
|
||||
});
|
||||
answer = answerKeys.join(', ');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -4,6 +4,7 @@
|
||||
--poll-stats-element-width: 17%;
|
||||
|
||||
--poll-sm-margin: 0.3125rem;
|
||||
--poll-md-margin: 0.7rem;
|
||||
--poll-user-line-height: 1.75rem;
|
||||
--poll-result-width: 15rem;
|
||||
}
|
||||
|
@ -58,7 +58,7 @@
|
||||
.customInputWrapper {
|
||||
width: 100%;
|
||||
margin-top: var(--md-padding-y);
|
||||
|
||||
|
||||
> Button {
|
||||
width: 100%;
|
||||
margin-top: var(--sm-padding-y);
|
||||
@ -177,7 +177,7 @@
|
||||
|
||||
.input {
|
||||
@include inputFocus(var(--color-blue-light));
|
||||
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
@extend %highContrastOutline;
|
||||
@ -186,7 +186,7 @@
|
||||
&:focus {
|
||||
outline-style: solid !important;
|
||||
}
|
||||
|
||||
|
||||
margin: 0;
|
||||
color: var(--color-text);
|
||||
background: var(--color-white);
|
||||
@ -200,6 +200,13 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
display: inline-block;
|
||||
margin-right: var(--poll-sm-margin);
|
||||
margin-bottom: var(--poll-md-margin);
|
||||
}
|
||||
|
||||
|
||||
.responseType {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@ -279,7 +286,7 @@
|
||||
background-color: var(--color-gray-lightest);
|
||||
height: var(--lg-padding-y);
|
||||
border-radius: var(--border-radius);
|
||||
width: 75%;
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
.addItemBtn {
|
||||
|
@ -7,6 +7,7 @@ import cx from 'classnames';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { styles } from './styles.scss';
|
||||
import AudioService from '/imports/ui/components/audio/service';
|
||||
import Checkbox from '/imports/ui/components/checkbox/component';
|
||||
|
||||
const MAX_INPUT_CHARS = Meteor.settings.public.poll.maxTypedAnswerLength;
|
||||
|
||||
@ -52,10 +53,15 @@ class Polling extends Component {
|
||||
|
||||
this.state = {
|
||||
typedAns: '',
|
||||
checkedAnswers: [],
|
||||
};
|
||||
|
||||
this.play = this.play.bind(this);
|
||||
this.handleUpdateResponseInput = this.handleUpdateResponseInput.bind(this);
|
||||
this.renderButtonAnswers = this.renderButtonAnswers.bind(this);
|
||||
this.handleCheckboxChange = this.handleCheckboxChange.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.renderCheckboxAnswers = this.renderCheckboxAnswers.bind(this);
|
||||
this.handleMessageKeyDown = this.handleMessageKeyDown.bind(this);
|
||||
}
|
||||
|
||||
@ -75,6 +81,23 @@ class Polling extends Component {
|
||||
this.setState({ typedAns: this.responseInput.value });
|
||||
}
|
||||
|
||||
handleSubmit(pollId) {
|
||||
const { handleVote } = this.props;
|
||||
const { checkedAnswers } = this.state;
|
||||
handleVote(pollId, checkedAnswers);
|
||||
}
|
||||
|
||||
handleCheckboxChange(pollId, answerId) {
|
||||
const { checkedAnswers } = this.state;
|
||||
if (checkedAnswers.includes(answerId)) {
|
||||
checkedAnswers.splice(checkedAnswers.indexOf(answerId), 1);
|
||||
} else {
|
||||
checkedAnswers.push(answerId);
|
||||
}
|
||||
checkedAnswers.sort();
|
||||
this.setState({ checkedAnswers });
|
||||
}
|
||||
|
||||
handleMessageKeyDown(e) {
|
||||
const {
|
||||
poll,
|
||||
@ -90,7 +113,7 @@ class Polling extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
renderButtonAnswers(pollAnswerStyles) {
|
||||
const {
|
||||
isMeteorConnected,
|
||||
intl,
|
||||
@ -108,9 +131,185 @@ class Polling extends Component {
|
||||
|
||||
if (!poll) return null;
|
||||
|
||||
const { stackOptions, answers, question, pollType } = poll;
|
||||
const { answers, question, pollType } = poll;
|
||||
const defaultPoll = isDefaultPoll(pollType);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
poll.pollType !== pollTypes.Response && (
|
||||
<span>
|
||||
{
|
||||
question.length === 0 && (
|
||||
<div className={styles.pollingTitle}>
|
||||
{intl.formatMessage(intlMessages.pollingTitleLabel)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className={cx(pollAnswerStyles)}>
|
||||
{answers.map((pollAnswer) => {
|
||||
const formattedMessageIndex = pollAnswer.key.toLowerCase();
|
||||
let label = pollAnswer.key;
|
||||
if (defaultPoll && pollAnswerIds[formattedMessageIndex]) {
|
||||
label = intl.formatMessage(pollAnswerIds[formattedMessageIndex]);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={pollAnswer.id}
|
||||
className={styles.pollButtonWrapper}
|
||||
>
|
||||
<Button
|
||||
disabled={!isMeteorConnected}
|
||||
className={styles.pollingButton}
|
||||
color="primary"
|
||||
size="md"
|
||||
label={label}
|
||||
key={pollAnswer.key}
|
||||
onClick={() => handleVote(poll.pollId, [pollAnswer.id])}
|
||||
aria-labelledby={`pollAnswerLabel${pollAnswer.key}`}
|
||||
aria-describedby={`pollAnswerDesc${pollAnswer.key}`}
|
||||
data-test="pollAnswerOption"
|
||||
/>
|
||||
<div
|
||||
className={styles.hidden}
|
||||
id={`pollAnswerLabel${pollAnswer.key}`}
|
||||
>
|
||||
{intl.formatMessage(intlMessages.pollAnswerLabel, { 0: label })}
|
||||
</div>
|
||||
<div
|
||||
className={styles.hidden}
|
||||
id={`pollAnswerDesc${pollAnswer.key}`}
|
||||
>
|
||||
{intl.formatMessage(intlMessages.pollAnswerDesc, { 0: label })}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
{
|
||||
poll.pollType === pollTypes.Response
|
||||
&& (
|
||||
<div className={styles.typedResponseWrapper}>
|
||||
<input
|
||||
data-test="pollAnswerOption"
|
||||
onChange={(e) => {
|
||||
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; }}
|
||||
/>
|
||||
<Button
|
||||
data-test="submitAnswer"
|
||||
className={styles.submitVoteBtn}
|
||||
disabled={typedAns.length === 0}
|
||||
color="primary"
|
||||
size="sm"
|
||||
label={intl.formatMessage(intlMessages.submitLabel)}
|
||||
aria-label={intl.formatMessage(intlMessages.submitAriaLabel)}
|
||||
onClick={() => {
|
||||
handleTypedVote(poll.pollId, typedAns);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className={styles.pollingSecret}>
|
||||
{intl.formatMessage(poll.secretPoll ? intlMessages.responseIsSecret : intlMessages.responseNotSecret)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderCheckboxAnswers() {
|
||||
const {
|
||||
isMeteorConnected,
|
||||
intl,
|
||||
poll,
|
||||
pollAnswerIds,
|
||||
} = this.props;
|
||||
const { checkedAnswers } = this.state;
|
||||
const { question } = poll;
|
||||
return (
|
||||
<div>
|
||||
{question.length === 0
|
||||
&& (
|
||||
<div className={styles.pollingTitle}>
|
||||
{intl.formatMessage(intlMessages.pollingTitleLabel)}
|
||||
</div>
|
||||
)}
|
||||
<table className={styles.multipleResponseAnswersTable}>
|
||||
{poll.answers.map((pollAnswer) => {
|
||||
const formattedMessageIndex = pollAnswer.key.toLowerCase();
|
||||
let label = pollAnswer.key;
|
||||
if (pollAnswerIds[formattedMessageIndex]) {
|
||||
label = intl.formatMessage(pollAnswerIds[formattedMessageIndex]);
|
||||
}
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={pollAnswer.id}
|
||||
className={styles.checkboxContainer}
|
||||
>
|
||||
<td>
|
||||
<Checkbox
|
||||
disabled={!isMeteorConnected}
|
||||
id={`answerInput${pollAnswer.key}`}
|
||||
onChange={() => this.handleCheckboxChange(poll.pollId, pollAnswer.id)}
|
||||
checked={checkedAnswers.includes(pollAnswer.id)}
|
||||
className={styles.checkbox}
|
||||
ariaLabelledBy={`pollAnswerLabel${pollAnswer.key}`}
|
||||
ariaDescribedBy={`pollAnswerDesc${pollAnswer.key}`}
|
||||
/>
|
||||
</td>
|
||||
<td className={styles.multipleResponseAnswersTableAnswerText}>
|
||||
<label id={`pollAnswerLabel${pollAnswer.key}`}>
|
||||
{label}
|
||||
</label>
|
||||
<div
|
||||
className={styles.hidden}
|
||||
id={`pollAnswerDesc${pollAnswer.key}`}
|
||||
>
|
||||
{intl.formatMessage(intlMessages.pollAnswerDesc, { 0: label })}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</table>
|
||||
<div>
|
||||
<Button
|
||||
className={styles.submitVoteBtn}
|
||||
disabled={!isMeteorConnected || checkedAnswers.length === 0}
|
||||
color="primary"
|
||||
size="sm"
|
||||
label={intl.formatMessage(intlMessages.submitLabel)}
|
||||
aria-label={intl.formatMessage(intlMessages.submitAriaLabel)}
|
||||
onClick={() => this.handleSubmit(poll.pollId)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 {
|
||||
</span>
|
||||
)
|
||||
}
|
||||
{
|
||||
poll.pollType !== pollTypes.Response && (
|
||||
<span>
|
||||
{
|
||||
question.length === 0 && (
|
||||
<div className={styles.pollingTitle}>
|
||||
{intl.formatMessage(intlMessages.pollingTitleLabel)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className={cx(pollAnswerStyles)}>
|
||||
{poll.answers.map((pollAnswer) => {
|
||||
const formattedMessageIndex = pollAnswer.key.toLowerCase();
|
||||
let label = pollAnswer.key;
|
||||
if (defaultPoll && pollAnswerIds[formattedMessageIndex]) {
|
||||
label = intl.formatMessage(pollAnswerIds[formattedMessageIndex]);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={pollAnswer.id}
|
||||
className={styles.pollButtonWrapper}
|
||||
>
|
||||
<Button
|
||||
disabled={!isMeteorConnected}
|
||||
className={styles.pollingButton}
|
||||
color="primary"
|
||||
size="md"
|
||||
label={label}
|
||||
key={pollAnswer.key}
|
||||
onClick={() => handleVote(poll.pollId, pollAnswer)}
|
||||
aria-labelledby={`pollAnswerLabel${pollAnswer.key}`}
|
||||
aria-describedby={`pollAnswerDesc${pollAnswer.key}`}
|
||||
data-test="pollAnswerOption"
|
||||
/>
|
||||
<div
|
||||
className={styles.hidden}
|
||||
id={`pollAnswerLabel${pollAnswer.key}`}
|
||||
>
|
||||
{intl.formatMessage(intlMessages.pollAnswerLabel, { 0: label })}
|
||||
</div>
|
||||
<div
|
||||
className={styles.hidden}
|
||||
id={`pollAnswerDesc${pollAnswer.key}`}
|
||||
>
|
||||
{intl.formatMessage(intlMessages.pollAnswerDesc, { 0: label })}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
{
|
||||
poll.pollType === pollTypes.Response
|
||||
&& (
|
||||
<div className={styles.typedResponseWrapper}>
|
||||
<input
|
||||
data-test="pollAnswerOption"
|
||||
onChange={(e) => {
|
||||
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; }}
|
||||
/>
|
||||
<Button
|
||||
data-test="submitAnswer"
|
||||
className={styles.submitVoteBtn}
|
||||
disabled={typedAns.length === 0}
|
||||
color="primary"
|
||||
size="sm"
|
||||
label={intl.formatMessage(intlMessages.submitLabel)}
|
||||
aria-label={intl.formatMessage(intlMessages.submitAriaLabel)}
|
||||
onClick={() => {
|
||||
handleTypedVote(poll.pollId, typedAns);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className={styles.pollingSecret}>
|
||||
{intl.formatMessage(poll.secretPoll ? intlMessages.responseIsSecret : intlMessages.responseNotSecret)}
|
||||
</div>
|
||||
{poll.isMultipleResponse ? this.renderCheckboxAnswers(pollAnswerStyles) : this.renderButtonAnswers(pollAnswerStyles)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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) => (
|
||||
<text
|
||||
x={line.keyColumn.xLeft}
|
||||
y={line.keyColumn.yLeft}
|
||||
@ -490,7 +489,7 @@ class PollDrawComponent extends Component {
|
||||
{line.keyColumn.keyString}
|
||||
</text>
|
||||
))}
|
||||
{extendedTextArray.map(line => (
|
||||
{extendedTextArray.map((line) => (
|
||||
<rect
|
||||
key={`${line.key}_bar`}
|
||||
x={line.barColumn.xBar}
|
||||
@ -510,7 +509,7 @@ class PollDrawComponent extends Component {
|
||||
fontSize={calcFontSize}
|
||||
textAnchor={isRTL ? 'start' : 'end'}
|
||||
>
|
||||
{extendedTextArray.map(line => (
|
||||
{extendedTextArray.map((line) => (
|
||||
<tspan
|
||||
x={line.percentColumn.xRight}
|
||||
y={line.percentColumn.yRight}
|
||||
@ -530,7 +529,7 @@ class PollDrawComponent extends Component {
|
||||
fontSize={calcFontSize}
|
||||
textAnchor={isRTL ? 'end' : 'start'}
|
||||
>
|
||||
{extendedTextArray.map(line => (
|
||||
{extendedTextArray.map((line) => (
|
||||
<tspan
|
||||
x={line.barColumn.xNumVotes + (line.barColumn.barWidth / 2)}
|
||||
y={line.barColumn.yNumVotes + (line.barColumn.barHeight / 2)}
|
||||
@ -604,7 +603,7 @@ class PollDrawComponent extends Component {
|
||||
}
|
||||
return (
|
||||
<g aria-hidden>
|
||||
{textArray.map(line => this.renderLine(line))}
|
||||
{textArray.map((line) => this.renderLine(line))}
|
||||
<text
|
||||
fontFamily="Arial"
|
||||
fontSize={calcFontSize}
|
||||
@ -623,7 +622,7 @@ class PollDrawComponent extends Component {
|
||||
const { prepareToDisplay, textArray } = this.state;
|
||||
|
||||
let ariaResultLabel = `${intl.formatMessage(intlMessages.pollResultAria)}: `;
|
||||
textArray.map((t, idx) => {
|
||||
textArray.forEach((t, idx) => {
|
||||
const pollLine = t.slice(0, -1);
|
||||
ariaResultLabel += `${idx > 0 ? ' |' : ''} ${pollLine.join(' | ')}`;
|
||||
});
|
||||
@ -632,8 +631,7 @@ class PollDrawComponent extends Component {
|
||||
<g aria-label={ariaResultLabel} data-test="pollResultAria">
|
||||
{prepareToDisplay
|
||||
? this.renderTestStrings()
|
||||
: this.renderPoll()
|
||||
}
|
||||
: this.renderPoll()}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
@ -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.",
|
||||
|
@ -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.",
|
||||
|
Loading…
Reference in New Issue
Block a user