def test_finalization_success(self, enroll_creator_in_new_course, logger, get_course_data, get_canvas_course_url):
     """
     If all finalization steps are successful, there should be no calls to logger.exception() and the return value
     should be the new course URL.
     """
     test_url = "test_url/"
     get_canvas_course_url.return_value = test_url
     self.test_return_value = finalize_new_canvas_course(self.canvas_course_id, self.sis_course_id, self.user_id)
     self.assertFalse(logger.exception.called)
     self.assertEquals(self.test_return_value, test_url)
 def test_enrollment_failure(self, enroll_creator_in_new_course, logger, get_course_data, get_canvas_course_url):
     """
     If the automatic enrollment of the course creator fails, we should be logging an exception and exiting the
     finalization process without a return value and before proceeding with other steps.
     """
     enroll_creator_in_new_course.side_effect = Exception("Mock exception")
     with self.assertRaises(RenderableException):
         self.test_return_value = finalize_new_canvas_course(self.canvas_course_id, self.sis_course_id, self.user_id)
     enroll_creator_in_new_course.assert_called_once_with(self.sis_course_id, self.user_id)
     self.assertFalse(get_course_data.called)
     self.assertTrue(logger.exception.called)
     self.assertIsNone(self.test_return_value)
Example #3
0
 def test_finalization_success(self, enroll_creator_in_new_course, logger,
                               get_course_data, get_canvas_course_url):
     """
     If all finalization steps are successful, there should be no calls to logger.exception() and the return value
     should be the new course URL.
     """
     test_url = 'test_url/'
     get_canvas_course_url.return_value = test_url
     self.test_return_value = finalize_new_canvas_course(
         self.canvas_course_id, self.sis_course_id, self.user_id)
     self.assertFalse(logger.exception.called)
     self.assertEqual(self.test_return_value, test_url)
 def test_creator_not_enrolled_for_bulk_created_course(
     self, enroll_creator_in_new_course, logger, get_course_data, get_canvas_course_url
 ):
     """
     Test that the the creator/initiator does not get enrolled in a course when the course is part of bulk job
     """
     test_url = "test_url/"
     get_canvas_course_url.return_value = test_url
     self.test_return_value = finalize_new_canvas_course(
         self.canvas_course_id, self.sis_course_id, self.user_id, self.bulk_job_id
     )
     self.assertFalse(enroll_creator_in_new_course.called)
Example #5
0
 def test_creator_not_enrolled_for_bulk_created_course(
         self, enroll_creator_in_new_course, logger, get_course_data,
         get_canvas_course_url):
     """
     Test that the the creator/initiator does not get enrolled in a course when the course is part of bulk job
     """
     test_url = 'test_url/'
     get_canvas_course_url.return_value = test_url
     self.test_return_value = finalize_new_canvas_course(
         self.canvas_course_id, self.sis_course_id, self.user_id,
         self.bulk_job_id)
     self.assertFalse(enroll_creator_in_new_course.called)
 def test_course_url_failure(self, enroll_creator_in_new_course, logger, get_course_data, get_canvas_course_url):
     """
     If unable to get a Canvas course URL for the new course, we should be logging an exception and exiting the
     finalization process without a return value and before proceeding with other steps.
     """
     get_canvas_course_url.side_effect = Exception("Mock exception")
     with self.assertRaises(RenderableException):
         self.test_return_value = finalize_new_canvas_course(
             self.canvas_course_id, self.sis_course_id, self.user_id, None
         )
     get_canvas_course_url.assert_called_once_with(canvas_course_id=self.canvas_course_id)
     self.assertFalse(get_course_data().set_sync_to_canvas().called)
     self.assertTrue(logger.exception.called)
     self.assertIsNone(self.test_return_value)
 def test_sync_failure(self, enroll_creator_in_new_course, logger, get_course_data, get_canvas_course_url):
     """
     If unable to set the sync enrollment to Canvas flag for the course we should be logging an exception and exiting
     the finalization process without a return value and before proceeding with other steps.
     """
     get_course_data().set_sync_to_canvas.side_effect = Exception("Mock exception")
     with self.assertRaises(RenderableException):
         self.test_return_value = finalize_new_canvas_course(
             self.canvas_course_id, self.sis_course_id, self.user_id, None
         )
     get_course_data().set_sync_to_canvas.assert_called_with(SISCourseData.TURN_ON_SYNC_TO_CANVAS)
     self.assertFalse(get_canvas_course_url.called)
     self.assertTrue(logger.exception.called)
     self.assertIsNone(self.test_return_value)
 def test_course_data_failure(self, enroll_creator_in_new_course, logger, get_course_data, get_canvas_course_url):
     """
     If unable to get SIS course data for the course (which we need to mark the Canvas course as official and
     sync enrollment to Canvas), we should be logging an exception and exiting the
     finalization process without a return value and before proceeding with other steps.
     """
     get_course_data.side_effect = ObjectDoesNotExist("Mock exception")
     with self.assertRaises(RenderableException):
         self.test_return_value = finalize_new_canvas_course(
             self.canvas_course_id, self.sis_course_id, self.user_id, None
         )
     enroll_creator_in_new_course.assert_called_once_with(self.sis_course_id, self.user_id)
     get_course_data.assert_called_once_with(self.sis_course_id)
     self.assertTrue(logger.exception.called)
     self.assertIsNone(self.test_return_value)
Example #9
0
 def test_course_url_failure(self, enroll_creator_in_new_course, logger,
                             get_course_data, get_canvas_course_url):
     """
     If unable to get a Canvas course URL for the new course, we should be logging an exception and exiting the
     finalization process without a return value and before proceeding with other steps.
     """
     get_canvas_course_url.side_effect = Exception('Mock exception')
     with self.assertRaises(RenderableException):
         self.test_return_value = finalize_new_canvas_course(
             self.canvas_course_id, self.sis_course_id, self.user_id, None)
     get_canvas_course_url.assert_called_once_with(
         canvas_course_id=self.canvas_course_id)
     self.assertFalse(get_course_data().set_sync_to_canvas().called)
     self.assertTrue(logger.exception.called)
     self.assertIsNone(self.test_return_value)
Example #10
0
 def test_enrollment_failure(self, enroll_creator_in_new_course, logger,
                             get_course_data, get_canvas_course_url):
     """
     If the automatic enrollment of the course creator fails, we should be logging an exception and exiting the
     finalization process without a return value and before proceeding with other steps.
     """
     enroll_creator_in_new_course.side_effect = Exception('Mock exception')
     with self.assertRaises(RenderableException):
         self.test_return_value = finalize_new_canvas_course(
             self.canvas_course_id, self.sis_course_id, self.user_id)
     enroll_creator_in_new_course.assert_called_once_with(
         self.sis_course_id, self.user_id)
     self.assertFalse(get_course_data.called)
     self.assertTrue(logger.exception.called)
     self.assertIsNone(self.test_return_value)
Example #11
0
 def test_sync_failure(self, enroll_creator_in_new_course, logger,
                       get_course_data, get_canvas_course_url):
     """
     If unable to set the sync enrollment to Canvas flag for the course we should be logging an exception and exiting
     the finalization process without a return value and before proceeding with other steps.
     """
     get_course_data().set_sync_to_canvas.side_effect = Exception(
         'Mock exception')
     with self.assertRaises(RenderableException):
         self.test_return_value = finalize_new_canvas_course(
             self.canvas_course_id, self.sis_course_id, self.user_id, None)
     get_course_data().set_sync_to_canvas.assert_called_with(
         SISCourseData.TURN_ON_SYNC_TO_CANVAS)
     self.assertFalse(get_canvas_course_url.called)
     self.assertTrue(logger.exception.called)
     self.assertIsNone(self.test_return_value)
Example #12
0
 def test_course_data_failure(self, enroll_creator_in_new_course, logger,
                              get_course_data, get_canvas_course_url):
     """
     If unable to get SIS course data for the course (which we need to mark the Canvas course as official and
     sync enrollment to Canvas), we should be logging an exception and exiting the
     finalization process without a return value and before proceeding with other steps.
     """
     get_course_data.side_effect = ObjectDoesNotExist('Mock exception')
     with self.assertRaises(RenderableException):
         self.test_return_value = finalize_new_canvas_course(
             self.canvas_course_id, self.sis_course_id, self.user_id, None)
     enroll_creator_in_new_course.assert_called_once_with(
         self.sis_course_id, self.user_id)
     get_course_data.assert_called_once_with(self.sis_course_id)
     self.assertTrue(logger.exception.called)
     self.assertIsNone(self.test_return_value)
    def handle(self, **options):
        """
        select all the active job in the CanvasCourseGenerationJob table and check
        the status using the canvas_sdk.progress method
        """

        # open and lock the file used for determining if another process is running
        _pid_file = getattr(settings, 'PROCESS_ASYNC_JOBS_PID_FILE',
                            'process_async_jobs.pid')
        _pid_file_handle = open(_pid_file, 'w')
        try:
            fcntl.lockf(_pid_file_handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
        except IOError as e:
            # another instance is running
            logger.warning(
                f"another instance of the command is already running: {e}")
            return

        start_time = datetime.now()

        jobs = CanvasCourseGenerationJob.objects.filter(
            Q(workflow_state=CanvasCourseGenerationJob.STATUS_QUEUED)
            | Q(workflow_state=CanvasCourseGenerationJob.STATUS_RUNNING)
            | Q(workflow_state=CanvasCourseGenerationJob.
                STATUS_PENDING_FINALIZE))

        for job in jobs:
            try:
                """
                TODO - it turns out we only really need the job_id of the content migration
                no the whole url since we are using the canvas_sdk to check the value. We should
                update this in the database and the setting method. In the meantime just parse out
                the job_id from the url.
                """

                job_start_message = '\nProcessing course with sis_course_id %s' % (
                    job.sis_course_id)
                logger.info(job_start_message)
                user_profile = None

                # Check if the job is flagged for migration or is running the migration
                workflow_state = job.workflow_state

                if workflow_state in (
                        CanvasCourseGenerationJob.STATUS_QUEUED,
                        CanvasCourseGenerationJob.STATUS_RUNNING):
                    response = client.get(SDK_CONTEXT, job.status_url)
                    progress_response = response.json()
                    workflow_state = progress_response['workflow_state']

                    if workflow_state == CanvasCourseGenerationJob.STATUS_COMPLETED:
                        logger.info(
                            'content migration complete for course with sis_course_id %s'
                            % job.sis_course_id)
                        # Update the Job table with the completed state immediately to indicate that the template
                        # migration was successful
                        job.workflow_state = CanvasCourseGenerationJob.STATUS_COMPLETED
                        job.save(update_fields=['workflow_state'])

                if workflow_state in (
                        CanvasCourseGenerationJob.STATUS_COMPLETED,
                        CanvasCourseGenerationJob.STATUS_PENDING_FINALIZE):

                    logger.debug(
                        'Workflow state updated, starting finalization process...'
                    )
                    try:
                        update_syllabus_body(job)
                        canvas_course_url = finalize_new_canvas_course(
                            job.canvas_course_id, job.sis_course_id,
                            'sis_user_id:%s' % job.created_by_user_id,
                            job.bulk_job_id)
                    except Exception:
                        # Catch exceptions from finalize method to set the workflow_state to STATUS_FINALIZE_FAILED
                        # and then re raise it so that generic tasks like tech logger, email generation will continue
                        # to be handled in the larger try block
                        logger.exception(
                            'Exception during finalize method, '
                            'setting state to STATUS_FINALIZE_FAILED '
                            'for sis_course_id id %s' % job.sis_course_id)
                        job.workflow_state = CanvasCourseGenerationJob.STATUS_FINALIZE_FAILED
                        job.save(update_fields=['workflow_state'])

                        raise

                    # Update the Job table with the STATUS_FINALIZED state if finalize is successful
                    job.workflow_state = CanvasCourseGenerationJob.STATUS_FINALIZED
                    job.save(update_fields=['workflow_state'])

                    # if this is not a bulk_job then proceed with email generation to user
                    if not job.bulk_job_id:
                        # Once finalized successfully, only the initiator needs to be emailed
                        user_profile = get_canvas_user_profile(
                            job.created_by_user_id)
                        to_address = [user_profile['primary_email']]
                        success_msg = settings.CANVAS_EMAIL_NOTIFICATION[
                            'course_migration_success_body']
                        logger.debug(
                            "notifying success via email: to_addr=%s and adding course url =%s"
                            % (to_address, canvas_course_url))

                        # add the course url to the  message
                        complete_msg = success_msg.format(canvas_course_url)
                        send_email_helper(
                            settings.CANVAS_EMAIL_NOTIFICATION[
                                'course_migration_success_subject'],
                            complete_msg, to_address)

                elif workflow_state == CanvasCourseGenerationJob.STATUS_FAILED:
                    error_text = 'Content migration failed for course with sis_course_id %s (HUID:%s)' \
                                 % (job.sis_course_id, job.created_by_user_id)
                    logger.info(error_text)
                    tech_logger.error(error_text)

                    # Update the Job table with the new state
                    job.workflow_state = CanvasCourseGenerationJob.STATUS_FAILED
                    job.save(update_fields=['workflow_state'])

                    if not job.bulk_job_id:
                        # send email to notify of failure if it's not a bulk fed course
                        user_profile = get_canvas_user_profile(
                            job.created_by_user_id)
                        send_failure_email(user_profile['primary_email'],
                                           job.sis_course_id)

                else:
                    """
                    if the workflow_state is 'queued' or 'running' the job
                    is not complete and a failure has not occured on Canvas.
                    log that we checked
                    Note: we won't need to update the DB as we will record only the completin or failure in the job table
                    """
                    message = 'content migration state is %s for course with sis_course_id %s' % (
                        workflow_state, job.sis_course_id)
                    logger.info(message)

            except Exception as e:
                error_text = "There was a problem in processing the job for canvas course sis_course_id %s (HUID:%s)" \
                             % (job.sis_course_id, job.created_by_user_id)
                # Note: equivalent to .error(error_text, exc_info=1) -- logs at ERROR level
                logger.exception(error_text)

                # Use the friendly display_text for the subject of the tech_logger email if it's available
                if isinstance(e, RenderableException):
                    error_text = '%s (HUID:%s)' % (e.display_text,
                                                   job.created_by_user_id)
                tech_logger.exception(error_text)

                # send email if it's not a bulk created course
                if not job.bulk_job_id:
                    try:
                        # if failure happened before user profile was fetched, get the user profile
                        # to retrieve email, else reuse the user_profile info
                        if not user_profile:
                            user_profile = get_canvas_user_profile(
                                job.created_by_user_id)

                        send_failure_email(user_profile['primary_email'],
                                           job.sis_course_id)
                    except Exception:
                        # If exception occurs while sending failure email, log it
                        error_text = "There was a problem in sending the failure notification email to initiator " \
                                     "and support staff for sis_course_id %s (HUID:%s)" \
                                     % (job.sis_course_id, job.created_by_user_id)
                        logger.exception(error_text)
                        tech_logger.exception(error_text)

        logger.info('command took %s seconds to run',
                    str(datetime.now() - start_time))

        # unlock and close the file used for determining if another process is running
        try:
            fcntl.lockf(_pid_file_handle, fcntl.LOCK_UN)
            _pid_file_handle.close()
        except IOError:
            logger.error(
                "could not release lock on pid file or close pid file properly"
            )