hint_manager.py 10.9 KB
Newer Older
1
"""
2 3
Views for hint management.

Felix Sun committed
4 5 6
Get to these views through courseurl/hint_manager.
For example: https://courses.edx.org/courses/MITx/2.01x/2013_Spring/hint_manager

7
These views will only be visible if FEATURES['ENABLE_HINTER_INSTRUCTOR_VIEW'] = True
8 9
"""

10 11 12 13 14
import json
import re

from django.http import HttpResponse, Http404
from django_future.csrf import ensure_csrf_cookie
15

David Baumgold committed
16
from edxmako.shortcuts import render_to_response, render_to_string
17 18

from courseware.courses import get_course_with_access
Calen Pennington committed
19
from courseware.models import XModuleUserStateSummaryField
20 21
import courseware.module_render as module_render
import courseware.model_data as model_data
22
from xmodule.modulestore.django import modulestore
23
from opaque_keys.edx.locations import SlashSeparatedCourseKey
24
from xmodule.modulestore.exceptions import ItemNotFoundError
25 26 27 28


@ensure_csrf_cookie
def hint_manager(request, course_id):
Felix Sun committed
29 30 31
    """
    The URL landing function for all calls to the hint manager, both POST and GET.
    """
32
    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
33
    try:
34
        get_course_with_access(request.user, 'staff', course_key, depth=None)
35 36
    except Http404:
        out = 'Sorry, but students are not allowed to access the hint manager!'
37
        return HttpResponse(out)
38
    if request.method == 'GET':
39
        out = get_hints(request, course_key, 'mod_queue')
40 41
        out.update({'error': ''})
        return render_to_response('instructor/hint_manager.html', out)
42 43 44
    field = request.POST['field']
    if not (field == 'mod_queue' or field == 'hints'):
        # Invalid field.  (Don't let users continue - they may overwrite other db's)
45 46 47
        out = 'Error in hint manager - an invalid field was accessed.'
        return HttpResponse(out)

48 49 50 51 52 53 54 55 56
    switch_dict = {
        'delete hints': delete_hints,
        'switch fields': lambda *args: None,    # Takes any number of arguments, returns None.
        'change votes': change_votes,
        'add hint': add_hint,
        'approve': approve,
    }

    # Do the operation requested, and collect any error messages.
57
    error_text = switch_dict[request.POST['op']](request, course_key, field)
58 59
    if error_text is None:
        error_text = ''
60
    render_dict = get_hints(request, course_key, field)
61 62
    render_dict.update({'error': error_text})
    rendered_html = render_to_string('instructor/hint_manager_inner.html', render_dict)
63 64 65 66
    return HttpResponse(json.dumps({'success': True, 'contents': rendered_html}))


def get_hints(request, course_id, field):
67 68 69 70
    """
    Load all of the hints submitted to the course.

    Args:
71 72 73
    `request` -- Django request object.
    `course_id` -- The course id, like 'Me/19.002/test_course'
    `field` -- Either 'hints' or 'mod_queue'; specifies which set of hints to load.
74 75

    Keys in returned dict:
76 77 78 79
        - 'field': Same as input
        - 'other_field': 'mod_queue' if `field` == 'hints'; and vice-versa.
        - 'field_label', 'other_field_label': English name for the above.
        - 'all_hints': A list of [answer, pk dict] pairs, representing all hints.
80
          Sorted by answer.
81
        - 'id_to_name': A dictionary mapping problem id to problem name.
82
    """
83 84 85 86 87 88 89 90
    if field == 'mod_queue':
        other_field = 'hints'
        field_label = 'Hints Awaiting Moderation'
        other_field_label = 'Approved Hints'
    elif field == 'hints':
        other_field = 'mod_queue'
        field_label = 'Approved Hints'
        other_field_label = 'Hints Awaiting Moderation'
Calen Pennington committed
91
    # We want to use the course_id to find all matching usage_id's.
92
    # To do this, just take the school/number part - leave off the classname.
93 94 95 96 97
    # FIXME: we need to figure out how to do this with opaque keys
    all_hints = XModuleUserStateSummaryField.objects.filter(
        field_name=field,
        usage_id__regex=re.escape(u'{0.org}/{0.course}'.format(course_id)),
    )
98
    # big_out_dict[problem id] = [[answer, {pk: [hint, votes]}], sorted by answer]
99
    # big_out_dict maps a problem id to a list of [answer, hints] pairs, sorted in order of answer.
100
    big_out_dict = {}
101 102
    # id_to name maps a problem id to the name of the problem.
    # id_to_name[problem id] = Display name of problem
103 104 105
    id_to_name = {}

    for hints_by_problem in all_hints:
106 107
        hints_by_problem.usage_id = hints_by_problem.usage_id.map_into_course(course_id)
        name = location_to_problem_name(course_id, hints_by_problem.usage_id)
108
        if name is None:
109
            continue
Calen Pennington committed
110
        id_to_name[hints_by_problem.usage_id] = name
111 112

        def answer_sorter(thing):
113
            """
114 115
            `thing` is a tuple, where `thing[0]` contains an answer, and `thing[1]` contains
            a dict of hints.  This function returns an index based on `thing[0]`, which
116
            is used as a key to sort the list of things.
117
            """
118 119 120 121 122 123
            try:
                return float(thing[0])
            except ValueError:
                # Put all non-numerical answers first.
                return float('-inf')

124 125
        # Answer list contains [answer, dict_of_hints] pairs.
        answer_list = sorted(json.loads(hints_by_problem.value).items(), key=answer_sorter)
Calen Pennington committed
126
        big_out_dict[hints_by_problem.usage_id] = answer_list
127 128

    render_dict = {'field': field,
129 130 131
                   'other_field': other_field,
                   'field_label': field_label,
                   'other_field_label': other_field_label,
132
                   'all_hints': big_out_dict,
133
                   'id_to_name': id_to_name}
134 135
    return render_dict

136

137
def location_to_problem_name(course_id, loc):
138 139 140 141 142
    """
    Given the location of a crowdsource_hinter module, try to return the name of the
    problem it wraps around.  Return None if the hinter no longer exists.
    """
    try:
143
        descriptor = modulestore().get_item(loc)
144
        return descriptor.get_children()[0].display_name
145
    except ItemNotFoundError:
146 147 148 149
        # Sometimes, the problem is no longer in the course.  Just
        # don't include said problem.
        return None

150

151
def delete_hints(request, course_id, field):
152 153 154
    """
    Deletes the hints specified.

155
    `request.POST` contains some fields keyed by integers.  Each such field contains a
156
    [problem_defn_id, answer, pk] tuple.  These tuples specify the hints to be deleted.
157

158
    Example `request.POST`:
159 160
    {'op': 'delete_hints',
     'field': 'mod_queue',
161 162
      1: ['problem_whatever', '42.0', '3'],
      2: ['problem_whatever', '32.5', '12']}
163 164
    """

165 166 167
    for key in request.POST:
        if key == 'op' or key == 'field':
            continue
168
        problem_id, answer, pk = request.POST.getlist(key)
169
        problem_key = course_id.make_usage_key_from_deprecated_string(problem_id)
170 171
        # Can be optimized - sort the delete list by problem_id, and load each problem
        # from the database only once.
172
        this_problem = XModuleUserStateSummaryField.objects.get(field_name=field, usage_id=problem_key)
173
        problem_dict = json.loads(this_problem.value)
174
        del problem_dict[answer][pk]
175 176 177
        this_problem.value = json.dumps(problem_dict)
        this_problem.save()

178

179
def change_votes(request, course_id, field):
180 181 182
    """
    Updates the number of votes.

183
    The numbered fields of `request.POST` contain [problem_id, answer, pk, new_votes] tuples.
Felix Sun committed
184 185 186 187 188 189 190
    See `delete_hints`.

    Example `request.POST`:
    {'op': 'delete_hints',
     'field': 'mod_queue',
      1: ['problem_whatever', '42.0', '3', 42],
      2: ['problem_whatever', '32.5', '12', 9001]}
191 192
    """

193 194 195
    for key in request.POST:
        if key == 'op' or key == 'field':
            continue
196
        problem_id, answer, pk, new_votes = request.POST.getlist(key)
197 198
        problem_key = course_id.make_usage_key_from_deprecated_string(problem_id)
        this_problem = XModuleUserStateSummaryField.objects.get(field_name=field, usage_id=problem_key)
199
        problem_dict = json.loads(this_problem.value)
200 201
        # problem_dict[answer][pk] points to a [hint_text, #votes] pair.
        problem_dict[answer][pk][1] = int(new_votes)
202 203 204
        this_problem.value = json.dumps(problem_dict)
        this_problem.save()

205

206
def add_hint(request, course_id, field):
207
    """
208
    Add a new hint.  `request.POST`:
209 210 211 212 213
    op
    field
    problem - The problem id
    answer - The answer to which a hint will be added
    hint - The text of the hint
214 215
    """

216
    problem_id = request.POST['problem']
217
    problem_key = course_id.make_usage_key_from_deprecated_string(problem_id)
218 219
    answer = request.POST['answer']
    hint_text = request.POST['hint']
220 221 222

    # Validate the answer.  This requires initializing the xmodules, which
    # is annoying.
223 224 225 226 227
    try:
        descriptor = modulestore().get_item(problem_key)
        descriptors = [descriptor]
    except ItemNotFoundError:
        descriptors = []
Calen Pennington committed
228
    field_data_cache = model_data.FieldDataCache(descriptors, course_id, request.user)
229
    hinter_module = module_render.get_module(request.user, request, problem_key, field_data_cache, course_id)
230 231 232 233 234
    if not hinter_module.validate_answer(answer):
        # Invalid answer.  Don't add it to the database, or else the
        # hinter will crash when we encounter it.
        return 'Error - the answer you specified is not properly formatted: ' + str(answer)

235
    this_problem = XModuleUserStateSummaryField.objects.get(field_name=field, usage_id=problem_key)
236

237
    hint_pk_entry = XModuleUserStateSummaryField.objects.get(field_name='hint_pk', usage_id=problem_key)
238 239 240 241 242
    this_pk = int(hint_pk_entry.value)
    hint_pk_entry.value = this_pk + 1
    hint_pk_entry.save()

    problem_dict = json.loads(this_problem.value)
243 244 245
    if answer not in problem_dict:
        problem_dict[answer] = {}
    problem_dict[answer][this_pk] = [hint_text, 1]
246 247 248
    this_problem.value = json.dumps(problem_dict)
    this_problem.save()

249

250
def approve(request, course_id, field):
251
    """
252 253 254
    Approve a list of hints, moving them from the mod_queue to the real
    hint list.  POST:
    op, field
255
    (some number) -> [problem, answer, pk]
Felix Sun committed
256 257

    The numbered fields are analogous to those in `delete_hints` and `change_votes`.
258 259
    """

260 261 262
    for key in request.POST:
        if key == 'op' or key == 'field':
            continue
263
        problem_id, answer, pk = request.POST.getlist(key)
264
        problem_key = course_id.make_usage_key_from_deprecated_string(problem_id)
265 266
        # Can be optimized - sort the delete list by problem_id, and load each problem
        # from the database only once.
267
        problem_in_mod = XModuleUserStateSummaryField.objects.get(field_name=field, usage_id=problem_key)
268
        problem_dict = json.loads(problem_in_mod.value)
269 270
        hint_to_move = problem_dict[answer][pk]
        del problem_dict[answer][pk]
271 272 273
        problem_in_mod.value = json.dumps(problem_dict)
        problem_in_mod.save()

274
        problem_in_hints = XModuleUserStateSummaryField.objects.get(field_name='hints', usage_id=problem_key)
275
        problem_dict = json.loads(problem_in_hints.value)
276 277 278
        if answer not in problem_dict:
            problem_dict[answer] = {}
        problem_dict[answer][pk] = hint_to_move
279
        problem_in_hints.value = json.dumps(problem_dict)
280
        problem_in_hints.save()