Exemplo n.º 1
0
def test_queryparams():
    q = QueryParams([("a", "123"), ("a", "456"), ("b", "789")])
    assert "a" in q
    assert "A" not in q
    assert "c" not in q
    assert q["a"] == "123"
    assert q.get("a") == "123"
    assert q.get("nope", default=None) is None
    assert q.getlist("a") == ["123", "456"]
    assert q.keys() == ["a", "a", "b"]
    assert q.values() == ["123", "456", "789"]
    assert q.items() == [("a", "123"), ("a", "456"), ("b", "789")]
    assert list(q) == [("a", "123"), ("a", "456"), ("b", "789")]
    assert dict(q) == {"a": "123", "b": "789"}
    assert repr(q) == "QueryParams([('a', '123'), ('a', '456'), ('b', '789')])"
    assert QueryParams({
        "a": "123",
        "b": "456"
    }) == QueryParams([("a", "123"), ("b", "456")])
    assert QueryParams({"a": "123", "b": "456"}) == {"b": "456", "a": "123"}
    assert QueryParams({
        "a": "123",
        "b": "456"
    }) == [("b", "456"), ("a", "123")]
    assert {"b": "456", "a": "123"} == QueryParams({"a": "123", "b": "456"})
    assert [("b", "456"), ("a", "123")] == QueryParams({
        "a": "123",
        "b": "456"
    })
    assert QueryParams() == {}
Exemplo n.º 2
0
def test_queryparams():
    q = QueryParams("a=123&a=456&b=789")
    assert "a" in q
    assert "A" not in q
    assert "c" not in q
    assert q["a"] == "456"
    assert q.get("a") == "456"
    assert q.get("nope", default=None) is None
    assert q.getlist("a") == ["123", "456"]
    assert list(q.keys()) == ["a", "b"]
    assert list(q.values()) == ["456", "789"]
    assert list(q.items()) == [("a", "456"), ("b", "789")]
    assert len(q) == 2
    assert list(q) == ["a", "b"]
    assert dict(q) == {"a": "456", "b": "789"}
    assert str(q) == "a=123&a=456&b=789"
    assert repr(q) == "QueryParams('a=123&a=456&b=789')"
    assert QueryParams({"a": "123", "b": "456"}) == QueryParams(
        [("a", "123"), ("b", "456")]
    )
    assert QueryParams({"a": "123", "b": "456"}) == QueryParams("a=123&b=456")
    assert QueryParams({"a": "123", "b": "456"}) == QueryParams(
        {"b": "456", "a": "123"}
    )
    assert QueryParams() == QueryParams({})
    assert QueryParams([("a", "123"), ("a", "456")]) == QueryParams("a=123&a=456")
    assert QueryParams({"a": "123", "b": "456"}) != "invalid"

    q = QueryParams([("a", "123"), ("a", "456")])
    assert QueryParams(q) == q
Exemplo n.º 3
0
    async def _get_response(self, request: Request, data: QueryParams) -> Response:
        try:
            query = data["query"]
        except KeyError:
            return PlainTextResponse("No GraphQL query found in the request", 400)

        config = get_graphql_config(request)
        background = BackgroundTasks()
        context = {"req": request, "background": background, **config.context}

        engine: Engine = config.engine
        result: dict = await engine.execute(
            query,
            context=context,
            variables=data.get("variables"),
            operation_name=data.get("operationName"),
        )

        content = {"data": result["data"]}
        has_errors = "errors" in result
        if has_errors:
            content["errors"] = format_errors(result["errors"])
        status = 400 if has_errors else 200

        return JSONResponse(content=content, status_code=status, background=background)
Exemplo n.º 4
0
def test_url_blank_params():
    q = QueryParams("a=123&abc&def&b=456")
    assert "a" in q
    assert "abc" in q
    assert "def" in q
    assert "b" in q
    assert len(q.get("abc")) == 0
    assert len(q["a"]) == 3
    assert list(q.keys()) == ["a", "abc", "def", "b"]
Exemplo n.º 5
0
def get_page_number(url: URL) -> int:
    """
    Return a page number specified in the URL query parameters.
    """
    query_params = QueryParams(url.query)
    try:
        return int(query_params.get("page", default="1"))
    except (TypeError, ValueError):
        return 1
Exemplo n.º 6
0
def get_ordering(url: URL, columns: typing.Dict[str, str]) -> typing.Optional[str]:
    """
    Determine a column ordering based on the URL query string.
    """
    query_params = QueryParams(url.query)
    order_by = query_params.get("order")
    if order_by is not None and order_by.lstrip("-") not in columns:
        return None
    return order_by
Exemplo n.º 7
0
def get_ordering(
    url: URL,
    columns: typing.Dict[str,
                         str]) -> typing.Tuple[typing.Optional[str], bool]:
    """
    Determine a column ordering based on the URL query string.
    Returned as a tuple of (ordering, is_reverse).
    """
    query_params = QueryParams(url.query)

    ordering = query_params.get("order", default="")
    order_column = ordering.lstrip("-")
    order_reverse = ordering.startswith("-")
    if order_column not in columns:
        return None, False
    return order_column, order_reverse
Exemplo n.º 8
0
def list_resources(query_params: QueryParams = None):
    def _validate_query_params(query_params):
        errors = []
        stripped_keys = []

        for key in query_params:
            negated = key[0] == "-"
            stripped_key = key[1:] if negated else key

            if stripped_key not in settings.QUERY_PARAMS:
                errors.append({"param": key, "msg": "Invalid query-param"})
                continue  # no further tests needed

            if negated and not settings.QUERY_PARAMS[stripped_key].get(
                "negatable"
            ):
                errors.append({"param": key, "msg": "Param not negatable"})

            # Check if non-repeatable stripped_key already exists
            # eg. when using "-usability" and "usability"
            if stripped_key in stripped_keys and not settings.QUERY_PARAMS[
                stripped_key
            ].get("repeatable"):
                errors.append({"param": key, "msg": "Param not repeatable"})

            stripped_keys.append(stripped_key)

        # When all stripped_keys are iterated, test for series without collections.
        if "series" in stripped_keys and "collection" not in stripped_keys:
            errors.append(
                {
                    "param": "series",
                    "msg": "'Series'-key requires a 'collection'-key",
                }
            )

        return {"errors": errors}

    def _urlencode(params=None, remove=None, insert=None):

        temp_params = params[:] if params else []
        if insert:
            temp_params.append(insert)
        if remove and temp_params and (remove in temp_params):
            temp_params.remove(remove)
        return urlencode(temp_params)

    def _generate_filters(filters, params):
        # Adds links and creator-bools to filters
        out = []

        for f in filters:
            el = {}
            key = f.get("key")
            value = f.get("value")
            negated = f.get("negated")

            # get label if unresolved
            if f.get("unresolved"):
                r = get_entity_labels([(key, value)])
                if not r.get("errors"):
                    el["label"] = r["data"][0].get("label")

            # View-link
            # 'label' indicates an id-based filter which can be viewed
            if el.get("label"):
                if key == "collection":
                    el["view_link"] = "/".join(["collections", value])
                else:
                    el["view_link"] = "/".join([key, value])

            # Remove_link
            # If positive collection, also remove series
            # negative collection-params works like normal param
            if key == "collection" and not negated:
                new_params = [
                    (k, v)
                    for k, v in params
                    if k not in ["collection", "series", "start"]
                ]
                el["remove_link"] = urlencode(new_params)
            else:
                new_params = [(k, v) for k, v in params if k not in ["start"]]
                org_key = "-" + key if negated else key
                el["remove_link"] = _urlencode(
                    new_params, remove=(org_key, value)
                )

            # Inverse_link
            # If negated, replace with positive, vice versa
            # exception: if positive collection, remove series-param, as
            # it follows the collection
            if negated:
                new_params = [(k, v) for k, v in params if k not in ["start"]]
                el["invert_link"] = _urlencode(
                    new_params, remove=("-" + key, value), insert=(key, value)
                )
            else:
                if key == "collection":
                    new_params = [
                        (k, v)
                        for k, v in params
                        if k not in ["collection", "series", "start"]
                    ]
                    el["invert_link"] = _urlencode(
                        new_params, insert=("-" + key, value)
                    )
                else:
                    new_params = [
                        (k, v) for k, v in params if k not in ["start"]
                    ]
                    el["invert_link"] = _urlencode(
                        new_params,
                        insert=("-" + key, value),
                        remove=(key, value),
                    )
            # Creator and collector links and bools
            if key in ["people", "organisations"]:
                r = get_resource(key, int(value))
                if not r.get("errors"):
                    if r["data"].get("is_creative_creator"):
                        el["creator_link"] = "creators=" + value
                        el["creator"] = True
                    if r["data"].get("is_creator"):
                        el["creator_link"] = "collectors=" + value
                        el["collector"] = True

            el["negated"] = negated
            el["key"] = key
            el["value"] = value
            out.append(el)

        return out

    def _generate_facets(facets, params=None):
        # TODO: Does not work when excisting negative filter is set
        # and you click a positive facet: '-usability=4' is set, you click 'usability=2'
        result = {}
        params = [
            x for x in params if x[0] != "start"
        ]  # remove 'start'-param from facet-links
        for facet in facets:
            out = {}
            for b in facets[facet].get("buckets"):
                active = (facet, b.get("value"))  # tuple
                if params and (active in params):  # if
                    stripped_params = [x for x in params if x != active]
                    b["remove_link"] = urlencode(stripped_params)
                elif params:
                    b["add_link"] = urlencode(params + [active])
                else:
                    b["add_link"] = urlencode([active])
                out[b.get("value")] = b
            result[facet] = out
        return result

    def _generate_total_facets(aws_facets: Dict, params: List = None) -> Dict:
        # TODO: Does not work when excisting negative filter is set
        # and you click a positive facet: '-usability=4' is set, you click 'usability=2'

        def _recurse(
            facet_key: str,
            total_facet_values: List,
            active_facet_values: Dict,
            params: List = None,
        ) -> List:
            # total_facet_values is the "content"-key from each facet in settings.FACETS
            # active_facet_values is a dict of facet-values and facetinfo, e.g.
            # {"1": {"value": "1", "count": 63, "link": "add_or_remove_link_url"}}
            out = []
            for facet in total_facet_values:
                id_ = facet.get("id")
                label = facet.get("label")

                if id_ in active_facet_values:
                    current_tuple = (facet_key, id_)
                    el = {
                        "label": label,
                        "id": id_,
                        "count": active_facet_values[id_].get("count"),
                    }

                    if params and (
                        current_tuple in params
                    ):  # if the current tuple is in the params
                        stripped_params = [
                            x for x in params if x != current_tuple
                        ]
                        el["remove_link"] = urlencode(stripped_params)
                    elif params:
                        el["add_link"] = urlencode(params + [current_tuple])
                    else:
                        el["add_link"] = urlencode([current_tuple])

                    if facet.get("children"):
                        el["children"] = _recurse(
                            facet_key,
                            facet.get("children"),
                            active_facet_values,
                            params,
                        )
                    out.append(el)
            return out
            # return list of dicts with label, link and children

        # restructure aws-facets
        active_facets = {}
        for facet in aws_facets:
            active_facets[facet] = {
                b.get("value"): b for b in aws_facets[facet].get("buckets")
            }

        # remove 'start'-param from facet-links, retain all else
        params = [x for x in params if x[0] != "start"]
        result = {}

        for k, v in settings.FACETS.items():
            # label = facet.get("label")
            # recursively merge labels and links from total_facets and active_facets
            result[k] = _recurse(
                k, v.get("content"), active_facets.get(k), params
            )

        collection_tuples = [
            ("collection", key) for key in active_facets["collection"].keys()
        ]
        # result["collection_tuples"] = collection_tuples
        collection_labels = get_entity_labels(collection_tuples)
        # result["collection_labels"] = collection_labels

        result["collection"] = _recurse(
            "collection",
            collection_labels.get("data"),
            active_facets.get("collection"),
            params,
        )
        result["collection_labels"] = collection_labels
        return result

    def _generate_collection_labels(col_labels):
        return {d_.get("id"): d_.get("label") for d_ in col_labels.get("data")}

    def _generate_content_labels(type_list):
        return {d_.get("id"): d_.get("label") for d_ in type_list}

    def _generate_views(params, view):
        output = []
        views = [
            {
                "label": "Listevisning",
                "value": "list",
                "icon": "fas fa-list",  # 'view_list'
            },
            {
                "label": "Galleri-visning",
                "value": "gallery",
                "icon": "fas fa-th",  # 'view_module'
            },
        ]

        if params:
            stripped_params = [(t[0], t[1]) for t in params if t[0] != "view"]
        else:
            stripped_params = []

        for option in views:
            current = {}
            current["label"] = option.get("label")
            current["icon"] = option.get("icon")
            if option.get("value") == view:
                current["selected"] = True
            else:
                current["link"] = urlencode(
                    stripped_params + [("view", option.get("value"))]
                )
            output.append(current)
        return output

    def _generate_sorts(params, sort, direction):
        sorts = [
            {
                "label": "Ældste dato først",
                "sort": "date_from",
                "direction": "asc",
            },
            {
                "label": "Nyeste dato først",
                "sort": "date_to",
                "direction": "desc",
            },
            {"label": "Relevans", "sort": "_score", "direction": "desc"},
        ]
        output = []

        if params:
            stripped_params = [
                (t[0], t[1])
                for t in params
                if t[0] not in ["sort", "direction", "start"]
            ]
        else:
            stripped_params = []

        for option in sorts:
            current = {}
            current["icon"] = option.get("icon")
            current["label"] = option.get("label")
            if (
                option.get("sort") == sort
                and option.get("direction") == direction
            ):
                current["selected"] = True
            else:
                current["link"] = urlencode(
                    stripped_params
                    + [
                        ("sort", option.get("sort")),
                        ("direction", option.get("direction")),
                    ]
                )
            output.append(current)
        return output

    def _generate_sizes(params, size):
        sizes = [20, 50, 100]
        output = []

        if params:
            stripped_params = [(t[0], t[1]) for t in params if t[0] != "size"]
        else:
            stripped_params = []

        for option in sizes:
            current = {}
            current["label"] = option
            if option == size:
                current["selected"] = True
            else:
                current["link"] = urlencode(
                    stripped_params + [("size", option)]
                )
            output.append(current)
        return output

    # Validate params
    if query_params:
        validated_request = _validate_query_params(query_params)
        if validated_request.get("errors"):
            return validated_request

    # Make api-call
    api_resp = searchInterface.search_records(query_params)

    # If api-error
    if api_resp.get("errors"):
        return api_resp

    # If SAM-request, no need for further processing
    # api_resp on request with "view=ids"-param includes three keys: status_code, result, next_cursor (optional)
    if "ids" in query_params.getlist("view"):
        return api_resp

    # Else process and convert response
    # api_resp on normal request includes: sort, direction, size, date_from,
    # date_to, _query_string, total, start, server_facets, filters, query, result,
    # view_list, sort_list, size_list, view, non_query_params
    resp = {}

    # convert multidict to list of tuples
    # params = [tup for tup in query_params.items(multi=True)]  # Flask-syntax
    params = [tup for tup in query_params.multi_items()]  # starlette-syntax
    # Keys used for generating searchviews and facets
    resp["params"] = params
    resp["query"] = api_resp.get("query", None)

    # If filters, generate links and possibly labels
    if api_resp.get("filters"):
        resp["filters"] = _generate_filters(api_resp["filters"], params)

    # if facets, generate links
    if api_resp.get("facets"):
        resp["facets"] = _generate_total_facets(api_resp["facets"], params)
        # Generate collection-labels
        col_labels = resp["facets"].pop("collection_labels")
        if col_labels:
            resp["collection_labels"] = _generate_collection_labels(col_labels)

        # Generate content_types-labels
        # content_labels = resp["facets"].pop("content_labels")
        # if content_labels:
        resp["content_labels"] = _generate_content_labels(
            resp["facets"].get("content_types")
        )

    # 'non_query_params' is used to generate a remove_link for the q-param
    # on the zero-hits page
    if not api_resp.get("result") and api_resp.get("query"):
        other_params = [i for i in params if i != ("q", api_resp.get("query"))]
        resp["non_query_params"] = urlencode(other_params)

    # Pagination
    if api_resp.get("result"):
        total = api_resp["total"]
        start = api_resp["start"]
        size = api_resp["size"]
        rm_tup = ("start", str(start))
        if start > 0:
            resp["first"] = _urlencode(params, remove=rm_tup)
            resp["prev"] = _urlencode(
                params, remove=rm_tup, insert=("start", start - size)
            )

        if total <= 10000 and (start + size < total):
            last_start = total // size * size
            if last_start == total:
                last_start = total - size
            resp["last"] = _urlencode(
                params, remove=rm_tup, insert=("start", last_start)
            )

        if (start + size < total) and (start + size <= 10000):
            resp["next"] = _urlencode(
                params, remove=rm_tup, insert=("start", start + size)
            )

    # Proces size, sort, direction and view
    resp["size_list"] = _generate_sizes(params, api_resp["size"])
    resp["sort_list"] = _generate_sorts(
        params, api_resp["sort"], api_resp["direction"]
    )
    resp["view_list"] = _generate_views(
        params, query_params.get("view", "list")
    )
    resp["view"] = query_params.get("view", "list")
    resp["total"] = api_resp.get("total")
    resp["start"] = api_resp.get("start")
    resp["size"] = api_resp.get("size")
    resp["sort"] = api_resp.get("sort")
    resp["date_from"] = api_resp.get("date_from")
    resp["date_to"] = api_resp.get("date_to")
    resp["result"] = api_resp.get("result")
    # resp["api_response"] = api_resp.get("api_response")
    return resp
Exemplo n.º 9
0
    def prepare_fhir_scopes(
        scope: Scope,
        headers: Headers,
        params: QueryParams,
        errors: typing.List[typing.Any],
    ):
        # Generate Request ID
        scope["FHIR_REQUEST_ID"] = uuid.uuid4()

        # 1. Prepare Accept & FHIR Version
        # --------------------------------
        accept = headers.get("accept", None)
        if accept:
            parts = accept.split(";")
            accept = parts[0].strip()
            if accept in ("application/json", "text/json"):
                accept = "application/fhir+json"
            if accept in ALLOWED_ACCEPTS:
                scope["FHIR_RESPONSE_ACCEPT"] = accept
            else:
                errors.append({
                    "loc": "Header.Accept",
                    "msg": f"Accept mime '{accept}' is not supported.",
                    "original": headers.get("accept"),
                })
            if len(parts) > 1:
                version_str = None
                try:
                    name, version_str = parts[1].strip().split("=")
                    if name == "fhirVersion":
                        version = MIME_FHIR_VERSION_MAP[version_str]
                        scope["FHIR_VERSION"] = version
                        scope["FHIR_VERSION_ORIGINAL"] = version_str
                    else:
                        errors.append({
                            "loc": "Header.Accept",
                            "msg":
                            "Invalid format of FHIR Version is provided in mime",
                            "original": headers.get("accept"),
                        })
                except KeyError:
                    errors.append({
                        "loc": "Header.Accept",
                        "msg":
                        f"Unsupported FHIR Version '{version_str}' is provided in mime",
                        "original": headers.get("accept"),
                    })
                except ValueError:
                    errors.append({
                        "loc": "Header.Accept",
                        "msg":
                        "Invalid format of FHIR Version is provided in mime",
                        "original": headers.get("accept"),
                    })
        else:
            scope["FHIR_RESPONSE_ACCEPT"] = "application/fhir+json"

        if (scope.get("FHIR_VERSION_ORIGINAL", None) is None
                and scope.get("FHIR_VERSION", None) is None):
            scope["FHIR_VERSION"] = MIME_FHIR_VERSION_MAP[DEFAULT_FHIR_VERSION]

        # 2. Check Query String
        # ---------------------
        format_mime = params.get("_format", None)
        if format_mime is not None:
            if format_mime in ALLOWED_ACCEPTS:
                scope["FHIR_RESPONSE_FORMAT"] = format_mime
            else:
                errors.append({
                    "loc": "QueryString._format",
                    "msg": f"Format mime '{format_mime}' is not supported.",
                    "original": format_mime,
                })
        pretty_response = params.get("_pretty", None)
        if pretty_response is not None:
            if pretty_response in ("true", "false"):
                scope["FHIR_RESPONSE_PRETTY"] = pretty_response == "true"

            else:
                errors.append({
                    "loc": "QueryString._pretty",
                    "msg":
                    f"Invalid ``_pretty`` value '{pretty_response}' is provided.",
                    "original": pretty_response,
                })

        # 3. Prepare Conditional Headers
        # ------------------------------
        if headers.get("If-None-Exist"):
            scope["FHIR_CONDITION_NONE_EXIST"] = [
                tuple(
                    map(lambda x: x.strip(),
                        headers.get("If-None-Exist").split("=")))
            ]

        if headers.get("If-Modified-Since"):
            try:
                scope["FHIR_CONDITION_MODIFIED_SINCE"] = parsedate_to_datetime(
                    headers.get("If-Modified-Since"))
            except ValueError:
                errors.append({
                    "loc": "Header.If-Modified-Since",
                    "msg": "Invalid formatted datetime value is provided.",
                    "original": headers.get("If-Modified-Since"),
                })
        if headers.get("If-None-Match"):
            try:
                scope["FHIR_CONDITION_NONE_MATCH"] = literal_eval(
                    headers.get("If-None-Match").replace("W/", ""))
            except (SyntaxError, ValueError):
                errors.append({
                    "loc": "Header.If-None-Match",
                    "msg": "Invalid formatted ETag value is provided.",
                    "original": headers.get("If-None-Match"),
                })

        if headers.get("If-Match"):
            try:
                scope["FHIR_CONDITION_MATCH"] = literal_eval(
                    headers.get("If-Match").replace("W/", ""))
            except (SyntaxError, ValueError):
                errors.append({
                    "loc": "Header.If-Match",
                    "msg": "Invalid formatted ETag value is provided.",
                    "original": headers.get("If-Match"),
                })