Skip to content
This repository was archived by the owner on Nov 14, 2022. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,31 +1,25 @@
# ############################################################################### #
# ############################################################################ #
# Autoreduction Repository : https://github.com/autoreduction/autoreduce
#
# Copyright © 2019 ISIS Rutherford Appleton Laboratory UKRI
# Copyright © 2021 ISIS Rutherford Appleton Laboratory UKRI
# SPDX - License - Identifier: GPL-3.0-or-later
# ############################################################################### #
"""
Handles colouring table rows
"""
# ############################################################################ #
# pylint:disable=invalid-name
"""Handles colouring table rows."""
from django.template import Library

# pylint:disable=invalid-name
register = Library()

_STATUSES = {
"Error": "danger",
"Processing": "warning",
"Queued": "info",
"Completed": "success",
"Skipped": "dark",
}


@register.simple_tag
def colour_table_row(status):
"""
Switch statement for defining table colouring
"""
if status == 'Error':
return 'danger'
if status == 'Processing':
return 'warning'
if status == 'Queued':
return 'info'
if status == 'Completed':
return 'success'
if status == 'Skipped':
return 'dark'
return status
"""Switch statement for defining table colouring."""
return _STATUSES.get(status, status)
5 changes: 2 additions & 3 deletions autoreduce_frontend/autoreduce_webapp/views.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# ############################################################################### #
# Autoreduction Repository : https://github.com/autoreduction/autoreduce
#
# Copyright © 2019 ISIS Rutherford Appleton Laboratory UKRI
# Copyright © 2021 ISIS Rutherford Appleton Laboratory UKRI
# SPDX - License - Identifier: GPL-3.0-or-later
# ############################################################################### #
# ############################################################################ #
"""Handle page responses for the web app."""
# pylint: disable=unused-argument,bare-except,no-member
from django.http import HttpRequest
Expand All @@ -18,7 +18,6 @@ def render_error(request: HttpRequest, message: str):

Args:
request: The original sent request.

message: The message that will be displayed.

Return:
Expand Down
51 changes: 24 additions & 27 deletions autoreduce_frontend/reduction_viewer/view_utils.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# ############################################################################### #
# ############################################################################ #
# Autoreduction Repository : https://github.com/autoreduction/autoreduce
#
# Copyright © 2021 ISIS Rutherford Appleton Laboratory UKRI
# SPDX - License - Identifier: GPL-3.0-or-later
# ############################################################################### #
# ############################################################################ #
"""Utility functions for the view of django models."""
# pylint:disable=no-member
import functools
Expand All @@ -14,10 +14,11 @@
from django.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist
from django.utils.http import url_has_allowed_host_and_scheme

from autoreduce_db.reduction_viewer.models import Instrument, ReductionRun
from autoreduce_qp.queue_processor.reduction.service import ReductionScript
from autoreduce_frontend.autoreduce_webapp.settings import DATA_ANALYSIS_BASE_URL
from autoreduce_frontend.autoreduce_webapp.settings import (ALLOWED_HOSTS, UOWS_LOGIN_URL)
from autoreduce_frontend.autoreduce_webapp.settings import ALLOWED_HOSTS, UOWS_LOGIN_URL
from autoreduce_frontend.autoreduce_webapp.templatetags.colour_table_row import colour_table_row

LOGGER = logging.getLogger(__package__)
Expand Down Expand Up @@ -59,25 +60,34 @@ def get_interactive_plot_data(plot_locations):

def make_data_analysis_url(reduction_location: str) -> str:
"""
Makes a URL for the data.analysis website that will open the location of the
Make a URL for the data.analysis website that will open the location of the
data.
"""
if "/instrument/" in reduction_location:
return DATA_ANALYSIS_BASE_URL + reduction_location.split("/instrument/")[1]

return ""


def windows_to_linux_path(path: str) -> str:
"""Convert Windows path to Linux path."""
# '\\isis\inst$\' maps to '/isis/'
r"""
Convert Windows path to Linux path.

Note:
'\\isis\inst$\' maps to '/isis/'
"""
path = path.replace(r'\\isis\inst$' + '\\', '/isis/')
path = path.replace('\\', '/')
return path


def linux_to_windows_path(path: str) -> str:
"""Convert Linux path to Windows path."""
# '\\isis\inst$\' maps to '/isis/'
r"""
Convert Linux path to Windows path.

Note:
'\\isis\inst$\' maps to '/isis/'
"""
path = path.replace('/isis/', r'\\isis\inst$' + '\\')
path = path.replace('/', '\\')
return path
Expand All @@ -92,13 +102,8 @@ def started_by_id_to_name(started_by_id=None):
if not started by a user.

Returns:
If started by a valid user, return '[forename] [surname]'.

If started automatically, return 'Autoreducton service'.

If started manually, return 'Development team'.

Otherwise, return None.
A string of the name of the user or team that submitted an Autoreduction
run.
"""
if started_by_id is None or started_by_id < -1:
return None
Expand Down Expand Up @@ -136,9 +141,7 @@ def make_return_url(request, next_url):


def order_runs(sort_by: str, runs: ReductionRun.objects):
"""
Sort a queryset of runs based on the passed GET sort_by param
"""
"""Sort a queryset of runs based on the passed GET sort_by param."""
if sort_by == "-run_number":
runs = runs.order_by('-run_numbers__run_number', '-run_version')
elif sort_by == "run_number":
Expand All @@ -153,15 +156,9 @@ def order_runs(sort_by: str, runs: ReductionRun.objects):
return runs


# pylint:disable=no-method-argument
def data_status(status):
"""Function to add text-(status) class to status column for formatting

Returns:
"text- " concatonated with status fetched from record for formatting with colour_table_row

"""
return "text-" + colour_table_row(status) + " run-status"
def data_status(status: str) -> str:
"""Add text-(status) class to status column for formatting."""
return f"text-{colour_table_row(status)} run-status"


def get_navigation_runs(instrument_name: str, run: ReductionRun, page_type: str) -> Tuple[ReductionRun]:
Expand Down
109 changes: 78 additions & 31 deletions autoreduce_frontend/reduction_viewer/views/common.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
# ############################################################################ #
# Autoreduction Repository :
# https://github.com/ISISScientificComputing/autoreduce
#
# Copyright &copy; 2021 ISIS Rutherford Appleton Laboratory UKRI
# SPDX - License - Identifier: GPL-3.0-or-later
# ############################################################################ #
# pylint:disable=too-many-return-statements,broad-except
import base64
import itertools
import json
import os
from typing import Tuple

import requests

from autoreduce_db.reduction_viewer.models import ReductionArguments
from autoreduce_qp.queue_processor.variable_utils import VariableUtils

UNAUTHORIZED_MESSAGE = "User is not authorized to submit batch runs. Please contact the Autoreduce team "\
"at ISISREDUCE@stfc.ac.uk to request the permissions."
# Holds the default value used when there is no value for the variable
# in the default variables dictionary. Stored in a parameter for re-use in tests.
# in the default variables dictionary. Stored in a parameter for re-use in
# tests
DEFAULT_WHEN_NO_VALUE = ""


Expand All @@ -25,9 +38,10 @@ def _combine_dicts(current: dict, default: dict):

final = {}
for name in itertools.chain(current.keys(), default.keys()):
# the default value for argument, also used when the variable is missing from the current variables
# ideally there will always be a default for each variable name, but
# if the variable is missing from the default dictionary, then just default to empty string
# The default value for argument, also used when the variable is missing
# from the current variables ideally there will always be a default for
# each variable name, but if the variable is missing from the default
# dictionary, then just default to empty string
default_value = default.get(name, DEFAULT_WHEN_NO_VALUE)
final[name] = {"current": current.get(name, default_value), "default": default_value}

Expand All @@ -43,7 +57,8 @@ def unpack_arguments(arguments: dict) -> Tuple[dict, dict, dict]:
arguments: The arguments dictionary to unpack.

Returns:
A tuple containing the standard variables, advanced variables, and variable help.
A tuple containing the standard variables, advanced variables, and
variable help.
"""
standard_arguments = arguments.get("standard_vars", {})
advanced_arguments = arguments.get("advanced_vars", {})
Expand All @@ -60,8 +75,8 @@ def get_arguments_from_file(instrument: str) -> Tuple[dict, dict, dict]:

Raises:
FileNotFoundError: If the instrument's reduce_vars file is not found.
ImportError: If the instrument's reduce_vars file contains an import error.
SyntaxError: If the instrument's reduce_vars file contains a syntax error.
ImportError: If the reduce_vars file contains an import error.
SyntaxError: If the reduce_vars file contains a syntax error.
"""
default_variables = VariableUtils.get_default_variables(instrument)
default_standard_variables, default_advanced_variables, variable_help = unpack_arguments(default_variables)
Expand All @@ -70,7 +85,8 @@ def get_arguments_from_file(instrument: str) -> Tuple[dict, dict, dict]:

def prepare_arguments_for_render(arguments: ReductionArguments, instrument: str) -> Tuple[dict, dict, dict]:
"""
Converts the arguments into a dictionary containing their "current" and "default" values.
Converts the arguments into a dictionary containing their "current" and
"default" values.

Used to render the form in the webapp (with values from "current"), and
provide the defaults for resetting (with values from "default").
Expand All @@ -80,9 +96,12 @@ def prepare_arguments_for_render(arguments: ReductionArguments, instrument: str)
instrument: The instrument to get the default variables for.

Returns:
A dictionary containing the arguments and their current and default values.
A dictionary containing the arguments and their current and default
values.
"""
vars_kwargs = arguments.as_dict()
fetch_api_urls(vars_kwargs)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be done after the _combine_dicts calls, and called on each standard_vars and advanced_vars


standard_vars = vars_kwargs.get("standard_vars", {})
advanced_vars = vars_kwargs.get("advanced_vars", {})

Expand All @@ -94,56 +113,83 @@ def prepare_arguments_for_render(arguments: ReductionArguments, instrument: str)
return final_standard, final_advanced, variable_help


def fetch_api_urls(vars_kwargs):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Managed to get this function working with a few changes, I don't have a patch so I'll just paste it below.

I've also removed the second requests.get as we're not displaying the value anywhere. This saves a lot of API requests and we can just link to the github file directly if the user wants to see it!

def fetch_api_urls(vars_kwargs):
    """Convert file URLs in vars_kwargs into API URL strings."""
    for _, heading_value in vars_kwargs.items():
        if isinstance(heading_value["current"], dict) and "url" in heading_value["current"]:
            try:
                heading_value["all_files"] = {}
                base_url, _, path = heading_value["current"]["url"].partition("master")
                # TODO might want to support >1 origin, or allow different syntax
                repo = base_url.replace("https://raw.githubusercontent.com/", "")[:-1]  # :-1 drops the trailing slash
                path = path.lstrip("/")
                url = f"https://api.github.com/repos/{repo}/contents/{path}"
                auth_token = os.environ.get("AUTOREDUCTION_GITHUB_AUTH_TOKEN", None)
                headers = {"Authorization": f"Token {auth_token}"} if auth_token else {}
                req = requests.get(url, headers=headers)
                data = json.loads(req.content)

                for link in data:
                    file_name = link["name"]
                    # if this download_url is None, then it's a directory
                    if link["download_url"]:
                        url, _, default = link["download_url"].rpartition("/")
                        # req = requests.get(f"{url}/{default}", headers=headers)
                        # value = req.text
                        heading_value["all_files"][file_name] = {
                            "url": url,
                            "default": default,
                            # "value": value,
                        }
            except Exception as err:
                logger.error("Failed to fetch file from GitHub: %s\n%s", str(err), traceback.format_exc())

"""Convert file URLs in vars_kwargs into API URL strings."""
for category, headings in vars_kwargs.items():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After calling it on standard_vars and advanced_vars you'll have to remove this level of nesting and the [category] part from code below

for heading, heading_value in headings.items():
if "file" in heading.lower() and isinstance(heading_value, dict):
try:
vars_kwargs[category][heading]["all_files"] = {}
path = heading_value["url"].partition("master")[2]
url = "https://api.github.com/repos/mantidproject/scriptrepository/contents" + path
vars_kwargs[category][heading]["api"] = url
req = requests.get(url)
data = json.loads(req.content)

for link in data:
file_name = link["name"]
url, _, default = link["download_url"].rpartition("/")
auth_token = os.environ.get("AUTOREDUCTION_GITHUB_AUTH_TOKEN", "")
req = requests.get(f"{url}/{default}", headers={"Authorization": f"Token {auth_token}"})
Comment on lines +132 to +133
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't submit the request for me when the token is empty, I had to change it to this to work

auth_token = os.environ.get("AUTOREDUCTION_GITHUB_AUTH_TOKEN", None)
headers = {"Authorization": f"Token {auth_token}"} if auth_token else {}
req = requests.get(f"{url}/{default}", headers=headers)

value = req.text
vars_kwargs[category][heading]["all_files"][file_name] = {
"url": url,
"default": default,
"value": value,
}
except Exception:
pass
Comment on lines +140 to +141
Copy link
Contributor

@dtasev dtasev Dec 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also this should be logged as logger.error (check logger imports in other files), might be also useful to have a traceback, something like

logger.error("Failed to fetch file from GitHub: %s\n%s", str(err), traceback.format_exc())



def decode_b64(value: str):
"""
Decodes the base64 representation back to utf-8 string.
"""
"""Decodes the base64 representation back to utf-8 string."""
return base64.urlsafe_b64decode(value).decode("utf-8")


# pylint:disable=too-many-return-statements
def convert_to_python_type(value: str):
"""
Converts the string sent by the POST request to a real Python type that can be serialized by JSON
Converts the string sent by the POST request to a real Python type that can
be serialized by JSON.

Args:
value: The string value to convert
value: The string value to convert.

Returns:
The converted value
The converted value.
"""
try:
# json can directly load str/int/floats and lists of them
# JSON can directly load str/int/floats and lists of them
return json.loads(value)
except json.JSONDecodeError:
if value.lower() == "none" or value.lower() == "null":
lowered_value = value.lower()
if lowered_value in ("none", "null"):
return None
elif value.lower() == "true":
if lowered_value == "true":
return True
elif value.lower() == "false":
if lowered_value == "false":
return False
elif "," in value and "[" not in value and "]" not in value:
if "," in value and "[" not in value and "]" not in value:
return convert_to_python_type(f"[{value}]")
elif "'" in value:
if "'" in value:
return convert_to_python_type(value.replace("'", '"'))
else:
return value

return value


def make_reduction_arguments(post_arguments: dict, instrument: str) -> dict:
"""
Given new variables from the POST request and the default variables from reduce_vars.py
create a dictionary of the new variables
Given new variables from the POST request and the default variables from
reduce_vars.py create a dictionary of the new variables

Args:
post_arguments: The new variables to be created
default_variables: The default variables
post_arguments: The new variables to be created.
default_variables: The default variables.

Returns:
The new variables as a dict
The new variables as a dict.

Raises:
ValueError if any variable values exceed the allowed maximum
ValueError: If any variable values exceed the allowed maximum.
"""

defaults = VariableUtils.get_default_variables(instrument)
Expand All @@ -161,9 +207,10 @@ def make_reduction_arguments(post_arguments: dict, instrument: str) -> dict:

if name is not None:
name = decode_b64(name)
# skips variables that have been removed from the defaults
# Skips variables that have been removed from the defaults
if name not in defaults[dict_key]:
continue

defaults[dict_key][name] = convert_to_python_type(value)

return defaults
Loading