"""A Node is either a note or a question and is independent of a Survey."""
from collections import OrderedDict
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql as pg
from sqlalchemy.orm import relationship
from sqlalchemy.ext.orderinglist import ordering_list
from sqlalchemy.ext.declarative import declared_attr
from dokomoforms.models import util, Base
from dokomoforms.exc import NoSuchNodeTypeError
node_type_enum = sa.Enum(
'text', 'photo', 'integer', 'decimal', 'date', 'time', 'timestamp',
'location', 'facility', 'multiple_choice', 'note',
name='type_constraint_name',
inherit_schema=True,
metadata=Base.metadata,
)
"""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'),
)
"""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),
))
"""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 _QuestionMixin:
@declared_attr
def id(cls):
return util.pk('node.id', 'question.id')
def _asdict(self) -> OrderedDict:
return super()._default_asdict()
[docs]class TextQuestion(_QuestionMixin, Question):
"""A text question."""
__tablename__ = 'question_text'
__mapper_args__ = {'polymorphic_identity': 'text'}
[docs]class PhotoQuestion(_QuestionMixin, Question):
"""A photo question."""
__tablename__ = 'question_photo'
__mapper_args__ = {'polymorphic_identity': 'photo'}
[docs]class IntegerQuestion(_QuestionMixin, Question):
"""A integer question."""
__tablename__ = 'question_integer'
__mapper_args__ = {'polymorphic_identity': 'integer'}
[docs]class DecimalQuestion(_QuestionMixin, Question):
"""A decimal question."""
__tablename__ = 'question_decimal'
__mapper_args__ = {'polymorphic_identity': 'decimal'}
[docs]class DateQuestion(_QuestionMixin, Question):
"""A date question."""
__tablename__ = 'question_date'
__mapper_args__ = {'polymorphic_identity': 'date'}
[docs]class TimeQuestion(_QuestionMixin, Question):
"""A time (with time zone) question."""
__tablename__ = 'question_time'
__mapper_args__ = {'polymorphic_identity': 'time'}
[docs]class TimestampQuestion(_QuestionMixin, Question):
"""A timestamp (with time zone) question."""
__tablename__ = 'question_timestamp'
__mapper_args__ = {'polymorphic_identity': 'timestamp'}
[docs]class LocationQuestion(_QuestionMixin, Question):
"""A location question."""
__tablename__ = 'question_location'
__mapper_args__ = {'polymorphic_identity': 'location'}
[docs]class FacilityQuestion(_QuestionMixin, Question):
"""A facility question."""
__tablename__ = 'question_facility'
__mapper_args__ = {'polymorphic_identity': 'facility'}
[docs]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),
))
"""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),
))
NODE_TYPES = {
'text': TextQuestion,
'photo': PhotoQuestion,
'integer': IntegerQuestion,
'decimal': DecimalQuestion,
'date': DateQuestion,
'time': TimeQuestion,
'timestamp': TimestampQuestion,
'location': LocationQuestion,
'facility': FacilityQuestion,
'multiple_choice': MultipleChoiceQuestion,
'note': Note,
}
[docs]def construct_node(*, type_constraint: str, **kwargs) -> Node:
"""Return a subclass of dokomoforms.models.node.Node.
The subclass is determined by the type_constraint parameter. This utility
function makes it easy to create an instance of a Node or Question
subclass based on external input.
See http://stackoverflow.com/q/30518484/1475412
:param type_constraint: the type constraint of the node. Must be one of the
keys of
dokomoforms.models.survey.NODE_TYPES
:param kwargs: the keyword arguments to pass to the constructor
:returns: an instance of one of the Node subtypes
:raises: dokomoforms.exc.NoSuchNodeTypeError
"""
try:
create_node = NODE_TYPES[type_constraint]
except KeyError:
raise NoSuchNodeTypeError(type_constraint)
return create_node(**kwargs)