""" open/dulcinea/lib/survey.py """ from dulcinea.address import ContactAddress from dulcinea.base import DulcineaPersistent, Copyable from dulcinea.timestamped import Timestamped from durus.persistent_dict import PersistentDict from durus.persistent_list import PersistentList from qp.lib.keep import Keyed, Keep from qp.lib.spec import either, sequence, mapping, require, init, specify, anything from qp.lib.spec import spec, boolean, add_getters_and_setters, add_getters from qp.lib.spec import string, proper, Specified, Mixin from qp.pub.common import get_publisher from qp.pub.user import User def get_question_db(): return get_publisher().get_connection().get_root().get('question_db') def get_surveys_and_questions_db(): return get_publisher().get_connection().get_root().get( 'surveys_and_questions_db') class Question (DulcineaPersistent, Keyed): title_is = (string, None) sub_title_is = (string, None) def __init__(self, title, sub_title=None): assert self.__class__ is not Question, "abstract class" Keyed.__init__(self) specify(self, title=title, sub_title=sub_title) def __str__(self): if self.sub_title: return self.title + "\n" + self.sub_title return self.title def get_new_answer(self): raise NotImplementedError add_getters(Question) class TextQuestion (Question): dimensions_is = (int, int) def __init__(self, title, sub_title=None, rows=8, cols=100): Question.__init__(self, title, sub_title) self.dimensions = (rows, cols) def get_new_answer(self): return TextAnswer(self) def get_dimensions(self): # Fix with update_db. return getattr(self, 'dimensions', (8, 100)) class ContactAddressQuestion (Question): def get_new_answer(self): return ContactAddressAnswer(self) class SelectQuestion (Question): """ A SelectQuestion is an abstract class for representing selections from either checkboxes or radio buttons """ column_headings_is = spec( [(string, None)], "text to place above each column") row_headings_is = spec( [(string, None)], "text that preceeds each row") range_labels_is = spec( [string], "labels to be placed in a row above column headings at the right and " "left") last_row_has_text_is = spec( boolean, "flag to tell the widget to display a TextWidget after the last row " "label") def __init__(self, title, sub_title=None, column_headings=None, row_headings=None, range_labels=None, last_row_has_text=False): Question.__init__(self, title, sub_title=sub_title) specify(self, column_headings=column_headings or [], row_headings=row_headings or [], range_labels=range_labels or [], last_row_has_text = last_row_has_text) assert len(self.range_labels) in (0,2), "must have two range labels" def __str__(self): question = self.title if len(self.column_headings) > 1: # Radioset if len(self.column_headings) > 4: headings = '(%s ... %s)' % (self.column_headings[0], self.column_headings[-1]) else: headings = '(%s)' % (', '.join(self.column_headings)) question += "\n" + headings if self.range_labels: question += "\n ...where %s is %s and %s is %s" % ( self.column_headings[0], self.range_labels[0], self.column_headings[-1], self.range_labels[-1]) if self.sub_title: question += "\n" + self.sub_title return question def get_new_answer(self): return SelectAnswer(self) add_getters(SelectQuestion) class Answer (Specified, Copyable): question_is = Question def __eq__(self, other): if not isinstance(other, Answer): return False if self.get_question() is not other.get_question(): return False return True def __ne__(self, other): return not self.__eq__(other) def __init__(self, question=None): assert self.__class__ is not Answer, "abstract class" self.question = question def get_question(self): return self.question class ContactAddressAnswer (Answer): answer_is = ContactAddress def __init__(self, question=None): Answer.__init__(self, question) self.answer = ContactAddress() def __str__(self): return self.answer.format() def __eq__(self, other): if not isinstance(other, ContactAddressAnswer): return False if not Answer.__eq__(self, other): return False if self.get_answer() != other.get_answer(): return False return True def has_answer(self): return self.answer != ContactAddress() add_getters_and_setters(ContactAddressAnswer) class TextAnswer (Answer): answer_is = (None, string) def __init__(self, question=None): Answer.__init__(self, question) self.answer = None def __str__(self): return self.answer def __eq__(self, other): if not isinstance(other, TextAnswer): return False if not Answer.__eq__(self, other): return False if self.get_answer() != other.get_answer(): return False return True def get_answer(self): return self.answer def set_answer(self, answer): specify(self, answer=answer) def has_answer(self): return self.answer is not None add_getters_and_setters(TextAnswer) class SelectAnswer (Answer): selected_is = spec( (None, [either(string, boolean)]), "list of selected row answers") other_text_is = spec( (None, string), "contents of the filled in 'last_row_has_text' textfield") def __init__(self, question=None): Answer.__init__(self, question) init(self) def __str__(self): if self.question.row_headings: def format_heading_answer(heading, answer): if answer: return "%s: %s" % (heading, answer) return '' return "\n".join([format_heading_answer(*heading_answer) for heading_answer in zip(self.question.row_headings, self.selected)]) return ", ".join(self.selected) def __eq__(self, other): if not isinstance(other, SelectAnswer): return False if not Answer.__eq__(self, other): return False if self.get_selected() != other.get_selected(): return False if self.get_other_text() != other.get_other_text(): return False return True def set_other_text(self, other_text): specify(self, other_text=other_text) def get_other_text(self): return self.other_text def set_answer(self, selected): specify(self, selected=selected) def get_answer(self): return self.selected def has_answer(self): return self.selected is not None add_getters(SelectAnswer) class Survey (DulcineaPersistent, Timestamped): """A set of Questions and Answers attached to a single object. """ user_is = spec( (User, None), "user taking the survey") answers_is = spec( [proper(Answer)], "answers to the questions that comprise the object's survey") surveyed_is = spec( None, # forward declaration "If specified, the last Surveyed to assign itself this survey.") def __init__(self, answers): """(answers:[Answer]) """ Timestamped.__init__(self) init(self, answers=answers) def __str__(self): return "By %s on %s: %d answers" % ( self.user, self.get_timestamp(), len(self.answers)) def __len__(self): return len(self.answers) def __eq__(self, other): if not isinstance(other, Survey): return False if self.get_user() is not other.get_user(): return False # ignoring timestamp if len(self.answers) != len(other.get_answers()): return False for index, answer in enumerate(self.answers): if answer != other.get_answers()[index]: return False return True def __ne__(self, other): return not self.__eq__(other) def copy(self): copy = DulcineaPersistent.copy(self) copy.answers = [answer.copy() for answer in copy.answers] return copy def get_questions(self): return [answer.get_question() for answer in self.answers] def get_answer_for_question_key(self, question_key): for answer in self.answers: if answer.get_question().get_key() == question_key: return answer return None def set_user(self, user): """(user : DulcineaUser) """ specify(self, user=user) def set_surveyed(self, surveyed): specify(self, surveyed=surveyed) def set_answers(self, answers, user): """(answers : [Answer], user : DulcineaUser) """ specify(self, answers=answers, user=user) self.set_timestamp() def has_answers(self, omit_questions=None): """(omit_questions : [int] | None) -> boolean Return true if any answers have been answered for this survey ignoring omitted questions if specified """ for answer in self.answers: if answer.get_question().get_key() in (omit_questions or []): continue if answer.has_answer(): return True return False add_getters(Survey) class Surveyable (Mixin): """A mixin for objects that have Surveys """ surveys_is = (None, sequence(Survey, PersistentList)) def __init__(self): self.surveys = None def get_surveys(self): """() -> [Survey] """ return self.surveys or [] def get_survey(self, index): if self.surveys and (0 <= index < len(self.get_surveys())): return self.surveys[index] return None def get_new_survey(self): """() -> Survey """ raise NotImplementedError def add_survey(self, survey): if self.surveys is None: self.surveys = PersistentList() self.surveys.append(survey) class Surveyed (Mixin): survey_is = (None, Survey) def __init__(self, survey=None): self.survey = survey def connect(self, survey): assert survey is not None self.set_survey(survey) survey.set_surveyed(self) add_getters_and_setters(Surveyed) Survey.surveyed_is = spec( (None, Surveyed), "If specified, the last Surveyed to assign itself this survey.") class SurveyContainer (DulcineaPersistent, Surveyed): reference_is = anything def __init__(self, reference=None): Surveyed.__init__(self) self.reference = reference def has_answers(self): return self.survey and self.survey.has_answers() add_getters(SurveyContainer) class QuestionDatabase (DulcineaPersistent, Keep): def __init__(self): Keep.__init__(self, value_spec=Question) def get_questions(self): question_keys = sorted(self.get_mapping().keys()) return [self.get(question_key) for question_key in question_keys] def get_answers(self, question_keys=None): """(question_keys: [int]) -> [Answer] """ if not question_keys: question_keys = sorted(self.get_mapping().keys()) return [self.get(question_key).get_new_answer() for question_key in question_keys] class SurveysAndQuestions (QuestionDatabase, Surveyable): id_is = string title_is = string description_is = string def __init__(self, id, questions): """(id : string, questions : [Question]) """ Surveyable.__init__(self) QuestionDatabase.__init__(self) for question in questions: self.add(question) specify(self, id=id, title='', description='') def get_new_survey(self): return Survey(self.get_answers()) add_getters_and_setters(SurveysAndQuestions) class SurveysAndQuestionsDatabase (DulcineaPersistent): surveys_and_questions_is = mapping({string:SurveysAndQuestions}, PersistentDict) def __init__(self): self.surveys_and_questions = PersistentDict() def add(self, surveys_and_questions): require(surveys_and_questions, SurveysAndQuestions) id = surveys_and_questions.get_id() assert id not in self.surveys_and_questions, id self.surveys_and_questions[id] = surveys_and_questions def get(self, id): return self.surveys_and_questions.get(id) def get_mapping(self): return self.surveys_and_questions