Skip to content

Commit

Permalink
Merge pull request #66 from QuizApp-Group/develop
Browse files Browse the repository at this point in the history
Pull in master
  • Loading branch information
PlasmaSheep authored Sep 16, 2016
2 parents 9aee00e + 72fdab3 commit 82e9955
Show file tree
Hide file tree
Showing 14 changed files with 162 additions and 96 deletions.
1 change: 1 addition & 0 deletions docs/source/contents.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Contents
:maxdepth: 1

getting_started
understanding_quizapp
understanding_models
import_export
mturk
Expand Down
23 changes: 23 additions & 0 deletions docs/source/understanding_quizapp.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.. _understanding_quizapp:

###################################
Understanding the QuizApp structure
###################################

There are 3 principle parts of the quizApp system: the experiments, activities, and datasets. The experiment components handle tasks related to rendering, running, and updating experiments - this includes showing assignments to participants, saving their answers, generating experiment reports, and more. Activities is concerned primarily with rendering and managing activities. Datasets is primarily concerned with managing datasets as well as rendering and managing activities. Most components of quizApp are divided along these broad lines.

As far as application logic goes, quizApp follows a fairly typical MVC design pattern. The ``quizApp/`` directory is organized like so:

- ``forms``: Contains logic used for rendering and validating forms
- ``static``: Contains static files, like graphs, css, and js
- ``templates``: Contains template files, which specify what is displayed to the users and in what format
- ``views``: Contains view logic that interfaces with the models and sends data to templates for rendering
- ``models.py``: Database models that specify how information is stored in the database and take care of validation. More information about models is available in :ref:`understanding_models`.
- ``filters.py``: Various jinja filters that are used for formatting and rendering purposes
- ``__init__.py``: File that handles setup and initialization of the application

************
QuizApp Flow
************

In general, each view file registers a number of URLs that are handled by some function in the view file. When a user makes a request to a certain endpoint, the view function first checks authentication, then validates the URL (if applicable, e.g. checking to make sure the requested experiment exists), accesses the database (if necessary), performs any necessary processing, then sends some context variables to a template, which is rendered and shown to the user.
8 changes: 4 additions & 4 deletions quizApp/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ def get_graph_url_filter(graph):
else:
filename = current_app.config.get("EXPERIMENTS_PLACEHOLDER_GRAPH")

graph_path = url_for(
'static',
filename=os.path.join(current_app.config.get("GRAPH_DIRECTORY"),
filename))
graph_path = url_for('static',
filename=os.path.join(
current_app.config.get("GRAPH_DIRECTORY"),
filename))
return graph_path


Expand Down
10 changes: 7 additions & 3 deletions quizApp/forms/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,13 @@ def populate_object_type(self, mapping):


class OrderFormMixin(object):
'''
To apply add to Meta 'order' iterable
'''
"""This mixin allows us to set the order of fields in a ModelForm.
To use, specify a ``Meta`` class in your form class and define ``order`` as
an attribute in the ``Meta`` class.
Based on https://gist.github.com/rombr/89d4d9db0229237f40bbd46482764918/
"""
def __init__(self, *args, **kwargs):
super(OrderFormMixin, self).__init__(*args, **kwargs)

Expand Down
15 changes: 4 additions & 11 deletions quizApp/forms/datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import os
from wtforms import SubmitField, FileField
from wtforms_alchemy import ModelForm
from flask import current_app

from quizApp import db
from quizApp.forms.common import OrderFormMixin
Expand All @@ -15,16 +14,10 @@ class UploadedFileField(FileField):
"""This behaves just like FileField, however when ``populate_obj`` is
called it will overwrite the file pointed to by ``obj.name`` with the
uploaded file.
"""
file_dir = ""

def __init__(self, *args, **kwargs):
# TODO: can we move this elsewhere
self.file_dir = os.path.join(
current_app.static_folder,
current_app.config.get("GRAPH_DIRECTORY"))
super(UploadedFileField, self).__init__(*args, **kwargs)
``obj.directory`` must return the directory where the file is to be saved,
if ``getattr(obj, name)`` does not exist.
"""
def populate_obj(self, obj, name):
if not self.data:
return
Expand All @@ -36,7 +29,7 @@ def populate_obj(self, obj, name):
# Need to create a new file
file_name = str(obj.id) +\
os.path.splitext(self.data.filename)[1]
path = os.path.join(self.file_dir, file_name)
path = os.path.join(obj.directory, file_name)
setattr(obj, name, path)
db.session.commit()
self.data.save(path)
Expand Down
3 changes: 3 additions & 0 deletions quizApp/forms/experiments.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ def populate_assignment(self, assignment):
@property
def result(self):
"""Create a Result object based on this form's data.
The Result should be appropriate to the type of activity this form is
dealing with.
"""
raise NotImplementedError

Expand Down
79 changes: 35 additions & 44 deletions quizApp/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from datetime import datetime

from quizApp import db
from flask import current_app
from flask_security import UserMixin, RoleMixin


Expand Down Expand Up @@ -104,10 +105,8 @@ class Participant(User):
canvas, mechanical turk) it may be necessary to record their user
ID on the other service (e.g. preventing multiple submission). This
field holds the foreign ID of this user.
assignments (list of Assignments): List of assignments that this user
has
assignment_sets (list of AssignmentSets): List of
AssignmentSets that this participant has
assignment_sets (list of AssignmentSets): List of AssignmentSets that
this participant has
"""

opt_in = db.Column(db.Boolean)
Expand All @@ -122,15 +121,12 @@ class Participant(User):


class AssignmentSet(Base):
"""An Association Object that relates a User to an Experiment and also
stores the progress of the User in this Experiment as well as the order of
Questions that this user does.
Essentially, this tracks the progress of each User in each Experiment.
"""An AssignmentSet represents a sequence of Assignments within an
Experiment. All Assignments in an AssignmentSet are done in order by the
same Participant.
Attributes:
activities (list of Activity): Order of activities for this
user in this experiment
progress (int): Which question the user is currently working on.
progress (int): Which Assignment the user is currently working on.
complete (bool): True if the user has finalized their responses, False
otherwise
participant (Participant): Which Participant this refers to
Expand Down Expand Up @@ -195,8 +191,6 @@ class Assignment(Base):
they skipped this assignment.
Attributes:
skipped (bool): True if the Participant skipped this Question, False
otherwise
comment (string): An optional comment entered by the student.
choice_order (string): A JSON object in string form that represents the
order of choices that this participant was presented with when
Expand All @@ -206,12 +200,10 @@ class Assignment(Base):
the question being submitted.
media_items (list of MediaItem): What MediaItems should be shown
activity (Activity): Which Activity this Participant should see
choice (Choice): Which Choice this Participant chose as their answer
assignment_set (AssignmentSet): Which
AssignmentSet this Assignment belongs to
assignment_set (AssignmentSet): Which AssignmentSet this Assignment
belongs to
"""

skipped = db.Column(db.Boolean, info={"import_include": False})
comment = db.Column(db.String(500), info={"import_include": False})
choice_order = db.Column(db.String(80), info={"import_include": False})
time_to_submit = db.Column(db.Interval(), info={"import_include": False})
Expand Down Expand Up @@ -254,8 +246,7 @@ def score(self):

@db.validates("activity")
def validate_activity(self, _, activity):
"""Make sure that the activity is part of this experiment.
Make sure that the number of media items on the activity is the same as
"""Make sure that the number of media items on the activity is the same as
the number of media items this assignment has.
"""
try:
Expand Down Expand Up @@ -420,12 +411,12 @@ class Activity(Base):
they picked what they did after they answer the question.
category (string): A description of this assignment's category, for the
users' convenience.
experiments (list of Experiment): What Experiments include this
Activity
assignments (list of Assignment): What Assignments include this
Activity
scorecard_settings (ScorecardSettings): Settings for scorecards after
this Activity is done
include_in_scorecards (bool): Whether or not to show this Activity in
scorecards
"""
class Meta(object):
"""Define what kind of Result we are looking for.
Expand All @@ -444,8 +435,7 @@ class Meta(object):

scorecard_settings_id = db.Column(db.Integer,
db.ForeignKey("scorecard_settings.id"))
scorecard_settings = db.relationship("ScorecardSettings",
info={"import_include": False})
scorecard_settings = db.relationship("ScorecardSettings")

def __init__(self, *args, **kwargs):
"""Make sure to populate scorecard_settings.
Expand Down Expand Up @@ -547,20 +537,13 @@ class Question(Activity):
"label": "Number of media items to show"
})

choices = db.relationship("Choice", back_populates="question",
info={"import_include": False})
choices = db.relationship("Choice", back_populates="question")
datasets = db.relationship("Dataset", secondary=question_dataset_table,
back_populates="questions")

def __str__(self):
return self.question

def import_dict(self, **kwargs):
if "num_media_items" in kwargs:
self.num_media_items = kwargs.pop("num_media_items")

super(Question, self).import_dict(**kwargs)

__mapper_args__ = {
'polymorphic_identity': 'question',
}
Expand Down Expand Up @@ -784,7 +767,20 @@ class Graph(MediaItem):
def filename(self):
"""Return the filename of this graph.
"""
return os.path.split(os.path.basename(self.path))[1]
return os.path.basename(self.path)

@property
def directory(self):
"""Return the directory this graph is located in.
If ``path`` is not empty, return the lowest directory specified by
``path``. Otherwise, return the designated graph directory.
"""
current_directory = os.path.split(self.path)[0]
if current_directory:
return current_directory
return os.path.join(current_app.static_folder,
current_app.config.get("GRAPH_DIRECTORY"))

__mapper_args__ = {
'polymorphic_identity': 'graph'
Expand Down Expand Up @@ -843,12 +839,10 @@ class Experiment(Base):
"""An Experiment contains a list of Activities.
Attributes:
name (string
created (datetime
name (str): The name of this experiment
created (datetime): When this experiment was created
start (datetime): When this experiment becomes accessible for answers
stop (datetime): When this experiment stops accepting answers
activities (list of Activity): What Activities are included in this
Experiment's AssignmentSets
assignment_sets (list of ParticiapntExperiment): List of
AssignmentSets that are associated with this Experiment
disable_previous (bool): If True, don't allow Participants to view and
Expand All @@ -871,13 +865,12 @@ class Experiment(Base):
In addition, a scorecard will be rendered after the experiment
according to the Experiment's ``ScorecardSettings``.
flash (bool): If True, flash the MediaItem for flash_duration
milliseconds
milliseconds
flash_duration (int): How long to display the MediaItem in milliseconds
"""

name = db.Column(db.String(150), index=True, nullable=False,
info={"label": "Name"})
created = db.Column(db.DateTime, info={"import_include": False})
name = db.Column(db.String(150), nullable=False, info={"label": "Name"})
created = db.Column(db.DateTime)
start = db.Column(db.DateTime, nullable=False, info={"label": "Start"})
stop = db.Column(db.DateTime, nullable=False, info={"label": "Stop"})
blurb = db.Column(db.Text, info={"label": "Blurb"})
Expand All @@ -896,14 +889,12 @@ class Experiment(Base):
info={"label": "Show timers on activities"})

assignment_sets = db.relationship("AssignmentSet",
back_populates="experiment",
info={"import_include": False})
back_populates="experiment")

scorecard_settings_id = db.Column(db.Integer,
db.ForeignKey("scorecard_settings.id"))
scorecard_settings = db.relationship("ScorecardSettings",
uselist=False,
info={"import_include": False})
uselist=False)

def __init__(self, *args, **kwargs):
"""Make sure to populate scorecard_settings.
Expand Down
Binary file modified quizApp/static/graphs/11.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 1 addition & 2 deletions quizApp/views/activities.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ class ChoiceCollectionView(ObjectCollectionView):
decorators = [roles_required("experimenter")]
methods = ["POST"]
get_members = None
template = None

def resolve_kwargs(self, question_id):
question = validate_model_id(Question, question_id)
Expand All @@ -311,8 +312,6 @@ def resolve_kwargs(self, question_id):
def create_form(self):
return ChoiceForm(request.form, prefix="create")

template = None

def create_member(self, create_form, question):
choice = Choice()

Expand Down
Loading

0 comments on commit 82e9955

Please sign in to comment.