utils.py 13 KB
Newer Older
1
# -*- coding: utf-8 -*-
2
"""Helper functions for working with Programs."""
3
import datetime
4 5
import logging

6 7
from django.core.urlresolvers import reverse
from django.utils import timezone
8
from django.utils.functional import cached_property
9
from django.utils.text import slugify
10 11 12
from opaque_keys.edx.keys import CourseKey
import pytz

13
from lms.djangoapps.certificates import api as certificate_api
14
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
15
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
16
from openedx.core.lib.edx_api_utils import get_edx_api_data
17
from student.models import CourseEnrollment
18
from util.organizations_helpers import get_organization_by_short_name
19
from xmodule.course_metadata_utils import DEFAULT_START_DATE
20 21


22
log = logging.getLogger(__name__)
23 24


25
def get_programs(user, program_id=None):
26 27 28 29
    """Given a user, get programs from the Programs service.
    Returned value is cached depending on user permissions. Staff users making requests
    against Programs will receive unpublished programs, while regular users will only receive
    published programs.
30

31 32
    Arguments:
        user (User): The user to authenticate as when requesting programs.
33

34 35 36
    Keyword Arguments:
        program_id (int): Identifies a specific program for which to retrieve data.

37 38
    Returns:
        list of dict, representing programs returned by the Programs service.
39
    """
40 41
    programs_config = ProgramsApiConfig.current()

42 43 44
    # Bypass caching for staff users, who may be creating Programs and want
    # to see them displayed immediately.
    cache_key = programs_config.CACHE_KEY if programs_config.is_cache_enabled and not user.is_staff else None
45
    return get_edx_api_data(programs_config, user, 'programs', resource_id=program_id, cache_key=cache_key)
46 47


48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
def flatten_programs(programs, course_ids):
    """Flatten the result returned by the Programs API.

    Arguments:
        programs (list): Serialized programs
        course_ids (list): Course IDs to key on.

    Returns:
        dict, programs keyed by course ID
    """
    flattened = {}

    for program in programs:
        try:
            for course_code in program['course_codes']:
                for run in course_code['run_modes']:
                    run_id = run['course_key']
                    if run_id in course_ids:
                        flattened.setdefault(run_id, []).append(program)
        except KeyError:
            log.exception('Unable to parse Programs API response: %r', program)

    return flattened


73 74 75 76 77
def get_programs_for_dashboard(user, course_keys):
    """Build a dictionary of programs, keyed by course.

    Given a user and an iterable of course keys, find all the programs relevant
    to the user's dashboard and return them in a dictionary keyed by course key.
78 79

    Arguments:
80 81 82 83 84 85
        user (User): The user to authenticate as when requesting programs.
        course_keys (list): List of course keys representing the courses in which
            the given user has active enrollments.

    Returns:
        dict, containing programs keyed by course. Empty if programs cannot be retrieved.
86
    """
87 88 89 90 91 92 93 94 95 96 97 98
    programs_config = ProgramsApiConfig.current()
    course_programs = {}

    if not programs_config.is_student_dashboard_enabled:
        log.debug('Display of programs on the student dashboard is disabled.')
        return course_programs

    programs = get_programs(user)
    if not programs:
        log.debug('No programs found for the user with ID %d.', user.id)
        return course_programs

99 100
    course_ids = [unicode(c) for c in course_keys]
    course_programs = flatten_programs(programs, course_ids)
101

102
    return course_programs
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120


def get_programs_for_credentials(user, programs_credentials):
    """ Given a user and an iterable of credentials, get corresponding programs
    data and return it as a list of dictionaries.

    Arguments:
        user (User): The user to authenticate as for requesting programs.
        programs_credentials (list): List of credentials awarded to the user
            for completion of a program.

    Returns:
        list, containing programs dictionaries.
    """
    certificate_programs = []

    programs = get_programs(user)
    if not programs:
Ahsan Ulhaq committed
121
        log.debug('No programs for user %d.', user.id)
122 123 124 125 126
        return certificate_programs

    for program in programs:
        for credential in programs_credentials:
            if program['id'] == credential['credential']['program_id']:
127
                program['credential_url'] = credential['certificate_url']
128 129 130
                certificate_programs.append(program)

    return certificate_programs
131 132


133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
def get_program_detail_url(program, marketing_root):
    """Construct the URL to be used when linking to program details.

    Arguments:
        program (dict): Representation of a program.
        marketing_root (str): Root URL used to build links to XSeries marketing pages.

    Returns:
        str, a link to program details
    """
    if ProgramsApiConfig.current().show_program_details:
        base = reverse('program_details_view', kwargs={'program_id': program['id']}).rstrip('/')
        slug = slugify(program['name'])
    else:
        base = marketing_root.rstrip('/')
        slug = program['marketing_slug']

    return '{base}/{slug}'.format(base=base, slug=slug)


153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
def get_display_category(program):
    """ Given the program, return the category of the program for display
    Arguments:
        program (Program): The program to get the display category string from

    Returns:
        string, the category for display to the user.
        Empty string if the program has no category or is null.
    """
    display_candidate = ''
    if program and program.get('category'):
        if program.get('category') == 'xseries':
            display_candidate = 'XSeries'
        else:
            display_candidate = program.get('category', '').capitalize()
    return display_candidate


171 172 173
def get_completed_courses(student):
    """
    Determine which courses have been completed by the user.
174

175 176 177
    Args:
        student:
            User object representing the student
178 179

    Returns:
180 181
        iterable of dicts with structure {'course_id': course_key, 'mode': cert_type}

182
    """
183
    all_certs = certificate_api.get_certificates_for_user(student.username)
184 185 186
    return [
        {'course_id': unicode(cert['course_key']), 'mode': cert['type']}
        for cert in all_certs
187
        if certificate_api.is_passing_status(cert['status'])
188
    ]
189 190


191 192 193 194 195 196
class ProgramProgressMeter(object):
    """Utility for gauging a user's progress towards program completion.

    Arguments:
        user (User): The user for which to find programs.
    """
197
    def __init__(self, user):
198
        self.user = user
199
        self.course_ids = None
200

201 202
        self.programs = get_programs(self.user)
        self.course_certs = get_completed_courses(self.user)
203

204 205
    @cached_property
    def engaged_programs(self):
206 207 208 209 210
        """Derive a list of programs in which the given user is engaged.

        Returns:
            list of program dicts, ordered by most recent enrollment.
        """
211 212 213 214 215 216
        enrollments = CourseEnrollment.enrollments_for_user(self.user)
        enrollments = sorted(enrollments, key=lambda e: e.created, reverse=True)
        # enrollment.course_id is really a course key ಠ_ಠ
        self.course_ids = [unicode(e.course_id) for e in enrollments]

        flattened = flatten_programs(self.programs, self.course_ids)
217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240

        engaged_programs = []
        for course_id in self.course_ids:
            for program in flattened.get(course_id, []):
                if program not in engaged_programs:
                    engaged_programs.append(program)

        return engaged_programs

    @property
    def progress(self):
        """Gauge a user's progress towards program completion.

        Returns:
            list of dict, each containing information about a user's progress
                towards completing a program.
        """
        progress = []
        for program in self.engaged_programs:
            completed, in_progress, not_started = [], [], []

            for course_code in program['course_codes']:
                name = course_code['display_name']

241
                if self._is_course_code_complete(course_code):
242
                    completed.append(name)
243
                elif self._is_course_code_in_progress(course_code):
244 245 246 247 248 249 250 251 252 253 254 255 256
                    in_progress.append(name)
                else:
                    not_started.append(name)

            progress.append({
                'id': program['id'],
                'completed': completed,
                'in_progress': in_progress,
                'not_started': not_started,
            })

        return progress

257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
    @property
    def completed_programs(self):
        """Identify programs completed by the student.

        Returns:
            list of int, each the ID of a completed program.
        """
        return [program['id'] for program in self.programs if self._is_program_complete(program)]

    def _is_program_complete(self, program):
        """Check if a user has completed a program.

        A program is completed if the user has completed all nested course codes.

        Arguments:
            program (dict): Representing the program whose completion to assess.

        Returns:
            bool, whether the program is complete.
        """
        return all(self._is_course_code_complete(course_code) for course_code in program['course_codes'])

    def _is_course_code_complete(self, course_code):
280 281
        """Check if a user has completed a course code.

282 283
        A course code is completed if the user has earned a certificate
        in the right mode for any nested run.
284 285 286 287 288 289 290

        Arguments:
            course_code (dict): Containing nested run modes.

        Returns:
            bool, whether the course code is complete.
        """
291
        return any(self._parse(run_mode) in self.course_certs for run_mode in course_code['run_modes'])
292

293
    def _is_course_code_in_progress(self, course_code):
294 295 296 297 298 299 300 301 302 303 304
        """Check if a user is in the process of completing a course code.

        A user is in the process of completing a course code if they're
        enrolled in the course.

        Arguments:
            course_code (dict): Containing nested run modes.

        Returns:
            bool, whether the course code is in progress.
        """
305
        return any(run_mode['course_key'] in self.course_ids for run_mode in course_code['run_modes'])
306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321

    def _parse(self, run_mode):
        """Modify the structure of a run mode dict.

        Arguments:
            run_mode (dict): With `course_key` and `mode_slug` keys.

        Returns:
            dict, with `course_id` and `mode` keys.
        """
        parsed = {
            'course_id': run_mode['course_key'],
            'mode': run_mode['mode_slug'],
        }

        return parsed
322 323 324 325 326 327 328 329 330


def supplement_program_data(program_data, user):
    """Supplement program course codes with CourseOverview and CourseEnrollment data.

    Arguments:
        program_data (dict): Representation of a program.
        user (User): The user whose enrollments to inspect.
    """
331 332 333 334 335 336 337
    for organization in program_data['organizations']:
        # TODO cache the results of the get_organization_by_short_name call
        # so we don't have to hit database that frequently
        org_obj = get_organization_by_short_name(organization['key'])
        if org_obj and org_obj.get('logo'):
            organization['img'] = org_obj['logo'].url

338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361
    for course_code in program_data['course_codes']:
        for run_mode in course_code['run_modes']:
            course_key = CourseKey.from_string(run_mode['course_key'])
            course_overview = CourseOverview.get_from_id(course_key)

            run_mode['course_url'] = reverse('course_root', args=[course_key])
            run_mode['course_image_url'] = course_overview.course_image_url

            human_friendly_format = '%x'
            start_date = course_overview.start or DEFAULT_START_DATE
            end_date = course_overview.end or datetime.datetime.max.replace(tzinfo=pytz.UTC)
            run_mode['start_date'] = start_date.strftime(human_friendly_format)
            run_mode['end_date'] = end_date.strftime(human_friendly_format)

            run_mode['is_enrolled'] = CourseEnrollment.is_enrolled(user, course_key)

            enrollment_start = course_overview.enrollment_start or datetime.datetime.min.replace(tzinfo=pytz.UTC)
            enrollment_end = course_overview.enrollment_end or datetime.datetime.max.replace(tzinfo=pytz.UTC)
            is_enrollment_open = enrollment_start <= timezone.now() < enrollment_end
            run_mode['is_enrollment_open'] = is_enrollment_open

            # TODO: Currently unavailable on LMS.
            run_mode['marketing_url'] = ''

362 363 364 365 366 367 368 369
            certificate_data = certificate_api.certificate_downloadable_status(user, course_key)
            certificate_uuid = certificate_data.get('uuid')
            if certificate_uuid:
                run_mode['certificate_url'] = certificate_api.get_certificate_url(
                    course_id=course_key,
                    uuid=certificate_uuid,
                )

370
    return program_data