views.py 15.4 KB
Newer Older
1 2 3
import logging

from django.views.decorators.cache import cache_control
David Baumgold committed
4
from edxmako.shortcuts import render_to_response
5 6
from django.core.urlresolvers import reverse

7

Calen Pennington committed
8
from courseware.courses import get_course_with_access
9
from courseware.access import has_access
10
from courseware.tabs import EnrolledTab
11

12
from xmodule.open_ended_grading_classes.grading_service_module import GradingServiceError
13
import json
Vik Paruchuri committed
14
from student.models import unique_id_for_user
15

stv committed
16
from open_ended_grading import open_ended_notifications
17

18 19
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import search
20
from opaque_keys.edx.locations import SlashSeparatedCourseKey
cahrens committed
21
from xmodule.modulestore.exceptions import NoPathToItem
22

23
from django.http import HttpResponse, Http404, HttpResponseRedirect
24
from django.utils.translation import ugettext as _
Vik Paruchuri committed
25

26 27 28
from open_ended_grading.utils import (
    STAFF_ERROR_MESSAGE, StudentProblemList, generate_problem_url, create_controller_query_service
)
29
from xblock_django.models import XBlockDisableConfig
Calen Pennington committed
30

31
log = logging.getLogger(__name__)
Calen Pennington committed
32

33 34

def _reverse_with_slash(url_name, course_key):
35 36 37 38 39 40 41
    """
    Reverses the URL given the name and the course id, and then adds a trailing slash if
    it does not exist yet.
    @param url_name: The name of the url (eg 'staff_grading').
    @param course_id: The id of the course object (eg course.id).
    @returns: The reversed url with a trailing slash.
    """
42
    ajax_url = _reverse_without_slash(url_name, course_key)
43 44 45 46
    if not ajax_url.endswith('/'):
        ajax_url += '/'
    return ajax_url

Calen Pennington committed
47

48 49
def _reverse_without_slash(url_name, course_key):
    course_id = course_key.to_deprecated_string()
Vik Paruchuri committed
50 51
    ajax_url = reverse(url_name, kwargs={'course_id': course_id})
    return ajax_url
52

Vik Paruchuri committed
53

54
DESCRIPTION_DICT = {
55 56 57 58
    'Peer Grading': _("View all problems that require peer assessment in this particular course."),
    'Staff Grading': _("View ungraded submissions submitted by students for the open ended problems in the course."),
    'Problems you have submitted': _("View open ended problems that you have previously submitted for grading."),
    'Flagged Submissions': _("View submissions that have been flagged by students as inappropriate."),
Vik Paruchuri committed
59
}
60

61
ALERT_DICT = {
62 63 64 65
    'Peer Grading': _("New submissions to grade"),
    'Staff Grading': _("New submissions to grade"),
    'Problems you have submitted': _("New grades have been returned"),
    'Flagged Submissions': _("Submissions have been flagged for review"),
Vik Paruchuri committed
66
}
Calen Pennington committed
67

68

69
class StaffGradingTab(EnrolledTab):
70 71 72
    """
    A tab for staff grading.
    """
73
    type = 'staff_grading'
74 75 76 77 78
    title = _("Staff grading")
    view_name = "staff_grading"

    @classmethod
    def is_enabled(cls, course, user=None):  # pylint: disable=unused-argument
79 80
        if XBlockDisableConfig.is_block_type_disabled('combinedopenended'):
            return False
81 82 83 84 85
        if user and not has_access(user, 'staff', course, course.id):
            return False
        return "combinedopenended" in course.advanced_modules


86
class PeerGradingTab(EnrolledTab):
87 88 89
    """
    A tab for peer grading.
    """
90
    type = 'peer_grading'
91 92 93 94 95 96 97
    # Translators: "Peer grading" appears on a tab that allows
    # students to view open-ended problems that require grading
    title = _("Peer grading")
    view_name = "peer_grading"

    @classmethod
    def is_enabled(cls, course, user=None):  # pylint: disable=unused-argument
98 99
        if XBlockDisableConfig.is_block_type_disabled('combinedopenended'):
            return False
100 101 102 103 104
        if not super(PeerGradingTab, cls).is_enabled(course, user=user):
            return False
        return "combinedopenended" in course.advanced_modules


105
class OpenEndedGradingTab(EnrolledTab):
106 107 108
    """
    A tab for open ended grading.
    """
109
    type = 'open_ended'
110 111 112 113 114 115 116
    # Translators: "Open Ended Panel" appears on a tab that, when clicked, opens up a panel that
    # displays information about open-ended problems that a user has submitted or needs to grade
    title = _("Open Ended Panel")
    view_name = "open_ended_notifications"

    @classmethod
    def is_enabled(cls, course, user=None):  # pylint: disable=unused-argument
117 118
        if XBlockDisableConfig.is_block_type_disabled('combinedopenended'):
            return False
119 120 121 122 123
        if not super(OpenEndedGradingTab, cls).is_enabled(course, user=user):
            return False
        return "combinedopenended" in course.advanced_modules


124 125 126 127 128
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def staff_grading(request, course_id):
    """
    Show the instructor grading interface.
    """
129 130
    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
    course = get_course_with_access(request.user, 'staff', course_key)
131

132
    ajax_url = _reverse_with_slash('staff_grading', course_key)
Calen Pennington committed
133

134 135 136 137 138 139 140
    return render_to_response('instructor/staff_grading.html', {
        'course': course,
        'course_id': course_id,
        'ajax_url': ajax_url,
        # Checked above
        'staff_access': True, })

141

142
def find_peer_grading_module(course):
143 144 145 146 147
    """
    Given a course, finds the first peer grading module in it.
    @param course: A course object.
    @return: boolean found_module, string problem_url
    """
Vik Paruchuri committed
148 149

    # Reverse the base course url.
150 151 152 153
    base_course_url = reverse('courses')
    found_module = False
    problem_url = ""

Vik Paruchuri committed
154
    # Get the peer grading modules currently in the course.  Explicitly specify the course id to avoid issues with different runs.
155
    items = modulestore().get_items(course.id, qualifiers={'category': 'peergrading'})
156
    # See if any of the modules are centralized modules (ie display info from multiple problems)
157
    items = [i for i in items if not getattr(i, "use_for_single_location", True)]
Vik Paruchuri committed
158
    # Loop through all potential peer grading modules, and find the first one that has a path to it.
159
    for item in items:
Vik Paruchuri committed
160
        # Generate a url for the first module and redirect the user to it.
161
        try:
162
            problem_url_parts = search.path_to_location(modulestore(), item.location)
163
        except NoPathToItem:
Vik Paruchuri committed
164 165
            # In the case of nopathtoitem, the peer grading module that was found is in an invalid state, and
            # can no longer be accessed.  Log an informational message, but this will not impact normal behavior.
166
            log.info(u"Invalid peer grading module location %s in course %s.  This module may need to be removed.", item.location, course.id)
167
            continue
168 169 170 171
        problem_url = generate_problem_url(problem_url_parts, base_course_url)
        found_module = True

    return found_module, problem_url
Calen Pennington committed
172

173

174
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
175 176
def peer_grading(request, course_id):
    '''
177 178
    When a student clicks on the "peer grading" button in the open ended interface, link them to a peer grading
    xmodule in the course.
179
    '''
180
    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
Vik Paruchuri committed
181
    #Get the current course
182
    course = get_course_with_access(request.user, 'load', course_key)
183

184 185
    found_module, problem_url = find_peer_grading_module(course)
    if not found_module:
186
        error_message = _("""
Vik Paruchuri committed
187 188 189
        Error with initializing peer grading.
        There has not been a peer grading module created in the courseware that would allow you to grade others.
        Please check back later for this.
190 191
        """)
        log.exception(error_message + u"Current course is: {0}".format(course_id))
192
        return HttpResponse(error_message)
193

194
    return HttpResponseRedirect(problem_url)
Calen Pennington committed
195

196

197 198
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def student_problem_list(request, course_id):
199
    """
200 201 202 203 204
    Show a list of problems they have attempted to a student.
    Fetch the list from the grading controller server and append some data.
    @param request: The request object for this view.
    @param course_id: The id of the course to get the problem list for.
    @return: Renders an HTML problem list table.
205
    """
206 207
    assert isinstance(course_id, basestring)
    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
208
    # Load the course.  Don't catch any errors here, as we want them to be loud.
209
    course = get_course_with_access(request.user, 'load', course_key)
210

211 212
    # The anonymous student id is needed for communication with ORA.
    student_id = unique_id_for_user(request.user)
Vik Paruchuri committed
213
    base_course_url = reverse('courses')
214
    error_text = ""
215

216
    student_problem_list = StudentProblemList(course_key, student_id)
217 218 219 220 221 222 223 224 225 226
    # Get the problem list from ORA.
    success = student_problem_list.fetch_from_grading_service()
    # If we fetched the problem list properly, add in additional problem data.
    if success:
        # Add in links to problems.
        valid_problems = student_problem_list.add_problem_data(base_course_url)
    else:
        # Get an error message to show to the student.
        valid_problems = []
        error_text = student_problem_list.error_text
227

228
    ajax_url = _reverse_with_slash('open_ended_problems', course_key)
229

230
    context = {
231
        'course': course,
232
        'course_id': course_key.to_deprecated_string(),
233 234
        'ajax_url': ajax_url,
        'success': success,
235
        'problem_list': valid_problems,
236 237
        'error_text': error_text,
        # Checked above
238
        'staff_access': False,
239
    }
240

241
    return render_to_response('open_ended_problems/open_ended_problems.html', context)
Calen Pennington committed
242

243

244 245 246 247 248
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def flagged_problem_list(request, course_id):
    '''
    Show a student problem list
    '''
249 250
    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
    course = get_course_with_access(request.user, 'staff', course_key)
251 252 253 254 255 256

    # call problem list service
    success = False
    error_text = ""
    problem_list = []

257 258
    # Make a service that can query edX ORA.
    controller_qs = create_controller_query_service()
259
    try:
260
        problem_list_dict = controller_qs.get_flagged_problem_list(course_key)
261 262 263
        success = problem_list_dict['success']
        if 'error' in problem_list_dict:
            error_text = problem_list_dict['error']
Calen Pennington committed
264
            problem_list = []
265 266
        else:
            problem_list = problem_list_dict['flagged_submissions']
267

268
    except GradingServiceError:
269 270 271 272
        #This is a staff_facing_error
        error_text = STAFF_ERROR_MESSAGE
        #This is a dev_facing_error
        log.error("Could not get flagged problem list from external grading service for open ended.")
273 274 275
        success = False
    # catch error if if the json loads fails
    except ValueError:
276 277 278 279
        #This is a staff_facing_error
        error_text = STAFF_ERROR_MESSAGE
        #This is a dev_facing_error
        log.error("Could not parse problem list from external grading service response.")
280 281
        success = False

282
    ajax_url = _reverse_with_slash('open_ended_flagged_problems', course_key)
283
    context = {
Vik Paruchuri committed
284 285 286 287 288 289 290 291
        'course': course,
        'course_id': course_id,
        'ajax_url': ajax_url,
        'success': success,
        'problem_list': problem_list,
        'error_text': error_text,
        # Checked above
        'staff_access': True,
292 293
    }
    return render_to_response('open_ended_problems/open_ended_flagged_problems.html', context)
294

Calen Pennington committed
295

296
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
297
def combined_notifications(request, course_id):
Vik Paruchuri committed
298 299 300
    """
    Gets combined notifications from the grading controller and displays them
    """
301 302
    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
    course = get_course_with_access(request.user, 'load', course_key)
303
    user = request.user
304
    notifications = open_ended_notifications.combined_notifications(course, user)
305
    response = notifications['response']
Calen Pennington committed
306
    notification_tuples = open_ended_notifications.NOTIFICATION_TYPES
307

308
    notification_list = []
309
    for response_num in xrange(len(notification_tuples)):
Calen Pennington committed
310
        tag = notification_tuples[response_num][0]
311 312 313
        if tag in response:
            url_name = notification_tuples[response_num][1]
            human_name = notification_tuples[response_num][2]
314
            url = _reverse_without_slash(url_name, course_key)
315 316
            has_img = response[tag]

317 318 319 320 321 322 323 324 325 326
            # check to make sure we have descriptions and alert messages
            if human_name in DESCRIPTION_DICT:
                description = DESCRIPTION_DICT[human_name]
            else:
                description = ""

            if human_name in ALERT_DICT:
                alert_message = ALERT_DICT[human_name]
            else:
                alert_message = ""
Calen Pennington committed
327

328
            notification_item = {
Calen Pennington committed
329 330 331
                'url': url,
                'name': human_name,
                'alert': has_img,
332 333
                'description': description,
                'alert_message': alert_message
334
            }
335 336 337 338
            #The open ended panel will need to link the "peer grading" button in the panel to a peer grading
            #xmodule defined in the course.  This checks to see if the human name of the server notification
            #that we are currently processing is "peer grading".  If it is, it looks for a peer grading
            #module in the course.  If none exists, it removes the peer grading item from the panel.
339 340 341 342 343 344
            if human_name == "Peer Grading":
                found_module, problem_url = find_peer_grading_module(course)
                if found_module:
                    notification_list.append(notification_item)
            else:
                notification_list.append(notification_item)
345

346
    ajax_url = _reverse_with_slash('open_ended_notifications', course_key)
347
    combined_dict = {
Calen Pennington committed
348 349 350 351 352
        'error_text': "",
        'notification_list': notification_list,
        'course': course,
        'success': True,
        'ajax_url': ajax_url,
353 354
    }

355
    return render_to_response('open_ended_problems/combined_notifications.html', combined_dict)
356

Calen Pennington committed
357

Vik Paruchuri committed
358
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
359 360
def take_action_on_flags(request, course_id):
    """
Vik Paruchuri committed
361 362
    Takes action on student flagged submissions.
    Currently, only support unflag and ban actions.
363
    """
364
    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
365 366 367 368 369 370
    if request.method != 'POST':
        raise Http404

    required = ['submission_id', 'action_type', 'student_id']
    for key in required:
        if key not in request.POST:
371 372 373 374 375 376
            error_message = u'Missing key {0} from submission.  Please reload and try again.'.format(key)
            response = {
                'success': False,
                'error': STAFF_ERROR_MESSAGE + error_message
            }
            return HttpResponse(json.dumps(response), mimetype="application/json")
377 378 379 380 381

    p = request.POST
    submission_id = p['submission_id']
    action_type = p['action_type']
    student_id = p['student_id']
Vik Paruchuri committed
382 383 384
    student_id = student_id.strip(' \t\n\r')
    submission_id = submission_id.strip(' \t\n\r')
    action_type = action_type.lower().strip(' \t\n\r')
385 386 387

    # Make a service that can query edX ORA.
    controller_qs = create_controller_query_service()
388
    try:
389
        response = controller_qs.take_action_on_flags(course_key, student_id, submission_id, action_type)
390
        return HttpResponse(json.dumps(response), mimetype="application/json")
391
    except GradingServiceError:
Vik Paruchuri committed
392
        log.exception(
393
            u"Error taking action on flagged peer grading submissions, "
David Baumgold committed
394 395
            u"submission_id: {0}, action_type: {1}, grader_id: {2}"
            .format(submission_id, action_type, student_id)
396 397 398 399 400
        )
        response = {
            'success': False,
            'error': STAFF_ERROR_MESSAGE
        }
401
        return HttpResponse(json.dumps(response), mimetype="application/json")