コード例 #1
0
ファイル: client.py プロジェクト: BbrSofiane/edx-enterprise
    def _search_for_canvas_user_by_email(self, user_email):
        """
        Helper method to make an api call to Canvas using the user's email as a search term.

        Args:
            user_email (string) : The email associated with both the user's Edx account and Canvas account.
        """
        get_user_id_from_email_url = '{url_base}/api/v1/accounts/{account_id}/users?search_term={email_address}'.format(
            url_base=self.enterprise_configuration.canvas_base_url,
            account_id=self.enterprise_configuration.canvas_account_id,
            email_address=user_email
        )
        rsps = self.session.get(get_user_id_from_email_url)

        if rsps.status_code >= 400:
            raise ClientError(
                "Failed to retrieve user from Canvas: received response-[{}]".format(rsps.reason),
                rsps.status_code
            )

        get_users_by_email_response = rsps.json()

        try:
            canvas_user_id = get_users_by_email_response[0]['id']
        except (KeyError, IndexError):
            raise ClientError(
                "No Canvas user ID found associated with email: {}".format(user_email),
                HTTPStatus.NOT_FOUND.value
            )
        return canvas_user_id
コード例 #2
0
ファイル: client.py プロジェクト: qiaoyafeng/edx-enterprise
    def _sync_content_metadata(self, serialized_data):
        """
        Create/update/delete content metadata records using the SuccessFactors OCN Course Import API endpoint.

        Arguments:
            serialized_data: Serialized JSON string representing a list of content metadata items.

        Raises:
            ClientError: If SuccessFactors API call fails.
        """
        url = self.enterprise_configuration.sapsf_base_url + self.global_sap_config.course_api_path
        try:
            status_code, response_body = self._call_post_with_session(url, serialized_data)
        except requests.exceptions.RequestException as exc:
            raise ClientError(
                'SAPSuccessFactorsAPIClient request failed: {error} {message}'.format(
                    error=exc.__class__.__name__,
                    message=str(exc)
                )
            )

        if status_code >= 400:
            raise ClientError(
                'SAPSuccessFactorsAPIClient request failed with status {status_code}: {message}'.format(
                    status_code=status_code,
                    message=response_body
                )
            )
コード例 #3
0
ファイル: client.py プロジェクト: edx-abolger/edx-enterprise
    def _sync_content_metadata(self, serialized_data, http_method):
        """
        Synchronize content metadata using the Degreed course content API.

        Args:
            serialized_data: JSON-encoded object containing content metadata.
            http_method: The HTTP method to use for the API request.

        Raises:
            ClientError: If Degreed API request fails.
        """
        try:
            status_code, response_body = getattr(self, '_' + http_method)(
                urljoin(self.enterprise_configuration.degreed_base_url, self.global_degreed_config.course_api_path),
                serialized_data,
                self.CONTENT_PROVIDER_SCOPE
            )
        except requests.exceptions.RequestException as exc:
            raise ClientError(
                'DegreedAPIClient request failed: {error} {message}'.format(
                    error=exc.__class__.__name__,
                    message=str(exc)
                )
            ) from exc

        if status_code >= 400:
            raise ClientError(
                'DegreedAPIClient request failed with status {status_code}: {message}'.format(
                    status_code=status_code,
                    message=response_body
                )
            )
コード例 #4
0
ファイル: client.py プロジェクト: BbrSofiane/edx-enterprise
    def _handle_canvas_assignment_retrieval(self, integration_id, course_id):
        """
        Helper method to handle course assignment creation or retrieval. Canvas requires an assignment
        in order for a user to get a grade, so first check the course for the "final grade"
        assignment. This assignment will have a matching integration id to the currently transmitting
        learner data. If this assignment is not yet created on Canvas, send a post request to do so.

        Args:
            integration_id (str) : the string integration id from the edx course.
            course_id (str) : the Canvas course ID relating to the course which the client is currently
            transmitting learner data to.
        """
        # First, check if the course assignment already exists
        canvas_assignments_url = '{canvas_base_url}/api/v1/courses/{course_id}/assignments'.format(
            canvas_base_url=self.enterprise_configuration.canvas_base_url,
            course_id=course_id
        )
        resp = self.session.get(canvas_assignments_url)

        assignments_resp = resp.json()
        assignment_id = None
        for assignment in assignments_resp:
            try:
                if assignment['integration_id'] == integration_id:
                    assignment_id = assignment['id']
                    break
            except (KeyError, ValueError):
                raise ClientError(
                    "Something went wrong retrieving assignments from Canvas. Got response: {}".format(
                        resp.text,
                    ),
                    resp.status_code
                )

        # Canvas requires a course assignment for a learner to be assigned a grade.
        # If no assignment has been made yet, create it.
        if not assignment_id:
            assignment_creation_data = {
                'assignment': {
                    'name': '(Edx integration) Final Grade',
                    'submission_types': 'none',
                    'integration_id': integration_id,
                    'published': True,
                    'points_possible': 100
                }
            }
            create_assignment_resp = self.session.post(canvas_assignments_url, json=assignment_creation_data)

            try:
                assignment_id = create_assignment_resp.json()['id']
            except (ValueError, KeyError):
                raise ClientError(
                    "Something went wrong creating an assignment on Canvas. Got response: {}".format(
                        create_assignment_resp.text,
                    ),
                    create_assignment_resp.status_code
                )
        return assignment_id
コード例 #5
0
ファイル: client.py プロジェクト: edx-abolger/edx-enterprise
 def inner(self, *args, **kwargs):
     if not self.token:
         self.token = self._get_access_token()  # pylint: disable=protected-access
     response = method(self, *args, **kwargs)
     try:
         body = response.json()
     except (AttributeError, ValueError) as error:
         # Moodle spits back an entire HTML page if something is wrong in our URL format.
         # This cannot be converted to JSON thus the above fails miserably.
         # The above can fail with different errors depending on the format of the returned page.
         # Moodle of course does not tell us what is wrong in any part of this HTML.
         raise ClientError(
             'Moodle API task "{method}" failed due to unknown error.'.
             format(method=method.__name__),
             response.status_code) from error
     if isinstance(body, list):
         # On course creation (and ONLY course creation) success,
         # Moodle returns a list of JSON objects, because of course it does.
         # Otherwise, it fails instantly and returns actual JSON.
         return response
     if isinstance(body, int):
         # This only happens for grades AFAICT. Zero also doesn't necessarily mean success,
         # but we have nothing else to go on
         if body == 0:
             return 200, ''
         raise ClientError(
             'Moodle API Grade Update failed with int code: {code}'.format(
                 code=body), 500)
     if isinstance(body, str):
         # Grades + debug can sometimes produce lines with debug errors and also "0"
         raise ClientError(
             'Moodle API Grade Update failed with possible error: {body}'.
             format(body=body), 500)
     error_code = body.get('errorcode')
     warnings = body.get('warnings')
     if error_code and error_code == 'invalidtoken':
         self.token = self._get_access_token()  # pylint: disable=protected-access
         response = method(self, *args, **kwargs)
     elif error_code:
         raise ClientError(
             'Moodle API Client Task "{method}" failed with error code '
             '"{code}" and message: "{msg}" '.format(
                 method=method.__name__,
                 code=error_code,
                 msg=body.get('message'),
             ), response.status_code)
     elif warnings:
         # More Moodle nonsense!
         errors = []
         for warning in warnings:
             if warning.get('message'):
                 errors.append(warning.get('message'))
         raise ClientError(
             'Moodle API Client Task "{method}" failed with the following error codes: '
             '"{code}"'.format(method=method.__name__, code=errors))
     return response
コード例 #6
0
    def create_course_completion(self, user_id, payload):
        """
        Post a final course grade to the integrated Blackboard course.

        Parameters:
        -----------
            user_id (str): The shared email between a user's edX account and Blackboard account
            payload (str): The (string representation) of the learner data information

        Example payload:
        ---------------
            '{
                courseID: course-edx+555+3T2020,
                score: 0.85,
                completedTimestamp: 1602265162589,
            }'

        """
        self._create_session()
        learner_data = json.loads(payload)
        external_id = learner_data.get('courseID')

        course_id = self._resolve_blackboard_course_id(external_id)

        # Sanity check for course id
        if not course_id:
            raise ClientError(
                'Could not find course:{} on Blackboard'.format(external_id),
                HTTPStatus.NOT_FOUND.value)

        blackboard_user_id = self._get_bb_user_id_from_enrollments(
            user_id, course_id)
        grade_column_id = self._get_or_create_integrated_grade_column(
            course_id)

        grade = learner_data.get('grade') * 100
        grade_percent = {'score': grade}
        response = self._patch(
            self.generate_post_users_grade_url(course_id, grade_column_id,
                                               blackboard_user_id),
            grade_percent)

        if response.json().get('score') != grade:
            raise ClientError(
                'Failed to post new grade for user={} enrolled in course={}'.
                format(user_id,
                       course_id), HTTPStatus.INTERNAL_SERVER_ERROR.value)

        success_body = 'Successfully posted grade of {grade} to course:{course_id} for user:{user_email}.'.format(
            grade=grade,
            course_id=external_id,
            user_email=user_id,
        )
        return response.status_code, success_body
コード例 #7
0
    def create_integration_content_for_course(self, bb_course_id,
                                              channel_metadata_item):
        """
        Helper method to generate the default blackboard course content page with the integration custom information.
        """
        content_url = self.generate_create_course_content_url(bb_course_id)
        error_message = ''
        try:
            content_resp = self._post(
                content_url,
                channel_metadata_item.get('course_content_metadata'))
            bb_content_id = content_resp.json().get('id')
        except ClientError as error:
            bb_content_id = None
            error_message = ' and received error response={}'.format(
                error.message)

        if not bb_content_id:
            error_handling_response = self.delete_course_from_blackboard(
                bb_course_id)
            error_message = '' if not error_message else error_message
            raise ClientError(
                'Something went wrong while creating course content object on Blackboard. Could not retrieve content '
                'ID{}. Deleted course with response (status_code={}, body={})'.
                format(error_message, error_handling_response.status_code,
                       error_handling_response.text),
                HTTPStatus.NOT_FOUND.value)

        try:
            content_child_url = self.generate_create_course_content_child_url(
                bb_course_id, bb_content_id)
            course_fully_created_response = self._post(
                content_child_url,
                channel_metadata_item.get('course_child_content_metadata'))
            content_child_id = course_fully_created_response.json().get('id')
        except ClientError as error:
            content_child_id = None
            error_message = ' and received error response={}'.format(
                error.message)

        if not content_child_id:
            error_message = '' if not error_message else error_message
            error_handling_response = self.delete_course_from_blackboard(
                bb_course_id, bb_content_id)
            raise ClientError(
                'Something went wrong while creating course content child object on Blackboard. Could not retrieve a '
                'content child ID and got error response={}. Deleted associated course and content with response: '
                '(status_code={}, body={})'.format(
                    error_message, error_handling_response.status_code,
                    error_handling_response.text), HTTPStatus.NOT_FOUND.value)

        return course_fully_created_response
コード例 #8
0
ファイル: client.py プロジェクト: edx-abolger/edx-enterprise
    def _get_oauth_access_token(self):
        """Fetch access token using refresh_token workflow from Blackboard

        Returns:
            access_token (str): the OAuth access token to access the Blackboard server
            expires_in (int): the number of seconds after which token will expire
        Raises:
            HTTPError: If we received a failure response code.
            ClientError: If an unexpected response format was received that we could not parse.
        """

        if not self.enterprise_configuration.refresh_token:
            raise ClientError(
                "Failed to generate oauth access token: Refresh token required.",
                HTTPStatus.INTERNAL_SERVER_ERROR.value
            )

        if (not self.enterprise_configuration.blackboard_base_url
                or not self.config.oauth_token_auth_path):
            raise ClientError(
                "Failed to generate oauth access token: oauth path missing from configuration.",
                HTTPStatus.INTERNAL_SERVER_ERROR.value
            )
        auth_token_url = urljoin(
            self.enterprise_configuration.blackboard_base_url,
            self.config.oauth_token_auth_path,
        )

        auth_token_params = {
            'grant_type': 'refresh_token',
            'refresh_token': self.enterprise_configuration.refresh_token,
        }

        auth_response = requests.post(
            auth_token_url,
            auth_token_params,
            headers={
                'Authorization': self._create_auth_header(),
                'Content-Type': 'application/x-www-form-urlencoded'
            }
        )
        if auth_response.status_code >= 400:
            raise ClientError(auth_response.text, auth_response.status_code)
        try:
            data = auth_response.json()
            # do not forget to save the new refresh token otherwise subsequent requests will fail
            self.enterprise_configuration.refresh_token = data["refresh_token"]
            self.enterprise_configuration.save()
            return data['access_token'], data["expires_in"]
        except (KeyError, ValueError) as error:
            raise ClientError(auth_response.text, auth_response.status_code) from error
コード例 #9
0
    def _get_oauth_access_token(self, client_id, client_secret):
        """Uses the client id, secret and refresh token to request the user's auth token from Canvas.

        Args:
            client_id (str): API client ID
            client_secret (str): API client secret

        Returns:
            access_token (str): the OAuth access token to access the Canvas API as the user
        Raises:
            HTTPError: If we received a failure response code from Canvas.
            RequestException: If an unexpected response format was received that we could not parse.
        """
        if not client_id:
            raise ClientError(
                "Failed to generate oauth access token: Client ID required.")
        if not client_secret:
            raise ClientError(
                "Failed to generate oauth access token: Client secret required."
            )
        if not self.enterprise_configuration.refresh_token:
            raise ClientError(
                "Failed to generate oauth access token: Refresh token required."
            )

        if not self.enterprise_configuration.canvas_base_url or not self.config.oauth_token_auth_path:
            raise ClientError(
                "Failed to generate oauth access token: Canvas oauth path missing from configuration."
            )
        auth_token_url = urljoin(
            self.enterprise_configuration.canvas_base_url,
            self.config.oauth_token_auth_path,
        )

        auth_token_params = {
            'grant_type': 'refresh_token',
            'client_id': client_id,
            'client_secret': client_secret,
            'state':
            str(self.enterprise_configuration.enterprise_customer.uuid),
            'refresh_token': self.enterprise_configuration.refresh_token,
        }

        auth_response = requests.post(auth_token_url, auth_token_params)
        auth_response.raise_for_status()
        try:
            data = auth_response.json()
            return data['access_token']
        except (KeyError, ValueError):
            raise requests.RequestException(response=auth_response)
コード例 #10
0
ファイル: client.py プロジェクト: edx-abolger/edx-enterprise
    def _get_or_create_integrated_grade_column(self,
                                               bb_course_id,
                                               grade_column_name,
                                               external_id,
                                               points_possible=100):
        """
        Helper method to search an edX integrated Blackboard course for the designated edX grade column.
        If the column does not yet exist within the course, create it.

        Parameters:
        -----------
            bb_course_id (str): The Blackboard course ID in which to search for the edX final grade,
            grade column.
        """
        grade_column_response = self._get(
            self.generate_gradebook_url(bb_course_id))
        parsed_response = grade_column_response.json()
        grade_columns = parsed_response.get('results')

        grade_column_id = None
        for grade_column in grade_columns:
            if grade_column.get('externalId') == external_id:
                grade_column_id = grade_column.get('id')
        if not grade_column_id:
            # Potential customization here per-customer, if the need arises.
            grade_column_data = {
                "externalId": external_id,
                "name": grade_column_name,
                "displayName": grade_column_name,
                "description": "edX learner's grade.",
                "externalGrade": False,
                "score": {
                    "possible": points_possible
                },
                "availability": {
                    "available": "Yes"
                },
                "grading": {
                    "type": "Manual",
                    "scoringModel": "Last",
                    "anonymousGrading": {
                        "type": "None",
                    }
                },
            }

            response = self._post(
                self.generate_create_grade_column_url(bb_course_id),
                grade_column_data)
            parsed_response = response.json()
            grade_column_id = parsed_response.get('id')

            # Sanity check that we created the grade column properly
            if not grade_column_id:
                raise ClientError(
                    'Something went wrong while create edX integration grade column for course={}.'
                    .format(bb_course_id),
                    HTTPStatus.INTERNAL_SERVER_ERROR.value)

        return grade_column_id
コード例 #11
0
ファイル: client.py プロジェクト: BbrSofiane/edx-enterprise
    def get_course_final_grade_module(self, course_id):
        """
        Sort through a Moodle course's components for the specific shell assignment designated
        to be the edX integrated Final Grade. This is currently done by module name.

        Returns:
            - course_module_id (int): The ID of the shell assignment
            - module_name (string): The string name of the module. Required for sending a grade update request.
        """
        response = self._get_course_contents(course_id)

        course_module_id = None
        if isinstance(response.json(), list):
            for course in response.json():
                if course.get('name') == 'General':
                    modules = course.get('modules')
                    for module in modules:
                        if module.get(
                                'name') == MOODLE_FINAL_GRADE_ASSIGNMENT_NAME:
                            course_module_id = module.get('id')
                            module_name = module.get('modname')

        if not course_module_id:
            raise ClientError(
                'MoodleAPIClient request failed: 404 Completion course module not found in Moodle.'
                ' The enterprise customer needs to create an activity within the course with the name '
                '"(edX integration) Final Grade"', HTTPStatus.NOT_FOUND.value)
        return course_module_id, module_name
コード例 #12
0
ファイル: client.py プロジェクト: BbrSofiane/edx-enterprise
    def _get_access_token(self):
        """
        Obtains a new access token from Moodle using username and password.
        """
        querystring = {
            'service': self.enterprise_configuration.service_short_name
        }

        response = requests.post(
            urljoin(
                self.enterprise_configuration.moodle_base_url,
                '/login/token.php',
            ),
            params=querystring,
            headers={
                'Content-Type': 'application/x-www-form-urlencoded',
            },
            data={
                'username': self.enterprise_configuration.username,
                'password': self.enterprise_configuration.password,
            },
        )

        try:
            data = response.json()
            token = data['token']
            return token
        except (KeyError, ValueError):
            raise ClientError(
                "Failed to post access token. Received message={} from Moodle".
                format(response.text), response.status_code)
コード例 #13
0
ファイル: client.py プロジェクト: edx-abolger/edx-enterprise
    def _handle_get_user_canvas_course(self, canvas_user_id, learner_data_course_id):
        """
        Helper method to take the Canvas user ID and edX course ID to find the matching Canvas course information.
        """
        # With the Canvas user ID, retrieve all courses for the user.
        user_courses = self._get_canvas_user_courses_by_id(canvas_user_id)

        # Find the course who's integration ID matches the learner data course ID. This integration ID can be either
        # an edX course run ID or course ID. Raise if no course found.
        canvas_course_id = None
        for course in user_courses:
            if course['integration_id'] == learner_data_course_id:
                canvas_course_id = course['id']
                break

        if not canvas_course_id:
            raise ClientError(
                "Course: {course_id} not found registered in Canvas for Canvas learner: {canvas_user_id}.".format(
                    course_id=learner_data_course_id,
                    canvas_user_id=canvas_user_id,
                ),
                HTTPStatus.NOT_FOUND.value,
            )

        return canvas_course_id
コード例 #14
0
    def test_transmit_update_failure(self):
        """
        Test unsuccessful update of content metadata during transmission.
        """
        content_id = 'course:DemoX'
        channel_metadata = {'update': True}
        ContentMetadataItemTransmission(
            enterprise_customer=self.enterprise_config.enterprise_customer,
            integrated_channel_code=self.enterprise_config.channel_code(),
            content_id=content_id,
            channel_metadata={}).save()
        payload = {
            content_id:
            ContentMetadataItemExport(
                {
                    'key': content_id,
                    'content_type': 'course'
                }, channel_metadata)
        }
        self.update_content_metadata_mock.side_effect = ClientError(
            'error occurred')
        transmitter = ContentMetadataTransmitter(self.enterprise_config)
        transmitter.transmit(payload)

        self.create_content_metadata_mock.assert_not_called()
        self.update_content_metadata_mock.assert_called()
        self.delete_content_metadata_mock.assert_not_called()

        updated_transmission = ContentMetadataItemTransmission.objects.get(
            enterprise_customer=self.enterprise_config.enterprise_customer,
            integrated_channel_code=self.enterprise_config.channel_code(),
            content_id=content_id,
        )

        assert updated_transmission.channel_metadata == {}
コード例 #15
0
    def unlink_learners(self):
        """
        Iterate over each learner and unlink inactive SAP channel learners.

        This method iterates over each enterprise learner and unlink learner
        from the enterprise if the learner is marked inactive in the related
        integrated channel.
        """
        try:
            sap_inactive_learners = self.client.get_inactive_sap_learners()
        except RequestException as exc:
            raise ClientError(
                'SAPSuccessFactorsAPIClient request failed: {error} {message}'.format(
                    error=exc.__class__.__name__,
                    message=str(exc)
                )
            )
        total_sap_inactive_learners = len(sap_inactive_learners) if sap_inactive_learners else 0
        enterprise_customer = self.enterprise_configuration.enterprise_customer
        LOGGER.info(
            'Found [%d] SAP inactive learners for enterprise customer [%s]',
            total_sap_inactive_learners, enterprise_customer.name
        )
        if not sap_inactive_learners:
            return None

        provider_id = enterprise_customer.identity_provider
        tpa_provider = get_identity_provider(provider_id)
        if not tpa_provider:
            LOGGER.info(
                'Enterprise customer [%s] has no associated identity provider',
                enterprise_customer.name
            )
            return None

        for sap_inactive_learner in sap_inactive_learners:
            sap_student_id = sap_inactive_learner['studentID']
            social_auth_user = get_user_from_social_auth(tpa_provider, sap_student_id)
            if not social_auth_user:
                LOGGER.info(
                    'No social auth data found for inactive user with SAP student id [%s] of enterprise '
                    'customer [%s] with identity provider [%s]',
                    sap_student_id, enterprise_customer.name, tpa_provider.provider_id
                )
                continue

            try:
                # Unlink user email from related Enterprise Customer
                EnterpriseCustomerUser.objects.unlink_user(
                    enterprise_customer=enterprise_customer,
                    user_email=social_auth_user.email,
                )
            except (EnterpriseCustomerUser.DoesNotExist, PendingEnterpriseCustomerUser.DoesNotExist):
                LOGGER.info(
                    'Learner with email [%s] and SAP student id [%s] is not linked with enterprise [%s]',
                    social_auth_user.email,
                    sap_student_id,
                    enterprise_customer.name
                )
        return None
コード例 #16
0
    def test_transmit_create_failure(self):
        """
        Test unsuccessful creation of content metadata during transmission.
        """
        content_id = 'course:DemoX'
        channel_metadata = {'update': True}
        payload = {
            content_id:
            ContentMetadataItemExport(
                {
                    'key': content_id,
                    ContentType.METADATA_KEY: ContentType.COURSE,
                },
                channel_metadata,
            )
        }
        self.create_content_metadata_mock.side_effect = ClientError(
            'error occurred')
        transmitter = ContentMetadataTransmitter(self.enterprise_config)
        transmitter.transmit(payload)

        self.create_content_metadata_mock.assert_called()
        self.update_content_metadata_mock.assert_not_called()
        self.delete_content_metadata_mock.assert_not_called()

        assert not ContentMetadataItemTransmission.objects.filter(
            enterprise_customer=self.enterprise_config.enterprise_customer,
            integrated_channel_code=self.enterprise_config.channel_code(),
            content_id=content_id,
        )
コード例 #17
0
    def test_transmit_create_failure(self, update_content_metadata_mock):
        """
        Test unsuccessful creation of content metadata during transmission.
        """
        content_id = 'course:DemoX'
        update_content_metadata_mock.side_effect = ClientError(
            'error occurred')
        responses.add(responses.POST,
                      self.url_base + self.oauth_api_path,
                      json=self.expected_token_response_body,
                      status=200)

        with LogCapture(level=logging.ERROR) as log_capture:
            transmitter = SapSuccessFactorsContentMetadataTransmitter(
                self.enterprise_config)
            transmitter.transmit({
                content_id:
                ContentMetadataItemExport(
                    {
                        'key': content_id,
                        'content_type': 'course'
                    }, {
                        'courseID': content_id,
                        'update': True
                    })
            })
            assert len(log_capture.records) == 2
            assert 'Failed to update [1] content metadata items' in log_capture.records[
                0].getMessage()
            assert not ContentMetadataItemTransmission.objects.filter(
                enterprise_customer=self.enterprise_config.enterprise_customer,
                integrated_channel_code=self.enterprise_config.channel_code(),
                content_id=content_id,
            ).exists()
コード例 #18
0
    def test_transmit_delete_failure(self):
        """
        Test successful deletion of content metadata during transmission.
        """
        content_id = 'course:DemoX'
        channel_metadata = {'update': True}
        ContentMetadataItemTransmission(
            enterprise_customer=self.enterprise_config.enterprise_customer,
            integrated_channel_code=self.enterprise_config.channel_code(),
            content_id=content_id,
            channel_metadata=channel_metadata).save()
        payload = {}
        self.delete_content_metadata_mock.side_effect = ClientError(
            'error occurred')
        transmitter = ContentMetadataTransmitter(self.enterprise_config)
        transmitter.transmit(payload)

        self.create_content_metadata_mock.assert_not_called()
        self.update_content_metadata_mock.assert_not_called()
        self.delete_content_metadata_mock.assert_called()

        assert ContentMetadataItemTransmission.objects.filter(
            enterprise_customer=self.enterprise_config.enterprise_customer,
            integrated_channel_code=self.enterprise_config.channel_code(),
            content_id=content_id,
        )
コード例 #19
0
ファイル: client.py プロジェクト: edx-abolger/edx-enterprise
    def _handle_canvas_assignment_submission(self, grade, course_id, assignment_id, canvas_user_id):
        """
        Helper method to take necessary learner data and post to Canvas as a submission to the correlated assignment.
        """
        submission_url = '{base_url}/api/v1/courses/{course_id}/assignments/' \
                         '{assignment_id}/submissions/{user_id}'.format(
                             base_url=self.enterprise_configuration.canvas_base_url,
                             course_id=course_id,
                             assignment_id=assignment_id,
                             user_id=canvas_user_id
                         )

        # The percent grade from the grades api is represented as a decimal
        submission_data = {
            'submission': {
                'posted_grade': grade
            }
        }
        submission_response = self.session.put(submission_url, json=submission_data)

        if submission_response.status_code >= 400:
            raise ClientError(
                "Something went wrong while posting a submission to Canvas assignment: {} under Canvas course: {}."
                " Recieved response {} with the status code: {}".format(
                    assignment_id,
                    course_id,
                    submission_response.text,
                    submission_response.status_code
                )
            )
        return submission_response
コード例 #20
0
 def _delete(self, url):
     """
     Returns request's delete response and raises Client Errors if appropriate.
     """
     response = self.session.delete(url)
     if response.status_code >= 400:
         raise ClientError(response.text, response.status_code)
     return response
コード例 #21
0
 def _post(self, url, data):
     """
     Returns request's post response and raises Client Errors if appropriate.
     """
     post_response = self.session.post(url, json=data)
     if post_response.status_code >= 400:
         raise ClientError(post_response.text, post_response.status_code)
     return post_response
コード例 #22
0
 def _get(self, url, data=None):
     """
     Returns request's get response and raises Client Errors if appropriate.
     """
     get_response = self.session.get(url, params=data)
     if get_response.status_code >= 400:
         raise ClientError(get_response.text, get_response.status_code)
     return get_response
コード例 #23
0
 def _create_auth_header(self):
     """
     auth header in oauth2 token format as required by blackboard doc
     """
     if not self.enterprise_configuration.client_id:
         raise ClientError(
             "Failed to generate oauth access token: Client ID required.",
             HTTPStatus.INTERNAL_SERVER_ERROR.value)
     if not self.enterprise_configuration.client_secret:
         raise ClientError(
             "Failed to generate oauth access token: Client secret required.",
             HTTPStatus.INTERNAL_SERVER_ERROR.value)
     return 'Basic {}'.format(
         base64.b64encode(u'{key}:{secret}'.format(
             key=self.enterprise_configuration.client_id,
             secret=self.enterprise_configuration.client_secret).encode(
                 'utf-8')).decode())
コード例 #24
0
 def _validate_course_id(course_id, external_id):
     """
     Raise error if course_id invalid
     """
     if not course_id:
         raise ClientError(
             'Could not find course:{} on Blackboard'.format(external_id),
             HTTPStatus.NOT_FOUND.value)
コード例 #25
0
 def _validate_channel_metadata(channel_metadata_item):
     """
     Raise error if external_id invalid or not found
     """
     if 'externalId' not in channel_metadata_item:
         raise ClientError(
             "No externalId found in metadata, please check json data format",
             400)
コード例 #26
0
ファイル: client.py プロジェクト: BbrSofiane/edx-enterprise
    def _call_search_students_recursively(self, sap_search_student_url,
                                          all_inactive_learners, page_size,
                                          start_at):
        """
        Make recursive GET calls to traverse the paginated API response for search students.
        """
        search_student_paginated_url = '{sap_search_student_url}&{pagination_criterion}'.format(
            sap_search_student_url=sap_search_student_url,
            pagination_criterion='$count=true&$top={page_size}&$skip={start_at}'
            .format(
                page_size=page_size,
                start_at=start_at,
            ),
        )
        try:
            response = self.session.get(search_student_paginated_url)
            sap_inactive_learners = response.json()
        except ValueError:
            raise ClientError(response, response.status_code)
        except (ConnectionError, Timeout):
            LOGGER.warning(
                'Unable to fetch inactive learners from SAP searchStudent API with url '
                '"{%s}".',
                search_student_paginated_url,
            )
            return None

        if 'error' in sap_inactive_learners:
            LOGGER.warning(
                'SAP searchStudent API for customer %s and base url %s returned response with '
                'error message "%s" and with error code "%s".',
                self.enterprise_configuration.enterprise_customer.name,
                self.enterprise_configuration.sapsf_base_url,
                sap_inactive_learners['error'].get('message'),
                sap_inactive_learners['error'].get('code'),
            )
            return None

        new_page_start_at = page_size + start_at
        total_inactive_learners = sap_inactive_learners['@odata.count']
        inactive_learners_on_page = sap_inactive_learners['value']
        LOGGER.info(
            'SAP SF searchStudent API returned [%d] inactive learners of total [%d] starting from [%d] for '
            'enterprise customer [%s]', len(inactive_learners_on_page),
            total_inactive_learners, start_at,
            self.enterprise_configuration.enterprise_customer.name)

        all_inactive_learners += inactive_learners_on_page
        if total_inactive_learners > new_page_start_at:
            return self._call_search_students_recursively(
                sap_search_student_url,
                all_inactive_learners,
                page_size=page_size,
                start_at=new_page_start_at,
            )

        return all_inactive_learners
コード例 #27
0
ファイル: client.py プロジェクト: BbrSofiane/edx-enterprise
    def create_course_completion(self, user_id, payload):  # pylint: disable=unused-argument
        learner_data = json.loads(payload)
        self._create_session()

        # Retrieve the Canvas user ID from the user's edx email (it is assumed that the learner's Edx
        # and Canvas emails will match).
        canvas_user_id = self._search_for_canvas_user_by_email(user_id)

        # With the Canvas user ID, retrieve all courses for the user.
        user_courses = self._get_canvas_user_courses_by_id(canvas_user_id)

        # Find the course who's integration ID matches the learner data course ID
        course_id = None
        for course in user_courses:
            integration_id = course['integration_id']
            if '+'.join(integration_id.split(":")[1].split("+")[:2]) == learner_data['courseID']:
                course_id = course['id']
                break

        if not course_id:
            raise ClientError(
                "Course: {course_id} not found registered in Canvas for Edx learner: {user_id}"
                "/Canvas learner: {canvas_user_id}.".format(
                    course_id=learner_data['courseID'],
                    user_id=learner_data['userID'],
                    canvas_user_id=canvas_user_id
                ),
                HTTPStatus.NOT_FOUND.value,
            )

        # Depending on if the assignment already exists, either retrieve or create it.
        try:
            assignment_id = self._handle_canvas_assignment_retrieval(integration_id, course_id)
        except ClientError as client_error:
            return client_error.status_code, client_error.message

        # Post a grade for the assignment. This shouldn't create a submission for the user, but still update the grade.
        submission_url = '{base_url}/api/v1/courses/{course_id}/assignments/' \
                         '{assignment_id}/submissions/{user_id}'.format(
                             base_url=self.enterprise_configuration.canvas_base_url,
                             course_id=course_id,
                             assignment_id=assignment_id,
                             user_id=canvas_user_id
                         )

        # The percent grade from the grades api is represented as a decimal
        submission_data = {
            'submission': {
                'posted_grade': learner_data['grade'] * 100
            }
        }
        update_grade_response = self.session.put(submission_url, json=submission_data)

        return update_grade_response.status_code, update_grade_response.text
コード例 #28
0
ファイル: client.py プロジェクト: BbrSofiane/edx-enterprise
    def _delete(self, url):
        """
        Make a DELETE request using the session object to the Canvas course delete endpoint.

        Args:
            url (str): The canvas url to send delete requests to.
        """
        delete_response = self.session.delete(url, data='{"event":"delete"}')
        if delete_response.status_code >= 400:
            raise ClientError(delete_response.text, delete_response.status_code)
        return delete_response.status_code, delete_response.text
コード例 #29
0
ファイル: client.py プロジェクト: BbrSofiane/edx-enterprise
    def _extract_integration_id(self, data):
        """
        Retrieve the integration ID string from the encoded transmission data and apply appropriate
        error handling.

        Args:
            data (bytearray): The json encoded payload intended for a Canvas endpoint.
        """
        if not data:
            raise ClientError("No data to transmit.", HTTPStatus.NOT_FOUND.value)
        try:
            integration_id = json.loads(
                data.decode("utf-8")
            )['course']['integration_id']
        except KeyError:
            raise ClientError("Could not transmit data, no integration ID present.", HTTPStatus.NOT_FOUND.value)
        except AttributeError:
            raise ClientError("Unable to decode data.", HTTPStatus.BAD_REQUEST.value)

        return integration_id
コード例 #30
0
ファイル: client.py プロジェクト: BbrSofiane/edx-enterprise
    def _post(self, url, data):
        """
        Make a POST request using the session object to a Canvas endpoint.

        Args:
            url (str): The url to send a POST request to.
            data (bytearray): The json encoded payload to POST.
        """
        post_response = self.session.post(url, data=data)
        if post_response.status_code >= 400:
            raise ClientError(post_response.text, post_response.status_code)
        return post_response.status_code, post_response.text