def run_queue(request):
    """
    Render status of queue
    """
    # Get all runs that should be shown
    queued_status = Status.get_queued()
    processing_status = Status.get_processing()
    pending_jobs = ReductionRun.objects.filter(
        Q(status=queued_status)
        | Q(status=processing_status)).order_by('created')
    # Filter those which the user shouldn't be able to see
    if USER_ACCESS_CHECKS and not request.user.is_superuser:
        try:
            with ICATCache(AUTH='uows',
                           SESSION={'sessionid':
                                    request.session['sessionid']}) as icat:
                pending_jobs = filter(
                    lambda job: job.experiment.reference_number in icat.
                    get_associated_experiments(int(request.user.username)),
                    pending_jobs)  # check RB numbers
                pending_jobs = filter(
                    lambda job: job.instrument.name in icat.
                    get_owned_instruments(int(request.user.username)),
                    pending_jobs)  # check instrument
        except ICATConnectionException as excep:
            return render_error(request, str(excep))
    # Initialise list to contain the names of user/team that started runs
    started_by = []
    # cycle through all filtered runs and retrieve the name of the user/team that started the run
    for run in pending_jobs:
        started_by.append(started_by_id_to_name(run.started_by))
    # zip the run information with the user/team name to enable simultaneous iteration with django
    context_dictionary = {'queue': zip(pending_jobs, started_by)}
    return context_dictionary
def submit_and_wait_for_result(test, expected_runs=1):
    """
    Submit after a reset button has been clicked. Then waits until the queue listener has finished processing.

    Sticks the submission in a loop in case the first time doesn't work. The reason
    it may not work is that resetting actually swaps out the whole form using JS, which
    replaces ALL the elements and triggers a bunch of DOM re-renders/updates, and that isn't fast.
    """
    test.listener._processing = True  # pylint:disable=protected-access
    expected_url = reverse("run_confirmation",
                           kwargs={"instrument": test.instrument_name})

    def submit_successful(driver) -> bool:
        try:
            test.page.submit_button.click()
        except ElementClickInterceptedException:
            pass
        # the submit is successful if the URL has changed
        return expected_url in driver.current_url

    WebDriverWait(test.driver, 30).until(submit_successful)
    if expected_runs == 1:
        WebDriverWait(
            test.driver,
            30).until(lambda _: not test.listener.is_processing_message())
    else:
        num_current_runs = ReductionRun.objects.filter(
            status=Status.get_completed()).count()
        WebDriverWait(test.driver, 30).until(
            lambda _: ReductionRun.objects.filter(status=Status.get_completed(
            )).count() == num_current_runs + expected_runs)

    return find_run_in_database(test)
def fail_queue(request):
    """
    Render status of failed queue
    """
    # render the page
    error_status = Status.get_error()
    failed_jobs = ReductionRun.objects.filter(
        Q(status=error_status)
        & Q(hidden_in_failviewer=False)).order_by('-created')
    context_dictionary = {
        'queue': failed_jobs,
        'status_success': Status.get_completed(),
        'status_failed': Status.get_error()
    }

    if request.method == 'POST':
        # perform the specified action
        action = request.POST.get("action", "default")
        selected_run_string = request.POST.get("selectedRuns", [])
        selected_runs = json.loads(selected_run_string)
        try:
            for run in selected_runs:
                run_number = int(run[0])
                run_version = int(run[1])

                reduction_run = failed_jobs.get(run_number=run_number,
                                                run_version=run_version)

                if action == "hide":
                    reduction_run.hidden_in_failviewer = True
                    reduction_run.save()

                elif action == "rerun":
                    highest_version = max([
                        int(runL[1]) for runL in selected_runs
                        if int(runL[0]) == run_number
                    ])
                    if run_version != highest_version:
                        continue  # do not run multiples of the same run

                    ReductionRunUtils.send_retry_message_same_args(
                        request.user.id, reduction_run)

                elif action == "default":
                    pass

        # pylint:disable=broad-except
        except Exception as exception:
            fail_str = "Selected action failed: %s %s" % (
                type(exception).__name__, exception)
            LOGGER.info("Failed to carry out fail_queue action - %s", fail_str)
            context_dictionary["message"] = fail_str

    return context_dictionary
Exemplo n.º 4
0
 def reduction_started(self, reduction_run: ReductionRun, message: Message):
     """
     Update the run as 'started' / 'processing' in the database. This is
     called when the run is ready to start.
     """
     self._logger.info("Run %s has started reduction", message.run_number)
     reduction_run.status = Status.get_processing()
     reduction_run.started = timezone.now()
     reduction_run.save()
def submit_runs(request, instrument=None):
    """
    Handles run submission request
    """
    LOGGER.info('Submitting runs')
    # pylint:disable=no-member
    instrument = Instrument.objects.prefetch_related('reduction_runs').get(
        name=instrument)
    if request.method == 'GET':
        processing_status = Status.get_processing()
        queued_status = Status.get_queued()

        # pylint:disable=no-member
        runs_for_instrument = instrument.reduction_runs.all()
        last_run = instrument.get_last_for_rerun(runs_for_instrument)

        kwargs = ReductionRunUtils.make_kwargs_from_runvariables(last_run)
        standard_vars = kwargs["standard_vars"]
        advanced_vars = kwargs["advanced_vars"]

        try:
            default_variables = VariableUtils.get_default_variables(instrument)
        except (FileNotFoundError, ImportError, SyntaxError) as err:
            return {"message": str(err)}

        final_standard = _combine_dicts(standard_vars,
                                        default_variables["standard_vars"])
        final_advanced = _combine_dicts(advanced_vars,
                                        default_variables["advanced_vars"])

        # pylint:disable=no-member
        context_dictionary = {
            'instrument': instrument,
            'last_instrument_run': last_run,
            'processing': runs_for_instrument.filter(status=processing_status),
            'queued': runs_for_instrument.filter(status=queued_status),
            'standard_variables': final_standard,
            'advanced_variables': final_advanced,
        }

        return context_dictionary
Exemplo n.º 6
0
    def do_create_reduction_record(message: Message, experiment: Experiment, instrument: Instrument, run_version: int,
                                   software: Software):
        """Create the reduction record."""
        # Make the new reduction run with the information collected so far
        reduction_run, message = records.create_reduction_run_record(experiment=experiment,
                                                                     instrument=instrument,
                                                                     message=message,
                                                                     run_version=run_version,
                                                                     software=software,
                                                                     status=Status.get_queued())

        return reduction_run, message, instrument, software
Exemplo n.º 7
0
    def reduction_error(self, reduction_run: ReductionRun, message: Message):
        """
        Update the run as 'errored' in the database. This is called when the run
        encounters an error.
        """
        if message.message:
            self._logger.info("Run %s has encountered an error - %s", message.run_number, message.message)
        else:
            self._logger.info("Run %s has encountered an error - No error message was found", message.run_number)

        self._common_reduction_run_update(reduction_run, Status.get_error(), message)
        reduction_run.save()
Exemplo n.º 8
0
    def reduction_skipped(self, reduction_run: ReductionRun, message: Message):
        """
        Update the run status to 'skipped' in the database. This is called when
        there was a reason to skip the run. Will NOT attempt re-run.
        """
        if message.message is not None:
            self._logger.info("Run %s has been skipped - %s", message.run_number, message.message)
        else:
            self._logger.info("Run %s has been skipped - No error message was found", message.run_number)

        self._common_reduction_run_update(reduction_run, Status.get_skipped(), message)
        reduction_run.save()
Exemplo n.º 9
0
    def reduction_complete(self, reduction_run: ReductionRun, message: Message):
        """
        Update the run as 'completed' in the database. This is called when the
        run has completed.
        """
        self._logger.info("Run %s has completed reduction", message.run_number)
        self._common_reduction_run_update(reduction_run, Status.get_completed(), message)
        reduction_run.save()

        if message.reduction_data is not None:
            reduction_location = ReductionLocation(file_path=message.reduction_data, reduction_run=reduction_run)
            reduction_location.save()
Exemplo n.º 10
0
    def test_retrieve_status(self):
        """Test that retrieving the status returns the expected one"""

        assert len(Status._cached_statuses.values()) == 0

        assert Status.get_error() is not None
        assert str(Status.get_error()) == "Error"
        assert len(Status._cached_statuses.values()) == 1

        assert Status.get_completed() is not None
        assert str(Status.get_completed()) == "Completed"
        assert len(Status._cached_statuses.values()) == 2

        assert Status.get_processing() is not None
        assert str(Status.get_processing()) == "Processing"
        assert len(Status._cached_statuses.values()) == 3

        assert Status.get_queued() is not None
        assert str(Status.get_queued()) == "Queued"
        assert len(Status._cached_statuses.values()) == 4

        assert Status.get_skipped() is not None
        assert str(Status.get_skipped()) == "Skipped"
        assert len(Status._cached_statuses.values()) == 5
def find_reason_to_avoid_re_run(matching_previous_runs, run_number):
    """
    Check whether the most recent run exists
    """
    most_recent_run = matching_previous_runs.first()

    # Check old run exists - if it doesn't exist there's nothing to re-run!
    if most_recent_run is None:
        return False, f"Run number {run_number} hasn't been ran by autoreduction yet."

    # Prevent multiple queueings of the same re-run
    queued_runs = matching_previous_runs.filter(
        status=Status.get_queued()).first()
    if queued_runs is not None:
        return False, f"Run number {queued_runs.run_number} is already queued to run"

    return True, ""
def runs_list(request, instrument=None):
    """
    Render instrument summary
    """
    try:
        filter_by = request.GET.get('filter', 'run')
        instrument_obj = Instrument.objects.get(name=instrument)
    except Instrument.DoesNotExist:
        return {'message': "Instrument not found."}

    try:
        sort_by = request.GET.get('sort', 'run')
        if sort_by == 'run':
            runs = (ReductionRun.objects.only(
                'status', 'last_updated', 'run_number', 'run_version',
                'run_description').select_related('status').filter(
                    instrument=instrument_obj).order_by(
                        '-run_number', 'run_version'))
        else:
            runs = (ReductionRun.objects.only(
                'status', 'last_updated', 'run_number', 'run_version',
                'run_description').select_related('status').filter(
                    instrument=instrument_obj).order_by('-last_updated'))

        if len(runs) == 0:
            return {'message': "No runs found for instrument."}

        try:
            current_variables = VariableUtils.get_default_variables(
                instrument_obj.name)
            error_reason = ""
        except FileNotFoundError:
            current_variables = {}
            error_reason = "reduce_vars.py is missing for this instrument"
        except (ImportError, SyntaxError):
            current_variables = {}
            error_reason = "reduce_vars.py has an import or syntax error"

        has_variables = bool(current_variables)

        context_dictionary = {
            'instrument': instrument_obj,
            'instrument_name': instrument_obj.name,
            'runs': runs,
            'last_instrument_run': runs[0],
            'processing': runs.filter(status=Status.get_processing()),
            'queued': runs.filter(status=Status.get_queued()),
            'filtering': filter_by,
            'sort': sort_by,
            'has_variables': has_variables,
            'error_reason': error_reason
        }

        if filter_by == 'experiment':
            experiments_and_runs = {}
            experiments = Experiment.objects.filter(reduction_runs__instrument=instrument_obj). \
                order_by('-reference_number').distinct()
            for experiment in experiments:
                associated_runs = runs.filter(experiment=experiment). \
                    order_by('-created')
                experiments_and_runs[experiment] = associated_runs
            context_dictionary['experiments'] = experiments_and_runs
        else:
            max_items_per_page = request.GET.get('pagination', 10)
            custom_paginator = CustomPaginator(
                page_type=sort_by,
                query_set=runs,
                items_per_page=max_items_per_page,
                page_tolerance=3,
                current_page=request.GET.get('page', 1))
            context_dictionary['paginator'] = custom_paginator
            context_dictionary['last_page_index'] = len(
                custom_paginator.page_list)
            context_dictionary['max_items'] = max_items_per_page

    # pylint:disable=broad-except
    except Exception:
        LOGGER.error(traceback.format_exc())
        return {
            'message':
            "An unexpected error has occurred when loading the instrument."
        }

    return context_dictionary
def run_confirmation(request, instrument: str):
    """
    Handles request for user to confirm re-run
    """
    range_string = request.POST.get('run_range')
    run_description = request.POST.get('run_description')

    # pylint:disable=no-member
    queue_count = ReductionRun.objects.filter(
        instrument__name=instrument, status=Status.get_queued()).count()
    context_dictionary = {
        # list stores (run_number, run_version)
        'runs': [],
        'variables': None,
        'queued': queue_count,
        'instrument_name': instrument,
        'run_description': run_description
    }

    try:
        run_numbers = input_processing.parse_user_run_numbers(range_string)
    except SyntaxError as exception:
        context_dictionary['error'] = exception.msg
        return context_dictionary

    if not run_numbers:
        context_dictionary[
            'error'] = f"Could not correctly parse range input {range_string}"
        return context_dictionary

    # Determine user level to set a maximum limit to the number of runs that can be re-queued
    if request.user.is_superuser:
        max_runs = 500
    elif request.user.is_staff:
        max_runs = 50
    else:
        max_runs = 20

    if len(run_numbers) > max_runs:
        context_dictionary["error"] = "{0} runs were requested, but only {1} runs can be " \
                                      "queued at a time".format(len(run_numbers), max_runs)
        return context_dictionary

    related_runs: QuerySet[ReductionRun] = ReductionRun.objects.filter(
        instrument__name=instrument, run_number__in=run_numbers)
    # Check that RB numbers are the same for the range entered
    # pylint:disable=no-member
    rb_number = related_runs.values_list('experiment__reference_number',
                                         flat=True).distinct()
    if len(rb_number) > 1:
        context_dictionary['error'] = 'Runs span multiple experiment numbers ' \
                                      '(' + ','.join(str(i) for i in rb_number) + ')' \
                                      ' please select a different range.'
        return context_dictionary

    try:
        script_text = InstrumentVariablesUtils.get_current_script_text(
            instrument)
        default_variables = VariableUtils.get_default_variables(instrument)
    except (FileNotFoundError, ImportError, SyntaxError) as err:
        context_dictionary['error'] = err
        return context_dictionary

    try:
        new_script_arguments = make_reduction_arguments(
            request.POST.items(), default_variables)
        context_dictionary['variables'] = new_script_arguments
    except ValueError as err:
        context_dictionary['error'] = err
        return context_dictionary

    for run_number in run_numbers:
        matching_previous_runs = related_runs.filter(
            run_number=run_number).order_by('-run_version')
        run_suitable, reason = find_reason_to_avoid_re_run(
            matching_previous_runs, run_number)
        if not run_suitable:
            context_dictionary['error'] = reason
            break

        # run_description gets stored in run_description in the ReductionRun object
        max_run_description_length = ReductionRun._meta.get_field(
            'run_description').max_length
        if len(run_description) > max_run_description_length:
            context_dictionary["error"] = "The description contains {0} characters, " \
                                          "a maximum of {1} are allowed".\
                format(len(run_description), max_run_description_length)
            return context_dictionary

        most_recent_run: ReductionRun = matching_previous_runs.first()
        # User can choose whether to overwrite with the re-run or create new data
        ReductionRunUtils.send_retry_message(request.user.id, most_recent_run,
                                             run_description, script_text,
                                             new_script_arguments, False)
        # list stores (run_number, run_version)
        context_dictionary["runs"].append(
            (run_number, most_recent_run.run_version + 1))

    return context_dictionary
def configure_new_runs_get(instrument_name,
                           start=0,
                           end=0,
                           experiment_reference=0):
    """
    GET for the configure new runs page
    """
    instrument = Instrument.objects.get(name__iexact=instrument_name)

    editing = (start > 0 or experiment_reference > 0)

    last_run = instrument.get_last_for_rerun()

    run_variables = ReductionRunUtils.make_kwargs_from_runvariables(last_run)
    standard_vars = run_variables["standard_vars"]
    advanced_vars = run_variables["advanced_vars"]

    # if a specific start is provided, include vars upcoming for the specific start
    filter_kwargs = {"start_run__gte": start if start else last_run.run_number}
    if end:
        # if an end run is provided - don't show variables outside the [start-end) range
        filter_kwargs["start_run__lt"] = end

    upcoming_variables = instrument.instrumentvariable_set.filter(
        **filter_kwargs)
    if experiment_reference:
        upcoming_experiment_variables = instrument.instrumentvariable_set.filter(
            experiment_reference=experiment_reference)
    else:
        upcoming_experiment_variables = []

    # Updates the variables values. Experiment variables are chained second
    # so they values will overwrite any changes from the run variables
    for upcoming_var in chain(upcoming_variables,
                              upcoming_experiment_variables):
        name = upcoming_var.name
        if name in standard_vars or not upcoming_var.is_advanced:
            standard_vars[name] = upcoming_var
        elif name in advanced_vars or upcoming_var.is_advanced:
            advanced_vars[name] = upcoming_var

    # Unique, comma-joined list of all start runs belonging to the upcoming variables.
    # This seems to be used to prevent submission if trying to resubmit variables for already
    # configured future run numbers - check the checkForConflicts function
    # This should probably be done by the POST method anyway.. so remove it when
    if upcoming_variables:
        upcoming_run_variables = ','.join(
            {str(var.start_run)
             for var in upcoming_variables})
    else:
        upcoming_run_variables = ""

    try:
        reduce_vars_variables = VariableUtils.get_default_variables(instrument)
    except (FileNotFoundError, ImportError, SyntaxError) as err:
        return {"message": str(err)}

    final_standard = _combine_dicts(standard_vars,
                                    reduce_vars_variables["standard_vars"])
    final_advanced = _combine_dicts(advanced_vars,
                                    reduce_vars_variables["advanced_vars"])
    run_start = start if start else last_run.run_number + 1

    context_dictionary = {
        'instrument':
        instrument,
        'last_instrument_run':
        last_run,
        'processing':
        ReductionRun.objects.filter(instrument=instrument,
                                    status=Status.get_processing()),
        'queued':
        ReductionRun.objects.filter(instrument=instrument,
                                    status=Status.get_queued()),
        'standard_variables':
        final_standard,
        'advanced_variables':
        final_advanced,
        'run_start':
        run_start,
        # used to determine whether the current form is for an experiment reference
        'current_experiment_reference':
        experiment_reference,
        # used to create the link to an experiment reference form, using this number
        'submit_for_experiment_reference':
        last_run.experiment.reference_number,
        'minimum_run_start':
        run_start,
        'minimum_run_end':
        run_start + 1,
        'upcoming_run_variables':
        upcoming_run_variables,
        'editing':
        editing,
        'tracks_script':
        '',
    }

    return context_dictionary