File: //snap/google-cloud-cli/396/lib/googlecloudsdk/command_lib/survey/question.py
# -*- 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'])