class EnumeratorOnlySubmission(Submission): """An EnumeratorOnlySubmission must have an enumerator. Use an EnumeratorOnlySubmission for an EnumeratorOnlySurvey. """ __tablename__ = 'submission_enumerator_only' id = util.pk() the_survey_id = sa.Column(pg.UUID, nullable=False) enumerator_user_id = sa.Column( pg.UUID, util.fk('auth_user.id'), nullable=False ) enumerator = relationship('User') __mapper_args__ = {'polymorphic_identity': 'enumerator_only_submission'} __table_args__ = ( sa.ForeignKeyConstraint( ['id', 'the_survey_id'], ['submission.id', 'submission.survey_id'] ), sa.ForeignKeyConstraint( ['the_survey_id', 'enumerator_user_id'], ['enumerator.enumerator_only_survey_id', 'enumerator.user_id'] ), ) def _asdict(self) -> OrderedDict: result = super()._default_asdict() result['enumerator_user_id'] = self.enumerator_user_id result['enumerator_user_name'] = self.enumerator.name return result
class MultipleChoiceBucket(Bucket): """Choice id bucket.""" __tablename__ = 'bucket_multiple_choice' id = util.pk() the_sub_survey_id = sa.Column(pg.UUID, nullable=False) choice_id = sa.Column(pg.UUID, nullable=False) bucket = relationship('Choice') parent_survey_node_id = sa.Column(pg.UUID, nullable=False) parent_node_id = sa.Column(pg.UUID, nullable=False) __mapper_args__ = {'polymorphic_identity': 'multiple_choice'} __table_args__ = ( sa.ForeignKeyConstraint(['choice_id', 'parent_node_id'], ['choice.id', 'choice.question_id'], onupdate='CASCADE', ondelete='CASCADE'), sa.ForeignKeyConstraint([ 'id', 'the_sub_survey_id', 'parent_survey_node_id', 'parent_node_id', ], [ 'bucket.id', 'bucket.sub_survey_id', 'bucket.sub_survey_parent_survey_node_id', 'bucket.sub_survey_parent_node_id', ], onupdate='CASCADE', ondelete='CASCADE'), )
class Administrator(User): """A User who can create Surveys and add Users. Regular users can answer surveys, but only Administrator instances can create surveys. """ __tablename__ = 'administrator' id = util.pk('auth_user.id') surveys = relationship( 'Survey', order_by='Survey.created_on', backref='creator', cascade='all, delete-orphan', passive_deletes=True, ) token = sa.Column(pg.BYTEA) token_expiration = sa.Column( pg.TIMESTAMP(timezone=True), nullable=False, server_default=current_timestamp(), ) __mapper_args__ = {'polymorphic_identity': 'administrator'} def _asdict(self) -> OrderedDict: result = super()._asdict() result['surveys'] = [s.id for s in self.surveys] result['admin_surveys'] = [s.id for s in self.admin_surveys] result['token_expiration'] = self.token_expiration return result
class PublicSubmission(Submission): """A PublicSubmission might have an enumerator. Use a PublicSubmission for a Survey. """ __tablename__ = 'submission_public' id = util.pk() enumerator_user_id = sa.Column(pg.UUID, util.fk('auth_user.id')) enumerator = relationship('User') survey_type = sa.Column(survey_type_enum, nullable=False) __table_args__ = ( sa.ForeignKeyConstraint( ['id', 'survey_type'], ['submission.id', 'submission.survey_type'], onupdate='CASCADE', ondelete='CASCADE' ), sa.CheckConstraint("survey_type::TEXT = 'public'"), ) __mapper_args__ = {'polymorphic_identity': 'public_submission'} def _asdict(self): result = super()._default_asdict() if self.enumerator_user_id is not None: result['enumerator_user_id'] = self.enumerator_user_id result['enumerator_user_name'] = self.enumerator.name return result
class _AnswerMixin: id = util.pk() the_allow_other = sa.Column(sa.Boolean, nullable=False) the_allow_dont_know = sa.Column(sa.Boolean, nullable=False) other = sa.Column(pg.TEXT) dont_know = sa.Column(pg.TEXT)
class User(Base): """Models a user. A user has at least one e-mail address.""" __tablename__ = 'auth_user' id = util.pk() name = sa.Column(pg.TEXT, nullable=False) emails = relationship( 'Email', order_by='Email.address', backref='user', cascade='all, delete-orphan', passive_deletes=True, ) role = sa.Column( sa.Enum( 'enumerator', 'administrator', name='user_roles', inherit_schema=True ), nullable=False, ) preferences = util.json_column( 'preferences', default='{"default_language": "English"}' ) last_update_time = util.last_update_time() __mapper_args__ = { 'polymorphic_identity': 'enumerator', 'polymorphic_on': role, } __table_args__ = ( sa.CheckConstraint( "((preferences->>'default_language')) IS NOT NULL", name='must_specify_default_language' ), ) def _asdict(self) -> OrderedDict: return OrderedDict(( ('id', self.id), ('deleted', self.deleted), ('name', self.name), ('emails', [email.address for email in self.emails]), ('role', self.role), ('preferences', self.preferences), ('allowed_surveys', [s.id for s in self.allowed_surveys]), ('last_update_time', self.last_update_time), ))
class EnumeratorOnlySurvey(Survey): """Only enumerators (designated Users) can submit to this.""" __tablename__ = 'survey_enumerator_only' id = util.pk('survey') enumerators = relationship( 'User', secondary=_enumerator_table, backref='allowed_surveys', passive_deletes=True, ) __mapper_args__ = {'polymorphic_identity': 'enumerator_only'}
class NonAnswerableSurveyNode(SurveyNode): """Contains a Node which is not answerable (e.g., a Note).""" __tablename__ = 'survey_node_non_answerable' id = util.pk() the_node_id = sa.Column(pg.UUID, util.fk('note.id'), nullable=False) the_type_constraint = sa.Column(node_type_enum, nullable=False) node = relationship('Note') __mapper_args__ = {'polymorphic_identity': 'non_answerable'} __table_args__ = (sa.ForeignKeyConstraint( ['id', 'the_node_id', 'the_type_constraint'], [ 'survey_node.id', 'survey_node.node_id', 'survey_node.type_constraint' ]), )
class Choice(Base): """A choice for a MultipleChoiceQuestion. Models a choice for a dokomoforms.models.survey.MultipleChoiceQuestion. """ __tablename__ = 'choice' id = util.pk() choice_text = util.json_column('choice_text') choice_number = sa.Column(sa.Integer, nullable=False) question_id = sa.Column(pg.UUID, nullable=False) question_languages = util.languages_column('question_languages') last_update_time = util.last_update_time() __table_args__ = ( sa.UniqueConstraint('question_id', 'choice_number', name='unique_choice_number'), sa.UniqueConstraint('question_id', 'choice_text', name='unique_choice_text'), sa.UniqueConstraint('id', 'question_id'), util.languages_constraint('choice_text', 'question_languages'), sa.ForeignKeyConstraint(['question_id', 'question_languages'], [ 'question_multiple_choice.id', 'question_multiple_choice.node_languages' ], onupdate='CASCADE', ondelete='CASCADE'), ) def _asdict(self) -> OrderedDict: return OrderedDict(( ('id', self.id), ('deleted', self.deleted), ('choice_text', OrderedDict(sorted(self.choice_text.items()))), ('choice_number', self.choice_number), ('question', OrderedDict(( ('question_id', self.question_id), ('question_title', OrderedDict(sorted(self.question.title.items()))), ))), ('last_update_time', self.last_update_time), ))
class _RangeBucketMixin: id = util.pk() the_survey_node_id = sa.Column(pg.UUID, nullable=False) @declared_attr def __table_args__(self): return ( pg.ExcludeConstraint( (sa.cast(self.the_survey_node_id, pg.TEXT), '='), ('bucket', '&&')), sa.CheckConstraint('NOT isempty(bucket)'), sa.ForeignKeyConstraint( ['id', 'the_survey_node_id'], ['bucket.id', 'bucket.sub_survey_parent_survey_node_id'], onupdate='CASCADE', ondelete='CASCADE'), )
class MultipleChoiceQuestion(_QuestionMixin, Question): """A multiple_choice question.""" __tablename__ = 'question_multiple_choice' id = util.pk('node.id') node_languages = util.languages_column('node_languages') choices = relationship( 'Choice', order_by='Choice.choice_number', collection_class=ordering_list('choice_number'), backref='question', cascade='all, delete-orphan', passive_deletes=True, ) __mapper_args__ = {'polymorphic_identity': 'multiple_choice'} __table_args__ = ( sa.UniqueConstraint('id', 'node_languages'), sa.ForeignKeyConstraint(['id', 'node_languages'], ['question.id', 'question.the_languages'], onupdate='CASCADE', ondelete='CASCADE'), ) def _asdict(self) -> OrderedDict: return OrderedDict(( ('id', self.id), ('deleted', self.deleted), ('title', OrderedDict(sorted(self.title.items()))), ('hint', self.hint), ('choices', [ OrderedDict(( ('choice_id', choice.id), ('choice_text', OrderedDict(sorted(choice.choice_text.items()))), )) for choice in self.choices ]), ('allow_multiple', self.allow_multiple), ('allow_other', self.allow_other), ('type_constraint', self.type_constraint), ('logic', self.logic), ('last_update_time', self.last_update_time), ))
class Question(Node): """A Question has a response type associated with it. A Question has a type constraint associated with it (integer, date, text...). Only a dokomoforms.models.survey.MultipleChoiceQuestion has a list of dokomoforms.models.survey.Choice instances. """ __tablename__ = 'question' id = util.pk() the_languages = sa.Column(pg.ARRAY(pg.TEXT, as_tuple=True), nullable=False) the_type_constraint = sa.Column(node_type_enum, nullable=False) allow_multiple = sa.Column(sa.Boolean, nullable=False, server_default='false') allow_other = sa.Column(sa.Boolean, nullable=False, server_default='false') __table_args__ = ( sa.UniqueConstraint('id', 'the_languages'), sa.UniqueConstraint('id', 'the_languages', 'allow_multiple', 'allow_other'), sa.CheckConstraint( "(the_type_constraint = 'multiple_choice') OR (NOT allow_other)", name='only_multiple_choice_can_allow_other'), sa.ForeignKeyConstraint( ['id', 'the_languages', 'the_type_constraint'], ['node.id', 'node.languages', 'node.type_constraint']), ) def _default_asdict(self) -> OrderedDict: return OrderedDict(( ('id', self.id), ('deleted', self.deleted), ('languages', self.languages), ('title', self.title), ('hint', self.hint), ('allow_multiple', self.allow_multiple), ('allow_other', self.allow_multiple), ('type_constraint', self.type_constraint), ('logic', self.logic), ('last_update_time', self.last_update_time), ))
class MultipleChoiceAnswer(_AnswerMixin, Answer): """A Choice answer.""" __tablename__ = 'answer_multiple_choice' id = util.pk() the_allow_other = sa.Column(sa.Boolean, nullable=False) the_allow_dont_know = sa.Column(sa.Boolean, nullable=False) main_answer = sa.Column(pg.UUID) choice = relationship('Choice') answer = synonym('main_answer') the_survey_node_id = sa.Column(pg.UUID, nullable=False) the_question_id = sa.Column(pg.UUID, nullable=False) the_submission_id = sa.Column(pg.UUID, nullable=False) __mapper_args__ = {'polymorphic_identity': 'multiple_choice'} __table_args__ = _answer_mixin_table_args()[1:] + ( sa.ForeignKeyConstraint([ 'id', 'the_allow_other', 'the_allow_dont_know', 'the_survey_node_id', 'the_question_id', 'the_submission_id', ], [ 'answer.id', 'answer.allow_other', 'answer.allow_dont_know', 'answer.survey_node_id', 'answer.question_id', 'answer.submission_id', ], onupdate='CASCADE', ondelete='CASCADE'), sa.ForeignKeyConstraint(['main_answer', 'the_question_id'], ['choice.id', 'choice.question_id']), sa.UniqueConstraint( 'the_survey_node_id', 'main_answer', 'the_submission_id', name='cannot_pick_the_same_choice_twice', ), )
class Email(Base): """Models an e-mail address.""" __tablename__ = 'email' id = util.pk() address = sa.Column( pg.TEXT, sa.CheckConstraint("address ~ '.*@.*'"), nullable=False, unique=True ) user_id = sa.Column(pg.UUID, util.fk('auth_user.id'), nullable=False) last_update_time = util.last_update_time() def _asdict(self) -> OrderedDict: return OrderedDict(( ('id', self.id), ('address', self.address), ('user', self.user.name), ('last_update_time', self.last_update_time), ))
class Node(Base): """A node is a Note or Question independent of any Survey. A node is its own entity. A node can be a dokomoforms.models.survey.Note or a dokomoforms.models.survey.Question. You can use this class for querying, e.g. session.query(Node).filter_by(title='Some Title') To create the specific kind of Node you want, use dokomoforms.models.survey.node.construct_node. """ __tablename__ = 'node' id = util.pk() languages = util.languages_column('languages') title = util.json_column('title') hint = util.json_column('hint', default='{"English": ""}') type_constraint = sa.Column(node_type_enum, nullable=False) logic = util.json_column('logic', default='{}') last_update_time = util.last_update_time() __mapper_args__ = {'polymorphic_on': type_constraint} __table_args__ = ( sa.CheckConstraint( "(type_constraint::TEXT != 'facility') OR (" " ((logic->>'nlat')) IS NOT NULL AND " " ((logic->>'slat')) IS NOT NULL AND " " ((logic->>'wlng')) IS NOT NULL AND " " ((logic->>'elng')) IS NOT NULL" ")", name='facility_questions_must_have_bounds'), sa.UniqueConstraint('id', 'type_constraint'), sa.UniqueConstraint('id', 'languages', 'type_constraint'), util.languages_constraint('title', 'languages'), util.languages_constraint('hint', 'languages'), )
class Note(Node): """Notes provide information interspersed with survey questions.""" __tablename__ = 'note' id = util.pk() the_type_constraint = sa.Column(node_type_enum, nullable=False) __mapper_args__ = {'polymorphic_identity': 'note'} __table_args__ = (sa.ForeignKeyConstraint( ['id', 'the_type_constraint'], ['node.id', 'node.type_constraint']), ) def _asdict(self) -> OrderedDict: return OrderedDict(( ('id', self.id), ('deleted', self.deleted), ('languages', self.languages), ('title', self.title), ('hint', self.hint), ('type_constraint', self.type_constraint), ('logic', self.logic), ('last_update_time', self.last_update_time), ))
class Photo(Base): """A BYTEA holding an image.""" __tablename__ = 'photo' id = util.pk() image = sa.Column(pg.BYTEA, nullable=False) mime_type = sa.Column(pg.TEXT, nullable=False) # image = sa.Column(LObject) created_on = sa.Column( pg.TIMESTAMP(timezone=True), nullable=False, server_default=current_timestamp(), ) def _asdict(self) -> OrderedDict: return OrderedDict(( ('id', self.id), ('deleted', self.deleted), ('image', self.image), ('mime_type', self.mime_type), ('created_on', self.created_on), ))
class SurveyNode(Base): """A SurveyNode contains a Node and adds survey-specific metadata.""" __tablename__ = 'survey_node' id = util.pk() node_number = sa.Column(sa.Integer, nullable=False) survey_node_answerable = sa.Column( sa.Enum('non_answerable', 'answerable', name='answerable_enum', inherit_schema=True), nullable=False, ) node_id = sa.Column(pg.UUID, nullable=False) node_languages = sa.Column(pg.ARRAY(pg.TEXT, as_tuple=True), nullable=False) type_constraint = sa.Column(node_type_enum, nullable=False) the_node = relationship('Node') @property # pragma: no cover @abc.abstractmethod def node(self): """The Node instance.""" root_survey_id = sa.Column(pg.UUID) containing_survey_id = sa.Column(pg.UUID, nullable=False) root_survey_languages = sa.Column(pg.ARRAY(pg.TEXT, as_tuple=True), nullable=False) sub_survey_id = sa.Column(pg.UUID) sub_survey_repeatable = sa.Column(sa.Boolean) non_null_repeatable = sa.Column(sa.Boolean, nullable=False, server_default='FALSE') logic = util.json_column('logic', default='{}') last_update_time = util.last_update_time() __mapper_args__ = {'polymorphic_on': survey_node_answerable} __table_args__ = ( sa.UniqueConstraint('id', 'node_id', 'type_constraint'), sa.UniqueConstraint('id', 'containing_survey_id', 'root_survey_languages', 'node_id', 'type_constraint', 'non_null_repeatable'), sa.CheckConstraint( '(sub_survey_repeatable IS NULL) != ' '(sub_survey_repeatable = non_null_repeatable)', name='you_must_mark_survey_nodes_repeatable_explicitly'), sa.CheckConstraint( '(root_survey_id IS NULL) != (sub_survey_id IS NULL)'), sa.CheckConstraint( 'root_survey_languages @> node_languages', name='all_survey_languages_present_in_node_languages'), sa.ForeignKeyConstraint([ 'root_survey_id', 'containing_survey_id', 'root_survey_languages' ], ['survey.id', 'survey.containing_id', 'survey.languages'], onupdate='CASCADE', ondelete='CASCADE'), sa.ForeignKeyConstraint([ 'sub_survey_id', 'root_survey_languages', 'containing_survey_id', 'sub_survey_repeatable' ], [ 'sub_survey.id', 'sub_survey.root_survey_languages', 'sub_survey.containing_survey_id', 'sub_survey.repeatable' ]), sa.ForeignKeyConstraint( ['node_id', 'node_languages', 'type_constraint'], ['node.id', 'node.languages', 'node.type_constraint']), ) def _asdict(self) -> OrderedDict: result = self.node._asdict() if result['logic']: result['logic'].update(self.logic) result['node_id'] = result.pop('id') result['id'] = self.id result['deleted'] = self.deleted result['last_update_time'] = self.last_update_time return result
class Survey(Base): """A Survey has a list of SurveyNodes. Use an EnumeratorOnlySurvey to restrict submissions to enumerators. """ __tablename__ = 'survey' id = util.pk() containing_id = sa.Column(pg.UUID, unique=True, server_default=sa.func.uuid_generate_v4()) languages = util.languages_column('languages') title = util.json_column('title') url_slug = sa.Column( pg.TEXT, sa.CheckConstraint("url_slug != '' AND " "url_slug !~ '[%%#;/?:@&=+$,\s]' AND " "url_slug !~ '{}'".format(util.UUID_REGEX), name='url_safe_slug'), unique=True, ) default_language = sa.Column( pg.TEXT, sa.CheckConstraint("default_language != ''", name='non_empty_default_language'), nullable=False, server_default='English', ) survey_type = sa.Column(survey_type_enum, nullable=False) administrators = relationship( 'Administrator', secondary=_administrator_table, backref='admin_surveys', passive_deletes=True, ) submissions = relationship( 'Submission', order_by='Submission.save_time', backref='survey', cascade='all, delete-orphan', passive_deletes=True, ) # dokomoforms.models.column_properties # num_submissions # earliest_submission_time # latest_submission_time # TODO: expand upon this version = sa.Column(sa.Integer, nullable=False, server_default='1') # ODOT creator_id = sa.Column(pg.UUID, util.fk('administrator.id'), nullable=False) # This is survey_metadata rather than just metadata because all models # have a metadata attribute which is important for SQLAlchemy. survey_metadata = util.json_column('survey_metadata', default='{}') created_on = sa.Column( pg.TIMESTAMP(timezone=True), nullable=False, server_default=current_timestamp(), ) nodes = relationship( 'SurveyNode', order_by='SurveyNode.node_number', collection_class=ordering_list('node_number'), cascade='all, delete-orphan', passive_deletes=True, ) last_update_time = util.last_update_time() __mapper_args__ = { 'polymorphic_on': survey_type, 'polymorphic_identity': 'public', } __table_args__ = ( sa.Index( 'unique_survey_title_in_default_language_per_user', sa.column(quoted_name('(title->>default_language)', quote=False)), 'creator_id', unique=True, ), sa.UniqueConstraint('id', 'containing_id', 'survey_type'), sa.UniqueConstraint('id', 'containing_id', 'languages'), util.languages_constraint('title', 'languages'), sa.CheckConstraint("languages @> ARRAY[default_language]", name='default_language_in_languages_exists'), sa.CheckConstraint("(title->>default_language) != ''", name='title_in_default_langauge_non_empty'), ) def _asdict(self) -> OrderedDict: return OrderedDict(( ('id', self.id), ('deleted', self.deleted), ('languages', self.languages), ('title', OrderedDict(sorted(self.title.items()))), ('url_slug', self.url_slug), ('default_language', self.default_language), ('survey_type', self.survey_type), ('version', self.version), ('creator_id', self.creator_id), ('creator_name', self.creator.name), ('metadata', self.survey_metadata), ('created_on', self.created_on), ('last_update_time', self.last_update_time), ('nodes', self.nodes), )) def _sequentialize(self, *, include_non_answerable=True): """Generate a pre-order traversal of this survey's nodes. https://en.wikipedia.org/wiki/Tree_traversal#Depth-first """ for node in self.nodes: if isinstance(node, NonAnswerableSurveyNode): if include_non_answerable: yield node else: # See https://bitbucket.org/ned/coveragepy/issues/198/ continue # pragma: no cover else: yield node for sub_survey in node.sub_surveys: yield from Survey._sequentialize( sub_survey, include_non_answerable=include_non_answerable)
class Bucket(Base): """A Bucket determines how to arrive at a SubSurvey. A Bucket can be a range or a Choice. """ __tablename__ = 'bucket' id = util.pk() sub_survey_id = sa.Column(pg.UUID, nullable=False) sub_survey_parent_type_constraint = sa.Column(node_type_enum, nullable=False) sub_survey_parent_survey_node_id = sa.Column(pg.UUID, nullable=False) sub_survey_parent_node_id = sa.Column(pg.UUID, nullable=False) bucket_type = sa.Column( sa.Enum( 'integer', 'decimal', 'date', 'time', 'timestamp', 'multiple_choice', name='bucket_type_name', inherit_schema=True, ), nullable=False, ) @property # pragma: no cover @abc.abstractmethod def bucket(self): """The bucket is a range or Choice. Buckets for a given SubSurvey cannot overlap. """ last_update_time = util.last_update_time() __mapper_args__ = {'polymorphic_on': bucket_type} __table_args__ = ( sa.CheckConstraint( 'bucket_type::TEXT = sub_survey_parent_type_constraint::TEXT', name='bucket_type_matches_question_type'), sa.ForeignKeyConstraint([ 'sub_survey_id', 'sub_survey_parent_type_constraint', 'sub_survey_parent_survey_node_id', 'sub_survey_parent_node_id', ], [ 'sub_survey.id', 'sub_survey.parent_type_constraint', 'sub_survey.parent_survey_node_id', 'sub_survey.parent_node_id', ], onupdate='CASCADE', ondelete='CASCADE'), sa.UniqueConstraint('id', 'sub_survey_parent_survey_node_id'), sa.UniqueConstraint('id', 'sub_survey_id', 'sub_survey_parent_survey_node_id', 'sub_survey_parent_node_id'), ) def _asdict(self) -> OrderedDict: return OrderedDict(( ('id', self.id), ('bucket_type', self.bucket_type), ('bucket', self.bucket), ))
class SubSurvey(Base): """A SubSurvey behaves like a Survey but belongs to a SurveyNode. The way to arrive at a certain SubSurvey is encoded in its buckets. """ __tablename__ = 'sub_survey' id = util.pk() sub_survey_number = sa.Column(sa.Integer, nullable=False) containing_survey_id = sa.Column(pg.UUID, nullable=False) root_survey_languages = sa.Column(pg.ARRAY(pg.TEXT, as_tuple=False), nullable=False) parent_survey_node_id = sa.Column(pg.UUID, nullable=False) parent_node_id = sa.Column(pg.UUID, nullable=False) parent_type_constraint = sa.Column(node_type_enum, nullable=False) parent_allow_multiple = sa.Column(sa.Boolean, nullable=False) buckets = relationship( 'Bucket', cascade='all, delete-orphan', passive_deletes=True, ) repeatable = sa.Column(sa.Boolean, nullable=False, server_default='false') nodes = relationship( 'SurveyNode', order_by='SurveyNode.node_number', collection_class=ordering_list('node_number'), cascade='all, delete-orphan', passive_deletes=True, ) __table_args__ = ( sa.UniqueConstraint('id', 'containing_survey_id', 'root_survey_languages', 'repeatable'), sa.UniqueConstraint('id', 'parent_type_constraint', 'parent_survey_node_id', 'parent_node_id'), sa.CheckConstraint( 'NOT parent_allow_multiple', name='allow_multiple_question_cannot_have_sub_surveys'), sa.ForeignKeyConstraint([ 'parent_survey_node_id', 'containing_survey_id', 'root_survey_languages', 'parent_type_constraint', 'parent_node_id', 'parent_allow_multiple', ], [ 'survey_node_answerable.id', 'survey_node_answerable.the_containing_survey_id', 'survey_node_answerable.the_root_survey_languages', 'survey_node_answerable.the_type_constraint', 'survey_node_answerable.the_node_id', 'survey_node_answerable.allow_multiple', ], onupdate='CASCADE', ondelete='CASCADE'), ) def _asdict(self) -> OrderedDict: is_mc = self.parent_type_constraint == 'multiple_choice' bucket_name = 'choice_id' if is_mc else 'bucket' return OrderedDict(( ('deleted', self.deleted), ('buckets', [getattr(bucket, bucket_name) for bucket in self.buckets]), ('repeatable', self.repeatable), ('nodes', self.nodes), ))
def id(cls): return util.pk('node.id', 'question.id')
class Answer(Base): """An Answer is a response to a SurveyNode. An Answer can be one of an answer, an "other" response or a "don't know" response. Answer.response abstracts over these 3 possibilites. """ __tablename__ = 'answer' id = util.pk() answer_number = sa.Column(sa.Integer, nullable=False) submission_id = sa.Column(pg.UUID, nullable=False) # save_time is here so that AnswerableSurveyNode can have a list of # answers to that node ordered by save time of the submission save_time = sa.Column(pg.TIMESTAMP(timezone=True), nullable=False) survey_id = sa.Column(pg.UUID, nullable=False) survey_containing_id = sa.Column(pg.UUID, nullable=False) survey_node_containing_survey_id = sa.Column(pg.UUID, nullable=False) survey_node_id = sa.Column(pg.UUID, nullable=False) survey_node = relationship('AnswerableSurveyNode') allow_multiple = sa.Column(sa.Boolean, nullable=False) repeatable = sa.Column(sa.Boolean, nullable=False) allow_other = sa.Column(sa.Boolean, nullable=False) allow_dont_know = sa.Column(sa.Boolean, nullable=False) question_id = sa.Column(pg.UUID, nullable=False) # dokomoforms.models.column_properties # question_title type_constraint = sa.Column(node_type_enum, nullable=False) answer_type = sa.Column( sa.Enum( 'text', 'photo', 'integer', 'decimal', 'date', 'time', 'timestamp', 'location', 'facility', 'multiple_choice', name='answer_type_name', inherit_schema=True, metadata=Base.metadata, ), nullable=False, ) last_update_time = util.last_update_time() answer_metadata = util.json_column('answer_metadata', default='{}') @property # pragma: no cover @abc.abstractmethod def main_answer(self): """The representative part of a provided answer. The main_answer is the only answer for simple types (integer, text, etc.) and for other types is the part of the answer that is most important. In practice, the main_answer is special only in that all Answer models have it, which is necessary for certain constraints and for the response property. """ @property # pragma: no cover @abc.abstractmethod def answer(self): """The answer. Could be the same as main_answer in simple cases. This property is the most useful representation available of the answer. In the simplest case it is just a synonym for main_answer. It could otherwise be a dictionary or another model. """ @property # pragma: no cover @abc.abstractmethod def other(self): """A text field containing "other" responses.""" @property # pragma: no cover @abc.abstractmethod def dont_know(self): """A text field containing "don't know" responses.""" @hybrid_property def response(self) -> OrderedDict: """A dictionary that abstracts over answer, other, and dont_know. { 'type_constraint': <self.answer_type>, 'response_type': 'answer|other|dont_know', 'response': <one of self.answer, self.other, self.dont_know> } """ possible_resps = [ ('answer', self.main_answer), ('other', self.other), ('dont_know', self.dont_know), ] response_type, response = next(filter(_is_response, possible_resps)) if response_type == 'answer': if self.type_constraint == 'multiple_choice': response = { 'id': self.choice.id, 'choice_number': self.choice.choice_number, 'choice_text': self.choice.choice_text, } elif self.type_constraint == 'location': lng, lat = json_decode(self.geo_json)['coordinates'] response = {'lng': lng, 'lat': lat} elif self.type_constraint == 'facility': response = self.answer geo_json = json_decode(response['facility_location']) response['lng'], response['lat'] = geo_json['coordinates'] del response['facility_location'] elif self.type_constraint == 'photo': response = self.actual_photo_id else: response = self.answer return OrderedDict(( ('type_constraint', self.answer_type), ('response_type', response_type), ('response', response), )) @response.setter def response(self, response_dict): """Set the appropriate field using the response dict.""" response_type = response_dict['response_type'] if response_type not in {'answer', 'other', 'dont_know'}: raise NotAResponseTypeError(response_type) setattr(self, response_type, response_dict['response']) __mapper_args__ = {'polymorphic_on': answer_type} __table_args__ = ( sa.UniqueConstraint('id', 'allow_other', 'allow_dont_know'), sa.UniqueConstraint( 'id', 'allow_other', 'allow_dont_know', 'survey_node_id', 'question_id', 'submission_id', ), sa.CheckConstraint( 'survey_containing_id = survey_node_containing_survey_id'), sa.CheckConstraint('type_constraint::TEXT = answer_type::TEXT'), sa.ForeignKeyConstraint( [ 'submission_id', 'survey_containing_id', 'save_time', 'survey_id' ], [ 'submission.id', 'submission.survey_containing_id', 'submission.save_time', 'submission.survey_id' ], onupdate='CASCADE', ondelete='CASCADE', ), sa.ForeignKeyConstraint([ 'survey_node_id', 'survey_node_containing_survey_id', 'question_id', 'type_constraint', 'allow_multiple', 'repeatable', 'allow_other', 'allow_dont_know', ], [ 'survey_node_answerable.id', 'survey_node_answerable.the_containing_survey_id', 'survey_node_answerable.the_node_id', 'survey_node_answerable.the_type_constraint', 'survey_node_answerable.allow_multiple', 'survey_node_answerable.the_sub_survey_repeatable', 'survey_node_answerable.allow_other', 'survey_node_answerable.allow_dont_know', ]), sa.Index( 'only_one_answer_allowed', 'survey_node_id', 'submission_id', unique=True, postgresql_where=sa.not_(sa.or_(allow_multiple, repeatable)), ), ) def _asdict(self, mode='json') -> OrderedDict: items = ( ('id', self.id), ('deleted', self.deleted), ('answer_number', self.answer_number), ('submission_id', self.submission_id), ('save_time', self.save_time), ('survey_id', self.survey_id), ('survey_node_id', self.survey_node_id), ('question_id', self.question_id), ('type_constraint', self.type_constraint), ('last_update_time', self.last_update_time), ) if mode == 'csv': response = self.response['response'] if isinstance(response, dict): response = json_encode(response) items += ( ('main_answer', self.main_answer), ('response', response), ('response_type', self.response['response_type']), ) else: items += (('response', self.response), ) items += (('metadata', self.answer_metadata), ) return OrderedDict(items)
class AnswerableSurveyNode(SurveyNode): """Contains a Node which is answerable (.e.g, a Question).""" __tablename__ = 'survey_node_answerable' id = util.pk() the_containing_survey_id = sa.Column(pg.UUID, nullable=False) the_root_survey_languages = sa.Column(pg.ARRAY(pg.TEXT, as_tuple=True), nullable=False) the_node_id = sa.Column(pg.UUID, nullable=False) the_node_languages = sa.Column(pg.ARRAY(pg.TEXT, as_tuple=True), nullable=False) the_type_constraint = sa.Column(node_type_enum, nullable=False) the_sub_survey_repeatable = sa.Column(sa.Boolean, nullable=False) allow_multiple = sa.Column(sa.Boolean, nullable=False) allow_other = sa.Column(sa.Boolean, nullable=False) node = relationship('Question') sub_surveys = relationship( 'SubSurvey', order_by='SubSurvey.sub_survey_number', collection_class=ordering_list('sub_survey_number'), cascade='all, delete-orphan', passive_deletes=True, ) required = sa.Column(sa.Boolean, nullable=False, server_default='false') allow_dont_know = sa.Column(sa.Boolean, nullable=False, server_default='false') answers = relationship('Answer', order_by='Answer.save_time') # dokomoforms.models.column_properties # count # other functions defined in that module # min # max # sum # avg # mode # stddev_pop # stddev_samp __mapper_args__ = {'polymorphic_identity': 'answerable'} __table_args__ = ( sa.UniqueConstraint('id', 'the_containing_survey_id', 'the_root_survey_languages', 'the_type_constraint', 'the_node_id', 'allow_multiple'), sa.UniqueConstraint('id', 'the_containing_survey_id', 'the_node_id', 'the_type_constraint', 'allow_multiple', 'the_sub_survey_repeatable', 'allow_other', 'allow_dont_know'), sa.ForeignKeyConstraint([ 'id', 'the_containing_survey_id', 'the_root_survey_languages', 'the_node_id', 'the_type_constraint', 'the_sub_survey_repeatable', ], [ 'survey_node.id', 'survey_node.containing_survey_id', 'survey_node.root_survey_languages', 'survey_node.node_id', 'survey_node.type_constraint', 'survey_node.non_null_repeatable', ], onupdate='CASCADE', ondelete='CASCADE'), sa.ForeignKeyConstraint([ 'the_node_id', 'the_node_languages', 'allow_multiple', 'allow_other', ], [ 'question.id', 'question.the_languages', 'question.allow_multiple', 'question.allow_other', ]), ) def _asdict(self) -> OrderedDict: result = super()._asdict() result['required'] = self.required result['allow_dont_know'] = self.allow_dont_know if self.sub_surveys: result['sub_surveys'] = self.sub_surveys return result
class Submission(Base): """A Submission references a Survey and has a list of Answers.""" __tablename__ = 'submission' id = util.pk() submission_type = sa.Column( sa.Enum( 'public_submission', 'enumerator_only_submission', name='submission_type_enum', inherit_schema=True ), nullable=False, ) survey_id = sa.Column(pg.UUID, nullable=False) survey_containing_id = sa.Column(pg.UUID, nullable=False) survey_type = sa.Column(survey_type_enum, nullable=False) # dokomoforms.models.column_properties # survey_title # survey_default_language start_time = sa.Column(pg.TIMESTAMP(timezone=True)) save_time = sa.Column( pg.TIMESTAMP(timezone=True), nullable=False, server_default=current_timestamp(), ) submission_time = sa.Column( pg.TIMESTAMP(timezone=True), nullable=False, server_default=current_timestamp(), ) submitter_name = sa.Column(pg.TEXT, nullable=False, server_default='') submitter_email = sa.Column( pg.TEXT, sa.CheckConstraint("submitter_email ~ '^$|.*@.*'"), nullable=False, server_default='' ) answers = relationship( 'Answer', order_by='Answer.answer_number', collection_class=ordering_list('answer_number'), cascade='all, delete-orphan', passive_deletes=True, ) last_update_time = util.last_update_time() __mapper_args__ = { 'polymorphic_on': submission_type, } __table_args__ = ( sa.ForeignKeyConstraint( ['survey_id', 'survey_containing_id', 'survey_type'], ['survey.id', 'survey.containing_id', 'survey.survey_type'], onupdate='CASCADE', ondelete='CASCADE' ), sa.UniqueConstraint('id', 'survey_type'), sa.UniqueConstraint('id', 'survey_id'), sa.UniqueConstraint( 'id', 'survey_containing_id', 'save_time', 'survey_id' ), ) def _default_asdict(self) -> OrderedDict: return OrderedDict(( ('id', self.id), ('deleted', self.deleted), ('survey_id', self.survey_id), ('start_time', self.start_time), ('save_time', self.save_time), ('submission_time', self.submission_time), ('last_update_time', self.last_update_time), ('submitter_name', self.submitter_name), ('submitter_email', self.submitter_email), ('answers', [ OrderedDict( answer.response, survey_node_id=answer.survey_node_id ) for answer in self.answers ]), ))