Skip to content

Commit

Permalink
Add frontend visualizations for answer summary views. Clean up some u…
Browse files Browse the repository at this point in the history
…nneeded backend code and resolve some review comments.
  • Loading branch information
seanlip committed Jun 13, 2015
1 parent 5f1195e commit a7e3ed6
Show file tree
Hide file tree
Showing 37 changed files with 575 additions and 228 deletions.
32 changes: 9 additions & 23 deletions core/controllers/editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from core.domain import stats_services
from core.domain import user_services
from core.domain import value_generators_domain
from core.domain import visualization_registry
from core.platform import models
current_user_services = models.Registry.import_current_user_services()
import feconf
Expand Down Expand Up @@ -178,6 +179,8 @@ def get(self, exploration_id):

value_generators_js = VALUE_GENERATORS_JS.value

visualizations_html = visualization_registry.Registry.get_full_html()

interaction_ids = (
interaction_registry.Registry.get_all_interaction_ids())

Expand Down Expand Up @@ -235,6 +238,8 @@ def get(self, exploration_id):
for skin_id in skins_services.Registry.get_all_skin_ids()],
'skin_templates': jinja2.utils.Markup(skin_templates),
'title': exploration.title,
'visualizations_html': jinja2.utils.Markup(
visualizations_html),
'ALL_LANGUAGE_CODES': feconf.ALL_LANGUAGE_CODES,
'ALLOWED_INTERACTION_CATEGORIES': (
feconf.ALLOWED_INTERACTION_CATEGORIES),
Expand Down Expand Up @@ -622,28 +627,6 @@ def get(self, exploration_id, exploration_version):
exploration_id, exploration_version))


class AnswerSummarizersHandler(EditorHandler):
"""
Returns output of calculations performed on recorded state answers.
"""

def get(self, exploration_id, exploration_version, escaped_state_name):
"""Handles GET requests."""

try:
exp_services.get_exploration_by_id(exploration_id)
except:
raise self.PageNotFoundException

state_name = self.unescape_state_name(escaped_state_name)
calculation_outputs = (
stats_jobs.InteractionAnswerSummariesAggregator.get_calc_output(
exploration_id, exploration_version,
state_name)).calculation_outputs

self.render_json({'calculation_outputs': calculation_outputs})


class ExplorationStatsVersionsHandler(EditorHandler):
"""Returns statistics versions for an exploration."""

Expand All @@ -666,6 +649,7 @@ def get(self, exploration_id, escaped_state_name):
"""Handles GET requests."""
try:
exploration = exp_services.get_exploration_by_id(exploration_id)
current_version = exploration.version
except:
raise self.PageNotFoundException

Expand All @@ -677,7 +661,9 @@ def get(self, exploration_id, escaped_state_name):

self.render_json({
'rules_stats': stats_services.get_state_rules_stats(
exploration_id, state_name)
exploration_id, state_name),
'visualizations_info': stats_services.get_visualizations_info(
exploration_id, current_version, state_name),
})


Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# coding: utf-8
#
# Copyright 2014 The Oppia Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
Expand All @@ -12,26 +14,28 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""Registry of all interaction answer view calculations."""

__author__ = 'Marcel Schmittfull'
"""Registry for calculations."""

import copy
import inspect
import os

from extensions.answer_summarizers import models
import feconf
import utils


class Registry(object):
"""Registry of all calculations."""
"""Registry of all calculations for summarizing answers."""

# Dict mapping calculation names to calculation classes.
# Dict mapping calculation class names to their classes.
calculations_dict = {}

@classmethod
def _refresh_registry(cls):
cls.calculations_dict.clear()

# Add new calculation instances to the registry.
for name, clazz in inspect.getmembers(calculations, inspect.isclass):
# Add new visualization instances to the registry.
for name, clazz in inspect.getmembers(models, inspect.isclass):
if name.endswith('_test') or name == 'BaseCalculation':
continue

Expand All @@ -43,20 +47,15 @@ def _refresh_registry(cls):
cls.calculations_dict[clazz.__name__] = clazz

@classmethod
def get_all_calculation_classes(cls):
"""Get the dict of all calculation classes."""
cls._refresh_registry()
return copy.deepcopy(cls.calculations_dict)
def get_calculation_by_id(cls, calculation_id):
"""Gets a calculation instance by its id (which is also its class name).
@classmethod
def get_calculation_class_by_name(cls, calc_name):
"""Gets a calculation class by its name.
Refreshes once if the class is not found; subsequently, throws an
error."""
if calc_name not in cls.calculations_dict:
error.
"""
if calculation_id not in cls.calculations_dict:
cls._refresh_registry()
if calc_name not in cls.calculations_dict:
raise TypeError('\'%s\' is not a valid calculation name.' % calc_name)
return cls.calculations_dict[calc_name]

if calculation_id not in cls.calculations_dict:
raise TypeError(
'\'%s\' is not a valid calculation id.' % calculation_id)
return cls.calculations_dict[calculation_id]()
33 changes: 0 additions & 33 deletions core/domain/interaction_registry_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@

from core.domain import interaction_registry
from core.tests import test_utils
from extensions.answer_summarizers import calculations
from extensions.interactions import base


Expand Down Expand Up @@ -71,35 +70,3 @@ def test_get_all_specs(self):

self.assertEqual(
terminal_interactions_count, EXPECTED_TERMINAL_INTERACTIONS_COUNT)


class InteractionVisualizationsTests(test_utils.GenericTestBase):
"""Test the visualizations specified by interactions."""

def test_visualization_dicts(self):
"""
Check structure of all visualization_dicts.
"""
all_interactions = interaction_registry.Registry.get_all_interactions()
for interaction in all_interactions:
visualizations = interaction.answer_visualizations
for viz in visualizations:
# validate visualization dict
self.assertTrue(viz.has_key('data_source'))
self.assertTrue(viz['data_source'].has_key('calculation_id'))

def test_all_calculations_exist(self):
"""
Check that all calculation_ids specified by visualizations really
exist.
"""
available_calc_ids = []
for calc in calculations.LIST_OF_CALCULATION_CLASSES:
available_calc_ids.append(calc.calculation_id)

all_interactions = interaction_registry.Registry.get_all_interactions()
for interaction in all_interactions:
visualizations = interaction.answer_visualizations
for viz in visualizations:
self.assertTrue(viz['data_source']['calculation_id']
in available_calc_ids)
10 changes: 2 additions & 8 deletions core/domain/stats_domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,18 +204,12 @@ def validate(self):
'Expected calculation_id to be a string, received %s' %
self.calculation_id)

if not isinstance(self.calculation_output, dict):
raise utils.ValidationError(
'Expected calculation_output to be a dict, received %s' %
self.calculation_output)

output_data = self.calculation_output['data']
output_data = self.calculation_output
if not (sys.getsizeof(output_data) <=
MAX_BYTES_PER_CALC_OUTPUT_DATA):
# TODO(msl): find a better way to deal with big
# calculation output data, e.g. just skip. At the moment,
# too long answers produce a ValidationError.
raise utils.ValidationError(
"calculation_output['data'] is too big to be stored: %s" %
"calculation_output is too big to be stored: %s" %
str(output_data))

50 changes: 12 additions & 38 deletions core/domain/stats_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,17 +409,23 @@ def entity_classes_to_map_over(cls):

@staticmethod
def map(item):
from core.domain import calculation_registry
from core.domain import interaction_registry

if InteractionAnswerSummariesMRJobManager._entity_created_before_job_queued(
item):

# Get all calculations desired for interaction type of the
# current state
calculations = (
InteractionAnswerSummariesMRJobManager._get_desired_calculations(
item.interaction_id))
# All visualizations desired for the interaction.
visualizations = interaction_registry.Registry.get_interaction_by_id(
item.interaction_id).answer_visualizations

# Get all desired calculations for the current state interaction id.
calc_ids = list(set(viz.calculation_id for viz in visualizations))
calculations = [
calculation_registry.Registry.get_calculation_by_id(calc_id)
for calc_id in calc_ids]

# Perform calculations and store the output
# Perform each calculation, and store the output.
for calc in calculations:
calc_output = calc.calculate_from_state_answers_entity(item)
calc_output.save()
Expand All @@ -428,38 +434,6 @@ def map(item):
def reduce(id, state_answers_model):
pass

@classmethod
def _get_desired_calculations(cls, interaction_id):
"""
Get all calculations desired for a given interaction_id by inspecting
data_source entries of answer_visualizations.
"""

from core.domain import interaction_registry
from extensions.answer_summarizers import calculations

# All visualizations desired for the interaction.
visualizations = interaction_registry.Registry.get_interaction_by_id(
interaction_id).answer_visualizations

# All desired calculation ids.
calc_ids = []
for viz in visualizations:
if not viz['data_source']['calculation_id'] in calc_ids:
calc_ids.append(viz['data_source']['calculation_id'])

# Get calculations from their ids.
calcs = []
for calc in calculations.LIST_OF_CALCULATION_CLASSES:
if calc.calculation_id in calc_ids:
calcs.append(calc)

# Note: interaction_registry_test tests if the visualization dicts
# have the correct form and that all calculations/calculation_ids
# specified by interactions really exist.

return calcs


class InteractionAnswerSummariesRealtimeModel(
jobs.BaseRealtimeDatastoreClassForContinuousComputations):
Expand Down
25 changes: 13 additions & 12 deletions core/domain/stats_jobs_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,35 +407,36 @@ def test_one_answer(self):
# get job output of first state and check it
calc_output_domain_object = (
stats_jobs.InteractionAnswerSummariesAggregator.get_calc_output(
exp_id, exp_version, FIRST_STATE_NAME, calc_id)
)
exp_id, exp_version, FIRST_STATE_NAME, calc_id))
self.assertEqual('AnswerCounts',
calc_output_domain_object.calculation_id)

calculation_output = calc_output_domain_object.calculation_output

expected_calculation_output = {
'calculation_description': (
'Calculate answer counts for each answer.'),
'data': [['answer1', 2], ['answer2', 1]]}
expected_calculation_output = [{
'answer': 'answer1',
'frequency': 2
}, {
'answer': 'answer2',
'frequency': 1
}]

self.assertEqual(calculation_output,
expected_calculation_output)

# get job output of second state and check it
calc_output_domain_object = (
stats_jobs.InteractionAnswerSummariesAggregator.get_calc_output(
exp_id, exp_version, SECOND_STATE_NAME, calc_id)
)
exp_id, exp_version, SECOND_STATE_NAME, calc_id))

self.assertEqual('AnswerCounts',
calc_output_domain_object.calculation_id)

calculation_output = calc_output_domain_object.calculation_output

expected_calculation_output = {
'calculation_description': (
'Calculate answer counts for each answer.'),
'data': [['answer3', 1]]}
expected_calculation_output = [{
'answer': 'answer3',
'frequency': 1
}]

self.assertEqual(calculation_output, expected_calculation_output)
41 changes: 41 additions & 0 deletions core/domain/stats_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

from core.domain import exp_domain
from core.domain import exp_services
from core.domain import interaction_registry
from core.domain import stats_domain
from core.domain import stats_jobs
from core.platform import models
Expand Down Expand Up @@ -78,6 +79,46 @@ def get_state_rules_stats(exploration_id, state_name):
return results


def get_visualizations_info(exploration_id, exploration_version, state_name):
"""Returns a list of visualization info. Each item in the list is a dict
with keys 'data' and 'options'.
"""
exploration = exp_services.get_exploration_by_id(exploration_id)
if exploration.states[state_name].interaction.id is None:
return []

visualizations = interaction_registry.Registry.get_interaction_by_id(
exploration.states[state_name].interaction.id).answer_visualizations

calculation_ids = list(set([
visualization.calculation_id for visualization in visualizations]))

calculation_ids_to_outputs = {}
for calculation_id in calculation_ids:
# This is None if the calculation job has not yet been run for this
# state.
calc_output_domain_object = (
stats_jobs.InteractionAnswerSummariesAggregator.get_calc_output(
exploration_id, exploration_version, state_name, calculation_id))

# If the calculation job has not yet been run for this state, we simply
# exclude the corresponding visualization results.
if calc_output_domain_object is None:
continue

calculation_ids_to_outputs[calculation_id] = (
calc_output_domain_object.calculation_output)

results_list = [{
'id': visualization.id,
'data': calculation_ids_to_outputs[visualization.calculation_id],
'options': visualization.options,
} for visualization in visualizations if
visualization.calculation_id in calculation_ids_to_outputs]

return results_list


def get_state_improvements(exploration_id, exploration_version):
"""Returns a list of dicts, each representing a suggestion for improvement
to a particular state.
Expand Down
Loading

0 comments on commit a7e3ed6

Please sign in to comment.