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)
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)
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)
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_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)
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)
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" )