views.py 14 KB
Newer Older
1 2 3 4
"""
Views related to course groups functionality.
"""

5 6 7 8
import logging
import re

from django.contrib.auth.decorators import login_required
9
from django.contrib.auth.models import User
10
from django.core.exceptions import ValidationError
11
from django.core.paginator import EmptyPage, Paginator
12
from django.core.urlresolvers import reverse
13
from django.db import transaction
14
from django.http import Http404, HttpResponseBadRequest
15
from django.utils.translation import ugettext
16 17
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.decorators.http import require_http_methods, require_POST
18
from opaque_keys.edx.keys import CourseKey
19 20 21

from courseware.courses import get_course_with_access
from edxmako.shortcuts import render_to_response
22
from util.json_request import JsonResponse, expect_json
23

24
from . import cohorts
25
from .models import CohortMembership, CourseUserGroup, CourseUserGroupPartitionGroup
26 27 28

log = logging.getLogger(__name__)

Calen Pennington committed
29

30
def json_http_response(data):
31
    """
32 33
    Return an HttpResponse with the data json-serialized and the right content
    type header.
34
    """
35
    return JsonResponse(data)
36

Calen Pennington committed
37

38
def split_by_comma_and_whitespace(cstr):
39
    """
40
    Split a string both by commas and whitespace.  Returns a list.
41
    """
42
    return re.split(r'[\s,]+', cstr)
43 44


45
def link_cohort_to_partition_group(cohort, partition_id, group_id):
46
    """
47
    Create cohort to partition_id/group_id link.
48
    """
49 50 51 52 53
    CourseUserGroupPartitionGroup(
        course_user_group=cohort,
        partition_id=partition_id,
        group_id=group_id,
    ).save()
54 55


56 57 58 59 60
def unlink_cohort_partition_group(cohort):
    """
    Remove any existing cohort to partition_id/group_id link.
    """
    CourseUserGroupPartitionGroup.objects.filter(course_user_group=cohort).delete()
61 62


63
# pylint: disable=invalid-name
64
def _get_course_cohort_settings_representation(cohort_id, is_cohorted):
65 66 67
    """
    Returns a JSON representation of a course cohort settings.
    """
68 69 70 71 72 73
    return {
        'id': cohort_id,
        'is_cohorted': is_cohorted,
    }


74 75 76 77 78
def _get_cohort_representation(cohort, course):
    """
    Returns a JSON representation of a cohort.
    """
    group_id, partition_id = cohorts.get_group_info_for_cohort(cohort)
79
    assignment_type = cohorts.get_assignment_type(cohort)
80 81 82
    return {
        'name': cohort.name,
        'id': cohort.id,
83 84
        'user_count': cohort.users.filter(courseenrollment__course_id=course.location.course_key,
                                          courseenrollment__is_active=1).count(),
85
        'assignment_type': assignment_type,
86
        'user_partition_id': partition_id,
87
        'group_id': group_id,
88
    }
89 90


91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
@require_http_methods(("GET", "PATCH"))
@ensure_csrf_cookie
@expect_json
@login_required
def course_cohort_settings_handler(request, course_key_string):
    """
    The restful handler for cohort setting requests. Requires JSON.
    This will raise 404 if user is not staff.
    GET
        Returns the JSON representation of cohort settings for the course.
    PATCH
        Updates the cohort settings for the course. Returns the JSON representation of updated settings.
    """
    course_key = CourseKey.from_string(course_key_string)
    # Although this course data is not used this method will return 404 is user is not staff
106
    get_course_with_access(request.user, 'staff', course_key)
107 108 109 110 111 112 113 114

    if request.method == 'PATCH':
        if 'is_cohorted' not in request.json:
            return JsonResponse({"error": unicode("Bad Request")}, 400)

        is_cohorted = request.json.get('is_cohorted')
        try:
            cohorts.set_course_cohorted(course_key, is_cohorted)
115 116 117 118
        except ValueError as err:
            # Note: error message not translated because it is not exposed to the user (UI prevents this state).
            return JsonResponse({"error": unicode(err)}, 400)

119 120 121 122
    return JsonResponse(_get_course_cohort_settings_representation(
        cohorts.get_course_cohort_id(course_key),
        cohorts.is_course_cohorted(course_key)
    ))
123 124


125
@require_http_methods(("GET", "PUT", "POST", "PATCH"))
126
@ensure_csrf_cookie
127 128 129
@expect_json
@login_required
def cohort_handler(request, course_key_string, cohort_id=None):
130
    """
131 132 133 134 135 136 137 138 139 140 141 142 143
    The restful handler for cohort requests. Requires JSON.
    GET
        If a cohort ID is specified, returns a JSON representation of the cohort
            (name, id, user_count, assignment_type, user_partition_id, group_id).
        If no cohort ID is specified, returns the JSON representation of all cohorts.
           This is returned as a dict with the list of cohort information stored under the
           key `cohorts`.
    PUT or POST or PATCH
        If a cohort ID is specified, updates the cohort with the specified ID. Currently the only
        properties that can be updated are `name`, `user_partition_id` and `group_id`.
        Returns the JSON representation of the updated cohort.
        If no cohort ID is specified, creates a new cohort and returns the JSON representation of the updated
        cohort.
144
    """
145
    course_key = CourseKey.from_string(course_key_string)
146 147 148 149 150 151 152 153 154 155 156 157
    course = get_course_with_access(request.user, 'staff', course_key)
    if request.method == 'GET':
        if not cohort_id:
            all_cohorts = [
                _get_cohort_representation(c, course)
                for c in cohorts.get_course_cohorts(course)
            ]
            return JsonResponse({'cohorts': all_cohorts})
        else:
            cohort = cohorts.get_cohort_by_id(course_key, cohort_id)
            return JsonResponse(_get_cohort_representation(cohort, course))
    else:
158 159 160 161 162 163 164 165
        name = request.json.get('name')
        assignment_type = request.json.get('assignment_type')
        if not name:
            # Note: error message not translated because it is not exposed to the user (UI prevents this state).
            return JsonResponse({"error": "Cohort name must be specified."}, 400)
        if not assignment_type:
            # Note: error message not translated because it is not exposed to the user (UI prevents this state).
            return JsonResponse({"error": "Assignment type must be specified."}, 400)
166 167 168 169
        # If cohort_id is specified, update the existing cohort. Otherwise, create a new cohort.
        if cohort_id:
            cohort = cohorts.get_cohort_by_id(course_key, cohort_id)
            if name != cohort.name:
170 171 172
                if cohorts.is_cohort_exists(course_key, name):
                    err_msg = ugettext("A cohort with the same name already exists.")
                    return JsonResponse({"error": unicode(err_msg)}, 400)
173 174
                cohort.name = name
                cohort.save()
175 176 177 178
            try:
                cohorts.set_assignment_type(cohort, assignment_type)
            except ValueError as err:
                return JsonResponse({"error": unicode(err)}, 400)
179 180
        else:
            try:
181
                cohort = cohorts.add_cohort(course_key, name, assignment_type)
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
            except ValueError as err:
                return JsonResponse({"error": unicode(err)}, 400)

        group_id = request.json.get('group_id')
        if group_id is not None:
            user_partition_id = request.json.get('user_partition_id')
            if user_partition_id is None:
                # Note: error message not translated because it is not exposed to the user (UI prevents this state).
                return JsonResponse(
                    {"error": "If group_id is specified, user_partition_id must also be specified."}, 400
                )
            existing_group_id, existing_partition_id = cohorts.get_group_info_for_cohort(cohort)
            if group_id != existing_group_id or user_partition_id != existing_partition_id:
                unlink_cohort_partition_group(cohort)
                link_cohort_to_partition_group(cohort, user_partition_id, group_id)
        else:
            # If group_id was specified as None, unlink the cohort if it previously was associated with a group.
            existing_group_id, _ = cohorts.get_group_info_for_cohort(cohort)
            if existing_group_id is not None:
                unlink_cohort_partition_group(cohort)

        return JsonResponse(_get_cohort_representation(cohort, course))
204 205 206


@ensure_csrf_cookie
Sarina Canelake committed
207
def users_in_cohort(request, course_key_string, cohort_id):
208
    """
209 210 211 212 213 214 215 216 217 218
    Return users in the cohort.  Show up to 100 per page, and page
    using the 'page' GET attribute in the call.  Format:

    Returns:
        Json dump of dictionary in the following format:
        {'success': True,
         'page': page,
         'num_pages': paginator.num_pages,
         'users': [{'username': ..., 'email': ..., 'name': ...}]
    }
219
    """
220
    # this is a string when we get it here
221
    course_key = CourseKey.from_string(course_key_string)
222

223
    get_course_with_access(request.user, 'staff', course_key)
224

225
    # this will error if called with a non-int cohort_id.  That's ok--it
226
    # shouldn't happen for valid clients.
227
    cohort = cohorts.get_cohort_by_id(course_key, int(cohort_id))
228 229 230

    paginator = Paginator(cohort.users.all(), 100)
    try:
231 232
        page = int(request.GET.get('page'))
    except (TypeError, ValueError):
233 234
        # These strings aren't user-facing so don't translate them
        return HttpResponseBadRequest('Requested page must be numeric')
235 236
    else:
        if page < 0:
237
            return HttpResponseBadRequest('Requested page must be greater than zero')
238 239

    try:
240 241
        users = paginator.page(page)
    except EmptyPage:
242
        users = []  # When page > number of pages, return a blank page
243 244 245 246

    user_info = [{'username': u.username,
                  'email': u.email,
                  'name': '{0} {1}'.format(u.first_name, u.last_name)}
247
                 for u in users]
248

249
    return json_http_response({'success': True,
250 251 252
                               'page': page,
                               'num_pages': paginator.num_pages,
                               'users': user_info})
253 254


255
@transaction.non_atomic_requests
256
@ensure_csrf_cookie
257
@require_POST
Sarina Canelake committed
258
def add_users_to_cohort(request, course_key_string, cohort_id):
259
    """
260 261 262
    Return json dict of:

    {'success': True,
263 264 265 266 267 268 269
     'added': [{'username': ...,
                'name': ...,
                'email': ...}, ...],
     'changed': [{'username': ...,
                  'name': ...,
                  'email': ...,
                  'previous_cohort': ...}, ...],
270
     'present': [str1, str2, ...],    # already there
271 272 273
     'unknown': [str1, str2, ...],
     'preassigned': [str1, str2, ...],
     'invalid': [str1, str2, ...]}
274 275

     Raises Http404 if the cohort cannot be found for the given course.
276
    """
277
    # this is a string when we get it here
278
    course_key = CourseKey.from_string(course_key_string)
279
    get_course_with_access(request.user, 'staff', course_key)
280

281 282 283 284 285 286 287
    try:
        cohort = cohorts.get_cohort_by_id(course_key, cohort_id)
    except CourseUserGroup.DoesNotExist:
        raise Http404("Cohort (ID {cohort_id}) not found for {course_key_string}".format(
            cohort_id=cohort_id,
            course_key_string=course_key_string
        ))
288 289 290

    users = request.POST.get('users', '')
    added = []
291
    changed = []
292 293
    present = []
    unknown = []
294 295
    preassigned = []
    invalid = []
296
    for username_or_email in split_by_comma_and_whitespace(users):
297 298 299
        if not username_or_email:
            continue

300
        try:
301 302 303 304 305 306 307 308 309
            # A user object is only returned by add_user_to_cohort if the user already exists.
            (user, previous_cohort, preassignedCohort) = cohorts.add_user_to_cohort(cohort, username_or_email)

            if preassignedCohort:
                preassigned.append(username_or_email)
            elif previous_cohort:
                info = {'email': user.email,
                        'previous_cohort': previous_cohort,
                        'username': user.username}
310 311
                changed.append(info)
            else:
312 313
                info = {'username': user.username,
                        'email': user.email}
314
                added.append(info)
315 316
        except User.DoesNotExist:
            unknown.append(username_or_email)
317 318 319 320
        except ValidationError:
            invalid.append(username_or_email)
        except ValueError:
            present.append(username_or_email)
321

322
    return json_http_response({'success': True,
323 324 325
                               'added': added,
                               'changed': changed,
                               'present': present,
326 327 328
                               'unknown': unknown,
                               'preassigned': preassigned,
                               'invalid': invalid})
329

Calen Pennington committed
330

331
@ensure_csrf_cookie
332
@require_POST
Sarina Canelake committed
333
def remove_user_from_cohort(request, course_key_string, cohort_id):
334 335 336 337 338 339 340 341 342
    """
    Expects 'username': username in POST data.

    Return json dict of:

    {'success': True} or
    {'success': False,
     'msg': error_msg}
    """
343
    # this is a string when we get it here
344
    course_key = CourseKey.from_string(course_key_string)
345
    get_course_with_access(request.user, 'staff', course_key)
346 347 348

    username = request.POST.get('username')
    if username is None:
349
        return json_http_response({'success': False,
350
                                   'msg': 'No username specified'})
351 352 353 354 355

    try:
        user = User.objects.get(username=username)
    except User.DoesNotExist:
        log.debug('no user')
356
        return json_http_response({'success': False,
357
                                   'msg': "No user '{0}'".format(username)})
358

359 360 361 362 363 364 365 366 367
    try:
        membership = CohortMembership.objects.get(user=user, course_id=course_key)
        membership.delete()

    except CohortMembership.DoesNotExist:
        pass

    return json_http_response({'success': True})

368

Sarina Canelake committed
369
def debug_cohort_mgmt(request, course_key_string):
370 371 372
    """
    Debugging view for dev.
    """
373
    # this is a string when we get it here
374
    course_key = CourseKey.from_string(course_key_string)
375
    # add staff check to make sure it's safe if it's accidentally deployed.
376
    get_course_with_access(request.user, 'staff', course_key)
377

378
    context = {'cohorts_url': reverse(
379
        'cohorts',
380
        kwargs={'course_key': course_key.to_deprecated_string()}
381
    )}
382
    return render_to_response('/course_groups/debug.html', context)