Ejemplo n.º 1
0
    def _infer_course_id_try(self, location):
        """
        Create, Update, Delete operations don't require a fully-specified course_id, but
        there's no complete & sound general way to compute the course_id except via the
        proper modulestore. This method attempts several sound but not complete methods.
        :param location: an old style Location
        """
        if isinstance(location, CourseLocator):
            return location.package_id
        elif isinstance(location, basestring):
            try:
                location = Location(location)
            except InvalidLocationError:
                # try to parse as a course_id
                try:
                    Location.parse_course_id(location)
                    # it's already a course_id
                    return location
                except ValueError:
                    # cannot interpret the location
                    return None

        # location is a Location at this point
        if location.category == 'course':  # easiest case
            return location.course_id
        # try finding in loc_mapper
        try:
            # see if the loc mapper knows the course id (requires double translation)
            locator = loc_mapper().translate_location_to_course_locator(
                None, location)
            location = loc_mapper().translate_locator_to_location(
                locator, get_course=True)
            return location.course_id
        except ItemNotFoundError:
            pass
        # expensive query against all location-based modulestores to look for location.
        for store in self.modulestores.itervalues():
            if isinstance(location, store.reference_type):
                try:
                    xblock = store.get_item(location)
                    course_id = self._get_course_id_from_block(xblock, store)
                    if course_id is not None:
                        return course_id
                except NotImplementedError:
                    blocks = store.get_items(location)
                    if len(blocks) == 1:
                        block = blocks[0]
                        try:
                            return block.course_id
                        except UndefinedContext:
                            pass
                except ItemNotFoundError:
                    pass
        # if we get here, it must be in a Locator based store, but we won't be able to find
        # it.
        return None
Ejemplo n.º 2
0
 def test_parse_course_id(self):
     """
     Test the parse_course_id class method
     """
     source_string = "myorg/mycourse/myrun"
     parsed = Location.parse_course_id(source_string)
     self.assertEqual(parsed['org'], 'myorg')
     self.assertEqual(parsed['course'], 'mycourse')
     self.assertEqual(parsed['name'], 'myrun')
     with self.assertRaises(ValueError):
         Location.parse_course_id('notlegit.id/foo')
Ejemplo n.º 3
0
    def _infer_course_id_try(self, location):
        """
        Create, Update, Delete operations don't require a fully-specified course_id, but
        there's no complete & sound general way to compute the course_id except via the
        proper modulestore. This method attempts several sound but not complete methods.
        :param location: an old style Location
        """
        if isinstance(location, CourseLocator):
            return location.package_id
        elif isinstance(location, basestring):
            try:
                location = Location(location)
            except InvalidLocationError:
                # try to parse as a course_id
                try:
                    Location.parse_course_id(location)
                    # it's already a course_id
                    return location
                except ValueError:
                    # cannot interpret the location
                    return None

        # location is a Location at this point
        if location.category == 'course':  # easiest case
            return location.course_id
        # try finding in loc_mapper
        try:
            # see if the loc mapper knows the course id (requires double translation)
            locator = loc_mapper().translate_location_to_course_locator(None, location)
            location = loc_mapper().translate_locator_to_location(locator, get_course=True)
            return location.course_id
        except ItemNotFoundError:
            pass
        # expensive query against all location-based modulestores to look for location.
        for store in self.modulestores.itervalues():
            if isinstance(location, store.reference_type):
                try:
                    xblock = store.get_item(location)
                    course_id = self._get_course_id_from_block(xblock, store)
                    if course_id is not None:
                        return course_id
                except NotImplementedError:
                    blocks = store.get_items(location)
                    if len(blocks) == 1:
                        block = blocks[0]
                        try:
                            return block.course_id
                        except UndefinedContext:
                            pass
                except ItemNotFoundError:
                    pass
        # if we get here, it must be in a Locator based store, but we won't be able to find
        # it.
        return None
 def test_parse_course_id(self):
     """
     Test the parse_course_id class method
     """
     source_string = "myorg/mycourse/myrun"
     parsed = Location.parse_course_id(source_string)
     self.assertEqual(parsed["org"], "myorg")
     self.assertEqual(parsed["course"], "mycourse")
     self.assertEqual(parsed["name"], "myrun")
     with self.assertRaises(ValueError):
         Location.parse_course_id("notlegit.id/foo")
Ejemplo n.º 5
0
    def handle(self, *args, **options):
        "Execute the command"
        if len(args) != 2:
            raise CommandError("clone requires 2 arguments: <source-course_id> <dest-course_id>")

        source_course_id = args[0]
        dest_course_id = args[1]

        mstore = modulestore('direct')
        cstore = contentstore()

        course_id_dict = Location.parse_course_id(dest_course_id)
        mstore.ignore_write_events_on_courses.append('{org}/{course}'.format(**course_id_dict))

        print("Cloning course {0} to {1}".format(source_course_id, dest_course_id))

        source_location = CourseDescriptor.id_to_location(source_course_id)
        dest_location = CourseDescriptor.id_to_location(dest_course_id)

        if clone_course(mstore, cstore, source_location, dest_location):
            # be sure to recompute metadata inheritance after all those updates
            mstore.refresh_cached_metadata_inheritance_tree(dest_location)

            print("copying User permissions...")
            # purposely avoids auth.add_user b/c it doesn't have a caller to authorize
            CourseInstructorRole(dest_location).add_users(
                *CourseInstructorRole(source_location).users_with_role()
            )
            CourseStaffRole(dest_location).add_users(
                *CourseStaffRole(source_location).users_with_role()
            )
Ejemplo n.º 6
0
    def handle_grade_event(block, event_type, event):
        user_id = event.get('user_id', user.id)

        # Construct the key for the module
        key = KeyValueStore.Key(
            scope=Scope.user_state,
            user_id=user_id,
            block_scope_id=descriptor.location,
            field_name='grade'
        )

        student_module = field_data_cache.find_or_create(key)
        # Update the grades
        student_module.grade = event.get('value')
        student_module.max_grade = event.get('max_value')
        # Save all changes to the underlying KeyValueStore
        student_module.save()

        # Bin score into range and increment stats
        score_bucket = get_score_bucket(student_module.grade, student_module.max_grade)
        course_id_dict = Location.parse_course_id(course_id)

        tags = [
            u"org:{org}".format(**course_id_dict),
            u"course:{course}".format(**course_id_dict),
            u"run:{name}".format(**course_id_dict),
            u"score_bucket:{0}".format(score_bucket)
        ]

        if grade_bucket_type is not None:
            tags.append('type:%s' % grade_bucket_type)

        dog_stats_api.increment("lms.courseware.question_answered", tags=tags)
Ejemplo n.º 7
0
    def handle(self, *args, **options):
        "Execute the command"
        if len(args) != 2:
            raise CommandError(
                "clone requires 2 arguments: <source-course_id> <dest-course_id>"
            )

        source_course_id = args[0]
        dest_course_id = args[1]

        mstore = modulestore('direct')
        cstore = contentstore()

        course_id_dict = Location.parse_course_id(dest_course_id)
        mstore.ignore_write_events_on_courses.append(
            '{org}/{course}'.format(**course_id_dict))

        print("Cloning course {0} to {1}".format(source_course_id,
                                                 dest_course_id))

        source_location = CourseDescriptor.id_to_location(source_course_id)
        dest_location = CourseDescriptor.id_to_location(dest_course_id)

        if clone_course(mstore, cstore, source_location, dest_location):
            # be sure to recompute metadata inheritance after all those updates
            mstore.refresh_cached_metadata_inheritance_tree(dest_location)

            print("copying User permissions...")
            # purposely avoids auth.add_user b/c it doesn't have a caller to authorize
            CourseInstructorRole(dest_location).add_users(
                *CourseInstructorRole(source_location).users_with_role())
            CourseStaffRole(dest_location).add_users(
                *CourseStaffRole(source_location).users_with_role())
Ejemplo n.º 8
0
    def create_course(self, course_id, user_id=None, store_name='default', **kwargs):
        """
        Creates and returns the course.

        :param org: the org
        :param fields: a dict of xblock field name - value pairs for the course module.
        :param metadata: the old way of setting fields by knowing which ones are scope.settings v scope.content
        :param definition_data: the complement to metadata which is also a subset of fields
        :returns: course xblock
        """
        store = self.modulestores[store_name]
        if not hasattr(store, 'create_course'):
            raise NotImplementedError(u"Cannot create a course on store %s" % store_name)
        if store.get_modulestore_type(course_id) == SPLIT_MONGO_MODULESTORE_TYPE:
            try:
                course_dict = Location.parse_course_id(course_id)
                org = course_dict['org']
                course_id = "{org}.{course}.{name}".format(**course_dict)
            except ValueError:
                org = None

            org = kwargs.pop('org', org)
            fields = kwargs.pop('fields', {})
            fields.update(kwargs.pop('metadata', {}))
            fields.update(kwargs.pop('definition_data', {}))
            course = store.create_course(course_id, org, user_id, fields=fields, **kwargs)
        else:  # assume mongo
            course = store.create_course(course_id, **kwargs)

        return course
Ejemplo n.º 9
0
def _section_course_info(course_id, access):
    """ Provide data for the corresponding dashboard section """
    course = get_course_by_id(course_id, depth=None)

    course_id_dict = Location.parse_course_id(course_id)

    section_data = {
        'section_key': 'course_info',
        'section_display_name': _('Course Info'),
        'access': access,
        'course_id': course_id,
        'course_org': course_id_dict['org'],
        'course_num': course_id_dict['course'],
        'course_name': course_id_dict['name'],
        'course_display_name': course.display_name,
        'enrollment_count': CourseEnrollment.num_enrolled_in(course_id),
        'has_started': course.has_started(),
        'has_ended': course.has_ended(),
        'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': course_id}),
    }

    try:
        advance = lambda memo, (letter, score): "{}: {}, ".format(letter, score) + memo
        section_data['grade_cutoffs'] = reduce(advance, course.grade_cutoffs.items(), "")[:-2]
    except Exception:
        section_data['grade_cutoffs'] = "Not Available"
    # section_data['offline_grades'] = offline_grades_available(course_id)

    try:
        section_data['course_errors'] = [(escape(a), '') for (a, _unused) in modulestore().get_item_errors(course.location)]
    except Exception:
        section_data['course_errors'] = [('Error fetching errors', '')]

    return section_data
Ejemplo n.º 10
0
def delete_course_and_groups(course_id, commit=False):
    """
    This deletes the courseware associated with a course_id as well as cleaning update_item
    the various user table stuff (groups, permissions, etc.)
    """
    module_store = modulestore('direct')
    content_store = contentstore()

    course_id_dict = Location.parse_course_id(course_id)
    module_store.ignore_write_events_on_courses.append(
        '{org}/{course}'.format(**course_id_dict))

    loc = CourseDescriptor.id_to_location(course_id)
    if delete_course(module_store, content_store, loc, commit):

        print 'removing User permissions from course....'
        # in the django layer, we need to remove all the user permissions groups associated with this course
        if commit:
            try:
                staff_role = CourseStaffRole(loc)
                staff_role.remove_users(*staff_role.users_with_role())
                instructor_role = CourseInstructorRole(loc)
                instructor_role.remove_users(
                    *instructor_role.users_with_role())
            except Exception as err:
                log.error(
                    "Error in deleting course groups for {0}: {1}".format(
                        loc, err))

            # remove location of this course from loc_mapper and cache
            loc_mapper().delete_course_mapping(loc)
Ejemplo n.º 11
0
def delete_course_and_groups(course_id, commit=False):
    """
    This deletes the courseware associated with a course_id as well as cleaning update_item
    the various user table stuff (groups, permissions, etc.)
    """
    module_store = modulestore('direct')
    content_store = contentstore()

    course_id_dict = Location.parse_course_id(course_id)
    module_store.ignore_write_events_on_courses.append('{org}/{course}'.format(**course_id_dict))

    loc = CourseDescriptor.id_to_location(course_id)
    if delete_course(module_store, content_store, loc, commit):

        print 'removing User permissions from course....'
        # in the django layer, we need to remove all the user permissions groups associated with this course
        if commit:
            try:
                staff_role = CourseStaffRole(loc)
                staff_role.remove_users(*staff_role.users_with_role())
                instructor_role = CourseInstructorRole(loc)
                instructor_role.remove_users(*instructor_role.users_with_role())
            except Exception as err:
                log.error("Error in deleting course groups for {0}: {1}".format(loc, err))

            # remove location of this course from loc_mapper and cache
            loc_mapper().delete_course_mapping(loc)
Ejemplo n.º 12
0
    def create_item(self, course_or_parent_loc, category, user_id=None, **kwargs):
        """
        Create and return the item. If parent_loc is a specific location v a course id,
        it installs the new item as a child of the parent (if the parent_loc is a specific
        xblock reference).

        :param course_or_parent_loc: Can be a course_id (org/course/run), CourseLocator,
        Location, or BlockUsageLocator but must be what the persistence modulestore expects
        """
        # find the store for the course
        course_id = self._infer_course_id_try(course_or_parent_loc)
        if course_id is None:
            raise ItemNotFoundError(u"Cannot find modulestore for %s" % course_or_parent_loc)

        store = self._get_modulestore_for_courseid(course_id)

        location = kwargs.pop('location', None)
        # invoke its create_item
        if isinstance(store, MongoModuleStore):
            block_id = kwargs.pop('block_id', getattr(location, 'name', uuid4().hex))
            # convert parent loc if it's legit
            if isinstance(course_or_parent_loc, basestring):
                parent_loc = None
                if location is None:
                    loc_dict = Location.parse_course_id(course_id)
                    loc_dict['name'] = block_id
                    location = Location(category=category, **loc_dict)
            else:
                parent_loc = course_or_parent_loc
                # must have a legitimate location, compute if appropriate
                if location is None:
                    location = parent_loc.replace(category=category, name=block_id)
            # do the actual creation
            xblock = store.create_and_save_xmodule(location, **kwargs)
            # don't forget to attach to parent
            if parent_loc is not None and not 'detached' in xblock._class_tags:
                parent = store.get_item(parent_loc)
                parent.children.append(location.url())
                store.update_item(parent)
        elif isinstance(store, SplitMongoModuleStore):
            if isinstance(course_or_parent_loc, basestring):  # course_id
                course_or_parent_loc = loc_mapper().translate_location_to_course_locator(
                    # hardcode draft version until we figure out how we're handling branches from app
                    course_or_parent_loc, None, published=False
                )
            elif not isinstance(course_or_parent_loc, CourseLocator):
                raise ValueError(u"Cannot create a child of {} in split. Wrong repr.".format(course_or_parent_loc))

            # split handles all the fields in one dict not separated by scope
            fields = kwargs.get('fields', {})
            fields.update(kwargs.pop('metadata', {}))
            fields.update(kwargs.pop('definition_data', {}))
            kwargs['fields'] = fields

            xblock = store.create_item(course_or_parent_loc, category, user_id, **kwargs)
        else:
            raise NotImplementedError(u"Cannot create an item on store %s" % store)

        return xblock
Ejemplo n.º 13
0
def generate_location(course_id):
    """
    Generate the locations for the given ids
    """
    course_dict = Location.parse_course_id(course_id)
    course_dict['tag'] = 'i4x'
    course_dict['category'] = 'course'
    return Location(course_dict)
Ejemplo n.º 14
0
 def id_to_location(course_id):
     '''Convert the given course_id (org/course/name) to a location object.
     Throws ValueError if course_id is of the wrong format.
     '''
     course_id_dict = Location.parse_course_id(course_id)
     course_id_dict['tag'] = 'i4x'
     course_id_dict['category'] = 'course'
     return Location(course_id_dict)
Ejemplo n.º 15
0
 def id_to_location(course_id):
     '''Convert the given course_id (org/course/name) to a location object.
     Throws ValueError if course_id is of the wrong format.
     '''
     course_id_dict = Location.parse_course_id(course_id)
     course_id_dict['tag'] = 'i4x'
     course_id_dict['category'] = 'course'
     return Location(course_id_dict)
Ejemplo n.º 16
0
 def get_course(self, course_id):
     """
     Get the course with the given courseid (org/course/run)
     """
     id_components = Location.parse_course_id(course_id)
     id_components['tag'] = 'i4x'
     id_components['category'] = 'course'
     try:
         return self.get_item(Location(id_components))
     except ItemNotFoundError:
         return None
Ejemplo n.º 17
0
 def get_course(self, course_id):
     """
     Get the course with the given courseid (org/course/run)
     """
     id_components = Location.parse_course_id(course_id)
     id_components['tag'] = 'i4x'
     id_components['category'] = 'course'
     try:
         return self.get_item(Location(id_components))
     except ItemNotFoundError:
         return None
Ejemplo n.º 18
0
    def update_enrollment(self, mode=None, is_active=None):
        """
        Updates an enrollment for a user in a class.  This includes options
        like changing the mode, toggling is_active True/False, etc.

        Also emits relevant events for analytics purposes.

        This saves immediately.
        """
        activation_changed = False
        # if is_active is None, then the call to update_enrollment didn't specify
        # any value, so just leave is_active as it is
        if self.is_active != is_active and is_active is not None:
            self.is_active = is_active
            activation_changed = True

        mode_changed = False
        # if mode is None, the call to update_enrollment didn't specify a new
        # mode, so leave as-is
        if self.mode != mode and mode is not None:
            self.mode = mode
            mode_changed = True

        if activation_changed or mode_changed:
            self.save()
        if activation_changed:
            course_id_dict = Location.parse_course_id(self.course_id)
            if self.is_active:
                self.emit_event(EVENT_NAME_ENROLLMENT_ACTIVATED)

                dog_stats_api.increment(
                    "common.student.enrollment",
                    tags=[
                        u"org:{org}".format(**course_id_dict),
                        u"course:{course}".format(**course_id_dict),
                        u"run:{name}".format(**course_id_dict),
                        u"mode:{}".format(self.mode)
                    ])

            else:
                unenroll_done.send(sender=None, course_enrollment=self)

                self.emit_event(EVENT_NAME_ENROLLMENT_DEACTIVATED)

                dog_stats_api.increment(
                    "common.student.unenrollment",
                    tags=[
                        u"org:{org}".format(**course_id_dict),
                        u"course:{course}".format(**course_id_dict),
                        u"run:{name}".format(**course_id_dict),
                        u"mode:{}".format(self.mode)
                    ])
Ejemplo n.º 19
0
 def create_course(self, course_id, definition_data=None, metadata=None, runtime=None):
     """
     Create a course with the given course_id.
     """
     if isinstance(course_id, Location):
         location = course_id
         if location.category != 'course':
             raise ValueError(u"Course roots must be of category 'course': {}".format(unicode(location)))
     else:
         course_dict = Location.parse_course_id(course_id)
         course_dict['category'] = 'course'
         course_dict['tag'] = 'i4x'
         location = Location(course_dict)
     return self.create_and_save_xmodule(location, definition_data, metadata, runtime)
Ejemplo n.º 20
0
    def update_enrollment(self, mode=None, is_active=None):
        """
        Updates an enrollment for a user in a class.  This includes options
        like changing the mode, toggling is_active True/False, etc.

        Also emits relevant events for analytics purposes.

        This saves immediately.
        """
        activation_changed = False
        # if is_active is None, then the call to update_enrollment didn't specify
        # any value, so just leave is_active as it is
        if self.is_active != is_active and is_active is not None:
            self.is_active = is_active
            activation_changed = True

        mode_changed = False
        # if mode is None, the call to update_enrollment didn't specify a new
        # mode, so leave as-is
        if self.mode != mode and mode is not None:
            self.mode = mode
            mode_changed = True

        if activation_changed or mode_changed:
            self.save()
        if activation_changed:
            course_id_dict = Location.parse_course_id(self.course_id)
            if self.is_active:
                self.emit_event(EVENT_NAME_ENROLLMENT_ACTIVATED)

                dog_stats_api.increment(
                    "common.student.enrollment",
                    tags=[u"org:{org}".format(**course_id_dict),
                          u"course:{course}".format(**course_id_dict),
                          u"run:{name}".format(**course_id_dict),
                          u"mode:{}".format(self.mode)]
                )

            else:
                unenroll_done.send(sender=None, course_enrollment=self)

                self.emit_event(EVENT_NAME_ENROLLMENT_DEACTIVATED)

                dog_stats_api.increment(
                    "common.student.unenrollment",
                    tags=[u"org:{org}".format(**course_id_dict),
                          u"course:{course}".format(**course_id_dict),
                          u"run:{name}".format(**course_id_dict),
                          u"mode:{}".format(self.mode)]
                )
Ejemplo n.º 21
0
    def convert_legacy_static_url_with_course_id(path, course_id):
        """
        Returns a path to a piece of static content when we are provided with a filepath and
        a course_id
        """

        # Generate url of urlparse.path component
        scheme, netloc, orig_path, params, query, fragment = urlparse(path)
        course_id_dict = Location.parse_course_id(course_id)
        loc = StaticContent.compute_location(course_id_dict['org'], course_id_dict['course'], orig_path)
        loc_url = StaticContent.get_url_path_from_location(loc)

        # Reconstruct with new path
        return urlunparse((scheme, netloc, loc_url, params, query, fragment))
Ejemplo n.º 22
0
    def test_course_name_only(self):
        # Munge course id - common
        bad_id = Location.parse_course_id(self.course.id)['name']

        form_data = {'course_id': bad_id, 'email_enabled': True}
        form = CourseAuthorizationAdminForm(data=form_data)
        # Validation shouldn't work
        self.assertFalse(form.is_valid())

        error_msg = form._errors['course_id'][0]
        self.assertIn(u'--- Entered course id was: "{0}". '.format(bad_id), error_msg)
        self.assertIn(u'Please recheck that you have supplied a course id in the format: ORG/COURSE/RUN', error_msg)

        with self.assertRaisesRegexp(ValueError, "The CourseAuthorization could not be created because the data didn't validate."):
            form.save()
Ejemplo n.º 23
0
    def convert_legacy_static_url_with_course_id(path, course_id):
        """
        Returns a path to a piece of static content when we are provided with a filepath and
        a course_id
        """

        # Generate url of urlparse.path component
        scheme, netloc, orig_path, params, query, fragment = urlparse(path)
        course_id_dict = Location.parse_course_id(course_id)
        loc = StaticContent.compute_location(course_id_dict['org'],
                                             course_id_dict['course'],
                                             orig_path)
        loc_url = StaticContent.get_url_path_from_location(loc)

        # Reconstruct with new path
        return urlunparse((scheme, netloc, loc_url, params, query, fragment))
Ejemplo n.º 24
0
def _msk_from_problem_urlname(course_id, urlname):
    """
    Convert a 'problem urlname' (name that instructor's input into dashboard)
    to a module state key (db field)
    """
    if urlname.endswith(".xml"):
        urlname = urlname[:-4]

    # Combined open ended problems also have state that can be deleted.  However,
    # prepending "problem" will only allow capa problems to be reset.
    # Get around this for xblock problems.
    if "/" not in urlname:
        urlname = "problem/" + urlname

    parts = Location.parse_course_id(course_id)
    parts['urlname'] = urlname
    module_state_key = u"i4x://{org}/{course}/{urlname}".format(**parts)
    return module_state_key
Ejemplo n.º 25
0
def _msk_from_problem_urlname(course_id, urlname):
    """
    Convert a 'problem urlname' (name that instructor's input into dashboard)
    to a module state key (db field)
    """
    if urlname.endswith(".xml"):
        urlname = urlname[:-4]

    # Combined open ended problems also have state that can be deleted.  However,
    # appending "problem" will only allow capa problems to be reset.
    # Get around this for combinedopenended problems.
    if "combinedopenended" not in urlname:
        urlname = "problem/" + urlname

    parts = Location.parse_course_id(course_id)
    parts['urlname'] = urlname
    module_state_key = u"i4x://{org}/{course}/{urlname}".format(**parts)
    return module_state_key
Ejemplo n.º 26
0
def rewrite_nonportable_content_links(source_course_id, dest_course_id, text):
    """
    Does a regex replace on non-portable links:
         /c4x/<org>/<course>/asset/<name> -> /static/<name>
         /jump_to/i4x://<org>/<course>/<category>/<name> -> /jump_to_id/<id>

    """

    course_id_dict = Location.parse_course_id(source_course_id)
    course_id_dict['tag'] = 'i4x'
    course_id_dict['category'] = 'course'

    def portable_asset_link_subtitution(match):
        quote = match.group('quote')
        rest = match.group('rest')
        return quote + '/static/' + rest + quote

    def portable_jump_to_link_substitution(match):
        quote = match.group('quote')
        rest = match.group('rest')
        return quote + '/jump_to_id/' + rest + quote

    def generic_courseware_link_substitution(match):
        parts = Location.parse_course_id(dest_course_id)
        parts['quote'] = match.group('quote')
        parts['rest'] = match.group('rest')
        return u'{quote}/courses/{org}/{course}/{name}/{rest}{quote}'.format(
            **parts)

    course_location = Location(course_id_dict)

    # NOTE: ultimately link updating is not a hard requirement, so if something blows up with
    # the regex subsitution, log the error and continue
    try:
        c4x_link_base = u'{0}/'.format(
            StaticContent.get_base_url_path_for_course_assets(course_location))
        text = re.sub(_prefix_only_url_replace_regex(c4x_link_base),
                      portable_asset_link_subtitution, text)
    except Exception, e:
        logging.warning(
            "Error going regex subtituion %r on text = %r.\n\nError msg = %s",
            c4x_link_base, text, str(e))
Ejemplo n.º 27
0
 def setupClass(cls):
     """
     Set up the database for testing
     """
     cls.connection = pymongo.MongoClient(
         host=HOST,
         port=PORT,
         tz_aware=True,
     )
     cls.connection.drop_database(DB)
     cls.fake_location = Location('i4x', 'foo', 'bar', 'vertical', 'baz')
     import_course_dict = Location.parse_course_id(IMPORT_COURSEID)
     cls.import_org = import_course_dict['org']
     cls.import_course = import_course_dict['course']
     cls.import_run = import_course_dict['name']
     # NOTE: Creating a single db for all the tests to save time.  This
     # is ok only as long as none of the tests modify the db.
     # If (when!) that changes, need to either reload the db, or load
     # once and copy over to a tmp db for each test.
     cls.store = cls.initdb()
Ejemplo n.º 28
0
 def setupClass(cls):
     """
     Set up the database for testing
     """
     cls.connection = pymongo.MongoClient(
         host=HOST,
         port=PORT,
         tz_aware=True,
     )
     cls.connection.drop_database(DB)
     cls.fake_location = Location('i4x', 'foo', 'bar', 'vertical', 'baz')
     import_course_dict = Location.parse_course_id(IMPORT_COURSEID)
     cls.import_org = import_course_dict['org']
     cls.import_course = import_course_dict['course']
     cls.import_run = import_course_dict['name']
     # NOTE: Creating a single db for all the tests to save time.  This
     # is ok only as long as none of the tests modify the db.
     # If (when!) that changes, need to either reload the db, or load
     # once and copy over to a tmp db for each test.
     cls.store = cls.initdb()
Ejemplo n.º 29
0
    def test_course_name_only(self):
        # Munge course id - common
        bad_id = Location.parse_course_id(self.course.id)['name']

        form_data = {'course_id': bad_id, 'email_enabled': True}
        form = CourseAuthorizationAdminForm(data=form_data)
        # Validation shouldn't work
        self.assertFalse(form.is_valid())

        error_msg = form._errors['course_id'][0]
        self.assertIn(u'--- Entered course id was: "{0}". '.format(bad_id),
                      error_msg)
        self.assertIn(
            u'Please recheck that you have supplied a course id in the format: ORG/COURSE/RUN',
            error_msg)

        with self.assertRaisesRegexp(
                ValueError,
                "The CourseAuthorization could not be created because the data didn't validate."
        ):
            form.save()
Ejemplo n.º 30
0
 def create_course(self,
                   course_id,
                   definition_data=None,
                   metadata=None,
                   runtime=None):
     """
     Create a course with the given course_id.
     """
     if isinstance(course_id, Location):
         location = course_id
         if location.category != 'course':
             raise ValueError(
                 u"Course roots must be of category 'course': {}".format(
                     unicode(location)))
     else:
         course_dict = Location.parse_course_id(course_id)
         course_dict['category'] = 'course'
         course_dict['tag'] = 'i4x'
         location = Location(course_dict)
     return self.create_and_save_xmodule(location, definition_data,
                                         metadata, runtime)
Ejemplo n.º 31
0
def _get_source_address(course_id, course_title):
    """
    Calculates an email address to be used as the 'from-address' for sent emails.

    Makes a unique from name and address for each course, e.g.

        "COURSE_TITLE" Course Staff <*****@*****.**>

    """
    course_title_no_quotes = re.sub(r'"', '', course_title)

    # The course_id is assumed to be in the form 'org/course_num/run',
    # so pull out the course_num.  Then make sure that it can be used
    # in an email address, by substituting a '_' anywhere a non-(ascii, period, or dash)
    # character appears.
    course_num = Location.parse_course_id(course_id)['course']
    invalid_chars = re.compile(r"[^\w.-]")
    course_num = invalid_chars.sub('_', course_num)

    from_addr = u'{prefix} "{course_name}" <{email}>'.format(prefix = settings.BULK_EMAIL_PREFIX_FROM_EMAIL, course_name = course_title_no_quotes, email = settings.BULK_EMAIL_DEFAULT_FROM_EMAIL)
    return from_addr
Ejemplo n.º 32
0
    def create_course(self,
                      course_id,
                      user_id=None,
                      store_name='default',
                      **kwargs):
        """
        Creates and returns the course.

        :param org: the org
        :param fields: a dict of xblock field name - value pairs for the course module.
        :param metadata: the old way of setting fields by knowing which ones are scope.settings v scope.content
        :param definition_data: the complement to metadata which is also a subset of fields
        :returns: course xblock
        """
        store = self.modulestores[store_name]
        if not hasattr(store, 'create_course'):
            raise NotImplementedError(u"Cannot create a course on store %s" %
                                      store_name)
        if store.get_modulestore_type(
                course_id) == SPLIT_MONGO_MODULESTORE_TYPE:
            try:
                course_dict = Location.parse_course_id(course_id)
                org = course_dict['org']
                course_id = "{org}.{course}.{name}".format(**course_dict)
            except ValueError:
                org = None

            org = kwargs.pop('org', org)
            fields = kwargs.pop('fields', {})
            fields.update(kwargs.pop('metadata', {}))
            fields.update(kwargs.pop('definition_data', {}))
            course = store.create_course(course_id,
                                         org,
                                         user_id,
                                         fields=fields,
                                         **kwargs)
        else:  # assume mongo
            course = store.create_course(course_id, **kwargs)

        return course
Ejemplo n.º 33
0
    def publish(block, event, custom_user=None):
        """A function that allows XModules to publish events. This only supports grade changes right now."""
        if event.get('event_name') != 'grade':
            return

        if custom_user:
            user_id = custom_user.id
        else:
            user_id = user.id

        # Construct the key for the module
        key = KeyValueStore.Key(
            scope=Scope.user_state,
            user_id=user_id,
            block_scope_id=descriptor.location,
            field_name='grade'
        )

        student_module = field_data_cache.find_or_create(key)
        # Update the grades
        student_module.grade = event.get('value')
        student_module.max_grade = event.get('max_value')
        # Save all changes to the underlying KeyValueStore
        student_module.save()

        # Bin score into range and increment stats
        score_bucket = get_score_bucket(student_module.grade, student_module.max_grade)
        course_id_dict = Location.parse_course_id(course_id)

        tags = [
            u"org:{org}".format(**course_id_dict),
            u"course:{course}".format(**course_id_dict),
            u"run:{name}".format(**course_id_dict),
            u"score_bucket:{0}".format(score_bucket)
        ]

        if grade_bucket_type is not None:
            tags.append('type:%s' % grade_bucket_type)

        dog_stats_api.increment("lms.courseware.question_answered", tags=tags)
Ejemplo n.º 34
0
    def publish(block, event, custom_user=None):
        """A function that allows XModules to publish events. This only supports grade changes right now."""
        if event.get('event_name') != 'grade':
            return

        if custom_user:
            user_id = custom_user.id
        else:
            user_id = user.id

        # Construct the key for the module
        key = KeyValueStore.Key(
            scope=Scope.user_state,
            user_id=user_id,
            block_scope_id=descriptor.location,
            field_name='grade'
        )

        student_module = field_data_cache.find_or_create(key)
        # Update the grades
        student_module.grade = event.get('value')
        student_module.max_grade = event.get('max_value')
        # Save all changes to the underlying KeyValueStore
        student_module.save()

        # Bin score into range and increment stats
        score_bucket = get_score_bucket(student_module.grade, student_module.max_grade)
        course_id_dict = Location.parse_course_id(course_id)

        tags = [
            u"org:{org}".format(**course_id_dict),
            u"course:{course}".format(**course_id_dict),
            u"run:{name}".format(**course_id_dict),
            u"score_bucket:{0}".format(score_bucket)
        ]

        if grade_bucket_type is not None:
            tags.append('type:%s' % grade_bucket_type)

        dog_stats_api.increment("lms.courseware.question_answered", tags=tags)
Ejemplo n.º 35
0
def _get_source_address(course_id, course_title):
    """
    Calculates an email address to be used as the 'from-address' for sent emails.

    Makes a unique from name and address for each course, e.g.

        "COURSE_TITLE" Course Staff <*****@*****.**>

    """
    course_title_no_quotes = re.sub(r'"', '', course_title)

    # The course_id is assumed to be in the form 'org/course_num/run',
    # so pull out the course_num.  Then make sure that it can be used
    # in an email address, by substituting a '_' anywhere a non-(ascii, period, or dash)
    # character appears.
    course_num = Location.parse_course_id(course_id)['course']
    invalid_chars = re.compile(r"[^\w.-]")
    course_num = invalid_chars.sub('_', course_num)

    from_addr = u'"{0}" Course Staff <{1}-{2}>'.format(
        course_title_no_quotes, course_num,
        settings.BULK_EMAIL_DEFAULT_FROM_EMAIL)
    return from_addr
Ejemplo n.º 36
0
def rewrite_nonportable_content_links(source_course_id, dest_course_id, text):
    """
    Does a regex replace on non-portable links:
         /c4x/<org>/<course>/asset/<name> -> /static/<name>
         /jump_to/i4x://<org>/<course>/<category>/<name> -> /jump_to_id/<id>

    """

    course_id_dict = Location.parse_course_id(source_course_id)
    course_id_dict["tag"] = "i4x"
    course_id_dict["category"] = "course"

    def portable_asset_link_subtitution(match):
        quote = match.group("quote")
        rest = match.group("rest")
        return quote + "/static/" + rest + quote

    def portable_jump_to_link_substitution(match):
        quote = match.group("quote")
        rest = match.group("rest")
        return quote + "/jump_to_id/" + rest + quote

    def generic_courseware_link_substitution(match):
        parts = Location.parse_course_id(dest_course_id)
        parts["quote"] = match.group("quote")
        parts["rest"] = match.group("rest")
        return u"{quote}/courses/{org}/{course}/{name}/{rest}{quote}".format(**parts)

    course_location = Location(course_id_dict)

    # NOTE: ultimately link updating is not a hard requirement, so if something blows up with
    # the regex subsitution, log the error and continue
    try:
        c4x_link_base = u"{0}/".format(StaticContent.get_base_url_path_for_course_assets(course_location))
        text = re.sub(_prefix_only_url_replace_regex(c4x_link_base), portable_asset_link_subtitution, text)
    except Exception, e:
        logging.warning("Error going regex subtituion %r on text = %r.\n\nError msg = %s", c4x_link_base, text, str(e))
Ejemplo n.º 37
0
    def create_course(self, course_id, user_id=None, store_name="default", **kwargs):
        """
        Creates and returns the course.

        :param org: the org
        :param fields: a dict of xblock field name - value pairs for the course module.
        :param metadata: the old way of setting fields by knowing which ones are scope.settings v scope.content
        :param definition_data: the complement to metadata which is also a subset of fields
        :param id_root: the split-mongo course_id starting value (see split.create_course)
        :param pretty_id: a field split.create_course uses and may quit using
        :returns: course xblock
        """
        store = self.modulestores[store_name]
        if not hasattr(store, "create_course"):
            raise NotImplementedError(u"Cannot create a course on store %s" % store_name)
        if store.get_modulestore_type(course_id) == SPLIT_MONGO_MODULESTORE_TYPE:
            id_root = kwargs.get("id_root")
            try:
                course_dict = Location.parse_course_id(course_id)
                org = course_dict["org"]
                if id_root is None:
                    id_root = "{org}.{course}.{name}".format(**course_dict)
            except ValueError:
                org = None
                if id_root is None:
                    id_root = course_id
            org = kwargs.pop("org", org)
            pretty_id = kwargs.pop("pretty_id", id_root)
            fields = kwargs.pop("fields", {})
            fields.update(kwargs.pop("metadata", {}))
            fields.update(kwargs.pop("definition_data", {}))
            course = store.create_course(org, pretty_id, user_id, id_root=id_root, fields=fields, **kwargs)
        else:  # assume mongo
            course = store.create_course(course_id, **kwargs)

        return course
Ejemplo n.º 38
0
def perform_xlint(
        data_dir, course_dirs,
        default_class='xmodule.raw_module.RawDescriptor',
        load_error_modules=True):
    err_cnt = 0
    warn_cnt = 0

    module_store = XMLModuleStore(
        data_dir,
        default_class=default_class,
        course_dirs=course_dirs,
        load_error_modules=load_error_modules
    )

    # check all data source path information
    for course_dir in course_dirs:
        _err_cnt, _warn_cnt = validate_data_source_paths(path(data_dir), course_dir)
        err_cnt += _err_cnt
        warn_cnt += _warn_cnt

    # first count all errors and warnings as part of the XMLModuleStore import
    for err_log in module_store._location_errors.itervalues():
        for err_log_entry in err_log.errors:
            msg = err_log_entry[0]
            if msg.startswith('ERROR:'):
                err_cnt += 1
            else:
                warn_cnt += 1

    # then count outright all courses that failed to load at all
    for err_log in module_store.errored_courses.itervalues():
        for err_log_entry in err_log.errors:
            msg = err_log_entry[0]
            print(msg)
            if msg.startswith('ERROR:'):
                err_cnt += 1
            else:
                warn_cnt += 1

    for course_id in module_store.modules.keys():
        # constrain that courses only have 'chapter' children
        err_cnt += validate_category_hierarchy(
            module_store, course_id, "course", "chapter"
        )
        # constrain that chapters only have 'sequentials'
        err_cnt += validate_category_hierarchy(
            module_store, course_id, "chapter", "sequential"
        )
        # constrain that sequentials only have 'verticals'
        err_cnt += validate_category_hierarchy(
            module_store, course_id, "sequential", "vertical"
        )
        # validate the course policy overrides any defaults
        # which have changed over time
        warn_cnt += validate_course_policy(module_store, course_id)
        # don't allow metadata on verticals, since we can't edit them in studio
        err_cnt += validate_no_non_editable_metadata(
            module_store, course_id, "vertical"
        )
        # don't allow metadata on chapters, since we can't edit them in studio
        err_cnt += validate_no_non_editable_metadata(
            module_store, course_id, "chapter"
        )
        # don't allow metadata on sequences that we can't edit
        err_cnt += validate_no_non_editable_metadata(
            module_store, course_id, "sequential"
        )

        # check for a presence of a course marketing video
        location_elements = Location.parse_course_id(course_id)
        location_elements['tag'] = 'i4x'
        location_elements['category'] = 'about'
        location_elements['name'] = 'video'
        loc = Location(location_elements)
        if loc not in module_store.modules[course_id]:
            print(
                "WARN: Missing course marketing video. It is recommended "
                "that every course have a marketing video."
            )
            warn_cnt += 1

    print("\n")
    print("------------------------------------------")
    print("VALIDATION SUMMARY: {err} Errors   {warn} Warnings".format(
        err=err_cnt, warn=warn_cnt)
    )

    if err_cnt > 0:
        print(
            "This course is not suitable for importing. Please fix courseware "
            "according to specifications before importing."
        )
    elif warn_cnt > 0:
        print(
            "This course can be imported, but some errors may occur "
            "during the run of the course. It is recommend that you fix "
            "your courseware before importing"
        )
    else:
        print("This course can be imported successfully.")

    return err_cnt
Ejemplo n.º 39
0
def mobile_change_enrollment(request):
    user = request.user

    action = request.POST.get("enrollment_action")
    course_id = request.POST.get("course_id")
    if course_id is None:
        return HttpResponseBadRequest(_("Course id not specified"))

    if not user.is_authenticated():
        return HttpResponseForbidden()

    if action == "enroll":
        # Make sure the course exists
        # We don't do this check on unenroll, or a bad course id can't be unenrolled from
        try:
            course = course_from_id(course_id)
        except ItemNotFoundError:
            log.warning("User {0} tried to enroll in non-existent course {1}".format(user.username, course_id))
            return HttpResponseBadRequest(_("Course id is invalid"))

        if not has_access(user, course, "enroll"):
            return HttpResponseBadRequest(_("Enrollment is closed"))

        # see if we have already filled up all allowed enrollments
        is_course_full = CourseEnrollment.is_course_full(course)

        if is_course_full:
            return HttpResponseBadRequest(_("Course is full"))

        # If this course is available in multiple modes, redirect them to a page
        # where they can choose which mode they want.
        available_modes = CourseMode.modes_for_course(course_id)
        if len(available_modes) > 1:
            return HttpResponse(reverse("course_modes_choose", kwargs={"course_id": course_id}))

        current_mode = available_modes[0]

        course_id_dict = Location.parse_course_id(course_id)
        dog_stats_api.increment(
            "common.student.enrollment",
            tags=[
                u"org:{org}".format(**course_id_dict),
                u"course:{course}".format(**course_id_dict),
                u"run:{name}".format(**course_id_dict),
            ],
        )

        CourseEnrollment.enroll(user, course.id, mode=current_mode.slug)

        return HttpResponse("about")

    elif action == "unenroll":
        if not CourseEnrollment.is_enrolled(user, course_id):
            return HttpResponseBadRequest(_("You are not enrolled in this course"))
        CourseEnrollment.unenroll(user, course_id)
        course_id_dict = Location.parse_course_id(course_id)
        dog_stats_api.increment(
            "common.student.unenrollment",
            tags=[
                u"org:{org}".format(**course_id_dict),
                u"course:{course}".format(**course_id_dict),
                u"run:{name}".format(**course_id_dict),
            ],
        )
        return HttpResponse()
    else:
        return HttpResponseBadRequest(_("Enrollment action is invalid"))
Ejemplo n.º 40
0
def mobile_change_enrollment(request):
    """
    Modify the enrollment status for the logged-in user.

    The request parameter must be a POST request (other methods return 405)
    that specifies course_id and enrollment_action parameters. If course_id or
    enrollment_action is not specified, if course_id is not valid, if
    enrollment_action is something other than "enroll" or "unenroll", if
    enrollment_action is "enroll" and enrollment is closed for the course, or
    if enrollment_action is "unenroll" and the user is not enrolled in the
    course, a 400 error will be returned. If the user is not logged in, 403
    will be returned; it is important that only this case return 403 so the
    front end can redirect the user to a registration or login page when this
    happens. This function should only be called from an AJAX request or
    as a post-login/registration helper, so the error messages in the responses
    should never actually be user-visible.
    """
    user = request.user

    action = request.POST.get("enrollment_action")
    course_id = request.POST.get("course_id")
    if course_id is None:
        return HttpResponseBadRequest(_("Course id not specified"))

    if not user.is_authenticated():
        return HttpResponseForbidden()

    if action == "enroll":
        # Make sure the course exists
        # We don't do this check on unenroll, or a bad course id can't be unenrolled from
        try:
            course = course_from_id(course_id)
        except ItemNotFoundError:
            log.warning("User {0} tried to enroll in non-existent course {1}"
                        .format(user.username, course_id))
            return HttpResponseBadRequest(_("Course id is invalid"))

        if not has_access(user, course, 'enroll'):
            return HttpResponseBadRequest(_("Enrollment is closed"))

        # see if we have already filled up all allowed enrollments
        is_course_full = CourseEnrollment.is_course_full(course)

        if is_course_full:
            return HttpResponseBadRequest(_("Course is full"))

        # If this course is available in multiple modes, redirect them to a page
        # where they can choose which mode they want.
        available_modes = CourseMode.modes_for_course(course_id)
        if len(available_modes) > 1:
            return HttpResponse(
                reverse("course_modes_choose", kwargs={'course_id': course_id})
            )

        current_mode = available_modes[0]

        course_id_dict = Location.parse_course_id(course_id)
        dog_stats_api.increment(
            "common.student.enrollment",
            tags=[u"org:{org}".format(**course_id_dict),
                  u"course:{course}".format(**course_id_dict),
                  u"run:{name}".format(**course_id_dict)]
        )

        CourseEnrollment.enroll(user, course.id, mode=current_mode.slug)

        return HttpResponse('about')

    elif action == "add_to_cart":
        # Pass the request handling to shoppingcart.views
        # The view in shoppingcart.views performs error handling and logs different errors.  But this elif clause
        # is only used in the "auto-add after user reg/login" case, i.e. it's always wrapped in try_change_enrollment.
        # This means there's no good way to display error messages to the user.  So we log the errors and send
        # the user to the shopping cart page always, where they can reasonably discern the status of their cart,
        # whether things got added, etc

        shoppingcart.views.add_course_to_cart(request, course_id)
        return HttpResponse(
            reverse("shoppingcart.views.show_cart")
        )

    elif action == "unenroll":
        if not CourseEnrollment.is_enrolled(user, course_id):
            return HttpResponseBadRequest(_("You are not enrolled in this course"))
        CourseEnrollment.unenroll(user, course_id)
        course_id_dict = Location.parse_course_id(course_id)
        dog_stats_api.increment(
            "common.student.unenrollment",
            tags=[u"org:{org}".format(**course_id_dict),
                  u"course:{course}".format(**course_id_dict),
                  u"run:{name}".format(**course_id_dict)]
        )
        return HttpResponse()
    else:
        return HttpResponseBadRequest(_("Enrollment action is invalid"))
Ejemplo n.º 41
0
def import_from_xml(
        store, data_dir, course_dirs=None,
        default_class='xmodule.raw_module.RawDescriptor',
        load_error_modules=True, static_content_store=None,
        target_location_namespace=None, verbose=False, draft_store=None,
        do_import_static=True):
    """
    Import the specified xml data_dir into the "store" modulestore,
    using org and course as the location org and course.

    course_dirs: If specified, the list of course_dirs to load. Otherwise, load
    all course dirs

    target_location_namespace is the namespace [passed as Location]
    (i.e. {tag},{org},{course}) that all modules in the should be remapped to
    after import off disk. We do this remapping as a post-processing step
    because there's logic in the importing which expects a 'url_name' as an
    identifier to where things are on disk
    e.g. ../policies/<url_name>/policy.json as well as metadata keys in
    the policy.json. so we need to keep the original url_name during import

    :param do_import_static:
        if False, then static files are not imported into the static content
        store. This can be employed for courses which have substantial
        unchanging static content, which is to inefficient to import every
        time the course is loaded. Static content for some courses may also be
        served directly by nginx, instead of going through django.

    """

    xml_module_store = XMLModuleStore(
        data_dir,
        default_class=default_class,
        course_dirs=course_dirs,
        load_error_modules=load_error_modules,
        xblock_mixins=store.xblock_mixins,
        xblock_select=store.xblock_select,
    )

    # NOTE: the XmlModuleStore does not implement get_items()
    # which would be a preferable means to enumerate the entire collection
    # of course modules. It will be left as a TBD to implement that
    # method on XmlModuleStore.
    course_items = []
    for course_id in xml_module_store.modules.keys():

        if target_location_namespace is not None:
            pseudo_course_id = u'{0.org}/{0.course}'.format(target_location_namespace)
        else:
            course_id_components = Location.parse_course_id(course_id)
            pseudo_course_id = u'{org}/{course}'.format(**course_id_components)

        try:
            # turn off all write signalling while importing as this
            # is a high volume operation on stores that need it
            if (hasattr(store, 'ignore_write_events_on_courses') and
                    pseudo_course_id not in store.ignore_write_events_on_courses):
                store.ignore_write_events_on_courses.append(pseudo_course_id)

            course_data_path = None
            course_location = None

            if verbose:
                log.debug("Scanning {0} for course module...".format(course_id))

            # Quick scan to get course module as we need some info from there.
            # Also we need to make sure that the course module is committed
            # first into the store
            for module in xml_module_store.modules[course_id].itervalues():
                if module.scope_ids.block_type == 'course':
                    course_data_path = path(data_dir) / module.data_dir
                    course_location = module.location
                    course_org_lower = course_location.org.lower()
                    course_number_lower = course_location.course.lower()

                    # Check to see if a course with the same
                    # pseudo_course_id, but different run exists in
                    # the passed store to avoid broken courses
                    courses = store.get_courses()
                    bad_run = False
                    if target_location_namespace is None:
                        for course in courses:
                            if course.location.org.lower() == course_org_lower and \
                            course.location.course.lower() == course_number_lower:
                                log.debug('Import is overwriting existing course')
                                # Importing over existing course, check
                                # that runs match or fail
                                if course.location.name != module.location.name:
                                    log.error(
                                        'A course with ID %s exists, and this '
                                        'course has the same organization and '
                                        'course number, but a different term that '
                                        'is fully identified as %s.',
                                        course.location.course_id,
                                        module.location.course_id
                                    )
                                    bad_run = True
                                    break
                    if bad_run:
                        # Skip this course, but keep trying to import courses
                        continue

                    log.debug('======> IMPORTING course to location {loc}'.format(
                        loc=course_location
                    ))

                    module = remap_namespace(module, target_location_namespace)

                    if not do_import_static:
                        # for old-style xblock where this was actually linked to kvs
                        module.static_asset_path = module.data_dir
                        module.save()
                        log.debug('course static_asset_path={path}'.format(
                            path=module.static_asset_path
                        ))

                    log.debug('course data_dir={0}'.format(module.data_dir))

                    # cdodge: more hacks (what else). Seems like we have a
                    # problem when importing a course (like 6.002) which
                    # does not have any tabs defined in the policy file.
                    # The import goes fine and then displays fine in LMS,
                    # but if someone tries to add a new tab in the CMS, then
                    # the LMS barfs because it expects that -- if there are
                    # *any* tabs -- then there at least needs to be
                    # some predefined ones
                    if module.tabs is None or len(module.tabs) == 0:
                        module.tabs = [
                            {"type": "courseware"},
                            {"type": "course_info", "name": "Course Info"},
                            {"type": "discussion", "name": "Discussion"},
                            {"type": "wiki", "name": "Wiki"},
                            # note, add 'progress' when we can support it on Edge
                        ]

                    import_module(
                        module, store, course_data_path, static_content_store,
                        course_location,
                        target_location_namespace or course_location,
                        do_import_static=do_import_static
                    )

                    course_items.append(module)

            # then import all the static content
            if static_content_store is not None and do_import_static:
                if target_location_namespace is not None:
                    _namespace_rename = target_location_namespace
                else:
                    _namespace_rename = course_location

                # first pass to find everything in /static/
                import_static_content(
                    xml_module_store.modules[course_id], course_location,
                    course_data_path, static_content_store,
                    _namespace_rename, subpath='static', verbose=verbose
                )

            elif verbose and not do_import_static:
                log.debug(
                    "Skipping import of static content, "
                    "since do_import_static={0}".format(do_import_static)
                )

            # no matter what do_import_static is, import "static_import" directory

            # This is needed because the "about" pages (eg "overview") are
            # loaded via load_extra_content, and do not inherit the lms
            # metadata from the course module, and thus do not get
            # "static_content_store" properly defined. Static content
            # referenced in those extra pages thus need to come through the
            # c4x:// contentstore, unfortunately. Tell users to copy that
            # content into the "static_import" subdir.

            simport = 'static_import'
            if os.path.exists(course_data_path / simport):
                if target_location_namespace is not None:
                    _namespace_rename = target_location_namespace
                else:
                    _namespace_rename = course_location

                import_static_content(
                    xml_module_store.modules[course_id], course_location,
                    course_data_path, static_content_store,
                    _namespace_rename, subpath=simport, verbose=verbose
                )

            # finally loop through all the modules
            for module in xml_module_store.modules[course_id].itervalues():
                if module.scope_ids.block_type == 'course':
                    # we've already saved the course module up at the top
                    # of the loop so just skip over it in the inner loop
                    continue

                # remap module to the new namespace
                if target_location_namespace is not None:
                    module = remap_namespace(module, target_location_namespace)

                if verbose:
                    log.debug('importing module location {loc}'.format(
                        loc=module.location
                    ))

                import_module(
                    module, store, course_data_path, static_content_store,
                    course_location,
                    target_location_namespace if target_location_namespace else course_location,
                    do_import_static=do_import_static
                )

            # now import any 'draft' items
            if draft_store is not None:
                import_course_draft(
                    xml_module_store,
                    store,
                    draft_store,
                    course_data_path,
                    static_content_store,
                    course_location,
                    target_location_namespace if target_location_namespace else course_location
                )

        finally:
            # turn back on all write signalling on stores that need it
            if (hasattr(store, 'ignore_write_events_on_courses') and
                    pseudo_course_id in store.ignore_write_events_on_courses):
                store.ignore_write_events_on_courses.remove(pseudo_course_id)
                store.refresh_cached_metadata_inheritance_tree(
                    target_location_namespace if target_location_namespace is not None else course_location
                )

    return xml_module_store, course_items
Ejemplo n.º 42
0
 def generic_courseware_link_substitution(match):
     parts = Location.parse_course_id(dest_course_id)
     parts['quote'] = match.group('quote')
     parts['rest'] = match.group('rest')
     return u'{quote}/courses/{org}/{course}/{name}/{rest}{quote}'.format(**parts)
Ejemplo n.º 43
0
    def add_cert(self, student, course_id, course=None, forced_grade=None, template_file=None, title='None'):
        """
        Request a new certificate for a student.

        Arguments:
          student   - User.object
          course_id - courseenrollment.course_id (string)
          forced_grade - a string indicating a grade parameter to pass with
                         the certificate request. If this is given, grading
                         will be skipped.

        Will change the certificate status to 'generating'.

        Certificate must be in the 'unavailable', 'error',
        'deleted' or 'generating' state.

        If a student has a passing grade or is in the whitelist
        table for the course a request will be made for a new cert.

        If a student has allow_certificate set to False in the
        userprofile table the status will change to 'restricted'

        If a student does not have a passing grade the status
        will change to status.notpassing

        Returns the student's status
        """

        VALID_STATUSES = [status.generating,
                          status.unavailable,
                          status.deleted,
                          status.error,
                          status.notpassing]

        cert_status = certificate_status_for_student(student, course_id)['status']

        new_status = cert_status

        if cert_status in VALID_STATUSES:
            # grade the student

            # re-use the course passed in optionally so we don't have to re-fetch everything
            # for every student
            if course is None:
                course = courses.get_course_by_id(course_id)
            profile = UserProfile.objects.get(user=student)
            profile_name = profile.name

            # Needed
            self.request.user = student
            self.request.session = {}

            course_name = course.display_name or course_id
            is_whitelisted = self.whitelist.filter(user=student, course_id=course_id, whitelist=True).exists()
            grade = grades.grade(student, self.request, course)
            enrollment_mode = CourseEnrollment.enrollment_mode_for_user(student, course_id)
            mode_is_verified = (enrollment_mode == GeneratedCertificate.MODES.verified)
            user_is_verified = SoftwareSecurePhotoVerification.user_is_verified(student)
            user_is_reverified = SoftwareSecurePhotoVerification.user_is_reverified_for_all(course_id, student)
            course_id_dict = Location.parse_course_id(course_id)
            cert_mode = enrollment_mode
            if (mode_is_verified and user_is_verified and user_is_reverified):
                template_pdf = "certificate-template-{org}-{course}-verified.pdf".format(**course_id_dict)
            elif (mode_is_verified and not (user_is_verified and user_is_reverified)):
                template_pdf = "certificate-template-{org}-{course}.pdf".format(**course_id_dict)
                cert_mode = GeneratedCertificate.MODES.honor
            else:
                # honor code and audit students
                template_pdf = "certificate-template-{org}-{course}.pdf".format(**course_id_dict)
            if forced_grade:
                grade['grade'] = forced_grade

            cert, __ = GeneratedCertificate.objects.get_or_create(user=student, course_id=course_id)

            cert.mode = cert_mode
            cert.user = student
            cert.grade = grade['percent']
            cert.course_id = course_id
            cert.name = profile_name
            # Strip HTML from grade range label
            grade_contents = grade.get('grade', None)
            try:
                grade_contents = lxml.html.fromstring(grade_contents).text_content()
            except (TypeError, XMLSyntaxError, ParserError) as e:
                #   Despite blowing up the xml parser, bad values here are fine
                grade_contents = None

            if is_whitelisted or grade_contents is not None:

                # check to see whether the student is on the
                # the embargoed country restricted list
                # otherwise, put a new certificate request
                # on the queue

                if self.restricted.filter(user=student).exists():
                    new_status = status.restricted
                    cert.status = new_status
                    cert.save()
                else:
                    key = make_hashkey(random.random())
                    cert.key = key
                    contents = {
                        'action': 'create',
                        'username': student.username,
                        'course_id': course_id,
                        'course_name': course_name,
                        'name': profile_name,
                        'grade': grade_contents,
                        'template_pdf': template_pdf,
                    }
                    if template_file:
                        contents['template_pdf'] = template_file
                    new_status = status.generating
                    cert.status = new_status
                    cert.save()
                    self._send_to_xqueue(contents, key)
            else:
                cert_status = status.notpassing
                cert.status = cert_status
                cert.save()

        return new_status
Ejemplo n.º 44
0
    def add_cert(self,
                 student,
                 course_id,
                 course=None,
                 forced_grade=None,
                 template_file=None,
                 title='None'):
        """
        Request a new certificate for a student.

        Arguments:
          student   - User.object
          course_id - courseenrollment.course_id (string)
          forced_grade - a string indicating a grade parameter to pass with
                         the certificate request. If this is given, grading
                         will be skipped.

        Will change the certificate status to 'generating'.

        Certificate must be in the 'unavailable', 'error',
        'deleted' or 'generating' state.

        If a student has a passing grade or is in the whitelist
        table for the course a request will be made for a new cert.

        If a student has allow_certificate set to False in the
        userprofile table the status will change to 'restricted'

        If a student does not have a passing grade the status
        will change to status.notpassing

        Returns the student's status
        """

        VALID_STATUSES = [
            status.generating, status.unavailable, status.deleted,
            status.error, status.notpassing
        ]

        cert_status = certificate_status_for_student(student,
                                                     course_id)['status']

        new_status = cert_status

        if cert_status in VALID_STATUSES:
            # grade the student

            # re-use the course passed in optionally so we don't have to re-fetch everything
            # for every student
            if course is None:
                course = courses.get_course_by_id(course_id)
            profile = UserProfile.objects.get(user=student)

            # Needed
            self.request.user = student
            self.request.session = {}

            is_whitelisted = self.whitelist.filter(user=student,
                                                   course_id=course_id,
                                                   whitelist=True).exists()
            grade = grades.grade(student, self.request, course)
            enrollment_mode = CourseEnrollment.enrollment_mode_for_user(
                student, course_id)
            mode_is_verified = (
                enrollment_mode == GeneratedCertificate.MODES.verified)
            user_is_verified = SoftwareSecurePhotoVerification.user_is_verified(
                student)
            user_is_reverified = SoftwareSecurePhotoVerification.user_is_reverified_for_all(
                course_id, student)
            course_id_dict = Location.parse_course_id(course_id)
            cert_mode = enrollment_mode
            if (mode_is_verified and user_is_verified and user_is_reverified):
                template_pdf = "certificate-template-{org}-{course}-verified.pdf".format(
                    **course_id_dict)
            elif (mode_is_verified
                  and not (user_is_verified and user_is_reverified)):
                template_pdf = "certificate-template-{org}-{course}.pdf".format(
                    **course_id_dict)
                cert_mode = GeneratedCertificate.MODES.honor
            else:
                # honor code and audit students
                template_pdf = "certificate-template-{org}-{course}.pdf".format(
                    **course_id_dict)
            if forced_grade:
                grade['grade'] = forced_grade

            cert, __ = GeneratedCertificate.objects.get_or_create(
                user=student, course_id=course_id)

            cert.mode = cert_mode
            cert.user = student
            cert.grade = grade['percent']
            cert.course_id = course_id
            cert.name = profile.name

            if is_whitelisted or grade['grade'] is not None:

                # check to see whether the student is on the
                # the embargoed country restricted list
                # otherwise, put a new certificate request
                # on the queue

                if self.restricted.filter(user=student).exists():
                    new_status = status.restricted
                    cert.status = new_status
                    cert.save()
                else:
                    key = make_hashkey(random.random())
                    cert.key = key
                    contents = {
                        'action': 'create',
                        'username': student.username,
                        'course_id': course_id,
                        'name': profile.name,
                        'grade': grade['grade'],
                        'template_pdf': template_pdf,
                    }
                    if template_file:
                        contents['template_pdf'] = template_file
                    new_status = status.generating
                    cert.status = new_status
                    cert.save()
                    self._send_to_xqueue(contents, key)
            else:
                new_status = status.notpassing
                cert.status = new_status
                cert.save()

        return new_status
Ejemplo n.º 45
0
 def generic_courseware_link_substitution(match):
     parts = Location.parse_course_id(dest_course_id)
     parts["quote"] = match.group("quote")
     parts["rest"] = match.group("rest")
     return u"{quote}/courses/{org}/{course}/{name}/{rest}{quote}".format(**parts)
Ejemplo n.º 46
0
    def add_cert(self, student, course_id, course=None):
        """

        Arguments:
          student - User.object
          course_id - courseenrollment.course_id (string)

        Request a new certificate for a student.
        Will change the certificate status to 'generating'.

        Certificate must be in the 'unavailable', 'error',
        'deleted' or 'generating' state.

        If a student has a passing grade or is in the whitelist
        table for the course a request will made for a new cert.

        If a student has allow_certificate set to False in the
        userprofile table the status will change to 'restricted'

        If a student does not have a passing grade the status
        will change to status.notpassing

        Returns the student's status

        """

        VALID_STATUSES = [status.generating,
                          status.unavailable,
                          status.deleted,
                          status.error,
                          status.notpassing]

        cert_status = certificate_status_for_student(student, course_id)['status']

        new_status = cert_status

        if cert_status in VALID_STATUSES:
            # grade the student

            # re-use the course passed in optionally so we don't have to re-fetch everything
            # for every student
            if course is None:
                course = courses.get_course_by_id(course_id)
            profile = UserProfile.objects.get(user=student)

            # Needed
            self.request.user = student
            self.request.session = {}

            grade = grades.grade(student, self.request, course)
            is_whitelisted = self.whitelist.filter(
                user=student, course_id=course_id, whitelist=True).exists()
            enrollment_mode = CourseEnrollment.enrollment_mode_for_user(student, course_id)
            mode_is_verified = (enrollment_mode == GeneratedCertificate.MODES.verified)
            user_is_verified = SoftwareSecurePhotoVerification.user_is_verified(student)
            user_is_reverified = SoftwareSecurePhotoVerification.user_is_reverified_for_all(course_id, student)
            course_id_dict = Location.parse_course_id(course_id)
            cert_mode = enrollment_mode
            if (mode_is_verified and user_is_verified and user_is_reverified):
                template_pdf = "certificate-template-{org}-{course}-verified.pdf".format(**course_id_dict)
            elif (mode_is_verified and not (user_is_verified and user_is_reverified)):
                template_pdf = "certificate-template-{org}-{course}.pdf".format(**course_id_dict)
                cert_mode = GeneratedCertificate.MODES.honor
            else:
                # honor code and audit students
                template_pdf = "certificate-template-{org}-{course}.pdf".format(**course_id_dict)

            cert, __ = GeneratedCertificate.objects.get_or_create(user=student, course_id=course_id)

            cert.mode = cert_mode
            cert.user = student
            cert.grade = grade['percent']
            cert.course_id = course_id
            cert.name = profile.name

            if is_whitelisted or grade['grade'] is not None:

                # check to see whether the student is on the
                # the embargoed country restricted list
                # otherwise, put a new certificate request
                # on the queue

                if self.restricted.filter(user=student).exists():
                    new_status = status.restricted
                    cert.status = new_status
                    cert.save()
                else:
                    key = make_hashkey(random.random())
                    cert.key = key
                    contents = {
                        'action': 'create',
                        'username': student.username,
                        'course_id': course_id,
                        'name': profile.name,
                        'grade': grade['grade'],
                        'template_pdf': template_pdf,
                    }
                    new_status = status.generating
                    cert.status = new_status
                    cert.save()
                    self._send_to_xqueue(contents, key)
            else:
                new_status = status.notpassing
                cert.status = new_status
                cert.save()

        return new_status
Ejemplo n.º 47
0
def get_hints(request, course_id, field):
    """
    Load all of the hints submitted to the course.

    Args:
    `request` -- Django request object.
    `course_id` -- The course id, like 'Me/19.002/test_course'
    `field` -- Either 'hints' or 'mod_queue'; specifies which set of hints to load.

    Keys in returned dict:
        - 'field': Same as input
        - 'other_field': 'mod_queue' if `field` == 'hints'; and vice-versa.
        - 'field_label', 'other_field_label': English name for the above.
        - 'all_hints': A list of [answer, pk dict] pairs, representing all hints.
          Sorted by answer.
        - 'id_to_name': A dictionary mapping problem id to problem name.
    """
    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"
    # The course_id is of the form school/number/classname.
    # We want to use the course_id to find all matching usage_id's.
    # To do this, just take the school/number part - leave off the classname.
    course_id_dict = Location.parse_course_id(course_id)
    chopped_id = u"{org}/{course}".format(**course_id_dict)
    chopped_id = re.escape(chopped_id)
    all_hints = XModuleUserStateSummaryField.objects.filter(field_name=field, usage_id__regex=chopped_id)
    # big_out_dict[problem id] = [[answer, {pk: [hint, votes]}], sorted by answer]
    # big_out_dict maps a problem id to a list of [answer, hints] pairs, sorted in order of answer.
    big_out_dict = {}
    # id_to name maps a problem id to the name of the problem.
    # id_to_name[problem id] = Display name of problem
    id_to_name = {}

    for hints_by_problem in all_hints:
        loc = Location(hints_by_problem.usage_id)
        name = location_to_problem_name(course_id, loc)
        if name is None:
            continue
        id_to_name[hints_by_problem.usage_id] = name

        def answer_sorter(thing):
            """
            `thing` is a tuple, where `thing[0]` contains an answer, and `thing[1]` contains
            a dict of hints.  This function returns an index based on `thing[0]`, which
            is used as a key to sort the list of things.
            """
            try:
                return float(thing[0])
            except ValueError:
                # Put all non-numerical answers first.
                return float("-inf")

        # Answer list contains [answer, dict_of_hints] pairs.
        answer_list = sorted(json.loads(hints_by_problem.value).items(), key=answer_sorter)
        big_out_dict[hints_by_problem.usage_id] = answer_list

    render_dict = {
        "field": field,
        "other_field": other_field,
        "field_label": field_label,
        "other_field_label": other_field_label,
        "all_hints": big_out_dict,
        "id_to_name": id_to_name,
    }
    return render_dict
Ejemplo n.º 48
0
    def create_item(self,
                    course_or_parent_loc,
                    category,
                    user_id=None,
                    **kwargs):
        """
        Create and return the item. If parent_loc is a specific location v a course id,
        it installs the new item as a child of the parent (if the parent_loc is a specific
        xblock reference).

        :param course_or_parent_loc: Can be a course_id (org/course/run), CourseLocator,
        Location, or BlockUsageLocator but must be what the persistence modulestore expects
        """
        # find the store for the course
        course_id = self._infer_course_id_try(course_or_parent_loc)
        if course_id is None:
            raise ItemNotFoundError(u"Cannot find modulestore for %s" %
                                    course_or_parent_loc)

        store = self._get_modulestore_for_courseid(course_id)

        location = kwargs.pop('location', None)
        # invoke its create_item
        if isinstance(store, MongoModuleStore):
            block_id = kwargs.pop('block_id',
                                  getattr(location, 'name',
                                          uuid4().hex))
            # convert parent loc if it's legit
            if isinstance(course_or_parent_loc, basestring):
                parent_loc = None
                if location is None:
                    loc_dict = Location.parse_course_id(course_id)
                    loc_dict['name'] = block_id
                    location = Location(category=category, **loc_dict)
            else:
                parent_loc = course_or_parent_loc
                # must have a legitimate location, compute if appropriate
                if location is None:
                    location = parent_loc.replace(category=category,
                                                  name=block_id)
            # do the actual creation
            xblock = store.create_and_save_xmodule(location, **kwargs)
            # don't forget to attach to parent
            if parent_loc is not None and not 'detached' in xblock._class_tags:
                parent = store.get_item(parent_loc)
                parent.children.append(location.url())
                store.update_item(parent)
        elif isinstance(store, SplitMongoModuleStore):
            if isinstance(course_or_parent_loc, basestring):  # course_id
                course_or_parent_loc = loc_mapper(
                ).translate_location_to_course_locator(
                    # hardcode draft version until we figure out how we're handling branches from app
                    course_or_parent_loc,
                    None,
                    published=False)
            elif not isinstance(course_or_parent_loc, CourseLocator):
                raise ValueError(
                    u"Cannot create a child of {} in split. Wrong repr.".
                    format(course_or_parent_loc))

            # split handles all the fields in one dict not separated by scope
            fields = kwargs.get('fields', {})
            fields.update(kwargs.pop('metadata', {}))
            fields.update(kwargs.pop('definition_data', {}))
            kwargs['fields'] = fields

            xblock = store.create_item(course_or_parent_loc, category, user_id,
                                       **kwargs)
        else:
            raise NotImplementedError(u"Cannot create an item on store %s" %
                                      store)

        return xblock
Ejemplo n.º 49
0
def get_hints(request, course_id, field):
    """
    Load all of the hints submitted to the course.

    Args:
    `request` -- Django request object.
    `course_id` -- The course id, like 'Me/19.002/test_course'
    `field` -- Either 'hints' or 'mod_queue'; specifies which set of hints to load.

    Keys in returned dict:
        - 'field': Same as input
        - 'other_field': 'mod_queue' if `field` == 'hints'; and vice-versa.
        - 'field_label', 'other_field_label': English name for the above.
        - 'all_hints': A list of [answer, pk dict] pairs, representing all hints.
          Sorted by answer.
        - 'id_to_name': A dictionary mapping problem id to problem name.
    """
    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'
    # The course_id is of the form school/number/classname.
    # We want to use the course_id to find all matching usage_id's.
    # To do this, just take the school/number part - leave off the classname.
    course_id_dict = Location.parse_course_id(course_id)
    chopped_id = u'{org}/{course}'.format(**course_id_dict)
    chopped_id = re.escape(chopped_id)
    all_hints = XModuleUserStateSummaryField.objects.filter(
        field_name=field, usage_id__regex=chopped_id)
    # big_out_dict[problem id] = [[answer, {pk: [hint, votes]}], sorted by answer]
    # big_out_dict maps a problem id to a list of [answer, hints] pairs, sorted in order of answer.
    big_out_dict = {}
    # id_to name maps a problem id to the name of the problem.
    # id_to_name[problem id] = Display name of problem
    id_to_name = {}

    for hints_by_problem in all_hints:
        loc = Location(hints_by_problem.usage_id)
        name = location_to_problem_name(course_id, loc)
        if name is None:
            continue
        id_to_name[hints_by_problem.usage_id] = name

        def answer_sorter(thing):
            """
            `thing` is a tuple, where `thing[0]` contains an answer, and `thing[1]` contains
            a dict of hints.  This function returns an index based on `thing[0]`, which
            is used as a key to sort the list of things.
            """
            try:
                return float(thing[0])
            except ValueError:
                # Put all non-numerical answers first.
                return float('-inf')

        # Answer list contains [answer, dict_of_hints] pairs.
        answer_list = sorted(json.loads(hints_by_problem.value).items(),
                             key=answer_sorter)
        big_out_dict[hints_by_problem.usage_id] = answer_list

    render_dict = {
        'field': field,
        'other_field': other_field,
        'field_label': field_label,
        'other_field_label': other_field_label,
        'all_hints': big_out_dict,
        'id_to_name': id_to_name
    }
    return render_dict
Ejemplo n.º 50
0
 def generic_courseware_link_substitution(match):
     parts = Location.parse_course_id(dest_course_id)
     parts['quote'] = match.group('quote')
     parts['rest'] = match.group('rest')
     return u'{quote}/courses/{org}/{course}/{name}/{rest}{quote}'.format(
         **parts)