"""Views for CRUD of activities.
In order to be as general as possible, each of the activity-specific views
tests the kind of activity it is loading and then defers to a more specific
function (for example, questions are read by read_question rather than
read_activity itself).
"""
from collections import defaultdict, OrderedDict
from flask import Blueprint, render_template, url_for, jsonify, abort, request
from flask_security import roles_required
from quizApp.models import Activity, Dataset, Question, Choice
from quizApp.forms.experiments import get_answer_form
from quizApp.forms.activities import DatasetListForm,\
ChoiceForm, get_activity_form
from quizApp.forms.common import DeleteObjectForm, ObjectTypeForm
from quizApp import db
from quizApp.views.helpers import validate_model_id
from quizApp.views.common import ObjectCollectionView, ObjectView
activities = Blueprint("activities", __name__, url_prefix="/activities")
ACTIVITY_TYPES = {
"question_mc_singleselect": "Single select multiple choice",
"question_mc_multiselect": "Multi select multiple choice",
"question_mc_singleselect_scale": "Likert scale",
"question_integer": "Integer answer",
"question_freeanswer": "Free answer",
"scorecard": "Scorecard",
}
ACTIVITY_ROUTE = "/<int:activity_id>"
CHOICES_ROUTE = "/<int:question_id>/choices/"
CHOICE_ROUTE = CHOICES_ROUTE + "<int:choice_id>"
[docs]class ActivityCollectionView(ObjectCollectionView):
"""View for the activity collection.
"""
decorators = [roles_required("experimenter")]
methods = ["GET", "POST"]
resolve_kwargs = dict
template = "activities/read_activities.html"
[docs] def get_members(self):
return Activity.query.all()
[docs] def create_member(self, create_form):
activity = Activity(type=create_form.object_type.data)
activity.save()
return {"next_url": url_for("activities.settings_activity",
activity_id=activity.id)}
activities.add_url_rule("/",
view_func=ActivityCollectionView.as_view('activities'))
[docs]class ActivityView(ObjectView):
"""Views for a particular Activity.
"""
methods = ["GET", "PUT", "DELETE"]
object_key = "activity"
[docs] def resolve_kwargs(self, activity_id):
return {"activity": validate_model_id(Activity, activity_id)}
[docs] def collection_url(self, **_):
return url_for("activities.activities")
[docs] def get(self, activity):
rendered_activity = render_activity(activity, False)
return render_template("activities/read_activity.html",
activity=activity,
rendered_activity=rendered_activity)
activities.add_url_rule(ACTIVITY_ROUTE,
view_func=ActivityView.as_view('activity'))
[docs]def render_activity(activity, *args, **kwargs):
"""Given an activity, render the template that displays this activity and
return it.
This function is the central place where activities are rendered.
Extra args/kwargs will be passed on to the specific rendering function for
the type of activity.
"""
render_mapping = {
"question_mc_singleselect": render_question,
"question_mc_multiselect": render_question,
"question_freeanswer": render_question,
"question_integer": render_question,
"question_mc_singleselect_scale": render_question,
"scorecard": render_scorecard,
}
return render_mapping[activity.type](activity, *args, **kwargs)
[docs]def render_scorecard(scorecard, disabled=False, assignment_set=None,
assignment=None, this_index=0):
"""Render a scorecard. This is the central point of rendering for
scorecards.
"""
form = get_answer_form(scorecard)
if assignment:
form.populate_from_assignment(assignment)
# Each member of scorecard data is a tuple representing the cumulative
# score and the list of assignments
scorecard_data = defaultdict(lambda: dict(score=0, assignments=[]))
if assignment_set:
# sort the previous assignments by category
for assignment in assignment_set.assignments[:this_index]:
if assignment.activity.include_in_scorecards:
category = scorecard_data[assignment.activity.category]
category["score"] += assignment.score
category["assignments"].append(assignment)
# Sort by average score
sorted_scorecard_data = OrderedDict(
sorted(scorecard_data.items(),
key=lambda x: float(x[1]["score"]) / len(x[1]["assignments"]),
reverse=True))
return render_template("activities/render_scorecard.html",
assignment_set=assignment_set,
scorecard=scorecard,
form=form,
disabled=disabled,
scorecard_data=sorted_scorecard_data)
[docs]def render_question(question, disabled=False, assignment=None,
render_explanation=True):
"""Display a given question as it would appear to a participant.
Arguments:
question (Question): The question to render
disabled (bool): if True, disable the form controls
assignment (Assignment): if given, use the assignment's result and
comment to populate fields prior to rendering
render_explanation (bool): If True, render the explanation for this
question
"""
form = get_answer_form(question)
form.populate_from_activity(question)
if assignment:
form.populate_from_assignment(assignment)
template_mapping = {
"question_mc_singleselect": "activities/render_mc_question.html",
"question_mc_multiselect": "activities/render_mc_question.html",
"question_freeanswer": "activities/render_freeanswer_question.html",
"question_integer": "activities/render_integer_question.html",
"question_mc_singleselect_scale": "activities/render_mc_question.html",
}
return render_template(template_mapping[question.type],
question=question, form=form, disabled=disabled,
render_explanation=render_explanation)
@activities.route(ACTIVITY_ROUTE + "/settings", methods=["GET"])
@roles_required("experimenter")
[docs]def settings_activity(activity_id):
"""Display settings for a particular activity.
"""
activity = validate_model_id(Activity, activity_id)
settings_function_mapping = {
"question_mc_singleselect": settings_question,
"question_mc_multiselect": settings_question,
"question_freeanswer": settings_question,
"question_mc_singleselect_scale": settings_question,
"question_integer": settings_question,
"scorecard": settings_scorecard,
}
return settings_function_mapping[activity.type](activity)
[docs]def settings_scorecard(scorecard):
"""Show settings for a scorecard.
"""
general_form = get_activity_form(scorecard, obj=scorecard)
return render_template("activities/settings_scorecard.html",
scorecard=scorecard,
general_form=general_form)
[docs]def settings_question(question):
"""Return a settings page for a question.
"""
general_form = get_activity_form(question, obj=question)
dataset_form = DatasetListForm(prefix="dataset")
dataset_form.reset_objects()
dataset_form.populate_objects(Dataset.query.all())
dataset_form.objects.default = [x.id for x in question.datasets]
dataset_form.process()
activity_type_form = ObjectTypeForm()
activity_type_form.populate_object_type(ACTIVITY_TYPES)
# These kwargs should be passed to all templates, regardless of the type of
# question.
template_kwargs = {
"question": question,
"general_form": general_form,
"activity_type_form": activity_type_form,
"dataset_form": dataset_form,
}
# If a question needs special forms on its settings page, it should extend
# ``templates/activities/settings_question.html`` and add code as
# necessary. Then it should be added here with a handler function, which
# should render the template.
settings_question_function_mapping = {
"question_mc_singleselect": settings_mc_question,
"question_mc_multiselect": settings_mc_question,
"question_mc_singleselect_scale": settings_mc_question,
}
try:
return settings_question_function_mapping[question.type](
question, template_kwargs)
except KeyError:
pass
return render_template(
"activities/settings_question.html",
**template_kwargs)
[docs]def settings_mc_question(question, template_kwargs):
"""Display settings for the given question.
"""
template_kwargs["choices"] = question.choices
template_kwargs["create_choice_form"] = ChoiceForm(prefix="create")
template_kwargs["update_choice_form"] = ChoiceForm(prefix="update")
template_kwargs["confirm_delete_choice_form"] = DeleteObjectForm()
return render_template("activities/settings_mc_question.html",
**template_kwargs)
@activities.route(ACTIVITY_ROUTE + "/datasets/<int:dataset_id>",
methods=["DELETE"])
@roles_required("experimenter")
[docs]def delete_question_dataset(activity_id, dataset_id):
"""Disassociate this question from a dataset.
"""
activity = validate_model_id(Activity, activity_id)
dataset = validate_model_id(Dataset, dataset_id)
if dataset not in activity.datasets:
abort(400)
activity.datasets.remove(dataset)
db.session.commit()
return jsonify({"success": 1})
@activities.route(ACTIVITY_ROUTE + "/datasets/", methods=["POST"])
@roles_required("experimenter")
[docs]def create_question_dataset(activity_id):
"""Associate this question with a dataset.
The request should contain the ID of the dataset to be associated.
"""
activity = validate_model_id(Activity, activity_id)
dataset_id = request.form["dataset_id"]
dataset = validate_model_id(Dataset, dataset_id)
if dataset in activity.datasets:
abort(400)
activity.datasets.append(dataset)
db.session.commit()
return jsonify({"success": 1})
[docs]class ChoiceCollectionView(ObjectCollectionView):
"""Handle a collection of choices.
"""
decorators = [roles_required("experimenter")]
methods = ["POST"]
get_members = None
template = None
[docs] def resolve_kwargs(self, question_id):
question = validate_model_id(Question, question_id)
return {"question": question}
[docs] def create_member(self, create_form, question):
choice = Choice()
create_form.populate_obj(choice)
question.choices.append(choice)
choice.save()
db.session.commit()
return {"new_row": render_template("activities/render_choice_row.html",
choice=choice)}
activities.add_url_rule(CHOICES_ROUTE,
view_func=ChoiceCollectionView.as_view('choices'))
[docs]class ChoiceView(ObjectView):
"""View for handling a singular Choice object.
"""
methods = ["PUT", "DELETE"]
object_key = "choice"
[docs] def collection_url(self, **_):
return None
[docs] def resolve_kwargs(self, question_id, choice_id):
question = validate_model_id(Question, question_id)
choice = validate_model_id(Choice, choice_id)
if choice.question != question:
abort(404)
return {"question": question, "choice": choice}
activities.add_url_rule(CHOICE_ROUTE,
view_func=ChoiceView.as_view('choice'))