diff --git a/api/.openapi-generator/FILES b/api/.openapi-generator/FILES index 6ecd2bad9..17a4cc9a9 100644 --- a/api/.openapi-generator/FILES +++ b/api/.openapi-generator/FILES @@ -1,7 +1,5 @@ src/feeds/impl/__init__.py src/feeds_gen/apis/__init__.py -src/feeds_gen/apis/beta_api.py -src/feeds_gen/apis/beta_api_base.py src/feeds_gen/apis/datasets_api.py src/feeds_gen/apis/datasets_api_base.py src/feeds_gen/apis/feeds_api.py diff --git a/api/src/scripts/populate_db_test_data.py b/api/src/scripts/populate_db_test_data.py index 6435f561a..703e497d2 100644 --- a/api/src/scripts/populate_db_test_data.py +++ b/api/src/scripts/populate_db_test_data.py @@ -13,6 +13,8 @@ Notice, Feature, License, + LicenseTag, + LicenseTagGroup, t_feedsearch, Location, Officialstatushistory, @@ -224,6 +226,66 @@ def populate_test_datasets(self, filepath, db_session: "Session"): license_obj.rules.append(rule_obj) db_session.commit() + # License tag groups (optional section to seed group metadata used by license_tags) + if "license_tag_groups" in data: + for group in data["license_tag_groups"]: + group_id = group.get("id") + if not group_id: + continue + existing_group = db_session.get(LicenseTagGroup, group_id) + if existing_group: + continue + db_session.add( + LicenseTagGroup( + id=group_id, + short_name=group.get("short_name"), + description=group.get("description") or group_id, + ) + ) + db_session.commit() + + # License tags (optional section to seed tag metadata) + if "license_tags" in data: + for tag in data["license_tags"]: + tag_id = tag.get("id") + if not tag_id: + continue + existing_tag = db_session.get(LicenseTag, tag_id) + if existing_tag: + continue + db_session.add( + LicenseTag( + id=tag_id, + group=tag.get("group"), + tag=tag.get("tag"), + url=tag.get("url"), + description=tag.get("description"), + ) + ) + db_session.commit() + + # License tag associations: attach tags to licenses via the many-to-many relationship + if "license_license_tags" in data: + for lt in data["license_license_tags"]: + license_id = lt.get("license_id") + tag_id = lt.get("tag_id") + if not license_id or not tag_id: + continue + license_obj = db_session.get(License, license_id) + if not license_obj: + self.logger.error( + f"No license found with id: {license_id};" + f" skipping license_license_tag association for tag {tag_id}" + ) + continue + tag_obj = db_session.get(LicenseTag, tag_id) + if not tag_obj: + self.logger.error(f"No license tag found with id: {tag_id}; skipping") + continue + if tag_obj not in license_obj.tags: + license_obj.tags.append(tag_obj) + db_session.commit() + # GBFS version if "gbfs_versions" in data: for version in data["gbfs_versions"]: diff --git a/api/src/shared/db_models/basic_feed_impl.py b/api/src/shared/db_models/basic_feed_impl.py index e865d7d79..fd36155bc 100644 --- a/api/src/shared/db_models/basic_feed_impl.py +++ b/api/src/shared/db_models/basic_feed_impl.py @@ -22,8 +22,10 @@ def from_orm(cls, feed: Feed | None) -> BasicFeed | None: return None # Determine license_is_spdx from the related License ORM if available license_is_spdx = None + license_tags = None if getattr(feed, "license", None) is not None: license_is_spdx = feed.license.is_spdx + license_tags = sorted([tag.id for tag in getattr(feed.license, "tags", [])]) or None return cls( id=feed.stable_id, @@ -43,6 +45,7 @@ def from_orm(cls, feed: Feed | None) -> BasicFeed | None: license_id=feed.license_id, license_is_spdx=license_is_spdx, license_notes=feed.license_notes, + license_tags=license_tags, ), redirects=sorted([RedirectImpl.from_orm(item) for item in feed.redirectingids], key=lambda x: x.target_id), ) diff --git a/api/src/shared/db_models/license_base_impl.py b/api/src/shared/db_models/license_base_impl.py index 968cbd35a..01113ee08 100644 --- a/api/src/shared/db_models/license_base_impl.py +++ b/api/src/shared/db_models/license_base_impl.py @@ -28,4 +28,5 @@ def from_orm(cls, license_orm: Optional[LicenseOrm]) -> Optional[LicenseBase]: description=license_orm.description, created_at=license_orm.created_at, updated_at=license_orm.updated_at, + license_tags=sorted([tag.id for tag in getattr(license_orm, "tags", [])]) or None, ) diff --git a/api/tests/integration/test_data/extra_test_data.json b/api/tests/integration/test_data/extra_test_data.json index d0723902b..cdedccc8c 100644 --- a/api/tests/integration/test_data/extra_test_data.json +++ b/api/tests/integration/test_data/extra_test_data.json @@ -80,6 +80,45 @@ } ], + "license_tag_groups": [ + { + "id": "family", + "short_name": "Family", + "description": "License family taxonomy" + }, + { + "id": "license", + "short_name": "License", + "description": "License type taxonomy" + } + ], + + "license_tags": [ + { + "id": "family:ODC", + "group": "family", + "tag": "ODC", + "description": "Open Data Commons family" + }, + { + "id": "license:open-data-commons", + "group": "license", + "tag": "open-data-commons", + "description": "Open Data Commons license" + } + ], + + "license_license_tags": [ + { + "license_id": "license-1", + "tag_id": "family:ODC" + }, + { + "license_id": "license-1", + "tag_id": "license:open-data-commons" + } + ], + "feeds": [ { "id": "mdb-60", diff --git a/api/tests/integration/test_feeds_api.py b/api/tests/integration/test_feeds_api.py index 8c53a6d5a..066adf8a2 100644 --- a/api/tests/integration/test_feeds_api.py +++ b/api/tests/integration/test_feeds_api.py @@ -947,18 +947,23 @@ def test_gbfs_feed_id_get(client: TestClient, values): @pytest.mark.parametrize( - "feed_id, expected_license_id, expected_is_spdx, expected_license_notes", + "feed_id, expected_license_id, expected_is_spdx, expected_license_notes, expected_license_tags", [ - ("mdb-70", "license-1", True, "Notes for license-1"), - ("mdb-80", "license-2", False, None), + ("mdb-70", "license-1", True, "Notes for license-1", ["family:ODC", "license:open-data-commons"]), + ("mdb-80", "license-2", False, None, None), ], ) def test_feeds_have_expected_license_info( - client: TestClient, feed_id: str, expected_license_id: str, expected_is_spdx: bool, expected_license_notes: str + client: TestClient, + feed_id: str, + expected_license_id: str, + expected_is_spdx: bool, + expected_license_notes: str, + expected_license_tags: list, ): """ Verify that specified feeds have the expected license id, - license_is_spdx and license_notes from the test fixture. + license_is_spdx, license_notes, and license_tags from the test fixture. """ response = client.request( "GET", @@ -974,3 +979,8 @@ def test_feeds_have_expected_license_info( assert body["source_info"]["license_is_spdx"] is expected_is_spdx # Check license_notes (may be None) assert body["source_info"].get("license_notes") == expected_license_notes + # Check license_tags (may be None if no tags) + if expected_license_tags is None: + assert body["source_info"].get("license_tags") is None + else: + assert sorted(body["source_info"].get("license_tags", [])) == sorted(expected_license_tags) diff --git a/api/tests/integration/test_licenses_api.py b/api/tests/integration/test_licenses_api.py index bb5c4822c..02e36a6dc 100644 --- a/api/tests/integration/test_licenses_api.py +++ b/api/tests/integration/test_licenses_api.py @@ -6,7 +6,7 @@ @pytest.mark.parametrize( - "license_id, expected_is_spdx, expected_name, expected_url, expected_description, expected_rules", + "license_id, expected_is_spdx, expected_name, expected_url, expected_description, expected_rules, expected_tags", [ ( "license-1", @@ -28,6 +28,7 @@ "type": "condition", }, ], + ["family:ODC", "license:open-data-commons"], ), ( "license-2", @@ -49,6 +50,7 @@ "type": "limitation", }, ], + None, ), ], ) @@ -60,6 +62,7 @@ def test_get_license_by_id( expected_url: str, expected_description: str, expected_rules: list, + expected_tags: list, ): """GET /v1/licenses/{id} returns the expected license fields for known test licenses.""" response = client.request("GET", f"/v1/licenses/{license_id}", headers=authHeaders) @@ -74,6 +77,11 @@ def test_get_license_by_id( actual_rules = {rule["name"]: rule for rule in body.get("license_rules", [])} expected_rule_map = {rule["name"]: rule for rule in expected_rules} assert actual_rules == expected_rule_map + # Verify license_tags + if expected_tags is None: + assert body.get("license_tags") is None + else: + assert sorted(body.get("license_tags", [])) == sorted(expected_tags) def test_get_licenses_list_contains_test_licenses(client: TestClient): diff --git a/docs/DatabaseCatalogAPI.yaml b/docs/DatabaseCatalogAPI.yaml index 63c41ca80..2c1a8943c 100644 --- a/docs/DatabaseCatalogAPI.yaml +++ b/docs/DatabaseCatalogAPI.yaml @@ -145,7 +145,6 @@ paths: description: Get GBFS feeds from the Mobility Database. tags: - "feeds" - - "beta" operationId: getGbfsFeeds parameters: - $ref: "#/components/parameters/limit_query_param_gbfs_feeds_endpoint" @@ -175,7 +174,6 @@ paths: description: Get the specified GTFS feed from the Mobility Database. Once a week, we check if the latest dataset has been updated and, if so, we update it in our system accordingly. tags: - "feeds" - - "beta" operationId: getGtfsFeed security: @@ -1088,6 +1086,14 @@ components: description: Notes concerning the relation between the feed and the license. type: string example: Detected locale/jurisdiction port 'nl'. SPDX does not list ported CC licenses; using canonical ID. + license_tags: + description: List of taxonomy tags associated with the feed's license. + type: array + items: + type: string + example: + - "family:ODC" + - "license:open-data-commons" Locations: type: array @@ -1345,6 +1351,14 @@ components: type: string example: 2023-07-10T22:06:00Z format: date-time + license_tags: + description: List of taxonomy tags associated with the license. + type: array + items: + type: string + example: + - "family:ODC" + - "license:open-data-commons" LicenseWithRules: allOf: diff --git a/docs/OperationsAPI.yaml b/docs/OperationsAPI.yaml index b1ffc18d7..f147457e4 100644 --- a/docs/OperationsAPI.yaml +++ b/docs/OperationsAPI.yaml @@ -1022,6 +1022,14 @@ components: description: Notes concerning the relation between the feed and the license. type: string example: Detected locale/jurisdiction port 'nl'. SPDX does not list ported CC licenses; using canonical ID. + license_tags: + description: List of taxonomy tags associated with the feed's license. + type: array + items: + type: string + example: + - "family:ODC" + - "license:open-data-commons" Locations: type: array items: @@ -1268,6 +1276,14 @@ components: type: string example: 2023-07-10T22:06:00Z format: date-time + license_tags: + description: List of taxonomy tags associated with the license. + type: array + items: + type: string + example: + - "family:ODC" + - "license:open-data-commons" LicenseWithRules: allOf: - $ref: "#/components/schemas/LicenseBase" @@ -1478,6 +1494,7 @@ components: + * vp - vehicle positions * tu - trip updates * sa - service alerts @@ -1599,6 +1616,7 @@ components: + * vp - vehicle positions * tu - trip updates * sa - service alerts @@ -1677,6 +1695,7 @@ components: + * `active` Feed should be used in public trip planners. * `deprecated` Feed is explicitly deprecated and should not be used in public trip planners. * `inactive` Feed hasn't been recently updated and should be used at risk of providing outdated information. @@ -1697,6 +1716,7 @@ components: + * `gtfs` GTFS feed. * `gtfs_rt` GTFS-RT feed. * `gbfs` GBFS feed. diff --git a/web-app/src/app/services/feeds/types.ts b/web-app/src/app/services/feeds/types.ts index 0e1d9d612..634ff1441 100644 --- a/web-app/src/app/services/feeds/types.ts +++ b/web-app/src/app/services/feeds/types.ts @@ -624,6 +624,14 @@ export interface components { * @example Detected locale/jurisdiction port 'nl'. SPDX does not list ported CC licenses; using canonical ID. */ license_notes?: string; + /** + * @description List of taxonomy tags associated with the feed's license. + * @example [ + * "family:ODC", + * "license:open-data-commons" + * ] + */ + license_tags?: string[]; }; Locations: Array; Location: { @@ -848,6 +856,14 @@ export interface components { * @example "2023-07-10T22:06:00.000Z" */ updated_at?: string; + /** + * @description List of taxonomy tags associated with the license. + * @example [ + * "family:ODC", + * "license:open-data-commons" + * ] + */ + license_tags?: string[]; }; LicenseWithRules: components['schemas']['LicenseBase'] & { license_rules?: Array;