From 100f6bf11e7529b13bf285f6881bdd971eaac826 Mon Sep 17 00:00:00 2001 From: Felix Sun Date: Thu, 20 Jun 2013 09:44:19 -0400 Subject: [PATCH] Began work on instructor view to hinting system. Added moderation feature - you can now choose to hold all hints for moderator approval before showing. --- .../lib/xmodule/xmodule/crowdsource_hinter.py | 65 ++++++++- lms/djangoapps/instructor/hint_manager.py | 138 ++++++++++++++++++ lms/templates/courseware/hint_manager.html | 89 +++++++++++ .../courseware/hint_manager_inner.html | 39 +++++ lms/urls.py | 3 + 5 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 lms/djangoapps/instructor/hint_manager.py create mode 100644 lms/templates/courseware/hint_manager.html create mode 100644 lms/templates/courseware/hint_manager_inner.html diff --git a/common/lib/xmodule/xmodule/crowdsource_hinter.py b/common/lib/xmodule/xmodule/crowdsource_hinter.py index 1d424b7fff..97120bbf1c 100644 --- a/common/lib/xmodule/xmodule/crowdsource_hinter.py +++ b/common/lib/xmodule/xmodule/crowdsource_hinter.py @@ -42,6 +42,14 @@ class CrowdsourceHinterFields(object): user_voted = Boolean(help='Specifies if the user has voted on this problem or not.', scope=Scope.user_state, default=False) + moderate = String(help='''If 'True', then all hints must be approved by staff before + becoming visible. + This field is automatically populated from the xml metadata.''', scope=Scope.settings, + default='False') + + mod_queue = Dict(help='''Contains hints that have not been approved by the staff yet. Structured + identically to the hints dictionary.''', scope=Scope.content, default={}) + class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): ''' An Xmodule that makes crowdsourced hints. @@ -115,7 +123,11 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): print self.hints answer = self.ans_to_text(get) # Look for a hint to give. +<<<<<<< HEAD if answer not in self.hints: +======= + if (answer not in self.hints) or (len(self.hints[answer]) == 0): +>>>>>>> Began work on instructor view to hinting system. # No hints to give. Return. self.previous_answers += [(answer, (None, None, None))] return json.dumps({'contents': ' '}) @@ -126,12 +138,23 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): if len(self.hints[answer]) == 1: rand_hint_1 = '' rand_hint_2 = '' +<<<<<<< HEAD self.previous_answers += [(answer, (0, None, None))] elif len(self.hints[answer]) == 2: best_hint = self.hints[answer][0][0] rand_hint_1 = self.hints[answer][1][0] rand_hint_2 = '' self.previous_answers += [(answer, (0, 1, None))] +======= + self.previous_answers += [[answer, [best_hint_index, None, None]]] + elif n_hints == 2: + best_hint = self.hints[answer].values()[0][0] + best_hint_index = self.hints[answer].keys()[0] + rand_hint_1 = self.hints[answer].values()[1][0] + hint_index_1 = self.hints[answer].keys()[1] + rand_hint_2 = '' + self.previous_answers += [[answer, [best_hint_index, hint_index_1, None]]] +>>>>>>> Began work on instructor view to hinting system. else: hint_index_1, hint_index_2 = random.sample(xrange(len(self.hints[answer])), 2) rand_hint_1 = self.hints[answer][hint_index_1][0] @@ -163,10 +186,20 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): '" style="display:none"> Which hint was most helpful when you got the wrong answer of '\ + answer + '?' # Add each hint to the html string, with a vote button. - for j, hint_id in enumerate(hints_offered): + for hint_id in hints_offered: if hint_id != None: +<<<<<<< HEAD out += '
' + self.hints[answer][hint_id][0] +======= + hint_id = str(hint_id) + try: + out += '
' + self.hints[answer][hint_id][0] + except KeyError: + # Sometimes, the hint that a user saw will have been deleted by the instructor. + continue +>>>>>>> Began work on instructor view to hinting system. # Or, let the student create his own hint @@ -227,15 +260,45 @@ What would you say to help someone who got this wrong answer? answer = self.previous_answers[int(get['answer'])][0] # Add the new hint to self.hints. (Awkward because a direct write # is necessary.) +<<<<<<< HEAD temp_dict = self.hints temp_dict[answer].append([hint, 1]) # With one vote (the user himself). self.hints = temp_dict +======= + if self.moderate: + temp_dict = self.mod_queue + else: + temp_dict = self.hints + if answer in temp_dict: + temp_dict[answer][self.hint_pk] = [hint, 1] # With one vote (the user himself). + else: + temp_dict[answer] = {self.hint_pk: [hint, 1]} + self.hint_pk += 1 + if self.moderate: + self.mod_queue = temp_dict + else: + self.hints = temp_dict +>>>>>>> Began work on instructor view to hinting system. # Mark the user has having voted; reset previous_answers self.user_voted = True self.previous_answers = [] return json.dumps({'contents': 'Thank you for your hint!'}) +<<<<<<< HEAD +======= + def delete_hint(self, answer, hint_id): + ''' + From the answer, delete the hint with hint_id. + Not designed to be accessed via POST request, for now. + -LIKELY DEPRECATED. + ''' + temp_hints = self.hints + del temp_hints[answer][str(hint_id)] + self.hints = temp_hints + + +>>>>>>> Began work on instructor view to hinting system. class CrowdsourceHinterDescriptor(CrowdsourceHinterFields, XmlDescriptor): module_class = CrowdsourceHinterModule stores_state = True diff --git a/lms/djangoapps/instructor/hint_manager.py b/lms/djangoapps/instructor/hint_manager.py new file mode 100644 index 0000000000..431d3f5d7c --- /dev/null +++ b/lms/djangoapps/instructor/hint_manager.py @@ -0,0 +1,138 @@ +''' +Views for hint management. +''' + +from collections import defaultdict +import csv +import json +import logging +from markupsafe import escape +import os +import re +import requests +from requests.status_codes import codes +import urllib +from collections import OrderedDict + +from StringIO import StringIO + +from django.conf import settings +from django.contrib.auth.models import User, Group +from django.http import HttpResponse, Http404 +from django_future.csrf import ensure_csrf_cookie +from django.views.decorators.cache import cache_control +from mitxmako.shortcuts import render_to_response, render_to_string +from django.core.urlresolvers import reverse + +from courseware.courses import get_course_with_access +from courseware.models import XModuleContentField + + +@ensure_csrf_cookie +def hint_manager(request, course_id): + try: + course = get_course_with_access(request.user, course_id, 'staff', depth=None) + except Http404: + out = 'Sorry, but students are not allowed to access the hint manager!' + return + if request.method == 'GET': + out = get_hints(request, course_id, 'mod_queue') + return render_to_response('courseware/hint_manager.html', out) + 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) + return + if request.POST['op'] == 'delete hints': + delete_hints(request, course_id, field) + if request.POST['op'] == 'switch fields': + pass + if request.POST['op'] == 'change votes': + change_votes(request, course_id, field) + rendered_html = render_to_string('courseware/hint_manager_inner.html', get_hints(request, course_id, field)) + return HttpResponse(json.dumps({'success': True, 'contents': rendered_html})) + + + +def get_hints(request, course_id, field): + # field indicates the database entry that we are modifying. + # Right now, the options are 'hints' or 'mod_queue'. + # DON'T TRUST field attributes that come from ajax. Use an if statement + # to make sure the field is valid before plugging into functions. + + out = '' + 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' + chopped_id = '/'.join(course_id.split('/')[:-1]) + chopped_id = re.escape(chopped_id) + all_hints = XModuleContentField.objects.filter(field_name=field, definition_id__regex=chopped_id) + for problem in all_hints: + out += '

Problem: ' + problem.definition_id + '

' + for answer, hint_dict in json.loads(problem.value).items(): + out += '

Answer: ' + answer + '

' + for pk, hint in hint_dict.items(): + out += '

' + out += '' + hint[0] + \ + '
Votes: ' + out += '

' + out += '''

Add a hint to this problem

+ Answer (exact formatting): +
Hint:


' + + + out += ' ' + render_dict = {'out': out, + 'field': field, + 'other_field': other_field, + 'field_label': field_label, + 'other_field_label': other_field_label, + 'all_hints': all_hints} + return render_dict + +def delete_hints(request, course_id, field): + ''' + Deletes the hints specified by the [problem_defn_id, answer, pk] tuples in the numbered + fields of request.POST. + ''' + for key in request.POST: + if key == 'op' or key == 'field': + continue + problem_id, answer, pk = request.POST.getlist(key) + # Can be optimized - sort the delete list by problem_id, and load each problem + # from the database only once. + this_problem = XModuleContentField.objects.get(field_name=field, definition_id=problem_id) + problem_dict = json.loads(this_problem.value) + del problem_dict[answer][pk] + this_problem.value = json.dumps(problem_dict) + this_problem.save() + +def change_votes(request, course_id, field): + ''' + Updates the number of votes. The numbered fields of request.POST contain + [problem_id, answer, pk, new_votes] tuples. + - Very similar to delete_hints. Is there a way to merge them? Nah, too complicated. + ''' + for key in request.POST: + if key == 'op' or key == 'field': + continue + problem_id, answer, pk, new_votes = request.POST.getlist(key) + this_problem = XModuleContentField.objects.get(field_name=field, definition_id=problem_id) + problem_dict = json.loads(this_problem.value) + problem_dict[answer][pk][1] = new_votes + this_problem.value = json.dumps(problem_dict) + this_problem.save() + + + + + diff --git a/lms/templates/courseware/hint_manager.html b/lms/templates/courseware/hint_manager.html new file mode 100644 index 0000000000..94156d3d68 --- /dev/null +++ b/lms/templates/courseware/hint_manager.html @@ -0,0 +1,89 @@ +<%inherit file="/main.html" /> +<%namespace name='static' file='/static_content.html'/> +<%namespace name="content" file="/courseware/hint_manager_inner.html"/> + + +<%block name="headextra"> + <%static:css group='course'/> + + + + + + + + + + +
+
+ +
+ ${content.main()} +
+ +
+
diff --git a/lms/templates/courseware/hint_manager_inner.html b/lms/templates/courseware/hint_manager_inner.html new file mode 100644 index 0000000000..41e8d018c5 --- /dev/null +++ b/lms/templates/courseware/hint_manager_inner.html @@ -0,0 +1,39 @@ +<%block name="main"> + + +

${field_label}

+Switch to ${other_field_label} + + +% for problem in all_hints: +

Problem: ${problem.definition_id}

+ <% + import json + loaded_json = json.loads(problem.value).items() + %> + % for answer, hint_dict in loaded_json: +

Answer: ${answer}

+ % for pk, hint in hint_dict.items(): +

+ ${hint[0]} +
+ Votes: +

+ % endfor + % endfor + +

Add a hint to this problem

+ Answer (exact formatting): + +
+ Hint:
+ +
+ +
+% endfor + + + + + \ No newline at end of file diff --git a/lms/urls.py b/lms/urls.py index 3e5ffea015..febd0f1c0e 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -264,6 +264,9 @@ if settings.COURSEWARE_ENABLED: url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/instructor$', 'instructor.views.instructor_dashboard', name="instructor_dashboard"), + url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/hint_manager$', + 'instructor.hint_manager.hint_manager', name="hint_manager"), + url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/gradebook$', 'instructor.views.gradebook', name='gradebook'), url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/grade_summary$',