diff --git a/tests/integration/test_ao_elasticsearch.py b/tests/integration/test_ao_elasticsearch.py index b0e06ee4c..66bf8fd0a 100644 --- a/tests/integration/test_ao_elasticsearch.py +++ b/tests/integration/test_ao_elasticsearch.py @@ -58,8 +58,8 @@ def test_ao_requestor_filter(self): assert all(requestor in doc["requestor_names"] for doc in response) self.check_incorrect_values({"ao_requestor": "Incorrect"}, False) - types = [5, 15] - requstor_type_full = [REQUESTOR_TYPES[5], REQUESTOR_TYPES[15]] + types = ["5", "15"] + requstor_type_full = [REQUESTOR_TYPES["5"], REQUESTOR_TYPES["15"]] response = self._results_ao(api.url_for(UniversalSearch, ao_requestor_type=types)) # logging.info(response) @@ -67,13 +67,13 @@ def test_ao_requestor_filter(self): for requestor in doc["requestor_types"]) for doc in response) - type = 15 + type = "15" response = self._results_ao(api.url_for(UniversalSearch, ao_requestor_type=type)) # logging.info(response) assert all(REQUESTOR_TYPES[type] in doc["requestor_types"] for doc in response) - self.check_incorrect_values({"ao_requestor_type": 22}, True) + self.check_incorrect_values({"ao_requestor_type": "22"}, True) def test_ao_entity_name_filter(self): entity_name = "Francis Beaver" diff --git a/webservices/args.py b/webservices/args.py index 0d6522cdb..8804d66cc 100644 --- a/webservices/args.py +++ b/webservices/args.py @@ -350,8 +350,8 @@ def make_seek_args(field=fields.Int, description=None): 'from_hit': fields.Int(required=False, description=docs.FROM_HIT), 'hits_returned': fields.Int(required=False, description=docs.HITS_RETURNED), 'type': fields.Str( - validate=validate.OneOf(["admin_fines", "adrs", "advisory_opinions", - "murs", "statutes"]), + validate=validate.OneOf(['', 'admin_fines', 'adrs', 'advisory_opinions', + 'murs', 'statutes']), description=docs.LEGAL_DOC_TYPE), 'ao_no': fields.List(IStr, required=False, description=docs.AO_NUMBER), @@ -368,9 +368,10 @@ def make_seek_args(field=fields.Int, description=None): 'ao_is_pending': fields.Bool(description=docs.AO_IS_PENDING), 'ao_status': fields.Str(description=docs.AO_STATUS), 'ao_requestor': fields.Str(description=docs.AO_REQUESTOR), - 'ao_requestor_type': fields.List( - fields.Integer(validate=validate.OneOf(range(1, 17))), - description=docs.AO_REQUESTOR_TYPE), + 'ao_requestor_type': fields.List(IStr( + validate=validate.OneOf(['', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', + '12', '13', '14', '15', '16'])), + description=docs.AO_REQUESTOR_TYPE), 'ao_regulatory_citation': fields.List(IStr, required=False, description=docs.REGULATORY_CITATION), 'ao_statutory_citation': fields.List(IStr, required=False, description=docs.STATUTORY_CITATION), 'ao_citation_require_all': fields.Bool(description=docs.CITATION_REQUIRE_ALL), @@ -383,12 +384,12 @@ def make_seek_args(field=fields.Int, description=None): 'primary_subject_id': fields.List(IStr( validate=validate.OneOf( ['', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', - '12', '13', '14', '15', '16', '17', '18', '19', '20'])), + '12', '13', '14', '15', '16', '17', '18', '19', '20'])), description=docs.PRIMARY_SUBJECT_DESCRIPTION), 'secondary_subject_id': fields.List(IStr( validate=validate.OneOf( ['', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', - '12', '13', '14', '15', '16', '17', '18'])), + '12', '13', '14', '15', '16', '17', '18'])), description=docs.SECONDARY_SUBJECT_DESCRIPTION), 'case_max_open_date': Date(required=False, description=docs.CASE_MAX_OPEN_DATE), 'case_min_close_date': Date(required=False, description=docs.CASE_MIN_CLOSE_DATE), @@ -405,7 +406,7 @@ def make_seek_args(field=fields.Int, description=None): description=docs.CASE_DOCUMENT_CATEGORY_DESCRIPTION), 'mur_type': fields.Str( - required=False, validate=validate.OneOf(["archived", "current"]), description=docs.MUR_TYPE), + required=False, validate=validate.OneOf(['', 'archived', 'current']), description=docs.MUR_TYPE), 'mur_disposition_category_id': fields.List(IStr( validate=validate.OneOf([ '', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', @@ -433,7 +434,7 @@ def make_seek_args(field=fields.Int, description=None): citation = { 'doc_type': fields.Str( - required=False, validate=validate.OneOf(["adrs", "advisory_opinions", "murs"]), + required=False, validate=validate.OneOf(['', 'adrs', 'advisory_opinions', 'murs']), description=docs.CITATION_DOC_TYPE ) } diff --git a/webservices/docs.py b/webservices/docs.py index bd301a24f..57596521f 100644 --- a/webservices/docs.py +++ b/webservices/docs.py @@ -2150,6 +2150,23 @@ def add_ytd(var): AO_REQUESTOR_TYPE = ''' Code of the advisory opinion requestor type. +Select one or more codes to filter by advisory opinion requestor type:\n\ + - 1 - Federal candidate/candidate committee/officeholder\n\ + - 2 - Publicly funded candidates/committees\n\ + - 3 - Party committee, national\n\ + - 4 - Party committee, state or local\n\ + - 5 - Nonconnected political committee\n\ + - 6 - Separate segregated fun \n\ + - 7 - Labor Organization\n\ + - 8 - Trade Association\n\ + - 9 - Membership Organization, Cooperative, Corporation W/O Capital Stocks\n\ + - 10 - Corporation (including LLCs electing corporate status)\n\ + - 11 - Partnership (including LLCs electing partnership status)\n\ + - 12 - Governmental entity \n\ + - 13 - Research/Public Interest/Educational Institution\n\ + - 14 - Law Firm\n\ + - 15 - Individual\n\ + - 16 - Other\n\ ''' CITATION_DOC_TYPE = ''' diff --git a/webservices/filters.py b/webservices/filters.py index fc1d235da..35115a415 100644 --- a/webservices/filters.py +++ b/webservices/filters.py @@ -183,3 +183,14 @@ def get_cycle(kwargs): ) return kwargs['cycle'][0] return kwargs['cycle'] + + +def validate_multiselect_filter(filter, valid_values): + valid_results = [] + + # Validate each value in filter + for value in filter: + if isinstance(value, str) and ' ' not in value: + if value in valid_values: + valid_results.append(value) + return valid_results diff --git a/webservices/resources/legal.py b/webservices/resources/legal.py index ebf0ae834..6dd8f04fb 100644 --- a/webservices/resources/legal.py +++ b/webservices/resources/legal.py @@ -6,6 +6,7 @@ from flask_apispec import doc from webservices import docs from webservices import args +from webservices import filters from webservices.utils import ( create_es_client, Resource, @@ -49,22 +50,22 @@ ACCEPTED_DATE_FORMATS = "strict_date_optional_time_nanos||MM/dd/yyyy||M/d/yyyy||MM/d/yyyy||M/dd/yyyy" REQUESTOR_TYPES = { - 1: "Federal candidate/candidate committee/officeholder", - 2: "Publicly funded candidates/committees", - 3: "Party committee, national", - 4: "Party committee, state or local", - 5: "Nonconnected political committee", - 6: "Separate segregated fund", - 7: "Labor Organization", - 8: "Trade Association", - 9: "Membership Organization, Cooperative, Corporation W/O Capital Stock", - 10: "Corporation (including LLCs electing corporate status)", - 11: "Partnership (including LLCs electing partnership status)", - 12: "Governmental entity", - 13: "Research/Public Interest/Educational Institution", - 14: "Law Firm", - 15: "Individual", - 16: "Other", + "1": "Federal candidate/candidate committee/officeholder", + "2": "Publicly funded candidates/committees", + "3": "Party committee, national", + "4": "Party committee, state or local", + "5": "Nonconnected political committee", + "6": "Separate segregated fund", + "7": "Labor Organization", + "8": "Trade Association", + "9": "Membership Organization, Cooperative, Corporation W/O Capital Stock", + "10": "Corporation (including LLCs electing corporate status)", + "11": "Partnership (including LLCs electing partnership status)", + "12": "Governmental entity", + "13": "Research/Public Interest/Educational Institution", + "14": "Law Firm", + "15": "Individual", + "16": "Other", } # endpoint path: /legal/docs// @@ -128,8 +129,8 @@ def get(self, q="", from_hit=0, hits_returned=20, **kwargs): "adrs": case_query_builder, "admin_fines": case_query_builder, } - - if kwargs.get("type", "all") == "all": + # Get the type from kwargs, defaulting to '' if not present + if kwargs.get("type", "") == "": doc_types = ALL_DOCUMENT_TYPES else: doc_types = [kwargs.get("type")] @@ -235,13 +236,33 @@ def case_query_builder(q, type_, from_hit, hits_returned, **kwargs): if check_filter_exists(kwargs, "case_no"): must_clauses.append(Q("terms", no=kwargs.get("case_no"))) - if kwargs.get("primary_subject_id") and '' not in kwargs.get("primary_subject_id"): + # Get 'primary_subject_id' from kwargs + primary_subject_id = kwargs.get("primary_subject_id", []) + primary_subject_id_valid_values = ['1', '2', '3', '4', '5', '6', '7', '8', '9', + '10', '11', '12', '13', '14', '15', '16', + '17', '18', '19', '20'] + + # Validate 'primary_subject_id filter' + valid_primary_subject_id = filters.validate_multiselect_filter( + primary_subject_id, primary_subject_id_valid_values) + + if valid_primary_subject_id: must_clauses.append(Q("nested", path="subjects", - query=Q("terms", subjects__primary_subject_id=kwargs.get("primary_subject_id")))) + query=Q("terms", subjects__primary_subject_id=valid_primary_subject_id))) + + # Get 'secondary_subject_id' from kwargs + secondary_subject_id = kwargs.get("secondary_subject_id", []) + secondary_subject_id_valid_values = ['1', '2', '3', '4', '5', '6', '7', '8', '9', + '10', '11', '12', '13', '14', '15', '16', + '17', '18'] - if kwargs.get("secondary_subject_id") and '' not in kwargs.get("secondary_subject_id"): + # Validate 'secondary_subject_id filter' + valid_secondary_subject_id = filters.validate_multiselect_filter( + secondary_subject_id, secondary_subject_id_valid_values) + + if valid_secondary_subject_id: must_clauses.append(Q("nested", path="subjects", - query=Q("terms", subjects__secondary_subject_id=kwargs.get("secondary_subject_id")))) + query=Q("terms", subjects__secondary_subject_id=valid_secondary_subject_id))) if kwargs.get("case_respondents"): must_clauses.append(Q("simple_query_string", @@ -437,7 +458,7 @@ def apply_af_specific_query_params(query, **kwargs): def apply_mur_specific_query_params(query, **kwargs): must_clauses = [] - if kwargs.get("mur_type"): + if check_filter_exists(kwargs, "mur_type"): must_clauses.append(Q("match", mur_type=kwargs.get("mur_type"))) if kwargs.get("case_election_cycles"): @@ -736,13 +757,22 @@ def apply_ao_specific_query_params(query, **kwargs): else: must_clauses.append(Q("bool", should=citation_queries, minimum_should_match=1)) - if kwargs.get("ao_requestor_type"): + # Get 'ao_requestor_type' from kwargs + ao_requestor_type = kwargs.get("ao_requestor_type", []) + ao_requestor_type_valid_values = ['1', '2', '3', '4', '5', '6', '7', '8', '9', + '10', '11', '12', '13', '14', '15', '16'] + + # Validate 'ao_requestor_type filter' + valid_requestor_types = filters.validate_multiselect_filter( + ao_requestor_type, ao_requestor_type_valid_values) + # Always include valid values in the query construction + if valid_requestor_types: must_clauses.append( Q( "terms", requestor_types=[ - REQUESTOR_TYPES[r] for r in kwargs.get("ao_requestor_type") + REQUESTOR_TYPES[r] for r in valid_requestor_types ], ) )