views.py 14.3 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 19
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey
20 21 22 23 24

from courseware.courses import get_course_with_access
from edxmako.shortcuts import render_to_response
from lms.djangoapps.django_comment_client.constants import TYPE_ENTRY
from lms.djangoapps.django_comment_client.utils import get_discussion_categories_ids, get_discussion_category_map
25
from util.json_request import JsonResponse, expect_json
26

27
from . import cohorts
28
from .models import CohortMembership, CourseUserGroup, CourseUserGroupPartitionGroup
29 30 31

log = logging.getLogger(__name__)

Calen Pennington committed
32

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

Calen Pennington committed
40

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


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


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


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


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


94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
@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
109
    get_course_with_access(request.user, 'staff', course_key)
110 111 112 113 114 115 116 117

    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)
118 119 120 121
        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)

122 123 124 125
    return JsonResponse(_get_course_cohort_settings_representation(
        cohorts.get_course_cohort_id(course_key),
        cohorts.is_course_cohorted(course_key)
    ))
126 127


128
@require_http_methods(("GET", "PUT", "POST", "PATCH"))
129
@ensure_csrf_cookie
130 131 132
@expect_json
@login_required
def cohort_handler(request, course_key_string, cohort_id=None):
133
    """
134 135 136 137 138 139 140 141 142 143 144 145 146
    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.
147
    """
Sarina Canelake committed
148
    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_key_string)
149 150 151 152 153 154 155 156 157 158 159 160
    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:
161 162 163 164 165 166 167 168
        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)
169 170 171 172
        # 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:
173 174 175
                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)
176 177
                cohort.name = name
                cohort.save()
178 179 180 181
            try:
                cohorts.set_assignment_type(cohort, assignment_type)
            except ValueError as err:
                return JsonResponse({"error": unicode(err)}, 400)
182 183
        else:
            try:
184
                cohort = cohorts.add_cohort(course_key, name, assignment_type)
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
            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))
207 208 209


@ensure_csrf_cookie
Sarina Canelake committed
210
def users_in_cohort(request, course_key_string, cohort_id):
211
    """
212 213 214 215 216 217 218 219 220 221
    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': ...}]
    }
222
    """
223
    # this is a string when we get it here
Sarina Canelake committed
224
    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_key_string)
225

226
    get_course_with_access(request.user, 'staff', course_key)
227

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

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

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

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

252
    return json_http_response({'success': True,
253 254 255
                               'page': page,
                               'num_pages': paginator.num_pages,
                               'users': user_info})
256 257


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

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

     Raises Http404 if the cohort cannot be found for the given course.
279
    """
280
    # this is a string when we get it here
Sarina Canelake committed
281
    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_key_string)
282
    get_course_with_access(request.user, 'staff', course_key)
283

284 285 286 287 288 289 290
    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
        ))
291 292 293

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

303
        try:
304 305 306 307 308 309 310 311 312
            # 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}
313 314
                changed.append(info)
            else:
315 316
                info = {'username': user.username,
                        'email': user.email}
317
                added.append(info)
318 319
        except User.DoesNotExist:
            unknown.append(username_or_email)
320 321 322 323
        except ValidationError:
            invalid.append(username_or_email)
        except ValueError:
            present.append(username_or_email)
324

325
    return json_http_response({'success': True,
326 327 328
                               'added': added,
                               'changed': changed,
                               'present': present,
329 330 331
                               'unknown': unknown,
                               'preassigned': preassigned,
                               'invalid': invalid})
332

Calen Pennington committed
333

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

    Return json dict of:

    {'success': True} or
    {'success': False,
     'msg': error_msg}
    """
346
    # this is a string when we get it here
Sarina Canelake committed
347
    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_key_string)
348
    get_course_with_access(request.user, 'staff', course_key)
349 350 351

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

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

362 363 364 365 366 367 368 369 370
    try:
        membership = CohortMembership.objects.get(user=user, course_id=course_key)
        membership.delete()

    except CohortMembership.DoesNotExist:
        pass

    return json_http_response({'success': True})

371

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

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