예제 #1
0
class Choice(CompileBase, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    
    _question_id = db.Column(db.Integer, db.ForeignKey('question.id'))
    index = db.Column(db.Integer)
    _selected_id = db.Column(db.Integer, db.ForeignKey('question.id'))
    _selected_index = db.Column(db.Integer)
    _nonselected_id = db.Column(db.Integer, db.ForeignKey('question.id'))
    _nonselected_index = db.Column(db.Integer)
    
    text = db.Column(db.Text)
    label = db.Column(db.Text)
    value = db.Column(MutableType)
    debug = db.Column(FunctionType)
    
    def __init__(
            self, question=None, index=None,
            text=None, label=None, value=None, debug=None):
        self.set_question(question, index)
        self.set_all(text)
        self.value = value if value is not None else self.value
        self.label = label if label is not None else self.label
        self.debug = debug
        super().__init__()

    def set_question(self, question, index=None):
        self._set_parent(question, index, 'question', 'choices')
        
    def set_all(self, text):
        """Set text, value, and label to the same value"""
        self.text = self.label = self.value = text
    
    def compile_html(self):
        question=self.question
        classes = ' '.join(question.choice_div_classes)
        checked = 'checked' if self.is_default() else ''
        input = INPUT.format(
            cid=self.model_id, qid=question.model_id,
            type=question.choice_input_type, checked=checked
            )
        text = self.text or ''
        label = LABEL.format(cid=self.model_id, text=text)
        return DIV.format(classes=classes, input=input, label=label)
    
    def is_default(self):
        """Indicate if self is a default choice
        
        Question default is assumed to a be a choice or list of choices.
        """
        default = self.question.default
        if isinstance(default, list):
            return self in default
        return self == default
예제 #2
0
class Player(Base, db.Model):
    """Player model

    Players relate to a `Strategy` function, which is called each round by 
    the `Game` to output an `action`.
    """
    id = db.Column(db.Integer, primary_key=True)
    game_id = db.Column(db.Integer, db.ForeignKey('game.id'))

    strategy = db.relationship('Strategy', backref='player', uselist=False)

    index = db.Column(db.Integer)
    name = db.Column(db.String)
    actions = db.Column(MutableListType)
    payoffs = db.Column(MutableListType)
    cum_payoffs = db.Column(MutableListType)

    @Base.init('Player')
    def __init__(self, game=None, *args, **kwargs):
        self.actions, self.payoffs, self.cum_payoffs = [], [], []
        super().__init__()
        return {'game': game, **kwargs}

    def rewind(self, rounds=1):
        del self.actions[-rounds:]
        del self.payoffs[-rounds:]
        del self.cum_payoffs[-rounds:]
예제 #3
0
class Validator(Base, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    
    _question_id = db.Column(db.Integer, db.ForeignKey('question.id'))
    index = db.Column(db.Integer)
    validate = db.Column(FunctionType)
    
    def __init__(self, question=None, index=None, validate=None):
        self.set_question(question, index)
        self.validate = validate
        super().__init__()
    
    def set_question(self, question, index=None):
        self._set_parent(question, index, 'question', 'validators')
예제 #4
0
class Payoff(FunctionMixin, Base, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    game_id = db.Column(db.Integer, db.ForeignKey('game.id'))

    @property
    def parent(self):
        return self.game

    @parent.setter
    def parent(self, game):
        self.game = game

    @Base.init('Payoff')
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
예제 #5
0
class Strategy(FunctionMixin, Base, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    player_id = db.Column(db.Integer, db.ForeignKey('player.id'))

    @property
    def parent(self):
        return self.player

    @parent.setter
    def parent(self, player):
        self.player = player

    @Base.init('Strategy')
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
예제 #6
0
class Branch(BranchingBase, db.Model):
    id = db.Column(db.Integer, primary_key=True)

    _part_id = db.Column(db.Integer, db.ForeignKey('participant.id'))
    _part_head_id = db.Column(db.Integer, db.ForeignKey('participant.id'))
    index = db.Column(db.Integer)

    _origin_branch_id = db.Column(db.Integer, db.ForeignKey('branch.id'))
    origin_branch = db.relationship('Branch',
                                    back_populates='next_branch',
                                    uselist=False,
                                    foreign_keys='Branch._origin_branch_id')

    _next_branch_id = db.Column(db.Integer, db.ForeignKey('branch.id'))
    next_branch = db.relationship('Branch',
                                  back_populates='origin_branch',
                                  uselist=False,
                                  remote_side=id,
                                  foreign_keys='Branch._next_branch_id')

    _origin_page_id = db.Column(db.Integer, db.ForeignKey('page.id'))
    origin_page = db.relationship('Page',
                                  back_populates='next_branch',
                                  foreign_keys='Branch._origin_page_id')

    pages = db.relationship('Page',
                            backref='branch',
                            order_by='Page.index',
                            collection_class=ordering_list('index'),
                            foreign_keys='Page._branch_id')

    @property
    def start_page(self):
        """Return the start of the page queue"""
        return self.pages[0] if self.pages else None

    current_page = db.relationship('Page',
                                   uselist=False,
                                   foreign_keys='Page._branch_head_id')

    embedded = db.relationship('Embedded',
                               backref='branch',
                               order_by='Embedded.index',
                               collection_class=ordering_list('index'))

    @property
    def questions(self):
        page_questions = [
            q for p in self.pages for q in p.questions + [p.timer]
        ]
        return page_questions + self.embedded

    navigate = db.Column(FunctionType)
    _isroot = db.Column(db.Boolean)

    def __init__(self, pages=[], embedded=[], navigate=None):
        self.pages = pages
        self.embedded = embedded
        self.navigate = navigate
        self._isroot = False
        super().__init__()

    def _forward(self):
        """Advance forward to the next page in the queue"""
        if self.current_page is None:
            return
        new_head_index = self.current_page.index + 1
        if new_head_index == len(self.pages):
            self.current_page = None
            return
        self.current_page = self.pages[new_head_index]

    def _back(self):
        """Return to previous page in queue"""
        if not self.pages:
            return
        if self.current_page is None:
            self.current_page = self.pages[-1]
            return
        new_head_index = self.current_page.index - 1
        self.current_page = self.pages[new_head_index]

    def view_nav(self):
        """Print page queue for debugging purposes"""
        INDENT = '    '
        HEAD_PART = '<== head branch of participant'
        HEAD_BRANCH = '<== head page of branch'
        indent = INDENT * (0 if self.index is None else self.index)
        head_part = HEAD_PART if self == self.part.current_branch else ''
        print(indent, self, head_part)
        [p.view_nav(indent) for p in self.pages]
        head_branch = HEAD_BRANCH if None == self.current_page else ''
        print(indent, None, head_branch)
        if self.next_branch in self.part.branch_stack:
            self.next_branch.view_nav()
예제 #7
0
class Question(MutableModelBase, CompileBase, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    type = db.Column(db.String(50))
    __mapper_args__ = {
        'polymorphic_identity': 'question',
        'polymorphic_on': type
    }

    _page_id = db.Column(db.Integer, db.ForeignKey('page.id'))
    _page_timer_id = db.Column(db.Integer, db.ForeignKey('page.id'))
    index = db.Column(db.Integer)

    choice_div_classes = db.Column(MutableListType)
    choice_input_type = db.Column(db.String(50))
    choices = db.relationship('Choice',
                              backref='question',
                              order_by='Choice.index',
                              collection_class=ordering_list('index'),
                              foreign_keys='Choice._question_id')

    selected_choices = db.relationship(
        'Choice',
        order_by='Choice._selected_index',
        collection_class=ordering_list('_selected_index'),
        foreign_keys='Choice._selected_id')

    nonselected_choices = db.relationship(
        'Choice',
        order_by='Choice._nonselected_index',
        collection_class=ordering_list('_nonselected_index'),
        foreign_keys='Choice._nonselected_id')

    validators = db.relationship('Validator',
                                 backref='question',
                                 order_by='Validator.index',
                                 collection_class=ordering_list('index'))

    all_rows = db.Column(db.Boolean)
    data = db.Column(MutableType)
    _default = db.Column(MutableType)
    div_classes = db.Column(MutableListType)
    error = db.Column(db.Text)
    init_default = db.Column(MutableType)
    order = db.Column(db.Integer)
    response = db.Column(MutableType)
    text = db.Column(db.Text)
    var = db.Column(db.Text)

    compile = db.Column(FunctionType)
    debug = db.Column(FunctionType)
    post = db.Column(FunctionType)

    @property
    def default(self):
        return self._default

    @default.setter
    def default(self, default):
        """Set initial default the first time default is set"""
        if self.init_default is None:
            self.init_default = default
        self._default = default

    def __init__(self,
                 page=None,
                 index=None,
                 validators=[],
                 choice_div_classes=[],
                 choice_input_type=None,
                 choices=[],
                 all_rows=False,
                 data=None,
                 default=None,
                 div_classes=[],
                 text=None,
                 var=None,
                 compile=None,
                 debug=None,
                 post=None):
        self.set_page(page, index)
        self.choice_div_classes = choice_div_classes
        self.choice_input_type = choice_input_type
        self.choices = choices
        self.validators = validators

        self.all_rows = all_rows
        self.data = data
        self.default = default
        self.div_classes = div_classes or current_app.question_div_classes
        self.text = text
        self.var = var

        self.compile = compile or current_app.question_compile
        self.debug = debug or current_app.question_debug
        self.post = post or current_app.question_post

        super().__init__()

    """API methods"""

    def set_page(self, page, index=None):
        self._set_parent(page, index, 'page', 'questions')

    def reset_default(self):
        self.default = self.init_default

    """Methods executed during study"""

    def compile_html(self, content=''):
        """HTML compiler"""
        div_classes = self.get_div_classes()
        label = self.get_label()
        return DIV.format(id=self.model_id,
                          classes=div_classes,
                          label=label,
                          content=content)

    def get_div_classes(self):
        """Get question <div> classes
        
        Add the error class if the response was invalid.
        """
        div_classes = ' '.join(self.div_classes)
        if self.error is not None:
            return div_clases + ' error'
        return div_classes

    def get_label(self):
        """Get the question label"""
        error = self.error
        error = '' if error is None else ERROR.format(error=error)
        text = self.text if self.text is not None else ''
        return LABEL.format(id=self.model_id, text=error + text)

    def record_response(self, response):
        self.response = response

    def validate(self):
        """Validate Participant response
        
        Keep the error message associated with the first failed Validator.
        """
        for v in self.validators:
            self.error = v.validate(object=self)
            if self.error is not None:
                return False
        return True

    def record_data(self):
        self.data = self.response

    def pack_data(self, data=None):
        """Pack data for storing in DataStore
        
        Note: <var>Index is the index of the object; its order within its
        Branch, Page, or Question. <var>Order is the order of the Question
        relative to other Questions with the same variable.
        
        The optional `data` argument is prepacked data from the question type.
        """
        if self.var is None:
            return {}
        data = {self.var: self.data} if data is None else data
        if not self.all_rows:
            data[self.var + 'Order'] = self.order
        if self.index is not None:
            data[self.var + 'Index'] = self.index
        for c in self.choices:
            if c.label is not None:
                data[''.join([self.var, c.label, 'Index'])] = c.index
        return data
예제 #8
0
class Page(BranchingBase, CompileBase, db.Model):
    id = db.Column(db.Integer, primary_key=True)

    @property
    def part(self):
        return self.branch.part if self.branch is not None else None

    _branch_id = db.Column(db.Integer, db.ForeignKey('branch.id'))
    _branch_head_id = db.Column(db.Integer, db.ForeignKey('branch.id'))
    index = db.Column(db.Integer)

    next_branch = db.relationship('Branch',
                                  back_populates='origin_page',
                                  uselist=False,
                                  foreign_keys='Branch._origin_page_id')

    _back_to_id = db.Column(db.Integer, db.ForeignKey('page.id'))
    back_to = db.relationship('Page',
                              uselist=False,
                              foreign_keys='Page._back_to_id')

    _forward_to_id = db.Column(db.Integer, db.ForeignKey('page.id'))
    forward_to = db.relationship('Page',
                                 uselist=False,
                                 foreign_keys='Page._forward_to_id')

    _navbar_id = db.Column(db.Integer, db.ForeignKey('navbar.id'))

    questions = db.relationship('Question',
                                backref='page',
                                order_by='Question.index',
                                collection_class=ordering_list('index'),
                                foreign_keys='Question._page_id')

    start_time = db.Column(db.DateTime)
    timer = db.relationship('Question',
                            uselist=False,
                            foreign_keys='Question._page_timer_id')

    _back = db.Column(db.Boolean)
    back_button = db.Column(MarkupType)
    css = db.Column(MutableListType)
    _direction_from = db.Column(db.String(8))
    _direction_to = db.Column(db.String(8))
    _forward = db.Column(db.Boolean)
    forward_button = db.Column(MarkupType)
    js = db.Column(MutableListType)
    question_html = db.Column(MarkupType)
    survey_template = db.Column(db.Text)
    terminal = db.Column(db.Boolean)
    view_template = db.Column(db.Text)

    @property
    def back(self):
        return self._back and not self.first_page()

    @back.setter
    def back(self, back):
        self._back = back

    @property
    def direction_from(self):
        return self._direction_from

    @direction_from.setter
    def direction_from(self, value):
        assert value in DIRECTIONS, (
            'Direction must be one of: {}'.format(DIRECTIONS))
        self._direction_from = value

    @property
    def direction_to(self):
        return self._direction_to

    @direction_to.setter
    def direction_to(self, value):
        assert value in DIRECTIONS, (
            'Direction must be one of: {}'.format(DIRECTIONS))
        self._direction_to = value

    @property
    def forward(self):
        return self._forward and not self.terminal

    @forward.setter
    def forward(self, forward):
        self._forward = forward

    compile = db.Column(FunctionType)
    debug = db.Column(FunctionType)
    navigate = db.Column(FunctionType)
    post = db.Column(FunctionType)

    def __init__(self,
                 branch=None,
                 index=None,
                 back_to=None,
                 forward_to=None,
                 nav=None,
                 questions=[],
                 timer_var=None,
                 all_rows=False,
                 back=None,
                 back_button=None,
                 css=None,
                 forward=True,
                 forward_button=None,
                 js=None,
                 survey_template=None,
                 terminal=False,
                 view_template=None,
                 compile=None,
                 debug=None,
                 navigate=None,
                 post=None):
        self.set_branch(branch, index)
        self.back_to = back_to
        self.forward_to = forward_to
        self.nav = nav or current_app.nav
        self.questions = questions
        self.timer = Question(all_rows=all_rows, data=0, var=timer_var)

        self.back = back if back is not None else current_app.back
        self.back_button = back_button or current_app.back_button
        self.css = css or current_app.css
        self.forward = forward if forward is not None else current_app.forward
        self.forward_button = forward_button or current_app.forward_button
        self.js = js or current_app.js
        self.survey_template = survey_template or current_app.survey_template
        self.terminal = terminal
        self.view_template = view_template or current_app.view_template

        self.compile = compile or current_app.page_compile
        self.debug = debug or current_app.page_debug
        self.navigate = navigate
        self.post = post or current_app.page_post

        super().__init__()

    """API methods"""

    def set_branch(self, branch, index=None):
        self._set_parent(branch, index, 'branch', 'pages')

    def is_blank(self):
        return all([q.response is None for q in self.questions])

    def reset_compile(self):
        compile = default_compile

    def reset_default(self):
        [q.reset_default() for q in self.questions]

    def reset_post(self):
        post = default_post

    def reset_timer(self):
        self.timer.data = 0

    def is_valid(self):
        return all([q.error is None for q in self.questions])

    def first_page(self):
        """Indicate that this is the first Page in the experiment"""
        return (self.branch is not None and self.branch._isroot
                and self.index == 0)

    """Methods executed during study"""

    def compile_html(self, recompile=True):
        """Compile question html"""
        if self.question_html is None or recompile:
            self.compile(object=self)
            self.question_html = Markup(''.join(
                [q.compile_html() for q in self.questions]))
        self.start_time = datetime.utcnow()
        return self.render(render_template(self.survey_template, page=self))

    def _submit(self):
        """Operations executed on page submission
        
        1. Record responses
        2. If attempting to navigate backward, there is nothing more to do
        3. If attempting to navigate forward, check for valid responses
        4. If responses are invalid, return
        5. Record data
        6. Run post function
        """
        self._update_timer()
        self.direction_from = request.form['direction']
        [
            q.record_response(request.form.getlist(q.model_id))
            for q in self.questions
        ]

        if self.direction_from == 'back':
            return 'back'
        if not all([q.validate() for q in self.questions]):
            self.direction_from = 'invalid'
            return 'invalid'
        [q.record_data() for q in self.questions]
        self.post(object=self)
        # self.direction_from is 'forward' unless changed in post function
        return self.direction_from

    def _update_timer(self):
        if self.start_time is None:
            self.start_time = datetime.utcnow()
        delta = (datetime.utcnow() - self.start_time).total_seconds()
        self.timer.data += delta

    def view_nav(self, indent):
        """Print self and next branch for debugging purposes"""
        HEAD_PART = '<== head page of participant'
        HEAD_BRANCH = '<== head page of branch'
        head_part = HEAD_PART if self == self.part.current_page else ''
        head_branch = HEAD_BRANCH if self == self.branch.current_page else ''
        print(indent, self, head_branch, head_part)
        if self.next_branch in self.part.branch_stack:
            self.next_branch.view_nav()