338 lines
9.5 KiB
Python
338 lines
9.5 KiB
Python
# -*- coding: utf-8 -*- #
|
|
# Copyright 2018 Google LLC. All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
"""This module contains all survey question types."""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import unicode_literals
|
|
|
|
import abc
|
|
|
|
from googlecloudsdk.command_lib.survey import util as survey_util
|
|
from googlecloudsdk.core import exceptions
|
|
from googlecloudsdk.core import log
|
|
|
|
import six
|
|
|
|
|
|
class Error(exceptions.Error):
|
|
"""Base error class for this module."""
|
|
pass
|
|
|
|
|
|
class AnswerRejectedError(Error):
|
|
"""Raises when answer is rejected."""
|
|
pass
|
|
|
|
|
|
class RetrieveAnswerOfUnansweredQuestion(Error):
|
|
"""Raises when retrieving answer from an unanswered question."""
|
|
pass
|
|
|
|
|
|
class QuestionCreationError(Error):
|
|
"""Raises when question cannot be created with the provided data."""
|
|
|
|
def __init__(self, required_fields):
|
|
required_fields_in_string = ', '.join(required_fields)
|
|
super(QuestionCreationError, self).__init__(
|
|
'Question cannot be created because either some '
|
|
'required field is missing or there are redundant fields. Required '
|
|
'fields are {}.'.format(required_fields_in_string))
|
|
|
|
|
|
class Question(six.with_metaclass(abc.ABCMeta, object)):
|
|
"""Base class for survey questions.
|
|
|
|
Attributes:
|
|
_question: str, the question to ask.
|
|
_instruction: str, instruction on how to answer the question.
|
|
_instruction_on_rejection: str, instruction after the answer is rejected.
|
|
_answer: str, the answer to the question.
|
|
"""
|
|
|
|
def __init__(self,
|
|
question,
|
|
instruction,
|
|
instruction_on_rejection=None,
|
|
answer=None):
|
|
self._question = question
|
|
self._instruction = instruction
|
|
self._instruction_on_rejection = instruction_on_rejection
|
|
self._answer = answer
|
|
|
|
@abc.abstractmethod
|
|
def FromDictionary(self, content):
|
|
pass
|
|
|
|
@property
|
|
def question(self):
|
|
return self._question
|
|
|
|
@property
|
|
def instruction(self):
|
|
return self._instruction
|
|
|
|
@property
|
|
def instruction_on_rejection(self):
|
|
return self._instruction_on_rejection
|
|
|
|
def PrintQuestion(self):
|
|
self._PrintQuestion()
|
|
log.out.flush()
|
|
|
|
@abc.abstractmethod
|
|
def _PrintQuestion(self):
|
|
pass
|
|
|
|
def PrintInstruction(self):
|
|
if self._instruction:
|
|
log.err.write(self._instruction)
|
|
|
|
def PrintInstructionOnRejection(self):
|
|
if self._instruction_on_rejection:
|
|
log.err.write(self._instruction_on_rejection)
|
|
|
|
@abc.abstractmethod
|
|
def AcceptAnswer(self, answer):
|
|
"""Returns True if answer is accepted, otherwise returns False."""
|
|
pass
|
|
|
|
def IsAnswered(self):
|
|
return self._answer is not None
|
|
|
|
def AnswerQuestion(self, answer):
|
|
if self.AcceptAnswer(answer):
|
|
self._answer = answer
|
|
else:
|
|
raise AnswerRejectedError('Answer is invalid.')
|
|
|
|
@property
|
|
def answer(self):
|
|
if self.IsAnswered():
|
|
return self._answer
|
|
else:
|
|
raise RetrieveAnswerOfUnansweredQuestion('No answer for this question.')
|
|
|
|
def __eq__(self, other):
|
|
if isinstance(other, self.__class__):
|
|
# pylint: disable=protected-access
|
|
return (self._question == other._question and
|
|
self._instruction == other._instruction and
|
|
self._instruction_on_rejection == other._instruction_on_rejection)
|
|
# pylint: enable=protected-access
|
|
return False
|
|
|
|
def __ne__(self, other):
|
|
return not self == other # pylint: disable=g-comparison-negation
|
|
|
|
def __hash__(self):
|
|
return hash((self._question, self._instruction,
|
|
self._instruction_on_rejection))
|
|
|
|
|
|
class MultiChoiceQuestion(Question):
|
|
"""Multi-choice question.
|
|
|
|
Attributes:
|
|
_choices: [str], list of choices.
|
|
"""
|
|
|
|
def __init__(self,
|
|
question,
|
|
instruction,
|
|
instruction_on_rejection,
|
|
choices,
|
|
answer=None):
|
|
super(MultiChoiceQuestion, self).__init__(
|
|
question, instruction, instruction_on_rejection, answer=answer)
|
|
self._choices = choices
|
|
|
|
@classmethod
|
|
def FromDictionary(cls, content):
|
|
try:
|
|
return cls(**content)
|
|
except TypeError:
|
|
raise QuestionCreationError(required_fields=[
|
|
'question', 'instruction', 'instruction_on_rejection', 'choices'
|
|
])
|
|
|
|
def _PrintQuestion(self):
|
|
"""Prints question and lists all choices."""
|
|
# index choices from 1
|
|
question_repr = self._FormatQuestion(
|
|
indexes=range(1,
|
|
len(self._choices) + 1))
|
|
log.Print(question_repr)
|
|
|
|
def _FormatQuestion(self, indexes):
|
|
"""Formats question to present to users."""
|
|
choices_repr = [
|
|
'[{}] {}'.format(index, msg)
|
|
for index, msg in zip(indexes, self._choices)
|
|
]
|
|
choices_repr = [survey_util.Indent(content, 2) for content in choices_repr]
|
|
choices_repr = '\n'.join(choices_repr)
|
|
question_repr = survey_util.Indent(self._question, 1)
|
|
return '\n'.join([question_repr, choices_repr])
|
|
|
|
def AcceptAnswer(self, answer):
|
|
"""Returns True if answer is accepted, otherwise returns False."""
|
|
try:
|
|
answer_int = int(answer)
|
|
except ValueError:
|
|
return False
|
|
else:
|
|
return 1 <= answer_int <= len(self._choices)
|
|
|
|
def Choice(self, index):
|
|
"""Gets the choice at the given index."""
|
|
# choices are indexed from 1
|
|
return self._choices[index - 1]
|
|
|
|
def __eq__(self, other):
|
|
if isinstance(other, self.__class__):
|
|
# pylint: disable=protected-access
|
|
return (self._question == other._question and
|
|
self._instruction == other._instruction and
|
|
self._instruction_on_rejection == other._instruction_on_rejection
|
|
and self._choices == other._choices)
|
|
# pylint: enable=protected-access
|
|
return False
|
|
|
|
def __hash__(self):
|
|
return hash((self._question, self._instruction,
|
|
self._instruction_on_rejection, tuple(self._choices)))
|
|
|
|
def __len__(self):
|
|
return len(self._choices)
|
|
|
|
|
|
class SatisfactionQuestion(MultiChoiceQuestion):
|
|
"""Customer satisfaction question."""
|
|
|
|
def IsSatisfied(self):
|
|
"""Returns true is user answers "Very satisfied" or "Somewhat satisfied"."""
|
|
if self.IsAnswered():
|
|
return int(self.answer) > 3
|
|
else:
|
|
return None
|
|
|
|
def _PrintQuestion(self):
|
|
# index choices in the reverse order, e.g. 5 is "Very satisfied" and 1 is
|
|
# "Very dissatisfied".
|
|
choice_indexes = range(len(self._choices), 0, -1)
|
|
question_repr = self._FormatQuestion(indexes=choice_indexes)
|
|
log.Print(question_repr)
|
|
|
|
def Choice(self, index):
|
|
"""Gets the choice at the given index."""
|
|
# choices are indexed in the reverse order
|
|
return self._choices[len(self._choices) - index]
|
|
|
|
|
|
class RatingQuestion(Question):
|
|
""""Rating question.
|
|
|
|
Attributes:
|
|
min_answer: int, minimum acceptable value for answer.
|
|
max_answer: int, maximum acceptable value for answer.
|
|
"""
|
|
|
|
@classmethod
|
|
def FromDictionary(cls, content):
|
|
try:
|
|
return cls(**content)
|
|
except TypeError:
|
|
raise QuestionCreationError(required_fields=[
|
|
'question', 'instruction', 'instruction_on_rejection', 'min_answer',
|
|
'max_answer'
|
|
])
|
|
|
|
def __init__(self,
|
|
question,
|
|
instruction,
|
|
instruction_on_rejection,
|
|
min_answer,
|
|
max_answer,
|
|
answer=None):
|
|
super(RatingQuestion, self).__init__(
|
|
question=question,
|
|
instruction=instruction,
|
|
instruction_on_rejection=instruction_on_rejection,
|
|
answer=answer)
|
|
self._min = min_answer
|
|
self._max = max_answer
|
|
|
|
def _PrintQuestion(self):
|
|
question = survey_util.Indent(self._question, 1)
|
|
log.Print(question)
|
|
|
|
def AcceptAnswer(self, answer):
|
|
try:
|
|
answer_int = int(answer)
|
|
return self._min <= answer_int <= self._max
|
|
except ValueError:
|
|
return False
|
|
|
|
def __eq__(self, other):
|
|
if isinstance(other, self.__class__):
|
|
# pylint: disable=protected-access
|
|
return (self._question == other._question and
|
|
self._instruction == other._instruction and
|
|
self._instruction_on_rejection == other._instruction_on_rejection
|
|
and self._min == other._min and self._max == other._max)
|
|
# pylint: enable=protected-access
|
|
return False
|
|
|
|
def __ne__(self, other):
|
|
return not self == other # pylint: disable=g-comparison-negation
|
|
|
|
def __hash__(self):
|
|
return hash((self._question, self._instruction,
|
|
self._instruction_on_rejection, self._min, self._max))
|
|
|
|
|
|
class NPSQuestion(RatingQuestion):
|
|
"""Net promoter score question."""
|
|
|
|
|
|
class FreeTextQuestion(Question):
|
|
"""Free text question."""
|
|
|
|
def _PrintQuestion(self):
|
|
question = survey_util.Indent(self._question, 1)
|
|
log.Print(question)
|
|
|
|
def AcceptAnswer(self, answer):
|
|
"""Returns True if answer is accepted, otherwise returns False.
|
|
|
|
Accepts any answer for free text question.
|
|
|
|
Args:
|
|
answer: str, the answer to check.
|
|
|
|
Returns:
|
|
True
|
|
"""
|
|
return True
|
|
|
|
@classmethod
|
|
def FromDictionary(cls, content):
|
|
try:
|
|
return cls(**content)
|
|
except TypeError:
|
|
raise QuestionCreationError(required_fields=['question', 'instruction'])
|