diff --git a/quizApp/views/experiments.py b/quizApp/views/experiments.py index 58a67f4..4754be5 100644 --- a/quizApp/views/experiments.py +++ b/quizApp/views/experiments.py @@ -1,7 +1,7 @@ """Views that handle CRUD for experiments and rendering questions for participants. """ -from collections import defaultdict +from collections import defaultdict, Counter from datetime import datetime import json import os @@ -420,7 +420,7 @@ def results_experiment(experiment_id): num_participants = Participant.query.count() num_finished = AssignmentSet.query.\ filter_by(experiment_id=experiment.id).\ - filter_by(progress=-1).count() + filter_by(complete=True).count() percent_finished = num_finished / float(num_participants) @@ -471,6 +471,28 @@ def export_results_experiment(experiment_id): attachment_filename="experiment_{}_report.xlsx".format(experiment.id)) +def get_activity_column_index(activity, activity_column_mapping, + activity_counter, headers): + """Find the column index for this occurrence of the given activity. This + will update headers, counter, and mapping if necessary. + """ + activity_occurrence = activity_counter[activity.id] + activity_counter[activity.id] += 1 + try: + return activity_column_mapping[activity.id][activity_occurrence] + except KeyError: + activity_column_mapping[activity.id] = [len(headers) + 1] + headers.append("{}: {}".format(activity.id, activity)) + headers.append("Correct?") + headers.append("Points") + except IndexError: + activity_column_mapping[activity.id].append(len(headers) + 1) + headers.append("{}: {}".format(activity.id, activity)) + headers.append("Correct?") + headers.append("Points") + return activity_column_mapping[activity.id][activity_occurrence] + + def get_results_workbook(experiment): """Analyze the assignment sets in the experiment and return an excel workbook. @@ -481,11 +503,19 @@ def get_results_workbook(experiment): next_participant_row = 2 participant_row_mapping = {} + # The same activity can appear multiple times in an assignment set. To + # display them properly, we keep a list of their ocurrences in + # activity_column_mapping, like so: + # {1: [3, 7, 10], ...} means activity 1 occurs in columns 3, 7, and 10 + # When populating a row, we will use the earliest occurrence of the + # activity possible. + workbook = openpyxl.Workbook() sheet = workbook.active sheet.title = "Experiment {} - Report".format(experiment.id) for assignment_set in assignment_sets: + activity_counter = Counter() participant = assignment_set.participant if not participant: @@ -501,35 +531,28 @@ def get_results_workbook(experiment): for assignment in assignment_set.assignments: activity = assignment.activity - - if activity.id not in activity_column_mapping: - activity_column_mapping[activity.id] = len(headers) + 1 - headers.append("{}/{}: {}".format(assignment.id, - activity.id, activity)) - headers.append("Correct?") - headers.append("Points") + activity_column_index = get_activity_column_index( + activity, + activity_column_mapping, + activity_counter, + headers) if not assignment.result: - continue + row = ["_BLANK_"] * 3 + else: + row = ["{}:{}".format(assignment.id, assignment.result), + assignment.correct, + assignment.score] populate_row_segment( sheet, participant_row_mapping[participant.id], - activity_column_mapping[activity.id], - [str(assignment.result), - assignment.correct, - assignment.score] + activity_column_index, + row ) populate_row_segment(sheet, 1, 1, headers) - # Put a special token in all blank spaces - for r in range(1, next_participant_row): - for c in range(1, len(headers) + 1): - value = sheet.cell(row=r, column=c).value - if value is None: - sheet.cell(row=r, column=c).value = "_BLANK_" - # Specify experiment ID sheet.cell(row=1, column=len(headers) + 1).value = "Experiment ID" sheet.cell(row=2, column=len(headers) + 1).value = experiment.id diff --git a/tests/test_views_experiments.py b/tests/test_views_experiments.py index a00076e..9a68004 100644 --- a/tests/test_views_experiments.py +++ b/tests/test_views_experiments.py @@ -2,6 +2,7 @@ """ from __future__ import unicode_literals from builtins import str +from collections import Counter import json import random import mock @@ -11,9 +12,10 @@ import openpyxl from quizApp import db -from quizApp.models import AssignmentSet +from quizApp.models import AssignmentSet, Activity from quizApp.views.experiments import get_next_assignment_url, \ - POST_FINALIZE_HANDLERS, validate_assignment_set, populate_row_segment + POST_FINALIZE_HANDLERS, validate_assignment_set, populate_row_segment, \ + get_activity_column_index from tests.factories import ExperimentFactory, create_experiment, \ ParticipantFactory, create_result from tests.auth import login_participant, get_participant, \ @@ -726,3 +728,25 @@ def test_export_experiment_results(client, users): url = "/experiments/{}/results/export".format(exp.id) response = client.get(url) assert response.status_code == 200 + + +def test_get_activity_column_index(): + activity = mock.MagicMock(autospec=Activity) + activity.id = 5 + activity.__str__.return_value = "" + counter = Counter() + mapping = {} + headers = [] + + get_activity_column_index(activity, mapping, counter, headers) + + assert mapping[activity.id][0] == 1 # 1-indexed due to openpyxl + assert len(mapping[activity.id]) == 1 + assert counter[activity.id] == 1 + + get_activity_column_index(activity, mapping, counter, headers) + + assert mapping[activity.id][0] == 1 + assert mapping[activity.id][1] == 4 + assert len(mapping[activity.id]) == 2 + assert counter[activity.id] == 2