def test_course_agnostic(self):
     lib_key = CourseKey.from_string('library-v1:TestX+lib1+version@519665f6223ebd6980884f2b')
     lib_key2 = CourseKey.from_string('library-v1:version@519665f6223ebd6980884f2b')
     lib_key3 = lib_key.course_agnostic()
     self.assertEqual(lib_key2, lib_key3)
     self.assertEqual(lib_key3.org, None)
     self.assertEqual(lib_key3.library, None)
Exemple #2
0
 def test_three_courses_but_only_two_unique(self):
     self.user_create_and_signin(1)
     self.link_edx_account_to_social(self.users[1], self.BACKEND, self.USERS[1]['FB_ID'])
     self.set_sharing_preferences(self.users[1], True)
     self.course_2 = CourseFactory.create(mobile_available=True)
     self.enroll_in_course(self.users[1], self.course_2)
     self.enroll_in_course(self.users[1], self.course)
     self.user_create_and_signin(2)
     self.link_edx_account_to_social(self.users[2], self.BACKEND, self.USERS[2]['FB_ID'])
     self.set_sharing_preferences(self.users[2], True)
     # Enroll another user in course_2
     self.enroll_in_course(self.users[2], self.course_2)
     self.set_facebook_interceptor_for_friends(
         {'data': [
             {'name': self.USERS[1]['USERNAME'], 'id': self.USERS[1]['FB_ID']},
             {'name': self.USERS[2]['USERNAME'], 'id': self.USERS[2]['FB_ID']},
         ]}
     )
     url = reverse('courses-with-friends')
     response = self.client.get(url, {'oauth_token': self._FB_USER_ACCESS_TOKEN})
     self.assertEqual(response.status_code, 200)
     self.assertEqual(self.course.id, CourseKey.from_string(response.data[0]['course']['id']))  # pylint: disable=E1101
     self.assertEqual(self.course_2.id, CourseKey.from_string(response.data[1]['course']['id']))  # pylint: disable=E1101
     # Assert that only two courses are returned
     self.assertEqual(len(response.data), 2)  # pylint: disable=E1101
 def test_sandbox_inclusion(self):
     """
     Test to make sure that a match works across course runs
     """
     self.assertTrue(can_execute_unsafe_code(CourseKey.from_string('edX/full/2012_Fall')))
     self.assertTrue(can_execute_unsafe_code(CourseKey.from_string('edX/full/2013_Spring')))
     self.assertFalse(can_execute_unsafe_code(LibraryLocator('edX', 'test_bank')))
Exemple #4
0
def rerun_course(source_course_key_string, destination_course_key_string, user_id, fields=None):
    """
    Reruns a course in a new celery task.
    """
    # import here, at top level this import prevents the celery workers from starting up correctly
    from edxval.api import copy_course_videos

    source_course_key = CourseKey.from_string(source_course_key_string)
    destination_course_key = CourseKey.from_string(destination_course_key_string)
    try:
        # deserialize the payload
        fields = deserialize_fields(fields) if fields else None

        # use the split modulestore as the store for the rerun course,
        # as the Mongo modulestore doesn't support multiple runs of the same course.
        store = modulestore()
        with store.default_store('split'):
            execute_and_log_time(
                store.clone_course, source_course_key, destination_course_key, user_id, fields=fields
            )

        # set initial permissions for the user to access the course.
        execute_and_log_time(initialize_permissions, destination_course_key, User.objects.get(id=user_id))

        # update state: Succeeded
        CourseRerunState.objects.succeeded(course_key=destination_course_key)

        # call edxval to attach videos to the rerun
        execute_and_log_time(copy_course_videos, source_course_key, destination_course_key)

        # Copy OrganizationCourse
        organization_course = OrganizationCourse.objects.filter(course_id=source_course_key_string).first()

        if organization_course:
            clone_instance(organization_course, {'course_id': destination_course_key_string})

        execute_and_log_time(add_course_restrictions, source_course_key, destination_course_key)

        return "succeeded"

    except DuplicateCourseError:
        # do NOT delete the original course, only update the status
        CourseRerunState.objects.failed(course_key=destination_course_key)
        LOGGER.exception(u'Course Rerun Error')
        return "duplicate course"

    # catch all exceptions so we can update the state and properly cleanup the course.
    except Exception as exc:  # pylint: disable=broad-except
        # update state: Failed
        CourseRerunState.objects.failed(course_key=destination_course_key)
        LOGGER.exception(u'Course Rerun Error')

        try:
            # cleanup any remnants of the course
            modulestore().delete_course(destination_course_key, user_id)
        except ItemNotFoundError:
            # it's possible there was an error even before the course module was created
            pass

        return u"exception: " + text_type(exc)
    def test_get_courses_for_wiki(self, _from_json):
        """
        Test the get_courses_for_wiki method
        """
        for course_number in self.courses:
            course_locations = self.draft_store.get_courses_for_wiki(course_number)
            assert_equals(len(course_locations), 1)
            assert_equals(CourseKey.from_string('/'.join(['edX', course_number, '2012_Fall'])), course_locations[0])

        course_locations = self.draft_store.get_courses_for_wiki('no_such_wiki')
        assert_equals(len(course_locations), 0)

        # set toy course to share the wiki with simple course
        toy_course = self.draft_store.get_course(CourseKey.from_string('edX/toy/2012_Fall'))
        toy_course.wiki_slug = 'simple'
        self.draft_store.update_item(toy_course, ModuleStoreEnum.UserID.test)

        # now toy_course should not be retrievable with old wiki_slug
        course_locations = self.draft_store.get_courses_for_wiki('toy')
        assert_equals(len(course_locations), 0)

        # but there should be two courses with wiki_slug 'simple'
        course_locations = self.draft_store.get_courses_for_wiki('simple')
        assert_equals(len(course_locations), 2)
        for course_number in ['toy', 'simple']:
            assert_in(CourseKey.from_string('/'.join(['edX', course_number, '2012_Fall'])), course_locations)

        # configure simple course to use unique wiki_slug.
        simple_course = self.draft_store.get_course(CourseKey.from_string('edX/simple/2012_Fall'))
        simple_course.wiki_slug = 'edX.simple.2012_Fall'
        self.draft_store.update_item(simple_course, ModuleStoreEnum.UserID.test)
        # it should be retrievable with its new wiki_slug
        course_locations = self.draft_store.get_courses_for_wiki('edX.simple.2012_Fall')
        assert_equals(len(course_locations), 1)
        assert_in(CourseKey.from_string('edX/simple/2012_Fall'), course_locations)
    def test_get_org_courses(self, _from_json):
        """
        Make sure that we can query for a filtered list of courses for a given ORG
        """

        courses = self.draft_store.get_courses(org='guestx')
        assert_equals(len(courses), 1)
        course_ids = [course.id for course in courses]

        for course_key in [
            CourseKey.from_string('/'.join(fields))
            for fields in [
                ['guestx', 'foo', 'bar']
            ]
        ]:
            assert_in(course_key, course_ids)

        courses = self.draft_store.get_courses(org='edX')
        assert_equals(len(courses), 5)
        course_ids = [course.id for course in courses]

        for course_key in [
            CourseKey.from_string('/'.join(fields))
            for fields in [
                ['edX', 'simple', '2012_Fall'],
                ['edX', 'simple_with_draft', '2012_Fall'],
                ['edX', 'test_import_course', '2012_Fall'],
                ['edX', 'test_unicode', '2012_Fall'],
                ['edX', 'toy', '2012_Fall'],
            ]
        ]:
            assert_in(course_key, course_ids)
    def test_course_listing_has_pre_requisite_courses(self):
        """
        Creates four courses. Enroll test user in all courses
        Sets two of them as pre-requisites of another course.
        Checks course where pre-requisite course is set has appropriate info.
        """
        seed_milestone_relationship_types()
        course_location2 = CourseKey.from_string('Org1/Course2/Run2')
        self._create_course_with_access_groups(course_location2)
        pre_requisite_course_location = CourseKey.from_string('Org1/Course3/Run3')
        self._create_course_with_access_groups(pre_requisite_course_location)
        pre_requisite_course_location2 = CourseKey.from_string('Org1/Course4/Run4')
        self._create_course_with_access_groups(pre_requisite_course_location2)
        # create a course with pre_requisite_courses
        pre_requisite_courses = [
            unicode(pre_requisite_course_location),
            unicode(pre_requisite_course_location2),
        ]
        course_location = CourseKey.from_string('Org1/Course1/Run1')
        self._create_course_with_access_groups(course_location, {
            'pre_requisite_courses': pre_requisite_courses
        })

        set_prerequisite_courses(course_location, pre_requisite_courses)
        # get dashboard
        course_enrollment_pairs = list(get_course_enrollment_pairs(self.student, None, []))
        courses_having_prerequisites = frozenset(course.id for course, _enrollment in course_enrollment_pairs
                                                 if course.pre_requisite_courses)
        courses_requirements_not_met = get_pre_requisite_courses_not_completed(
            self.student,
            courses_having_prerequisites
        )
        self.assertEqual(len(courses_requirements_not_met[course_location]['courses']), len(pre_requisite_courses))
    def test_get_courses(self, _from_json):
        '''Make sure the course objects loaded properly'''
        courses = self.draft_store.get_courses()

        assert_equals(len(courses), 6)
        course_ids = [course.id for course in courses]

        for course_key in [

            CourseKey.from_string('/'.join(fields))
            for fields in [
                ['edX', 'simple', '2012_Fall'],
                ['edX', 'simple_with_draft', '2012_Fall'],
                ['edX', 'test_import_course', '2012_Fall'],
                ['edX', 'test_unicode', '2012_Fall'],
                ['edX', 'toy', '2012_Fall'],
                ['guestx', 'foo', 'bar'],
            ]
        ]:
            assert_in(course_key, course_ids)
            course = self.draft_store.get_course(course_key)
            assert_not_none(course)
            assert_true(self.draft_store.has_course(course_key))
            mix_cased = CourseKey.from_string(
                '/'.join([course_key.org.upper(), course_key.course.upper(), course_key.run.lower()])
            )
            assert_false(self.draft_store.has_course(mix_cased))
            assert_true(self.draft_store.has_course(mix_cased, ignore_case=True))
    def test_caching(self):
        deadlines = {
            CourseKey.from_string("edX/DemoX/Fall"): datetime.now(pytz.UTC),
            CourseKey.from_string("edX/DemoX/Spring"): datetime.now(pytz.UTC) + timedelta(days=1)
        }
        course_keys = deadlines.keys()

        # Initially, no deadlines are set
        with self.assertNumQueries(1):
            all_deadlines = VerificationDeadline.deadlines_for_courses(course_keys)
            self.assertEqual(all_deadlines, {})

        # Create the deadlines
        for course_key, deadline in deadlines.iteritems():
            VerificationDeadline.objects.create(
                course_key=course_key,
                deadline=deadline,
            )

        # Warm the cache
        with self.assertNumQueries(1):
            VerificationDeadline.deadlines_for_courses(course_keys)

        # Load the deadlines from the cache
        with self.assertNumQueries(0):
            all_deadlines = VerificationDeadline.deadlines_for_courses(course_keys)
            self.assertEqual(all_deadlines, deadlines)

        # Delete the deadlines
        VerificationDeadline.objects.all().delete()

        # Verify that the deadlines are updated correctly
        with self.assertNumQueries(1):
            all_deadlines = VerificationDeadline.deadlines_for_courses(course_keys)
            self.assertEqual(all_deadlines, {})
    def test_empty_path(self):
        """ Test InvalidKeyError when asset path is empty """
        with self.assertRaises(InvalidKeyError):
            CourseKey.from_string('course-locator:org+course+run').make_asset_key('asset', '')

        with self.assertRaises(InvalidKeyError):
            CourseKey.from_string('org/course/run').make_asset_key('asset', '')
Exemple #11
0
def validate_course_key(course_key):
    """
    Validate the course_key is correct.
    """
    try:
        CourseKey.from_string(course_key)
    except InvalidKeyError:
        raise ValidationError(_("Invalid course key."))
 def setUp(self):
     super(TestGetRunMarketingUrls, self).setUp()
     self.user = UserFactory()
     self.course_runs = [factories.CourseRun() for __ in range(2)]
     self.course_keys = [
         CourseKey.from_string(self.course_runs[0]["key"]),
         CourseKey.from_string(self.course_runs[1]["key"]),
     ]
 def test_map_into_course_location(self):
     original_course = CourseKey.from_string('org/course/run')
     new_course = CourseKey.from_string('edX/toy/2012_Fall')
     loc = BlockUsageLocator(original_course, 'cat', 'name:more_name', deprecated=True)
     self.assertEquals(
         BlockUsageLocator(new_course, 'cat', 'name:more_name', deprecated=True),
         loc.map_into_course(new_course)
     )
    def test_empty_path(self):
        with self.assertRaises(InvalidKeyError):
            CourseKey.from_string('course-locator:org+course+run').make_asset_key('asset', '')

        self.assertEquals(
            '/c4x/org/course/asset/',
            unicode(CourseKey.from_string('org/course/run').make_asset_key('asset', ''))
        )
 def test_map_into_course_asset_location(self):
     original_course = CourseKey.from_string('org/course/run')
     new_course = CourseKey.from_string('edX/toy/2012_Fall')
     loc = AssetLocator(original_course, 'asset', 'foo.bar')
     self.assertEquals(
         AssetLocator(new_course, 'asset', 'foo.bar', deprecated=True),
         loc.map_into_course(new_course)
     )
def course_key_is_valid(course_key):
    if course_key is None:
        return False
    try:
        CourseKey.from_string(unicode(course_key))
    except InvalidKeyError:
        return False
    return True
Exemple #17
0
    def get(self, request):
        """GET /api/team/v0/team_membership"""
        specified_username_or_team = False
        username = None
        team_id = None
        requested_course_id = None
        requested_course_key = None
        accessible_course_ids = None

        if 'course_id' in request.QUERY_PARAMS:
            requested_course_id = request.QUERY_PARAMS['course_id']
            try:
                requested_course_key = CourseKey.from_string(requested_course_id)
            except InvalidKeyError:
                return Response(status=status.HTTP_404_NOT_FOUND)

        if 'team_id' in request.QUERY_PARAMS:
            specified_username_or_team = True
            team_id = request.QUERY_PARAMS['team_id']
            try:
                team = CourseTeam.objects.get(team_id=team_id)
            except CourseTeam.DoesNotExist:
                return Response(status=status.HTTP_404_NOT_FOUND)
            if requested_course_key is not None and requested_course_key != team.course_id:
                return Response(status=status.HTTP_400_BAD_REQUEST)
            if not has_team_api_access(request.user, team.course_id):
                return Response(status=status.HTTP_404_NOT_FOUND)

        if 'username' in request.QUERY_PARAMS:
            specified_username_or_team = True
            username = request.QUERY_PARAMS['username']
            if not request.user.is_staff:
                enrolled_courses = (
                    CourseEnrollment.enrollments_for_user(request.user).values_list('course_id', flat=True)
                )
                staff_courses = (
                    CourseAccessRole.objects.filter(user=request.user, role='staff').values_list('course_id', flat=True)
                )
                accessible_course_ids = [item for sublist in (enrolled_courses, staff_courses) for item in sublist]
                if requested_course_id is not None and requested_course_id not in accessible_course_ids:
                    return Response(status=status.HTTP_400_BAD_REQUEST)

        if not specified_username_or_team:
            return Response(
                build_api_error(ugettext_noop("username or team_id must be specified.")),
                status=status.HTTP_400_BAD_REQUEST
            )

        course_keys = None
        if requested_course_key is not None:
            course_keys = [requested_course_key]
        elif accessible_course_ids is not None:
            course_keys = [CourseKey.from_string(course_string) for course_string in accessible_course_ids]

        queryset = CourseTeamMembership.get_memberships(username, course_keys, team_id)
        page = self.paginate_queryset(queryset)
        serializer = self.get_pagination_serializer(page)
        return Response(serializer.data)  # pylint: disable=maybe-no-member
    def test_course_key(self):
        """
        Test CourseKey
        """
        key = CourseKey.from_string('org.id/course_id/run')
        self.assertEqual(key.org, 'org.id')

        key = CourseKey.from_string('course-v1:org.id+course_id+run')
        self.assertEqual(key.org, 'org.id')
Exemple #19
0
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)

        entitlement = serializer.instance
        user = entitlement.user

        # find all course_runs within the course
        course_runs = get_course_runs_for_course(entitlement.course_uuid)

        # check if the user has enrollments for any of the course_runs
        user_run_enrollments = [
            CourseEnrollment.get_enrollment(user, CourseKey.from_string(course_run.get('key')))
            for course_run
            in course_runs
            if CourseEnrollment.get_enrollment(user, CourseKey.from_string(course_run.get('key')))
        ]

        # filter to just enrollments that can be upgraded.
        upgradeable_enrollments = [
            enrollment
            for enrollment
            in user_run_enrollments
            if enrollment.is_active and enrollment.upgrade_deadline and enrollment.upgrade_deadline > timezone.now()
        ]

        # if there is only one upgradeable enrollment, convert it from audit to the entitlement.mode
        # if there is any ambiguity about which enrollment to upgrade
        # (i.e. multiple upgradeable enrollments or no available upgradeable enrollment), dont enroll
        if len(upgradeable_enrollments) == 1:
            enrollment = upgradeable_enrollments[0]
            log.info(
                'Upgrading enrollment [%s] from %s to %s while adding entitlement for user [%s] for course [%s]',
                enrollment,
                enrollment.mode,
                serializer.data.get('mode'),
                user.username,
                serializer.data.get('course_uuid')
            )
            enrollment.update_enrollment(mode=entitlement.mode)
            entitlement.set_enrollment(enrollment)
        else:
            log.info(
                'No enrollment upgraded while adding entitlement for user [%s] for course [%s] ',
                user.username,
                serializer.data.get('course_uuid')
            )

        headers = self.get_success_headers(serializer.data)
        # Note, the entitlement is re-serialized before getting added to the Response,
        # so that the 'modified' date reflects changes that occur when upgrading enrollment.
        return Response(
            CourseEntitlementSerializer(entitlement).data,
            status=status.HTTP_201_CREATED, headers=headers
        )
    def get(self, request, *args, **kwargs):
        self.course_id = self.kwargs.get('course_id', request.QUERY_PARAMS.get('course_id', None))

        if not self.course_id:
            raise CourseNotSpecifiedError()
        try:
            CourseKey.from_string(self.course_id)
        except InvalidKeyError:
            raise CourseKeyMalformedError(course_id=self.course_id)
        return super(CourseViewMixin, self).get(request, *args, **kwargs)
 def _validate_course_ids(self, course_ids):
     """
     Validate a list of course key strings.
     """
     try:
         for course_id in course_ids:
             CourseKey.from_string(course_id)
         return course_ids
     except InvalidKeyError as error:
         raise CommandError('Invalid key specified: {}'.format(text_type(error)))
    def test_versions(self):
        lib_key = CourseKey.from_string('library-v1:TestX+lib1+version@519665f6223ebd6980884f2b')
        lib_key2 = CourseKey.from_string('library-v1:TestX+lib1')
        lib_key3 = lib_key.version_agnostic()
        self.assertEqual(lib_key2, lib_key3)
        self.assertEqual(lib_key3.version_guid, None)

        new_version = '123445678912345678912345'
        lib_key4 = lib_key.for_version(new_version)
        self.assertEqual(lib_key4.version_guid, ObjectId(new_version))
Exemple #23
0
    def inner(request, *args, **kwargs):
        course_key = kwargs.get('course_key_string') or kwargs.get('course_id')
        if course_key is not None:
            try:
                CourseKey.from_string(course_key)
            except InvalidKeyError:
                raise Http404

        response = view_func(request, *args, **kwargs)
        return response
Exemple #24
0
    def test_list(self):
        """ Verify the endpoint supports listing all CreditCourse objects. """
        cc1 = CreditCourse.objects.create(course_key=CourseKey.from_string('a/b/c'))
        cc2 = CreditCourse.objects.create(course_key=CourseKey.from_string('d/e/f'), enabled=True)
        expected = [self._serialize_credit_course(cc1), self._serialize_credit_course(cc2)]

        response = self.client.get(self.path)
        self.assertEqual(response.status_code, 200)

        # Verify the API returns a list of serialized CreditCourse objects
        self.assertListEqual(json.loads(response.content), expected)
Exemple #25
0
def delete_course_task(user_id, course_key_string):

    profile = UserProfile.objects.get(pk=user_id)
    user = User.objects.get(pk=profile.user_id)

    course_key = CourseKey.from_string(course_key_string)
    delete_course_and_groups(course_key, user.id)
    searcher = SearchEngine.get_search_engine(CoursewareSearchIndexer.INDEX_NAME)
    if searcher != None:
        CoursewareSearchIndexer.remove_deleted_items(searcher, CourseKey.from_string(course_key_string), [])
        searcher.remove(CourseAboutSearchIndexer.DISCOVERY_DOCUMENT_TYPE, [course_key_string])
    def setUp(self):
        super(BatchCompletionMethodTests, self).setUp()
        self.override_waffle_switch(True)

        self.user = UserFactory.create()
        self.other_user = UserFactory.create()
        self.course_key = CourseKey.from_string("edX/MOOC101/2049_T2")
        self.other_course_key = CourseKey.from_string("course-v1:ReedX+Hum110+1904")
        self.block_keys = [UsageKey.from_string("i4x://edX/MOOC101/video/{}".format(number)) for number in xrange(5)]

        self.submit_fake_completions()
 def validate_courses(self, value):
     """
     Check that each course key in list is valid.
     """
     course_keys = value
     for course in course_keys:
         try:
             CourseKey.from_string(course)
         except InvalidKeyError:
             raise serializers.ValidationError(u"Course key not valid: {}".format(course))
     return value
Exemple #28
0
def course_key_is_valid(course_key):
    """
    Course key object validation
    """
    if course_key is None:
        return False
    try:
        CourseKey.from_string(six.text_type(course_key))
    except InvalidKeyError:
        return False
    return True
Exemple #29
0
    def clean_course_key(self):
        """Clean the course key to make sure format is valid."""
        course_key = self.cleaned_data['course_key']
        try:
            CourseKey.from_string(course_key)
        except InvalidKeyError:
            raise forms.ValidationError('Invalid CourseKey {course_key}!'.format(
                course_key=course_key
            ))

        return course_key
def course_key_is_valid(course_key):
    """
    Course key object validation
    """
    if course_key is None:
        return False
    try:
        CourseKey.from_string(unicode(course_key))
    except (InvalidKeyError, UnicodeDecodeError):
        return False
    return True
    def handle(self, *args, **options):

        if not options['course']:
            raise CommandError('You must specify a course id')

        COURSE_ID = options['course']

        # Print update after this many students
        if options['info_interval']:
            STATUS_INTERVAL = options['info_interval']
        else:
            STATUS_INTERVAL = 100

        print "\nEnroll all active students to '%s'" % COURSE_ID

        try:
            # CourseKeys are needed to query databases (SQL, Mongo)
            course_key = CourseKey.from_string(COURSE_ID)
            if not modulestore().get_course(course_key, depth=2):
                print "\n Course %s not found." % COURSE_ID
                return

            enrolled = 0
            start = datetime.datetime.now(UTC)

            # Fetch all users that are not superuser, exclude non active students if necessary
            if options['exclude']:
                users = User.objects.exclude(
                    Q(is_active=False) | Q(profile__isnull=True))
            else:
                users = User.objects.exclude(profile__isnull=True)

            total_students = users.count()

            for user in users:

                if enrolled % STATUS_INTERVAL == 0:
                    # Print a status update with an approximation of
                    # how much time is left based on how long the last
                    # interval took
                    diff = datetime.datetime.now(UTC) - start
                    timeleft = diff * (total_students -
                                       enrolled) / STATUS_INTERVAL
                    hours, remainder = divmod(timeleft.seconds, 3600)
                    minutes, _seconds = divmod(remainder, 60)
                    print(
                        "{0} students out of {1} were successfully enrolled to "
                        "course {2} ~{3:02}:{4:02}m remaining").format(
                            enrolled, total_students, COURSE_ID, hours,
                            minutes)

                    start = datetime.datetime.now(UTC)

                if not CourseEnrollment.objects.filter(
                        user=user, course_id=course_key).exists():
                    CourseEnrollment.objects.create(user=user,
                                                    course_id=course_key,
                                                    is_active=True,
                                                    mode='honor')
                enrolled += 1

            print(
                "\nSubscription progress is over : {0} students out of {1}"
                " were successfully enrolled to course {2}").format(
                    enrolled, total_students, COURSE_ID)

        except InvalidKeyError:
            print("Course id {} could not be parsed as a CourseKey;".format(
                COURSE_ID))
Exemple #32
0
 def setUp(self, **kwargs):
     super(CreditApiTestBase, self).setUp()
     self.course_key = CourseKey.from_string("edX/DemoX/Demo_Course")
Exemple #33
0
def import_olx(self, user_id, course_key_string, archive_path, archive_name, language):
    """
    Import a course or library from a provided OLX .tar.gz archive.
    """
    courselike_key = CourseKey.from_string(course_key_string)
    try:
        user = User.objects.get(pk=user_id)
    except User.DoesNotExist:
        with respect_language(language):
            self.status.fail(_(u'Unknown User ID: {0}').format(user_id))
        return
    if not has_course_author_access(user, courselike_key):
        with respect_language(language):
            self.status.fail(_(u'Permission denied'))
        return

    is_library = isinstance(courselike_key, LibraryLocator)
    is_course = not is_library
    if is_library:
        root_name = LIBRARY_ROOT
        courselike_module = modulestore().get_library(courselike_key)
        import_func = import_library_from_xml
    else:
        root_name = COURSE_ROOT
        courselike_module = modulestore().get_course(courselike_key)
        import_func = import_course_from_xml

    # Locate the uploaded OLX archive (and download it from S3 if necessary)
    # Do everything in a try-except block to make sure everything is properly cleaned up.
    data_root = path(settings.GITHUB_REPO_ROOT)
    subdir = base64.urlsafe_b64encode(repr(courselike_key))
    course_dir = data_root / subdir
    try:
        self.status.set_state(u'Unpacking')

        if not archive_name.endswith(u'.tar.gz'):
            with respect_language(language):
                self.status.fail(_(u'We only support uploading a .tar.gz file.'))
                return

        temp_filepath = course_dir / get_valid_filename(archive_name)
        if not course_dir.isdir():  # pylint: disable=no-value-for-parameter
            os.mkdir(course_dir)

        LOGGER.debug(u'importing course to {0}'.format(temp_filepath))

        # Copy the OLX archive from where it was uploaded to (S3, Swift, file system, etc.)
        if not course_import_export_storage.exists(archive_path):
            LOGGER.info(u'Course import %s: Uploaded file %s not found', courselike_key, archive_path)
            with respect_language(language):
                self.status.fail(_(u'Tar file not found'))
            return
        with course_import_export_storage.open(archive_path, 'rb') as source:
            with open(temp_filepath, 'wb') as destination:
                def read_chunk():
                    """
                    Read and return a sequence of bytes from the source file.
                    """
                    return source.read(FILE_READ_CHUNK)
                for chunk in iter(read_chunk, b''):
                    destination.write(chunk)
        LOGGER.info(u'Course import %s: Download from storage complete', courselike_key)
        # Delete from source location
        course_import_export_storage.delete(archive_path)

        # If the course has an entrance exam then remove it and its corresponding milestone.
        # current course state before import.
        if is_course:
            if courselike_module.entrance_exam_enabled:
                fake_request = RequestFactory().get(u'/')
                fake_request.user = user
                from contentstore.views.entrance_exam import remove_entrance_exam_milestone_reference
                # TODO: Is this really ok?  Seems dangerous for a live course
                remove_entrance_exam_milestone_reference(fake_request, courselike_key)
                LOGGER.info(
                    u'entrance exam milestone content reference for course %s has been removed',
                    courselike_module.id
                )
    # Send errors to client with stage at which error occurred.
    except Exception as exception:  # pylint: disable=broad-except
        if course_dir.isdir():  # pylint: disable=no-value-for-parameter
            shutil.rmtree(course_dir)
            LOGGER.info(u'Course import %s: Temp data cleared', courselike_key)

        LOGGER.exception(u'Error importing course %s', courselike_key)
        self.status.fail(text_type(exception))
        return

    # try-finally block for proper clean up after receiving file.
    try:
        tar_file = tarfile.open(temp_filepath)
        try:
            safetar_extractall(tar_file, (course_dir + u'/').encode(u'utf-8'))
        except SuspiciousOperation as exc:
            LOGGER.info(u'Course import %s: Unsafe tar file - %s', courselike_key, exc.args[0])
            with respect_language(language):
                self.status.fail(_(u'Unsafe tar file. Aborting import.'))
            return
        finally:
            tar_file.close()

        LOGGER.info(u'Course import %s: Uploaded file extracted', courselike_key)
        self.status.set_state(u'Verifying')
        self.status.increment_completed_steps()

        # find the 'course.xml' file
        def get_all_files(directory):
            """
            For each file in the directory, yield a 2-tuple of (file-name,
            directory-path)
            """
            for directory_path, _dirnames, filenames in os.walk(directory):
                for filename in filenames:
                    yield (filename, directory_path)

        def get_dir_for_filename(directory, filename):
            """
            Returns the directory path for the first file found in the directory
            with the given name.  If there is no file in the directory with
            the specified name, return None.
            """
            for name, directory_path in get_all_files(directory):
                if name == filename:
                    return directory_path
            return None

        dirpath = get_dir_for_filename(course_dir, root_name)
        if not dirpath:
            with respect_language(language):
                self.status.fail(_(u'Could not find the {0} file in the package.').format(root_name))
                return

        dirpath = os.path.relpath(dirpath, data_root)
        LOGGER.debug(u'found %s at %s', root_name, dirpath)

        LOGGER.info(u'Course import %s: Extracted file verified', courselike_key)
        self.status.set_state(u'Updating')
        self.status.increment_completed_steps()

        with dog_stats_api.timer(
            u'courselike_import.time',
            tags=[u"courselike:{}".format(courselike_key)]
        ):
            courselike_items = import_func(
                modulestore(), user.id,
                settings.GITHUB_REPO_ROOT, [dirpath],
                load_error_modules=False,
                static_content_store=contentstore(),
                target_id=courselike_key
            )

        new_location = courselike_items[0].location
        LOGGER.debug(u'new course at %s', new_location)

        LOGGER.info(u'Course import %s: Course import successful', courselike_key)
        self.status.increment_completed_steps()
    except Exception as exception:   # pylint: disable=broad-except
        LOGGER.exception(u'error importing course')
        self.status.fail(text_type(exception))
    finally:
        if course_dir.isdir():  # pylint: disable=no-value-for-parameter
            shutil.rmtree(course_dir)
            LOGGER.info(u'Course import %s: Temp data cleared', courselike_key)

        if self.status.completed_steps == 3 and is_course:
            # Reload the course so we have the latest state
            course = modulestore().get_course(courselike_key)
            if course.entrance_exam_enabled:
                entrance_exam_chapter = modulestore().get_items(
                    course.id,
                    qualifiers={u'category': u'chapter'},
                    settings={u'is_entrance_exam': True}
                )[0]

                metadata = {u'entrance_exam_id': text_type(entrance_exam_chapter.location)}
                CourseMetadata.update_from_dict(metadata, course, user)
                from contentstore.views.entrance_exam import add_entrance_exam_milestone
                add_entrance_exam_milestone(course.id, entrance_exam_chapter)
                LOGGER.info(u'Course %s Entrance exam imported', course.id)
Exemple #34
0
 def prepare_org(self, obj):
     course_run = filter_visible_runs(obj.course_runs).first()
     if course_run:
         return CourseKey.from_string(course_run.key).org
     return None
Exemple #35
0
def render_html_view(request, course_id, certificate=None):  # pylint: disable=too-many-statements
    """
    This public view generates an HTML representation of the specified user and course
    If a certificate is not available, we display a "Sorry!" screen instead
    It can be overridden by setting `OVERRIDE_RENDER_CERTIFICATE_VIEW` to an alternative implementation.
    """
    user = certificate.user if certificate else request.user
    user_id = user.id
    preview_mode = request.GET.get('preview', None)
    platform_name = configuration_helpers.get_value("platform_name", settings.PLATFORM_NAME)
    configuration = CertificateHtmlViewConfiguration.get_config()

    # Kick the user back to the "Invalid" screen if the feature is disabled globally
    if not settings.FEATURES.get('CERTIFICATES_HTML_VIEW', False):
        return _render_invalid_certificate(request, course_id, platform_name, configuration)

    # Load the course and user objects
    try:
        course_key = CourseKey.from_string(course_id)
        course = get_course_by_id(course_key)

    # For any course or user exceptions, kick the user back to the "Invalid" screen
    except (InvalidKeyError, Http404) as exception:
        error_str = (
            "Invalid cert: error finding course %s "
            "Specific error: %s"
        )
        log.info(error_str, course_id, str(exception))
        return _render_invalid_certificate(request, course_id, platform_name, configuration)

    course_overview = get_course_overview_or_none(course_key)

    # Kick the user back to the "Invalid" screen if the feature is disabled for the course
    if not course.cert_html_view_enabled:
        log.info(
            "Invalid cert: HTML certificates disabled for %s. User id: %d",
            course_id,
            user_id,
        )
        return _render_invalid_certificate(request, course_id, platform_name, configuration)

    # Load user's certificate
    user_certificate = _get_user_certificate(request, user, course_key, course_overview, preview_mode)
    if not user_certificate:
        log.info(
            "Invalid cert: User %d does not have eligible cert for %s.",
            user_id,
            course_id,
        )
        return _render_invalid_certificate(request, course_id, platform_name, configuration)

    # Get the active certificate configuration for this course
    # If we do not have an active certificate, we'll need to send the user to the "Invalid" screen
    # Passing in the 'preview' parameter, if specified, will return a configuration, if defined
    active_configuration = get_active_web_certificate(course, preview_mode)
    if active_configuration is None:
        log.info(
            "Invalid cert: course %s does not have an active configuration. User id: %d",
            course_id,
            user_id,
        )
        return _render_invalid_certificate(request, course_id, platform_name, configuration)

    # Get data from Discovery service that will be necessary for rendering this Certificate.
    catalog_data = _get_catalog_data_for_course(course_key)

    # Determine whether to use the standard or custom template to render the certificate.
    custom_template = None
    custom_template_language = None
    if settings.FEATURES.get('CUSTOM_CERTIFICATE_TEMPLATES_ENABLED', False):
        log.info("Custom certificate for course %s", course_id)
        custom_template, custom_template_language = _get_custom_template_and_language(
            course.id,
            user_certificate.mode,
            catalog_data.pop('content_language', None)
        )

    # Determine the language that should be used to render the certificate.
    # For the standard certificate template, use the user language. For custom templates, use
    # the language associated with the template.
    user_language = translation.get_language()
    certificate_language = custom_template_language if custom_template else user_language

    log.info(
        "certificate language is: %s for the course: %s",
        certificate_language,
        course_key
    )

    # Generate the certificate context in the correct language, then render the template.
    with translation.override(certificate_language):
        context = {'user_language': user_language}

        _update_context_with_basic_info(context, course_id, platform_name, configuration)

        context['certificate_data'] = active_configuration

        # Append/Override the existing view context values with any mode-specific ConfigurationModel values
        context.update(configuration.get(user_certificate.mode, {}))

        # Append organization info
        _update_organization_context(context, course)

        # Append course info
        _update_course_context(request, context, course, platform_name)

        # Append course run info from discovery
        context.update(catalog_data)

        # Append user info
        _update_context_with_user_info(context, user, user_certificate)

        # Append social sharing info
        _update_social_context(request, context, course, user_certificate, platform_name)

        # Append/Override the existing view context values with certificate specific values
        _update_certificate_context(context, course, course_overview, user_certificate, platform_name)

        # Append badge info
        _update_badge_context(context, course, user)

        # Add certificate header/footer data to current context
        context.update(get_certificate_header_context(is_secure=request.is_secure()))
        context.update(get_certificate_footer_context())

        # Append/Override the existing view context values with any course-specific static values from Advanced Settings
        context.update(course.cert_html_view_overrides)

        # Track certificate view events
        _track_certificate_events(request, course, user, user_certificate)

        try:
            # .. filter_implemented_name: CertificateRenderStarted
            # .. filter_type: org.openedx.learning.certificate.render.started.v1
            context, custom_template = CertificateRenderStarted.run_filter(
                context=context,
                custom_template=custom_template,
            )
        except CertificateRenderStarted.RenderAlternativeInvalidCertificate as exc:
            response = _render_invalid_certificate(
                request,
                course_id,
                platform_name,
                configuration,
                cert_path=exc.template_name or INVALID_CERTIFICATE_TEMPLATE_PATH,
            )
        except CertificateRenderStarted.RedirectToPage as exc:
            response = HttpResponseRedirect(exc.redirect_to)
        except CertificateRenderStarted.RenderCustomResponse as exc:
            response = exc.response
        else:
            response = _render_valid_certificate(request, context, custom_template)

        # Render the certificate
        return response
Exemple #36
0
def change_enrollment(request, check_access=True):
    """
    Modify the enrollment status for the logged-in user.

    TODO: This is lms specific and does not belong in common code.

    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, so
    the error messages in the responses should never actually be user-visible.

    Args:
        request (`Request`): The Django request object

    Keyword Args:
        check_access (boolean): If True, we check that an accessible course actually
            exists for the given course_key before we enroll the student.
            The default is set to False to avoid breaking legacy code or
            code with non-standard flows (ex. beta tester invitations), but
            for any standard enrollment flow you probably want this to be True.

    Returns:
        Response

    """
    # Get the user
    user = request.user

    # Ensure the user is authenticated
    if not user.is_authenticated:
        return HttpResponseForbidden()

    # Ensure we received a course_id
    action = request.POST.get("enrollment_action")
    if 'course_id' not in request.POST:
        return HttpResponseBadRequest(_("Course id not specified"))

    try:
        course_id = CourseKey.from_string(request.POST.get("course_id"))
    except InvalidKeyError:
        log.warning(
            "User %s tried to %s with invalid course id: %s",
            user.username,
            action,
            request.POST.get("course_id"),
        )
        return HttpResponseBadRequest(_("Invalid course id"))

    # Allow us to monitor performance of this transaction on a per-course basis since we often roll-out features
    # on a per-course basis.
    monitoring_utils.set_custom_attribute('course_id', str(course_id))

    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
        if not modulestore().has_course(course_id):
            log.warning("User %s tried to enroll in non-existent course %s",
                        user.username, course_id)
            return HttpResponseBadRequest(_("Course id is invalid"))

        # Record the user's email opt-in preference
        if settings.FEATURES.get('ENABLE_MKTG_EMAIL_OPT_IN'):
            _update_email_opt_in(request, course_id.org)

        available_modes = CourseMode.modes_for_course_dict(course_id)

        # Check whether the user is blocked from enrolling in this course
        # This can occur if the user's IP is on a global blacklist
        # or if the user is enrolling in a country in which the course
        # is not available.
        redirect_url = embargo_api.redirect_if_blocked(
            course_id,
            user=user,
            ip_address=get_client_ip(request)[0],
            url=request.path)
        if redirect_url:
            return HttpResponse(redirect_url)

        if CourseEntitlement.check_for_existing_entitlement_and_enroll(
                user=user, course_run_key=course_id):
            return HttpResponse(reverse('courseware', args=[str(course_id)]))

        # Check that auto enrollment is allowed for this course
        # (= the course is NOT behind a paywall)
        if CourseMode.can_auto_enroll(course_id):
            # Enroll the user using the default mode (audit)
            # We're assuming that users of the course enrollment table
            # will NOT try to look up the course enrollment model
            # by its slug.  If they do, it's possible (based on the state of the database)
            # for no such model to exist, even though we've set the enrollment type
            # to "audit".
            try:
                enroll_mode = CourseMode.auto_enroll_mode(
                    course_id, available_modes)
                if enroll_mode:
                    CourseEnrollment.enroll(user,
                                            course_id,
                                            check_access=check_access,
                                            mode=enroll_mode)
            except Exception:  # pylint: disable=broad-except
                return HttpResponseBadRequest(_("Could not enroll"))

        # If we have more than one course mode or professional ed is enabled,
        # then send the user to the choose your track page.
        # (In the case of no-id-professional/professional ed, this will redirect to a page that
        # funnels users directly into the verification / payment flow)
        if CourseMode.has_verified_mode(
                available_modes) or CourseMode.has_professional_mode(
                    available_modes):
            return HttpResponse(
                reverse("course_modes_choose",
                        kwargs={'course_id': str(course_id)}))

        # Otherwise, there is only one mode available (the default)
        return HttpResponse()
    elif action == "unenroll":
        enrollment = CourseEnrollment.get_enrollment(user, course_id)
        if not enrollment:
            return HttpResponseBadRequest(
                _("You are not enrolled in this course"))

        certificate_info = cert_info(user, enrollment.course_overview)
        if certificate_info.get('status') in DISABLE_UNENROLL_CERT_STATES:
            return HttpResponseBadRequest(
                _("Your certificate prevents you from unenrolling from this course"
                  ))

        CourseEnrollment.unenroll(user, course_id)
        REFUND_ORDER.send(sender=None, course_enrollment=enrollment)
        return HttpResponse()
    else:
        return HttpResponseBadRequest(_("Enrollment action is invalid"))
def instructor_dashboard_2(request, course_id):
    """ Display the instructor dashboard for a course. """
    try:
        course_key = CourseKey.from_string(course_id)
    except InvalidKeyError:
        log.error(
            u"Unable to find course with course key %s while loading the Instructor Dashboard.",
            course_id)
        return HttpResponseServerError()

    course = get_course_by_id(course_key, depth=0)

    access = {
        'admin':
        request.user.is_staff,
        'instructor':
        bool(has_access(request.user, 'instructor', course)),
        'finance_admin':
        CourseFinanceAdminRole(course_key).has_user(request.user),
        'sales_admin':
        CourseSalesAdminRole(course_key).has_user(request.user),
        'staff':
        bool(has_access(request.user, 'staff', course)),
        'forum_admin':
        has_forum_access(request.user, course_key, FORUM_ROLE_ADMINISTRATOR),
    }

    if not access['staff']:
        raise Http404()

    is_white_label = CourseMode.is_white_label(course_key)

    sections = [
        _section_course_info(course, access),
        _section_membership(course, access, is_white_label),
        _section_cohort_management(course, access),
        _section_student_admin(course, access),
        _section_data_download(course, access),
    ]

    analytics_dashboard_message = None
    if settings.ANALYTICS_DASHBOARD_URL:
        # Construct a URL to the external analytics dashboard
        analytics_dashboard_url = '{0}/courses/{1}'.format(
            settings.ANALYTICS_DASHBOARD_URL, unicode(course_key))
        link_start = "<a href=\"{}\" target=\"_blank\">".format(
            analytics_dashboard_url)
        analytics_dashboard_message = _(
            "To gain insights into student enrollment and participation {link_start}"
            "visit {analytics_dashboard_name}, our new course analytics product{link_end}."
        )
        analytics_dashboard_message = analytics_dashboard_message.format(
            link_start=link_start,
            link_end="</a>",
            analytics_dashboard_name=settings.ANALYTICS_DASHBOARD_NAME)

        # Temporarily show the "Analytics" section until we have a better way of linking to Insights
        sections.append(_section_analytics(course, access))

    # Check if there is corresponding entry in the CourseMode Table related to the Instructor Dashboard course
    course_mode_has_price = False
    paid_modes = CourseMode.paid_modes_for_course(course_key)
    if len(paid_modes) == 1:
        course_mode_has_price = True
    elif len(paid_modes) > 1:
        log.error(
            u"Course %s has %s course modes with payment options. Course must only have "
            u"one paid course mode to enable eCommerce options.",
            unicode(course_key), len(paid_modes))

    if settings.FEATURES.get('INDIVIDUAL_DUE_DATES') and access['instructor']:
        sections.insert(3, _section_extensions(course))

    # Gate access to course email by feature flag & by course-specific authorization
    if bulk_email_is_enabled_for_course(course_key):
        sections.append(_section_send_email(course, access))

    # Gate access to Metrics tab by featue flag and staff authorization
    if settings.FEATURES['CLASS_DASHBOARD'] and access['staff']:
        sections.append(_section_metrics(course, access))

    # Gate access to Ecommerce tab
    if course_mode_has_price and (access['finance_admin']
                                  or access['sales_admin']):
        sections.append(
            _section_e_commerce(course, access, paid_modes[0], is_white_label,
                                is_white_label))

    # Gate access to Special Exam tab depending if either timed exams or proctored exams
    # are enabled in the course

    # NOTE: For now, if we only have procotred exams enabled, then only platform Staff
    # (user.is_staff) will be able to view the special exams tab. This may
    # change in the future
    can_see_special_exams = (
        ((course.enable_proctored_exams and request.user.is_staff)
         or course.enable_timed_exams)
        and settings.FEATURES.get('ENABLE_SPECIAL_EXAMS', False))
    if can_see_special_exams:
        sections.append(_section_special_exams(course, access))

    # Certificates panel
    # This is used to generate example certificates
    # and enable self-generated certificates for a course.
    certs_enabled = CertificateGenerationConfiguration.current().enabled
    if certs_enabled and access['admin']:
        sections.append(_section_certificates(course))

    disable_buttons = not _is_small_course(course_key)

    certificate_white_list = CertificateWhitelist.get_certificate_white_list(
        course_key)
    generate_certificate_exceptions_url = reverse(  # pylint: disable=invalid-name
        'generate_certificate_exceptions',
        kwargs={
            'course_id': unicode(course_key),
            'generate_for': ''
        })
    generate_bulk_certificate_exceptions_url = reverse(  # pylint: disable=invalid-name
        'generate_bulk_certificate_exceptions',
        kwargs={'course_id': unicode(course_key)})
    certificate_exception_view_url = reverse(
        'certificate_exception_view',
        kwargs={'course_id': unicode(course_key)})

    context = {
        'course': course,
        'studio_url': get_studio_url(course, 'course'),
        'sections': sections,
        'disable_buttons': disable_buttons,
        'analytics_dashboard_message': analytics_dashboard_message,
        'certificate_white_list': certificate_white_list,
        'generate_certificate_exceptions_url':
        generate_certificate_exceptions_url,
        'generate_bulk_certificate_exceptions_url':
        generate_bulk_certificate_exceptions_url,
        'certificate_exception_view_url': certificate_exception_view_url
    }
    if settings.FEATURES['ENABLE_INSTRUCTOR_LEGACY_DASHBOARD']:
        context['old_dashboard_url'] = reverse(
            'instructor_dashboard_legacy',
            kwargs={'course_id': unicode(course_key)})

    return render_to_response(
        'instructor/instructor_dashboard_2/instructor_dashboard_2.html',
        context)
Exemple #38
0
 def test_generate_no_such_course(self):
     response = self._generate(
         course_key=CourseKey.from_string("edx/invalid/course"),
         username=self.STUDENT_USERNAME)
     self.assertEqual(response.status_code, 400)
Exemple #39
0
class CertificateSupportTestCase(ModuleStoreTestCase):
    """
    Base class for tests of the certificate support views.
    """

    SUPPORT_USERNAME = "******"
    SUPPORT_EMAIL = "*****@*****.**"
    SUPPORT_PASSWORD = "******"

    STUDENT_USERNAME = "******"
    STUDENT_EMAIL = "*****@*****.**"
    STUDENT_PASSWORD = "******"

    CERT_COURSE_KEY = CourseKey.from_string("edX/DemoX/Demo_Course")
    COURSE_NOT_EXIST_KEY = CourseKey.from_string(
        "test/TestX/Test_Course_Not_Exist")
    EXISTED_COURSE_KEY_1 = CourseKey.from_string(
        "test1/Test1X/Test_Course_Exist_1")
    EXISTED_COURSE_KEY_2 = CourseKey.from_string(
        "test2/Test2X/Test_Course_Exist_2")
    CERT_GRADE = 0.89
    CERT_STATUS = CertificateStatuses.downloadable
    CERT_MODE = "verified"
    CERT_DOWNLOAD_URL = "http://www.example.com/cert.pdf"

    def setUp(self):
        """
        Create a support team member and a student with a certificate.
        Log in as the support team member.
        """
        super(CertificateSupportTestCase, self).setUp()
        CourseFactory(
            org=CertificateSupportTestCase.EXISTED_COURSE_KEY_1.org,
            course=CertificateSupportTestCase.EXISTED_COURSE_KEY_1.course,
            run=CertificateSupportTestCase.EXISTED_COURSE_KEY_1.run,
        )

        # Create the support staff user
        self.support = UserFactory(
            username=self.SUPPORT_USERNAME,
            email=self.SUPPORT_EMAIL,
            password=self.SUPPORT_PASSWORD,
        )
        SupportStaffRole().add_users(self.support)

        # Create a student
        self.student = UserFactory(
            username=self.STUDENT_USERNAME,
            email=self.STUDENT_EMAIL,
            password=self.STUDENT_PASSWORD,
        )

        # Create certificates for the student
        self.cert = GeneratedCertificate.eligible_certificates.create(
            user=self.student,
            course_id=self.CERT_COURSE_KEY,
            grade=self.CERT_GRADE,
            status=self.CERT_STATUS,
            mode=self.CERT_MODE,
            download_url=self.CERT_DOWNLOAD_URL,
        )

        # Login as support staff
        success = self.client.login(username=self.SUPPORT_USERNAME,
                                    password=self.SUPPORT_PASSWORD)
        self.assertTrue(success, msg="Couldn't log in as support staff")
Exemple #40
0
def search_certificates(request):
    """
    Search for certificates for a particular user OR along with the given course.

    Supports search by either username or email address along with course id.

    First filter the records for the given username/email and then filter against the given course id (if given).
    Show the 'Regenerate' button if a record found in 'generatedcertificate' model otherwise it will show the Generate
    button.

    Arguments:
        request (HttpRequest): The request object.

    Returns:
        JsonResponse

    Example Usage:
        GET /certificates/[email protected]
        GET /certificates/[email protected]&course_id=xyz

        Response: 200 OK
        Content-Type: application/json
        [
            {
                "username": "******",
                "course_key": "edX/DemoX/Demo_Course",
                "type": "verified",
                "status": "downloadable",
                "download_url": "http://www.example.com/cert.pdf",
                "grade": "0.98",
                "created": 2015-07-31T00:00:00Z,
                "modified": 2015-07-31T00:00:00Z
            }
        ]

    """
    user_filter = urllib.unquote(urllib.quote_plus(request.GET.get("user",
                                                                   "")))
    if not user_filter:
        msg = _("user is not given.")
        return HttpResponseBadRequest(msg)

    try:
        user = User.objects.get(Q(email=user_filter) | Q(username=user_filter))
    except User.DoesNotExist:
        return HttpResponseBadRequest(
            _("user '{user}' does not exist").format(user=user_filter))

    certificates = api.get_certificates_for_user(user.username)
    for cert in certificates:
        cert["course_key"] = unicode(cert["course_key"])
        cert["created"] = cert["created"].isoformat()
        cert["modified"] = cert["modified"].isoformat()
        cert["regenerate"] = True

    course_id = urllib.quote_plus(request.GET.get("course_id", ""), safe=':/')
    if course_id:
        try:
            course_key = CourseKey.from_string(course_id)
        except InvalidKeyError:
            return HttpResponseBadRequest(
                _("Course id '{course_id}' is not valid").format(
                    course_id=course_id))
        else:
            try:
                if CourseOverview.get_from_id(course_key):
                    certificates = [
                        certificate for certificate in certificates
                        if certificate['course_key'] == course_id
                    ]
                    if not certificates:
                        return JsonResponse([{
                            'username': user.username,
                            'course_key': course_id,
                            'regenerate': False
                        }])
            except CourseOverview.DoesNotExist:
                msg = _(
                    "The course does not exist against the given key '{course_key}'"
                ).format(course_key=course_key)
                return HttpResponseBadRequest(msg)

    return JsonResponse(certificates)
Exemple #41
0
    def get(self, request, *args, **kwargs):
        """Shows logs of imports that happened as a result of a git import"""

        course_id = kwargs.get('course_id')
        if course_id:
            course_id = CourseKey.from_string(course_id)

        page_size = 10

        # Set mongodb defaults even if it isn't defined in settings
        mongo_db = {
            'host': 'localhost',
            'user': '',
            'password': '',
            'db': 'xlog',
        }

        # Allow overrides
        if hasattr(settings, 'MONGODB_LOG'):
            for config_item in [
                    'host',
                    'user',
                    'password',
                    'db',
            ]:
                mongo_db[config_item] = settings.MONGODB_LOG.get(
                    config_item, mongo_db[config_item])

        mongouri = 'mongodb://{user}:{password}@{host}/{db}'.format(**mongo_db)

        error_msg = ''

        try:
            if mongo_db['user'] and mongo_db['password']:
                mdb = mongoengine.connect(mongo_db['db'], host=mongouri)
            else:
                mdb = mongoengine.connect(mongo_db['db'],
                                          host=mongo_db['host'])
        except mongoengine.connection.ConnectionError:
            log.exception('Unable to connect to mongodb to save log, '
                          'please check MONGODB_LOG settings.')

        if course_id is None:
            # Require staff if not going to specific course
            if not request.user.is_staff:
                raise Http404
            cilset = CourseImportLog.objects.order_by('-created')
        else:
            try:
                course = get_course_by_id(course_id)
            except Exception:
                log.info('Cannot find course %s', course_id)
                raise Http404

            # Allow only course team, instructors, and staff
            if not (request.user.is_staff
                    or CourseInstructorRole(course.id).has_user(request.user)
                    or CourseStaffRole(course.id).has_user(request.user)):
                raise Http404
            log.debug('course_id=%s', course_id)
            cilset = CourseImportLog.objects.filter(
                course_id=course_id).order_by('-created')
            log.debug('cilset length=%s', len(cilset))

        # Paginate the query set
        paginator = Paginator(cilset, page_size)
        try:
            logs = paginator.page(request.GET.get('page'))
        except PageNotAnInteger:
            logs = paginator.page(1)
        except EmptyPage:
            # If the page is too high or low
            given_page = int(request.GET.get('page'))
            page = min(max(1, given_page), paginator.num_pages)
            logs = paginator.page(page)

        mdb.disconnect()
        context = {
            'logs': logs,
            'course_id':
            course_id.to_deprecated_string() if course_id else None,
            'error_msg': error_msg,
            'page_size': page_size
        }

        return render_to_response(self.template_name, context)
Exemple #42
0
    def handle(self, *args, **options):
        if len(args) != 1:
            raise CommandError(
                "check_course requires one argument: <course_id>")

        try:
            course_key = CourseKey.from_string(args[0])
        except InvalidKeyError:
            course_key = SlashSeparatedCourseKey.from_deprecated_string(
                args[0])

        store = modulestore()

        course = store.get_course(course_key, depth=3)

        err_cnt = 0

        def _xlint_metadata(module):
            err_cnt = check_module_metadata_editability(module)
            for child in module.get_children():
                err_cnt = err_cnt + _xlint_metadata(child)
            return err_cnt

        err_cnt = err_cnt + _xlint_metadata(course)

        # we've had a bug where the xml_attributes field can we rewritten as a string rather than a dict
        def _check_xml_attributes_field(module):
            err_cnt = 0
            if hasattr(module, 'xml_attributes') and isinstance(
                    module.xml_attributes, basestring):
                print 'module = {0} has xml_attributes as a string. It should be a dict'.format(
                    module.location)
                err_cnt = err_cnt + 1
            for child in module.get_children():
                err_cnt = err_cnt + _check_xml_attributes_field(child)
            return err_cnt

        err_cnt = err_cnt + _check_xml_attributes_field(course)

        # check for dangling discussion items, this can cause errors in the forums
        def _get_discussion_items(module):
            discussion_items = []
            if module.location.category == 'discussion':
                discussion_items = discussion_items + [module.location]

            for child in module.get_children():
                discussion_items = discussion_items + _get_discussion_items(
                    child)

            return discussion_items

        discussion_items = _get_discussion_items(course)

        # now query all discussion items via get_items() and compare with the tree-traversal
        queried_discussion_items = store.get_items(
            course_key=course_key,
            category='discussion',
        )

        for item in queried_discussion_items:
            if item.location not in discussion_items:
                print 'Found dangling discussion module = {0}'.format(
                    item.location)
Exemple #43
0
    def test_get_items_with_course_items(self):
        store = modulestore()

        # fix was to allow get_items() to take the course_id parameter
        store.get_items(CourseKey.from_string('abc/def/ghi'),
                        qualifiers={'category': 'vertical'})
from lms.djangoapps.courseware.tests.tests import LoginEnrollmentTestCase
from lms.djangoapps.instructor_task.api_helper import encode_problem_and_student_input
from lms.djangoapps.instructor_task.models import PROGRESS, QUEUING, ReportStore
from lms.djangoapps.instructor_task.tests.factories import InstructorTaskFactory
from lms.djangoapps.instructor_task.views import instructor_task_status
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
from openedx.core.lib.url_utils import quote_slashes
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory

TEST_COURSE_ORG = 'edx'
TEST_COURSE_NAME = 'test_course'
TEST_COURSE_NUMBER = '1.23x'
TEST_COURSE_KEY = CourseKey.from_string('/'.join(
    [TEST_COURSE_ORG, TEST_COURSE_NUMBER, TEST_COURSE_NAME]))
TEST_CHAPTER_NAME = "Section"
TEST_SECTION_NAME = "Subsection"

TEST_FAILURE_MESSAGE = 'task failed horribly'
TEST_FAILURE_EXCEPTION = 'RandomCauseError'

OPTION_1 = 'Option 1'
OPTION_2 = 'Option 2'


class InstructorTaskTestCase(CacheIsolationTestCase):
    """
    Tests API and view methods that involve the reporting of status for background tasks.
    """
    def setUp(self):
Exemple #45
0
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.
    """
    for organization in program_data['organizations']:
        # TODO: Cache the results of the get_organization_by_short_name call so
        # the database is hit less frequently.
        org_obj = get_organization_by_short_name(organization['key'])
        if org_obj and org_obj.get('logo'):
            organization['img'] = org_obj['logo'].url

    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)

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

            start_date_string = course_overview.start_datetime_text()
            end_date_string = course_overview.end_datetime_text()

            end_date = course_overview.end or datetime.datetime.max.replace(
                tzinfo=pytz.UTC)
            is_course_ended = end_date < timezone.now()

            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

            enrollment_open_date = None if is_enrollment_open else strftime_localized(
                enrollment_start, 'SHORT_DATE')

            certificate_data = certificate_api.certificate_downloadable_status(
                user, course_key)
            certificate_uuid = certificate_data.get('uuid')
            certificate_url = certificate_api.get_certificate_url(
                course_id=course_key,
                uuid=certificate_uuid,
            ) if certificate_uuid else None

            required_mode_slug = run_mode['mode_slug']
            enrolled_mode_slug, _ = CourseEnrollment.enrollment_mode_for_user(
                user, course_key)
            is_mode_mismatch = required_mode_slug != enrolled_mode_slug
            is_upgrade_required = is_enrolled and is_mode_mismatch

            # Requires that the ecommerce service be in use.
            required_mode = CourseMode.mode_for_course(course_key,
                                                       required_mode_slug)
            ecommerce = EcommerceService()
            sku = getattr(required_mode, 'sku', None)

            if ecommerce.is_enabled(user) and sku:
                upgrade_url = ecommerce.checkout_page_url(
                    required_mode.sku) if is_upgrade_required else None
            else:
                upgrade_url = None

            run_mode.update({
                'certificate_url': certificate_url,
                'course_image_url': course_image_url,
                'course_url': course_url,
                'end_date': end_date_string,
                'enrollment_open_date': enrollment_open_date,
                'is_course_ended': is_course_ended,
                'is_enrolled': is_enrolled,
                'is_enrollment_open': is_enrollment_open,
                # TODO: Not currently available on LMS.
                'marketing_url': None,
                'start_date': start_date_string,
                'upgrade_url': upgrade_url,
            })

    return program_data
def cohort_discussion_topics(request, course_key_string):
    """
    The handler for cohort discussion categories requests.
    This will raise 404 if user is not staff.

    Returns the JSON representation of discussion topics w.r.t categories for the course.

    Example:
        >>> example = {
        >>>               "course_wide_discussions": {
        >>>                   "entries": {
        >>>                       "General": {
        >>>                           "sort_key": "General",
        >>>                           "is_cohorted": True,
        >>>                           "id": "i4x-edx-eiorguegnru-course-foobarbaz"
        >>>                       }
        >>>                   }
        >>>                   "children": ["General"]
        >>>               },
        >>>               "inline_discussions" : {
        >>>                   "subcategories": {
        >>>                       "Getting Started": {
        >>>                           "subcategories": {},
        >>>                           "children": [
        >>>                               "Working with Videos",
        >>>                               "Videos on edX"
        >>>                           ],
        >>>                           "entries": {
        >>>                               "Working with Videos": {
        >>>                                   "sort_key": None,
        >>>                                   "is_cohorted": False,
        >>>                                   "id": "d9f970a42067413cbb633f81cfb12604"
        >>>                               },
        >>>                               "Videos on edX": {
        >>>                                   "sort_key": None,
        >>>                                   "is_cohorted": False,
        >>>                                   "id": "98d8feb5971041a085512ae22b398613"
        >>>                               }
        >>>                           }
        >>>                       },
        >>>                       "children": ["Getting Started"]
        >>>                   },
        >>>               }
        >>>          }
    """
    course_key = CourseKey.from_string(course_key_string)
    course = get_course_with_access(request.user, 'staff', course_key)

    discussion_topics = {}
    discussion_category_map = get_discussion_category_map(
        course,
        request.user,
        cohorted_if_in_list=True,
        exclude_unstarted=False)

    # We extract the data for the course wide discussions from the category map.
    course_wide_entries = discussion_category_map.pop('entries')

    course_wide_children = []
    inline_children = []

    for name in discussion_category_map['children']:
        if name in course_wide_entries:
            course_wide_children.append(name)
        else:
            inline_children.append(name)

    discussion_topics['course_wide_discussions'] = {
        'entries': course_wide_entries,
        'children': course_wide_children
    }

    discussion_category_map['children'] = inline_children
    discussion_topics['inline_discussions'] = discussion_category_map

    return JsonResponse(discussion_topics)
Exemple #47
0
def async_course_overview_update(*args, **kwargs):
    course_keys = [CourseKey.from_string(arg) for arg in args]
    CourseOverview.update_select_courses(course_keys,
                                         force_update=kwargs['force_update'])
Exemple #48
0
    def get(self, request, course_id, error=None):
        """Displays the course mode choice page.

        Args:
            request (`Request`): The Django Request object.
            course_id (unicode): The slash-separated course key.

        Keyword Args:
            error (unicode): If provided, display this error message
                on the page.

        Returns:
            Response

        """
        course_key = CourseKey.from_string(course_id)

        # Check whether the user has access to this course
        # based on country access rules.
        embargo_redirect = embargo_api.redirect_if_blocked(
            course_key,
            user=request.user,
            ip_address=get_ip(request),
            url=request.path)
        if embargo_redirect:
            return redirect(embargo_redirect)

        enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(
            request.user, course_key)
        modes = CourseMode.modes_for_course_dict(course_key)
        ecommerce_service = EcommerceService()

        # We assume that, if 'professional' is one of the modes, it should be the *only* mode.
        # If there are both modes, default to non-id-professional.
        has_enrolled_professional = (
            CourseMode.is_professional_slug(enrollment_mode) and is_active)
        if CourseMode.has_professional_mode(
                modes) and not has_enrolled_professional:
            redirect_url = reverse('verify_student_start_flow',
                                   kwargs={'course_id': unicode(course_key)})
            if ecommerce_service.is_enabled(request.user):
                professional_mode = modes.get(
                    CourseMode.NO_ID_PROFESSIONAL_MODE) or modes.get(
                        CourseMode.PROFESSIONAL)
                if professional_mode.sku:
                    redirect_url = ecommerce_service.checkout_page_url(
                        professional_mode.sku)

            return redirect(redirect_url)

        # If there isn't a verified mode available, then there's nothing
        # to do on this page.  The user has almost certainly been auto-registered
        # in the "honor" track by this point, so we send the user
        # to the dashboard.
        if not CourseMode.has_verified_mode(modes):
            return redirect(reverse('dashboard'))

        # If a user has already paid, redirect them to the dashboard.
        if is_active and (enrollment_mode in CourseMode.VERIFIED_MODES +
                          [CourseMode.NO_ID_PROFESSIONAL_MODE]):
            return redirect(reverse('dashboard'))

        donation_for_course = request.session.get("donation_for_course", {})
        chosen_price = donation_for_course.get(unicode(course_key), None)

        course = modulestore().get_course(course_key)
        if CourseEnrollment.is_enrollment_closed(request.user, course):
            locale = to_locale(get_language())
            enrollment_end_date = format_datetime(course.enrollment_end,
                                                  'short',
                                                  locale=locale)
            params = urllib.urlencode({'course_closed': enrollment_end_date})
            return redirect('{0}?{1}'.format(reverse('dashboard'), params))

        # When a credit mode is available, students will be given the option
        # to upgrade from a verified mode to a credit mode at the end of the course.
        # This allows students who have completed photo verification to be eligible
        # for univerity credit.
        # Since credit isn't one of the selectable options on the track selection page,
        # we need to check *all* available course modes in order to determine whether
        # a credit mode is available.  If so, then we show slightly different messaging
        # for the verified track.
        has_credit_upsell = any(
            CourseMode.is_credit_mode(mode)
            for mode in CourseMode.modes_for_course(course_key,
                                                    only_selectable=False))

        context = {
            "course_modes_choose_url":
            reverse("course_modes_choose",
                    kwargs={'course_id': course_key.to_deprecated_string()}),
            "modes":
            modes,
            "has_credit_upsell":
            has_credit_upsell,
            "course_name":
            course.display_name_with_default_escaped,
            "course_org":
            course.display_org_with_default,
            "course_num":
            course.display_number_with_default,
            "chosen_price":
            chosen_price,
            "error":
            error,
            "responsive":
            True,
            "nav_hidden":
            True,
        }
        if "verified" in modes:
            verified_mode = modes["verified"]
            context["suggested_prices"] = [
                decimal.Decimal(x.strip())
                for x in verified_mode.suggested_prices.split(",")
                if x.strip()
            ]
            context["currency"] = verified_mode.currency.upper()
            context["min_price"] = verified_mode.min_price
            context["verified_name"] = verified_mode.name
            context["verified_description"] = verified_mode.description

            if verified_mode.sku:
                context[
                    "use_ecommerce_payment_flow"] = ecommerce_service.is_enabled(
                        request.user)
                context[
                    "ecommerce_payment_page"] = ecommerce_service.payment_page_url(
                    )
                context["sku"] = verified_mode.sku

        return render_to_response("course_modes/choose.html", context)
Exemple #49
0
def create_thread(request, course_id, commentable_id):
    """
    Given a course and commentble ID, create the thread
    """

    log.debug("Creating new thread in %r, id %r", course_id, commentable_id)
    course_key = CourseKey.from_string(course_id)
    course = get_course_with_access(request.user, 'load', course_key)
    post = request.POST
    user = request.user

    if course.allow_anonymous:
        anonymous = post.get('anonymous', 'false').lower() == 'true'
    else:
        anonymous = False

    if course.allow_anonymous_to_peers:
        anonymous_to_peers = post.get('anonymous_to_peers',
                                      'false').lower() == 'true'
    else:
        anonymous_to_peers = False

    if 'title' not in post or not post['title'].strip():
        return JsonError(_("Title can't be empty"))
    if 'body' not in post or not post['body'].strip():
        return JsonError(_("Body can't be empty"))

    params = {
        'anonymous': anonymous,
        'anonymous_to_peers': anonymous_to_peers,
        'commentable_id': commentable_id,
        'course_id': course_key.to_deprecated_string(),
        'user_id': user.id,
        'thread_type': post["thread_type"],
        'body': post["body"],
        'title': post["title"],
    }

    # Check for whether this commentable belongs to a team, and add the right context
    if get_team(commentable_id) is not None:
        params['context'] = ThreadContext.STANDALONE
    else:
        params['context'] = ThreadContext.COURSE

    thread = cc.Thread(**params)

    # Divide the thread if required
    try:
        group_id = get_group_id_for_comments_service(request, course_key,
                                                     commentable_id)
    except ValueError:
        return HttpResponseServerError("Invalid group id for commentable")
    if group_id is not None:
        thread.group_id = group_id

    thread.save()

    thread_created.send(sender=None, user=user, post=thread)

    # patch for backward compatibility to comments service
    if 'pinned' not in thread.attributes:
        thread['pinned'] = False

    follow = post.get('auto_subscribe', 'false').lower() == 'true'

    if follow:
        cc_user = cc.User.from_django_user(user)
        cc_user.follow(thread)

    data = thread.to_dict()

    add_courseware_context([data], course, user)

    track_thread_created_event(request, course, thread, follow)

    if request.is_ajax():
        return ajax_content_response(request, course_key, data)
    else:
        return JsonResponse(prepare_content(data, course_key))
Exemple #50
0
def get_course_key(request, course_id=None):
    if not course_id:
        return CourseKey.from_string(request.GET.get('course_id'))
    return CourseKey.from_string(course_id)
 def setup(self):
     self.course_key_string = COURSE_ID_STR_TEMPLATE.format(1)
     self.course_key = CourseKey.from_string(
         self.course_key_string)
Exemple #52
0
def course_discussions_settings_handler(request, course_key_string):
    """
    The restful handler for divided discussion setting requests. Requires JSON.
    This will raise 404 if user is not staff.
    GET
        Returns the JSON representation of divided discussion settings for the course.
    PATCH
        Updates the divided discussion settings for the course. Returns the JSON representation of updated settings.
    """
    course_key = CourseKey.from_string(course_key_string)
    course = get_course_with_access(request.user, 'staff', course_key)
    discussion_settings = get_course_discussion_settings(course_key)

    if request.method == 'PATCH':
        divided_course_wide_discussions, divided_inline_discussions = get_divided_discussions(
            course, discussion_settings
        )

        settings_to_change = {}

        if 'divided_course_wide_discussions' in request.json or 'divided_inline_discussions' in request.json:
            divided_course_wide_discussions = request.json.get(
                'divided_course_wide_discussions', divided_course_wide_discussions
            )
            divided_inline_discussions = request.json.get(
                'divided_inline_discussions', divided_inline_discussions
            )
            settings_to_change['divided_discussions'] = divided_course_wide_discussions + divided_inline_discussions

        if 'always_divide_inline_discussions' in request.json:
            settings_to_change['always_divide_inline_discussions'] = request.json.get(
                'always_divide_inline_discussions'
            )
        if 'division_scheme' in request.json:
            settings_to_change['division_scheme'] = request.json.get(
                'division_scheme'
            )

        if not settings_to_change:
            return JsonResponse({"error": unicode("Bad Request")}, 400)

        try:
            if settings_to_change:
                discussion_settings = set_course_discussion_settings(course_key, **settings_to_change)

        except ValueError as err:
            # Note: error message not translated because it is not exposed to the user (UI prevents this state).
            return JsonResponse({"error": unicode(err)}, 400)

    divided_course_wide_discussions, divided_inline_discussions = get_divided_discussions(
        course, discussion_settings
    )

    return JsonResponse({
        'id': discussion_settings.id,
        'divided_inline_discussions': divided_inline_discussions,
        'divided_course_wide_discussions': divided_course_wide_discussions,
        'always_divide_inline_discussions': discussion_settings.always_divide_inline_discussions,
        'division_scheme': discussion_settings.division_scheme,
        'available_division_schemes': available_division_schemes(course_key)
    })
Exemple #53
0
    def handle(self, *args, **options):
        # Find all courses that have ended

        if options['course']:
            course_id = CourseKey.from_string(options['course'])
        else:
            raise CommandError("You must specify a course")

        cert_data = {}

        # find students who are active
        # number of enrolled students = downloadable + notpassing
        print(u"Looking up certificate states for {0}".format(
            options['course']))
        enrolled_current = User.objects.filter(
            courseenrollment__course_id=course_id,
            courseenrollment__is_active=True)
        enrolled_total = User.objects.filter(
            courseenrollment__course_id=course_id)
        verified_enrolled = GeneratedCertificate.objects.filter(
            course_id__exact=course_id, mode__exact='verified')
        honor_enrolled = GeneratedCertificate.objects.filter(
            course_id__exact=course_id, mode__exact='honor')
        audit_enrolled = GeneratedCertificate.objects.filter(
            course_id__exact=course_id, mode__exact='audit')

        cert_data[course_id] = {
            'enrolled_current': enrolled_current.count(),
            'enrolled_total': enrolled_total.count(),
            'verified_enrolled': verified_enrolled.count(),
            'honor_enrolled': honor_enrolled.count(),
            'audit_enrolled': audit_enrolled.count()
        }

        status_tally = GeneratedCertificate.objects.filter(
            course_id__exact=course_id).values('status').annotate(
                dcount=Count('status'))

        cert_data[course_id].update(
            {status['status']: status['dcount']
             for status in status_tally})

        mode_tally = GeneratedCertificate.objects.filter(
            course_id__exact=course_id,
            status__exact='downloadable').values('mode').annotate(
                dcount=Count('mode'))
        cert_data[course_id].update(
            {mode['mode']: mode['dcount']
             for mode in mode_tally})

        # all states we have seen far all courses
        status_headings = sorted(
            set([
                status for course in cert_data for status in cert_data[course]
            ])  # lint-amnesty, pylint: disable=consider-using-set-comprehension
        )

        # print the heading for the report
        print("{:>26}".format("course ID"), end=' ')
        print(' '.join(
            ["{:>16}".format(heading) for heading in status_headings]))

        # print the report
        print("{0:>26}".format(text_type(course_id)), end=' ')
        for heading in status_headings:
            if heading in cert_data[course_id]:
                print("{:>16}".format(cert_data[course_id][heading]), end=' ')
            else:
                print(" " * 16, end=' ')
        print()
Exemple #54
0
 def wrapped_view(request, course_id, *args, **kwargs):  # pylint: disable=missing-docstring
     course_key = CourseKey.from_string(course_id)
     with modulestore().bulk_operations(course_key):
         return view_func(request, course_key, *args, **kwargs)
Exemple #55
0
from lti_provider.signature_validator import SignatureValidator
from opaque_keys.edx.keys import CourseKey, UsageKey
from student.tests.factories import UserFactory

LTI_DEFAULT_PARAMS = {
    'roles': u'Instructor,urn:lti:instrole:ims/lis/Administrator',
    'context_id': u'lti_launch_context_id',
    'oauth_version': u'1.0',
    'oauth_consumer_key': u'consumer_key',
    'oauth_signature': u'OAuth Signature',
    'oauth_signature_method': u'HMAC-SHA1',
    'oauth_timestamp': u'OAuth Timestamp',
    'oauth_nonce': u'OAuth Nonce',
}

COURSE_KEY = CourseKey.from_string('some/course/id')
USAGE_KEY = UsageKey.from_string(
    'i4x://some/course/problem/uuid').map_into_course(COURSE_KEY)
COURSE_PARAMS = {'course_key': COURSE_KEY, 'usage_key': USAGE_KEY}

ALL_PARAMS = dict(LTI_DEFAULT_PARAMS.items() + COURSE_PARAMS.items())


def build_launch_request(authenticated=True):
    """
    Helper method to create a new request object for the LTI launch.
    """
    request = RequestFactory().post('/')
    request.user = UserFactory.create()
    request.user.is_authenticated = MagicMock(return_value=authenticated)
    request.session = {}
Exemple #56
0
def listen_for_grade_calculation(sender, user, course_grade, course_key,
                                 deadline, **kwargs):  # pylint: disable=unused-argument
    """Receive 'MIN_GRADE_REQUIREMENT_STATUS' signal and update minimum grade requirement status.

    Args:
        sender: None
        user(User): User Model object
        course_grade(CourseGrade): CourseGrade object
        course_key(CourseKey): The key for the course
        deadline(datetime): Course end date or None

    Kwargs:
        kwargs : None

    """
    # This needs to be imported here to avoid a circular dependency
    # that can cause migrations to fail.
    from openedx.core.djangoapps.credit import api
    course_id = CourseKey.from_string(unicode(course_key))
    is_credit = api.is_credit_course(course_id)
    if is_credit:
        requirements = api.get_credit_requirements(course_id,
                                                   namespace='grade')
        if requirements:
            criteria = requirements[0].get('criteria')
            if criteria:
                min_grade = criteria.get('min_grade')
                passing_grade = course_grade.percent >= min_grade
                now = timezone.now()
                status = None
                reason = None

                if (deadline and now < deadline) or not deadline:
                    # Student completed coursework on-time

                    if passing_grade:
                        # Student received a passing grade
                        status = 'satisfied'
                        reason = {'final_grade': course_grade.percent}
                else:
                    # Submission after deadline

                    if passing_grade:
                        # Grade was good, but submission arrived too late
                        status = 'failed'
                        reason = {'current_date': now, 'deadline': deadline}
                    else:
                        # Student failed to receive minimum grade
                        status = 'failed'
                        reason = {
                            'final_grade': course_grade.percent,
                            'minimum_grade': min_grade
                        }

                # We do not record a status if the user has not yet earned the minimum grade, but still has
                # time to do so.
                if status and reason:
                    api.set_credit_requirement_status(user,
                                                      course_id,
                                                      'grade',
                                                      'grade',
                                                      status=status,
                                                      reason=reason)
Exemple #57
0
 def setUp(self):
     super(TestCredentialsSignalsSendGrade, self).setUp()
     self.user = UserFactory()
     self.key = CourseKey.from_string(CourseRunFactory()['key'])
Exemple #58
0
    def post(self, request):
        """Enrolls the currently logged-in user in a course.

        Server-to-server calls may deactivate or modify the mode of existing enrollments. All other requests
        go through `add_enrollment()`, which allows creation of new and reactivation of old enrollments.
        """
        # Get the User, Course ID, and Mode from the request.

        username = request.data.get('user', request.user.username)
        course_id = request.data.get('course_details', {}).get('course_id')

        if not course_id:
            return Response(
                status=status.HTTP_400_BAD_REQUEST,
                data={
                    "message":
                    u"Course ID must be specified to create a new enrollment."
                })

        try:
            course_id = CourseKey.from_string(course_id)
        except InvalidKeyError:
            return Response(
                status=status.HTTP_400_BAD_REQUEST,
                data={
                    "message":
                    u"No course '{course_id}' found for enrollment".format(
                        course_id=course_id)
                })

        mode = request.data.get('mode')

        has_api_key_permissions = self.has_api_key_permissions(request)

        # Check that the user specified is either the same user, or this is a server-to-server request.
        if not username:
            username = request.user.username
        if username != request.user.username and not has_api_key_permissions:
            # Return a 404 instead of a 403 (Unauthorized). If one user is looking up
            # other users, do not let them deduce the existence of an enrollment.
            return Response(status=status.HTTP_404_NOT_FOUND)

        if mode not in (CourseMode.AUDIT, CourseMode.HONOR,
                        None) and not has_api_key_permissions:
            return Response(
                status=status.HTTP_403_FORBIDDEN,
                data={
                    "message":
                    u"User does not have permission to create enrollment with mode [{mode}]."
                    .format(mode=mode)
                })

        try:
            # Lookup the user, instead of using request.user, since request.user may not match the username POSTed.
            user = User.objects.get(username=username)
        except ObjectDoesNotExist:
            return Response(status=status.HTTP_406_NOT_ACCEPTABLE,
                            data={
                                'message':
                                u'The user {} does not exist.'.format(username)
                            })

        embargo_response = embargo_api.get_embargo_response(
            request, course_id, user)

        if embargo_response:
            return embargo_response

        try:
            is_active = request.data.get('is_active')
            # Check if the requested activation status is None or a Boolean
            if is_active is not None and not isinstance(is_active, bool):
                return Response(
                    status=status.HTTP_400_BAD_REQUEST,
                    data={
                        'message':
                        (u"'{value}' is an invalid enrollment activation status."
                         ).format(value=is_active)
                    })

            enterprise_course_consent = request.data.get(
                'enterprise_course_consent')
            explicit_linked_enterprise = request.data.get(
                'linked_enterprise_customer')
            if (enterprise_course_consent or explicit_linked_enterprise
                ) and has_api_key_permissions and enterprise_enabled():
                enterprise_api_client = EnterpriseApiClient()
                consent_client = ConsentApiClient()
                # We received an explicitly-linked EnterpriseCustomer for the enrollment
                if explicit_linked_enterprise is not None:
                    try:
                        enterprise_api_client.post_enterprise_course_enrollment(
                            username, unicode(course_id), None)
                    except EnterpriseApiException as error:
                        log.exception(
                            "An unexpected error occurred while creating the new EnterpriseCourseEnrollment "
                            "for user [%s] in course run [%s]", username,
                            course_id)
                        raise CourseEnrollmentError(error.message)
                    kwargs = {
                        'username': username,
                        'course_id': unicode(course_id),
                        'enterprise_customer_uuid': explicit_linked_enterprise,
                    }
                    consent_client.provide_consent(**kwargs)

                # We received an implicit "consent granted" parameter from ecommerce
                # TODO: Once ecommerce has been deployed with explicit enterprise support, remove this
                # entire chunk of logic, related tests, and any supporting methods no longer required.
                elif enterprise_course_consent is not None:
                    # Check if the enterprise_course_enrollment is a boolean
                    if not isinstance(enterprise_course_consent, bool):
                        return Response(
                            status=status.HTTP_400_BAD_REQUEST,
                            data={
                                'message':
                                (u"'{value}' is an invalid enterprise course consent value."
                                 ).format(value=enterprise_course_consent)
                            })
                    try:
                        enterprise_api_client.post_enterprise_course_enrollment(
                            username, unicode(course_id),
                            enterprise_course_consent)
                    except EnterpriseApiException as error:
                        log.exception(
                            "An unexpected error occurred while creating the new EnterpriseCourseEnrollment "
                            "for user [%s] in course run [%s]", username,
                            course_id)
                        raise CourseEnrollmentError(error.message)

            enrollment_attributes = request.data.get('enrollment_attributes')
            enrollment = api.get_enrollment(username, unicode(course_id))
            mode_changed = enrollment and mode is not None and enrollment[
                'mode'] != mode
            active_changed = enrollment and is_active is not None and enrollment[
                'is_active'] != is_active
            missing_attrs = []
            if enrollment_attributes:
                actual_attrs = [
                    u"{namespace}:{name}".format(**attr)
                    for attr in enrollment_attributes
                ]
                missing_attrs = set(REQUIRED_ATTRIBUTES.get(
                    mode, [])) - set(actual_attrs)
            if has_api_key_permissions and (mode_changed or active_changed):
                if mode_changed and active_changed and not is_active:
                    # if the requester wanted to deactivate but specified the wrong mode, fail
                    # the request (on the assumption that the requester had outdated information
                    # about the currently active enrollment).
                    msg = u"Enrollment mode mismatch: active mode={}, requested mode={}. Won't deactivate.".format(
                        enrollment["mode"], mode)
                    log.warning(msg)
                    return Response(status=status.HTTP_400_BAD_REQUEST,
                                    data={"message": msg})

                if len(missing_attrs) > 0:
                    msg = u"Missing enrollment attributes: requested mode={} required attributes={}".format(
                        mode, REQUIRED_ATTRIBUTES.get(mode))
                    log.warning(msg)
                    return Response(status=status.HTTP_400_BAD_REQUEST,
                                    data={"message": msg})

                response = api.update_enrollment(
                    username,
                    unicode(course_id),
                    mode=mode,
                    is_active=is_active,
                    enrollment_attributes=enrollment_attributes,
                    # If we are updating enrollment by authorized api caller, we should allow expired modes
                    include_expired=has_api_key_permissions)
            else:
                # Will reactivate inactive enrollments.
                response = api.add_enrollment(
                    username,
                    unicode(course_id),
                    mode=mode,
                    is_active=is_active,
                    enrollment_attributes=enrollment_attributes)

            email_opt_in = request.data.get('email_opt_in', None)
            if email_opt_in is not None:
                org = course_id.org
                update_email_opt_in(request.user, org, email_opt_in)

            log.info(
                'The user [%s] has already been enrolled in course run [%s].',
                username, course_id)
            return Response(response)
        except CourseModeNotFoundError as error:
            return Response(
                status=status.HTTP_400_BAD_REQUEST,
                data={
                    "message":
                    (u"The [{mode}] course mode is expired or otherwise unavailable for course run [{course_id}]."
                     ).format(mode=mode, course_id=course_id),
                    "course_details":
                    error.data
                })
        except CourseNotFoundError:
            return Response(
                status=status.HTTP_400_BAD_REQUEST,
                data={
                    "message":
                    u"No course '{course_id}' found for enrollment".format(
                        course_id=course_id)
                })
        except CourseEnrollmentExistsError as error:
            log.warning(
                'An enrollment already exists for user [%s] in course run [%s].',
                username, course_id)
            return Response(data=error.enrollment)
        except CourseEnrollmentError:
            log.exception(
                "An error occurred while creating the new course enrollment for user "
                "[%s] in course run [%s]", username, course_id)
            return Response(
                status=status.HTTP_400_BAD_REQUEST,
                data={
                    "message":
                    (u"An error occurred while creating the new course enrollment for user "
                     u"'{username}' in course '{course_id}'").format(
                         username=username, course_id=course_id)
                })
        finally:
            # Assumes that the ecommerce service uses an API key to authenticate.
            if has_api_key_permissions:
                current_enrollment = api.get_enrollment(
                    username, unicode(course_id))
                audit_log('enrollment_change_requested',
                          course_id=unicode(course_id),
                          requested_mode=mode,
                          actual_mode=current_enrollment['mode']
                          if current_enrollment else None,
                          requested_activation=is_active,
                          actual_activation=current_enrollment['is_active']
                          if current_enrollment else None,
                          user_id=user.id)
 def _handouts_loc(self):
     """
     Return the locator string for the course handouts
     """
     course_key = CourseKey.from_string(self._course_key)
     return six.text_type(course_key.make_usage_key('course_info', 'handouts'))
Exemple #60
0
    def render_to_fragment(self, request, course_id=None, **kwargs):
        """
        Renders the course's home page as a fragment.
        """
        course_key = CourseKey.from_string(course_id)
        course = get_course_with_access(request.user, 'load', course_key)

        # Render the course dates as a fragment
        dates_fragment = CourseDatesFragmentView().render_to_fragment(
            request, course_id=course_id, **kwargs)

        # Render the full content to enrolled users, as well as to course and global staff.
        # Unenrolled users who are not course or global staff are given only a subset.
        user_access = {
            'is_anonymous': request.user.is_anonymous(),
            'is_enrolled':
            CourseEnrollment.is_enrolled(request.user, course_key),
            'is_staff': has_access(request.user, 'staff', course_key),
        }
        if user_access['is_enrolled'] or user_access['is_staff']:
            outline_fragment = CourseOutlineFragmentView().render_to_fragment(
                request, course_id=course_id, **kwargs)
            welcome_message_fragment = WelcomeMessageFragmentView(
            ).render_to_fragment(request, course_id=course_id, **kwargs)
            course_sock_fragment = CourseSockFragmentView().render_to_fragment(
                request, course=course, **kwargs)
            has_visited_course, resume_course_url = self._get_resume_course_info(
                request, course_id)
        else:
            # Redirect the user to the dashboard if they are not enrolled and
            # this is a course that does not support direct enrollment.
            if not can_self_enroll_in_course(course_key):
                raise CourseAccessRedirect(reverse('dashboard'))

            # Set all the fragments
            outline_fragment = None
            welcome_message_fragment = None
            course_sock_fragment = None
            has_visited_course = None
            resume_course_url = None

        # Get the handouts
        handouts_html = self._get_course_handouts(request, course)

        # Get the course tools enabled for this user and course
        course_tools = CourseToolsPluginManager.get_enabled_course_tools(
            request, course_key)

        # Grab the course home messages fragment to render any relevant django messages
        course_home_message_fragment = CourseHomeMessageFragmentView(
        ).render_to_fragment(request,
                             course_id=course_id,
                             user_access=user_access,
                             **kwargs)

        # Render the course home fragment
        context = {
            'request': request,
            'csrf': csrf(request)['csrf_token'],
            'course': course,
            'course_key': course_key,
            'outline_fragment': outline_fragment,
            'handouts_html': handouts_html,
            'course_home_message_fragment': course_home_message_fragment,
            'has_visited_course': has_visited_course,
            'resume_course_url': resume_course_url,
            'course_tools': course_tools,
            'dates_fragment': dates_fragment,
            'welcome_message_fragment': welcome_message_fragment,
            'course_sock_fragment': course_sock_fragment,
            'disable_courseware_js': True,
            'uses_pattern_library': True,
        }
        html = render_to_string('course_experience/course-home-fragment.html',
                                context)
        return Fragment(html)