Skip to content
Merged
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
5 changes: 1 addition & 4 deletions .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
extension-pkg-whitelist=

# Specify a score threshold to be exceeded before program exits with error.
fail-under=10.0
fail-under=10

# Add files or directories to the blacklist. They should be base names, not
# paths.
Expand Down Expand Up @@ -589,6 +589,3 @@ valid-metaclass-classmethod-first-arg=cls
# "BaseException, Exception".
overgeneral-exceptions=BaseException,
Exception

[MESSAGES CONTROL]
disable = C0330, C0326
5 changes: 3 additions & 2 deletions gramps_webapi/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from .resources.citation import CitationResource, CitationsResource
from .resources.event import EventResource, EventsResource
from .resources.family import FamiliesResource, FamilyResource
from .resources.filters import FilterResource
from .resources.filters import FilterResource, FiltersResource
from .resources.media import MediaObjectResource, MediaObjectsResource
from .resources.metadata import MetadataResource
from .resources.name_groups import NameGroupsResource
Expand Down Expand Up @@ -80,7 +80,8 @@ def register_endpt(resource: Type[Resource], url: str, name: str):
register_endpt(BookmarkResource, "/bookmarks/<string:namespace>", "bookmark")
register_endpt(BookmarksResource, "/bookmarks/", "bookmarks")
# Filter
register_endpt(FilterResource, "/filters/<string:namespace>", "filter")
register_endpt(FilterResource, "/filters/<string:namespace>/<string:name>", "filter")
register_endpt(FiltersResource, "/filters/<string:namespace>", "filters")
# Translate
register_endpt(TranslationResource, "/translations/<string:isocode>", "translation")
register_endpt(TranslationsResource, "/translations/", "translations")
Expand Down
78 changes: 54 additions & 24 deletions gramps_webapi/api/resources/filters.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
"""Gramps filter interface."""

import json
from typing import Dict, List, Set
from typing import Any, Dict, List, Set

import gramps.gen.filters as filters
from flask import Response, abort
from gramps.gen.db.base import DbReadBase
from gramps.gen.filters import GenericFilter
from marshmallow import Schema, ValidationError, fields
from webargs import validate
from marshmallow import Schema
from webargs import ValidationError, fields, validate
from webargs.flaskparser import use_args

from ...const import GRAMPS_NAMESPACES
Expand All @@ -29,13 +29,14 @@
}


def get_filter_rules(args: Dict[str, str], namespace: str) -> List[Dict]:
def get_filter_rules(args: Dict[str, Any], namespace: str) -> List[Dict]:
"""Return a list of available filter rules for a namespace."""
rule_list = []
for rule_class in _RULES_LOOKUP[namespace]:
add_rule = True
if args.get("rule") and args["rule"] != rule_class.__name__:
add_rule = False
if "rules" in args and args["rules"]:
if rule_class.__name__ not in args["rules"]:
add_rule = False
if add_rule:
rule_list.append(
{
Expand All @@ -46,17 +47,20 @@ def get_filter_rules(args: Dict[str, str], namespace: str) -> List[Dict]:
"rule": rule_class.__name__,
}
)
if "rules" in args and len(args["rules"]) != len(rule_list):
abort(404)
return rule_list


def get_custom_filters(args: Dict[str, str], namespace: str) -> List[Dict]:
def get_custom_filters(args: Dict[str, Any], namespace: str) -> List[Dict]:
"""Return a list of custom filters for a namespace."""
filter_list = []
filters.reload_custom_filters()
for filter_class in filters.CustomFilters.get_filters(namespace):
add_filter = True
if args.get("filter") and args["filter"] != filter_class.get_name():
add_filter = False
if "filters" in args and args["filters"]:
if filter_class.get_name() not in args["filters"]:
add_filter = False
if add_filter:
filter_list.append(
{
Expand All @@ -74,6 +78,8 @@ def get_custom_filters(args: Dict[str, str], namespace: str) -> List[Dict]:
],
}
)
if "filters" in args and len(args["filters"]) != len(filter_list):
abort(404)
Copy link
Member

Choose a reason for hiding this comment

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

Why 404 and not 400, isn't this an invalid request?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

My thinking is that it did not find all the filters in the list because one was missing, and we would provide 404 for a missing object. I did the same for rules just above and do the same below in build filter.

Copy link
Member

Choose a reason for hiding this comment

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

Ok.

return filter_list


Expand Down Expand Up @@ -113,7 +119,7 @@ def apply_filter(db_handle: DbReadBase, args: Dict, namespace: str) -> List[Hand
for filter_class in filters.CustomFilters.get_filters(namespace):
if args["filter"] == filter_class.get_name():
return filter_class.apply(db_handle)
abort(400)
abort(404)

try:
filter_parms = FilterSchema().load(json.loads(args["rules"]))
Expand Down Expand Up @@ -143,21 +149,31 @@ class FilterSchema(Schema):
validate=validate.OneOf(["and", "or", "xor", "one"]),
)
invert = fields.Boolean(required=False, missing=False)
rules = fields.List(fields.Nested(RuleSchema))
rules = fields.List(
fields.Nested(RuleSchema), required=True, validate=validate.Length(min=1)
)


class CustomFilterSchema(FilterSchema):
"""Structure for a custom filter."""

name = fields.Str(required=True, validate=validate.Length(min=1))
comment = fields.Str(required=False)
rules = fields.List(
fields.Nested(RuleSchema), required=True, validate=validate.Length(min=1)
)


class FilterResource(ProtectedResource, GrampsJSONEncoder):
"""Filter resource."""
class FiltersResource(ProtectedResource, GrampsJSONEncoder):
"""Filters resource."""

@use_args(
{"filter": fields.Str(), "rule": fields.Str()},
{
"filters": fields.DelimitedList(
fields.Str(validate=validate.Length(min=1))
),
"rules": fields.DelimitedList(fields.Str(validate=validate.Length(min=1))),
},
location="query",
)
def get(self, args: Dict[str, str], namespace: str) -> Response:
Expand All @@ -168,10 +184,10 @@ def get(self, args: Dict[str, str], namespace: str) -> Response:
abort(404)

rule_list = get_filter_rules(args, namespace)
if args.get("rule") and not args.get("filter"):
if "rules" in args and "filters" not in args:
return self.response(200, {"rules": rule_list})
filter_list = get_custom_filters(args, namespace)
if args.get("filter") and not args.get("rule"):
if "filters" in args and "rules" not in args:
return self.response(200, {"filters": filter_list})
return self.response(200, {"filters": filter_list, "rules": rule_list})

Expand Down Expand Up @@ -212,14 +228,30 @@ def put(self, args: Dict, namespace: str) -> Response:
)
return abort(404)


class FilterResource(ProtectedResource, GrampsJSONEncoder):
"""Filter resource."""

def get(self, namespace: str, name: str) -> Response:
"""Get a custom filter."""
try:
namespace = GRAMPS_NAMESPACES[namespace]
except KeyError:
abort(404)

args = {"filters": [name]}
filter_list = get_custom_filters(args, namespace)
if len(filter_list) == 0:
abort(404)
return self.response(200, filter_list[0])

@use_args(
{
"name": fields.Str(required=True),
"force": fields.Boolean(required=False, missing=False),
"force": fields.Str(validate=validate.Length(equal=0)),
},
location="json",
location="query",
)
def delete(self, args: Dict, namespace: str) -> Response:
def delete(self, args: Dict, namespace: str, name: str) -> Response:
"""Delete a custom filter."""
try:
namespace = GRAMPS_NAMESPACES[namespace]
Expand All @@ -229,17 +261,15 @@ def delete(self, args: Dict, namespace: str) -> Response:
filters.reload_custom_filters()
custom_filters = filters.CustomFilters.get_filters(namespace)
for custom_filter in custom_filters:
if args["name"] == custom_filter.get_name():
if name == custom_filter.get_name():
filter_set = set()
self._find_dependent_filters(namespace, custom_filter, filter_set)
if len(filter_set) > 1:
if "force" not in args or not args["force"]:
abort(405)
list(map(custom_filters.remove, filter_set))
filters.CustomFilters.save()
return self.response(
200, {"message": "Deleted filter: " + args["name"]}
)
return self.response(200, {"message": "Deleted filter: " + name})
return abort(404)

def _find_dependent_filters(
Expand Down
71 changes: 43 additions & 28 deletions gramps_webapi/data/apispec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1825,16 +1825,16 @@ paths:
required: true
type: string
description: "The namespace or category for the filters."
- name: rule
- name: rules
in: query
required: false
type: string
description: "The name of a specific filter rule to be returned."
- name: filter
description: "A comma delimited list of specific filter rules to be returned."
- name: filters
in: query
required: false
type: string
description: "The name of a specific custom filter to be returned."
description: "A comma delimited list of specific custom filters to be returned."
responses:
200:
description: "OK: Successful operation."
Expand Down Expand Up @@ -1908,10 +1908,39 @@ paths:
403:
description: "Forbidden: Bad credentials, authentication failed."

/filters/{namespace}/{name}:
get:
tags:
- filters
summary: "Get a custom filter for a given namespace or category."
operationId: getFilter
security:
- Bearer: []
parameters:
- name: namespace
in: path
required: true
type: string
description: "The namespace or category for the custom filter."
- name: name
in: path
required: true
type: string
description: "The name of a custom filter."
responses:
200:
description: "OK: Successful operation."
schema:
$ref: "#/definitions/CustomFilter"
401:
description: "Unauthorized: Missing authorization header."
404:
description: "Not Found: Namespace or filter not found."

delete:
tags:
- filters
summary: "Delete a custom filter."
summary: "Delete a custom filter in a given namespace or category."
operationId: deleteFilter
security:
- Bearer: []
Expand All @@ -1921,12 +1950,16 @@ paths:
required: true
type: string
description: "The namespace or category for the custom filter."
- name: filter
in: body
- name: name
in: path
required: true
description: "The custom filter to delete."
schema:
$ref: "#/definitions/CustomFilterObject"
type: string
description: "The name of the custom filter."
- name: force
in: query
required: false
type: string
description: "Force delete custom filter and all filters that depend upon it."
responses:
200:
description: "OK: Successful operation."
Expand Down Expand Up @@ -4045,24 +4078,6 @@ definitions:
items:
$ref: "#/definitions/FilterRule"

##############################################################################
# Model - CustomFilterObject
##############################################################################

CustomFilterObject:
type: object
required:
- name
properties:
name:
type: string
description: "Name of the custom rule to delete."
example: "MyTestRule"
force:
type: boolean
description: "Indicator to approve deletion of all dependent filters if any exist."
example: true

##############################################################################
# Model - Translations
##############################################################################
Expand Down
3 changes: 1 addition & 2 deletions tests/test_endpoints/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@
import gramps_webapi.app
from gramps_webapi.app import create_app
from gramps_webapi.const import ENV_CONFIG_FILE, TEST_EXAMPLE_GRAMPS_CONFIG

from .. import TEST_GRAMPSHOME, ExampleDbSQLite
from tests import TEST_GRAMPSHOME, ExampleDbSQLite


def get_object_count(gramps_object):
Expand Down
Loading