Example #1
0
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
Example #2
0
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'),
    )
Example #3
0
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
Example #4
0
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
Example #5
0
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)
Example #6
0
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),
        ))
Example #7
0
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'}
Example #8
0
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'
        ]), )
Example #9
0
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),
        ))
Example #10
0
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'),
        )
Example #11
0
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),
        ))
Example #12
0
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),
        ))
Example #13
0
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',
        ),
    )
Example #14
0
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),
        ))
Example #15
0
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'),
    )
Example #16
0
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),
        ))
Example #17
0
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),
        ))
Example #18
0
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
Example #19
0
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)
Example #20
0
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),
        ))
Example #21
0
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),
        ))
Example #22
0
 def id(cls):
     return util.pk('node.id', 'question.id')
Example #23
0
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)
Example #24
0
 def id(cls):
     return util.pk('node.id', 'question.id')
Example #25
0
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
Example #26
0
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
            ]),
        ))