def _send_notification(job):
    """
    helper function to encapsulate the process of sending a report via email to the user who created the bulk job
    :param job: a BulkJob
    :return: True if notification was successfully sent (passed to the Django framework, anyway);
             False if no notification was sent
    """
    notification_to_address_list = []
    canvas_user_profile = None

    logger.debug("Looking up notification email recipient address list...")

    try:
        canvas_user_profile = get_canvas_user_profile(job.created_by_user_id)
        notification_to_address_list = [canvas_user_profile['primary_email']]
    except Exception:
        # todo: do we need all these multilayered logs?
        error_text = (
            "Job %s: problem getting canvas user profile for user %s" %
            (job.id, job.created_by_user_id))
        logger.exception(error_text)
        _log_notification_failure(job)
        return False

    logger.debug("Building notification email...")

    completed_subjobs = job.get_completed_subjobs_count()
    failed_subjobs = job.get_failed_subjobs_count()

    try:
        term = Term.objects.get(term_id=int(job.sis_term_id))
        term_display_name = term.display_name
        school = School.objects.get(school_id=job.school_id)
        school_display_name = school.title_short
    except Exception:
        error_text = ("Canvas course create bulk job %s: "
                      "problem getting user-friendly term or school name")
        logger.exception(error_text)
        term_display_name = job.sis_term_id
        school_display_name = job.school_id

    subject = _format_notification_email_subject(school_display_name,
                                                 term_display_name)
    body = _format_notification_email_body(school_display_name,
                                           term_display_name,
                                           completed_subjobs, failed_subjobs)

    logger.debug("Sending notification email to %s...",
                 notification_to_address_list)

    try:
        send_email_helper(subject, body, notification_to_address_list)
    except Exception:
        # todo: do we need all these multilayered logs?
        logger.exception("Job %s: problem sending notification", job.id)
        _log_notification_failure(job)
        return False

    logger.debug("Notification email sent!")
    return True
 def test_get_canvas_user_profile_method_called_with_right_params(self, SDK_CONTEXT, get_user_profile):
     """
     Test get_user_profile is called with expected args
     """
     get_user_profile.return_value = DEFAULT
     result = get_canvas_user_profile(self.user_id)
     get_user_profile.assert_called_with(request_ctx=SDK_CONTEXT, user_id="sis_user_id:%s" % self.user_id)
 def test_get_canvas_user_profile_method_called_with_right_params(
         self, SDK_CONTEXT, get_user_profile):
     """
     Test get_user_profile is called with expected args
     """
     get_user_profile.return_value = DEFAULT
     result = get_canvas_user_profile(self.user_id)
     get_user_profile.assert_called_with(request_ctx=SDK_CONTEXT,
                                         user_id='sis_user_id:%s' %
                                         self.user_id)
    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"
            )
def _send_notification(job):
    """
    helper function to encapsulate the process of sending a report via email to the user who created the bulk job
    :param job: a BulkJob
    :return: True if notification was successfully sent (passed to the Django framework, anyway);
             False if no notification was sent
    """
    notification_to_address_list = []
    canvas_user_profile = None

    logger.debug("Looking up notification email recipient address list...")

    try:
        canvas_user_profile = get_canvas_user_profile(job.created_by_user_id)
        notification_to_address_list = [canvas_user_profile['primary_email']]
    except Exception as e:
        # todo: do we need all these multilayered logs?
        error_text = (
            "Job %s: problem getting canvas user profile for user %s" % (job.id, job.created_by_user_id)
        )
        logger.exception(error_text)
        _log_notification_failure(job)
        return False

    logger.debug("Building notification email...")

    completed_subjobs = job.get_completed_subjobs_count()
    failed_subjobs = job.get_failed_subjobs_count()

    try:
        term = Term.objects.get(term_id=int(job.sis_term_id))
        term_display_name = term.display_name
        school = School.objects.get(school_id=job.school_id)
        school_display_name = school.title_short
    except Exception as e:
        error_text = (
            "Canvas course create bulk job %s: "
            "problem getting user-friendly term or school name"
        )
        logger.exception(error_text)
        term_display_name = job.sis_term_id
        school_display_name = job.school_id

    subject = _format_notification_email_subject(
        school_display_name,
        term_display_name
    )
    body = _format_notification_email_body(
        school_display_name,
        term_display_name,
        completed_subjobs,
        failed_subjobs
    )

    logger.debug("Sending notification email to %s...", notification_to_address_list)

    try:
        send_email_helper(subject, body, notification_to_address_list)
    except Exception as e:
        # todo: do we need all these multilayered logs?
        logger.exception("Job %s: problem sending notification", job.id)
        _log_notification_failure(job)
        return False

    logger.debug("Notification email sent!")
    return True