diff --git a/spp_api_v2/__manifest__.py b/spp_api_v2/__manifest__.py index a6d9beb6a..cfbe15748 100644 --- a/spp_api_v2/__manifest__.py +++ b/spp_api_v2/__manifest__.py @@ -18,7 +18,6 @@ "spp_registry", "spp_consent", "spp_vocabulary", - "spp_programs", "spp_source_tracking", ], "data": [ @@ -30,8 +29,6 @@ "data/api_path_data.xml", "data/filter_config_individual.xml", "data/filter_config_group.xml", - "data/filter_config_program.xml", - "data/filter_config_program_membership.xml", "wizards/show_secret_wizard_views.xml", "views/api_client_views.xml", "views/api_extension_views.xml", diff --git a/spp_api_v2/data/api_path_data.xml b/spp_api_v2/data/api_path_data.xml index a71b86baf..0fef084d1 100644 --- a/spp_api_v2/data/api_path_data.xml +++ b/spp_api_v2/data/api_path_data.xml @@ -31,24 +31,4 @@ name="filter_domain" >[('is_registrant', '=', True), ('is_group', '=', True)] - - - - 30 - Program - - Social protection programs - False - 10 - - - - - 40 - ProgramMembership - - Beneficiary enrollments in programs - False - 10 - diff --git a/spp_api_v2/models/fastapi_endpoint_registry.py b/spp_api_v2/models/fastapi_endpoint_registry.py index dacbfb5ad..dd151c238 100644 --- a/spp_api_v2/models/fastapi_endpoint_registry.py +++ b/spp_api_v2/models/fastapi_endpoint_registry.py @@ -29,15 +29,11 @@ def _get_fastapi_routers(self) -> list[APIRouter]: from ..routers.filter import ( group_filter_router, individual_filter_router, - program_filter_router, - program_membership_filter_router, ) from ..routers.group import group_router from ..routers.individual import individual_router from ..routers.metadata import metadata_router from ..routers.oauth import oauth_router - from ..routers.program import program_router - from ..routers.program_membership import program_membership_router routers.extend( [ @@ -49,10 +45,6 @@ def _get_fastapi_routers(self) -> list[APIRouter]: group_filter_router, batch_router, bulk_router, - program_router, - program_filter_router, - program_membership_router, - program_membership_filter_router, consent_router, ] ) diff --git a/spp_api_v2/routers/__init__.py b/spp_api_v2/routers/__init__.py index 72ba7f58e..816920b47 100644 --- a/spp_api_v2/routers/__init__.py +++ b/spp_api_v2/routers/__init__.py @@ -6,5 +6,3 @@ from . import individual from . import metadata from . import oauth -from . import program -from . import program_membership diff --git a/spp_api_v2/routers/filter.py b/spp_api_v2/routers/filter.py index 7b00137cc..67c867e10 100644 --- a/spp_api_v2/routers/filter.py +++ b/spp_api_v2/routers/filter.py @@ -18,8 +18,6 @@ from ..services.filter_service import FilterService from ..services.group_service import GroupService from ..services.individual_service import IndividualService -from ..services.program_membership_service import ProgramMembershipService -from ..services.program_service import ProgramService _logger = logging.getLogger(__name__) @@ -27,11 +25,10 @@ # Router for filter metadata - mounted per resource individual_filter_router = APIRouter(tags=["Individual"], prefix="/Individual") group_filter_router = APIRouter(tags=["Group"], prefix="/Group") -program_filter_router = APIRouter(tags=["Program"], prefix="/Program") -program_membership_filter_router = APIRouter(tags=["ProgramMembership"], prefix="/ProgramMembership") -# Service mapping for resource types +# Service mapping for resource types. Companion modules (e.g. spp_api_v2_programs) +# register their own resources into this dict at import time. RESOURCE_SERVICES = { "Individual": { "service_class": IndividualService, @@ -45,18 +42,6 @@ "base_domain": [("is_registrant", "=", True), ("is_group", "=", True)], "consent_type": "group", }, - "Program": { - "service_class": ProgramService, - "model": "spp.program", - "base_domain": [], - "consent_type": None, - }, - "ProgramMembership": { - "service_class": ProgramMembershipService, - "model": "spp.program.membership", - "base_domain": [], - "consent_type": "program_membership", - }, } @@ -300,45 +285,3 @@ async def search( summary="Advanced Group Search", description="Search groups with complex filter conditions", ) - -# Register endpoints for Program -program_filter_router.add_api_route( - "/_filters", - _create_filter_metadata_endpoint("Program"), - methods=["GET"], - response_model=FilterMetadataResponse, - response_model_exclude_none=True, - summary="Get Program Filters", - description="Get available filters and presets for Program resource", -) - -program_filter_router.add_api_route( - "/_search", - _create_search_endpoint("Program"), - methods=["POST"], - response_model=Bundle, - response_model_exclude_none=True, - summary="Advanced Program Search", - description="Search programs with complex filter conditions", -) - -# Register endpoints for ProgramMembership -program_membership_filter_router.add_api_route( - "/_filters", - _create_filter_metadata_endpoint("ProgramMembership"), - methods=["GET"], - response_model=FilterMetadataResponse, - response_model_exclude_none=True, - summary="Get ProgramMembership Filters", - description="Get available filters and presets for ProgramMembership resource", -) - -program_membership_filter_router.add_api_route( - "/_search", - _create_search_endpoint("ProgramMembership"), - methods=["POST"], - response_model=Bundle, - response_model_exclude_none=True, - summary="Advanced ProgramMembership Search", - description="Search program memberships with complex filter conditions", -) diff --git a/spp_api_v2/schemas/__init__.py b/spp_api_v2/schemas/__init__.py index 6fd26c4a8..c85d70a75 100644 --- a/spp_api_v2/schemas/__init__.py +++ b/spp_api_v2/schemas/__init__.py @@ -9,6 +9,4 @@ from . import operation_outcome from . import patch from . import problem_detail -from . import program -from . import program_membership from . import search_result diff --git a/spp_api_v2/services/__init__.py b/spp_api_v2/services/__init__.py index 9b402a15b..1d0376a98 100644 --- a/spp_api_v2/services/__init__.py +++ b/spp_api_v2/services/__init__.py @@ -6,7 +6,5 @@ from . import filter_service from . import group_service from . import individual_service -from . import program_membership_service -from . import program_service from . import schema_builder from . import search_service diff --git a/spp_api_v2/tests/__init__.py b/spp_api_v2/tests/__init__.py index 4bfafb36e..806558639 100644 --- a/spp_api_v2/tests/__init__.py +++ b/spp_api_v2/tests/__init__.py @@ -31,10 +31,6 @@ from . import test_organization_type_security from . import test_pagination from . import test_patch_api -from . import test_program_api -from . import test_program_membership_api -from . import test_program_membership_service -from . import test_program_service from . import test_scope_enforcement from . import test_schema_builder from . import test_search_service diff --git a/spp_api_v2/tests/test_scope_enforcement.py b/spp_api_v2/tests/test_scope_enforcement.py index 8417bf966..34da39e1a 100644 --- a/spp_api_v2/tests/test_scope_enforcement.py +++ b/spp_api_v2/tests/test_scope_enforcement.py @@ -487,114 +487,16 @@ def test_group_membership_history_requires_scope(self): self.assertIn("group:read", data["detail"]) -class TestScopeEnforcementProgramMembership(ApiV2HttpTestCase): - """Test scope enforcement on Program Membership endpoints""" - - def setUp(self): - super().setUp() - # Create test data - self.program = self.create_test_program(name="Test Program", target_type="individual") - self.individual = self.create_test_individual(identifier_value="SCOPE-PM-001") - - # Create test membership - self.membership = self.create_test_membership( - partner=self.individual, - program=self.program, - state="enrolled", - ) - - # Base URL for program membership - self.pm_url = "/api/v2/spp/ProgramMembership" - self.pm_id_url = f"{self.pm_url}/urn:openspp:vocab:id-type%23test_national_id|SCOPE-PM-001" - - def _make_client_without_scope(self, excluded_resource, excluded_action): - """Create a client that has all scopes EXCEPT the specified one""" - scopes = [] - # Give it a different resource scope to prove it's not resource-agnostic - other_resource = "individual" if excluded_resource == "program_membership" else "program_membership" - scopes.append({"resource": other_resource, "action": "all"}) - client = self.create_api_client( - name=f"No {excluded_resource}:{excluded_action}", - scopes=scopes, - require_consent=False, - ) - return client, self.generate_jwt_token(client) - - def test_program_membership_create_requires_scope(self): - """POST /ProgramMembership returns 403 without program_membership:create scope""" - client, token = self._make_client_without_scope("program_membership", "create") - - # Create new individual for enrollment - self.create_test_individual(identifier_value="SCOPE-PM-NEW") - - payload = { - "resourceType": "ProgramMembership", - "status": "enrolled", - "beneficiary": { - "reference": "Individual/urn:openspp:vocab:id-type#test_national_id|SCOPE-PM-NEW", - }, - "program": { - "reference": "Program/urn:openspp:program|test-program", - }, - } - - response = self.url_open( - self.pm_url, - data=json.dumps(payload), - headers={ - "Content-Type": "application/json", - "Authorization": f"Bearer {token}", - }, - ) - - self.assertEqual(response.status_code, 403) - data = json.loads(response.content) - self.assertIn("Missing required scope", data["detail"]) - self.assertIn("program_membership:create", data["detail"]) - - def test_program_membership_update_requires_scope(self): - """PUT /ProgramMembership/{id} returns 403 without program_membership:update scope""" - client, token = self._make_client_without_scope("program_membership", "update") - - payload = { - "resourceType": "ProgramMembership", - "status": "paused", - "beneficiary": { - "reference": "Individual/urn:openspp:vocab:id-type#test_national_id|SCOPE-PM-001", - }, - "program": { - "reference": "Program/urn:openspp:program|test-program", - }, - } - - response = self.url_put( - self.pm_id_url, - data=json.dumps(payload), - headers={ - "Content-Type": "application/json", - "Authorization": f"Bearer {token}", - }, - ) - - self.assertEqual(response.status_code, 403) - data = json.loads(response.content) - self.assertIn("Missing required scope", data["detail"]) - self.assertIn("program_membership:update", data["detail"]) - - class TestScopeIsolation(ApiV2HttpTestCase): """Test that scopes for one resource don't grant access to another""" def setUp(self): super().setUp() - # Create test data for all resource types + # Create test data for the resource types served by the base module. + # Program / program-membership isolation lives in spp_api_v2_programs + # (the program endpoints moved there in OP#1081). self.individual = self.create_test_individual(identifier_value="ISOLATION-IND-001") self.group = self.create_test_group(identifier_value="ISOLATION-GRP-001") - self.program = self.create_test_program(name="Isolation Test Program") - self.membership = self.create_test_membership( - partner=self.individual, - program=self.program, - ) def test_individual_scope_does_not_grant_group_access(self): """individual:read scope does not grant access to group endpoints""" @@ -634,42 +536,6 @@ def test_group_scope_does_not_grant_individual_access(self): self.assertIn("Missing required scope", data["detail"]) self.assertIn("individual:read", data["detail"]) - def test_individual_scope_does_not_grant_program_membership_access(self): - """individual:create scope does not grant access to program_membership endpoints""" - # Create client with only individual:create scope - client = self.create_api_client( - name="Individual Create Only Client", - scopes=[{"resource": "individual", "action": "create"}], - require_consent=False, - ) - token = self.generate_jwt_token(client) - - # Try to create program membership - payload = { - "resourceType": "ProgramMembership", - "status": "enrolled", - "beneficiary": { - "reference": "Individual/urn:openspp:vocab:id-type#test_national_id|ISOLATION-IND-001", - }, - "program": { - "reference": "Program/urn:openspp:program|isolation-test-program", - }, - } - - response = self.url_open( - "/api/v2/spp/ProgramMembership", - data=json.dumps(payload), - headers={ - "Content-Type": "application/json", - "Authorization": f"Bearer {token}", - }, - ) - - self.assertEqual(response.status_code, 403) - data = json.loads(response.content) - self.assertIn("Missing required scope", data["detail"]) - self.assertIn("program_membership:create", data["detail"]) - def test_read_scope_does_not_grant_create_access(self): """individual:read scope does not grant create access""" # Create client with only individual:read scope diff --git a/spp_api_v2_programs/README.rst b/spp_api_v2_programs/README.rst new file mode 100644 index 000000000..c0a4f9cae --- /dev/null +++ b/spp_api_v2_programs/README.rst @@ -0,0 +1,113 @@ +========================= +OpenSPP API V2 - Programs +========================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:4471b988191c17dbafeaa6a0f0835db915192c85183aabb7f6a6bb66b8f0a213 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png + :target: https://odoo-community.org/page/development-status + :alt: Production/Stable +.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OpenSPP%2FOpenSPP2-lightgray.png?logo=github + :target: https://github.com/OpenSPP/OpenSPP2/tree/19.0/spp_api_v2_programs + :alt: OpenSPP/OpenSPP2 + +|badge1| |badge2| |badge3| + +Extends OpenSPP API V2 with REST endpoints for **Programs** and +**Program Memberships**. This module was split out of ``spp_api_v2`` so +that the base API module no longer depends on ``spp_programs`` — +deployments that don't use programs (e.g. a registry-only Social +Registry) can ship the API without installing the Programs stack. + +It auto-installs whenever both ``spp_api_v2`` and ``spp_programs`` are +present, so program API functionality is unchanged for deployments that +use programs. + +Key Capabilities +~~~~~~~~~~~~~~~~ + +- **Program endpoints**: read and search programs + (``/api/v2/spp/Program``) +- **Program Membership endpoints**: read, search, create, and update + beneficiary enrollments (``/api/v2/spp/ProgramMembership``) +- **Advanced filtering**: ``/_filters`` and ``/_search`` endpoints for + both resources, registered into the shared API V2 filter framework +- **OAuth scopes**: the ``program`` and ``program_membership`` + client-scope resources (defined in ``spp_api_v2``) gate access + +UI Location +~~~~~~~~~~~ + +No standalone menu. Endpoints are available under the API V2 app: + +- ``/api/v2/spp/Program``, ``/api/v2/spp/Program/{identifier}`` +- ``/api/v2/spp/ProgramMembership``, + ``/api/v2/spp/ProgramMembership/{identifier}`` + +Dependencies +~~~~~~~~~~~~ + +``spp_api_v2``, ``spp_programs`` + +**Table of contents** + +.. contents:: + :local: + +Changelog +========= + +19.0.1.0.0 +~~~~~~~~~~ + +- Split program and program-membership REST endpoints out of + ``spp_api_v2`` into this companion module (#1081), so ``spp_api_v2`` + no longer depends on ``spp_programs``. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* OpenSPP.org + +Maintainers +----------- + +.. |maintainer-jeremi| image:: https://github.com/jeremi.png?size=40px + :target: https://github.com/jeremi + :alt: jeremi +.. |maintainer-gonzalesedwin1123| image:: https://github.com/gonzalesedwin1123.png?size=40px + :target: https://github.com/gonzalesedwin1123 + :alt: gonzalesedwin1123 +.. |maintainer-emjay0921| image:: https://github.com/emjay0921.png?size=40px + :target: https://github.com/emjay0921 + :alt: emjay0921 + +Current maintainers: + +|maintainer-jeremi| |maintainer-gonzalesedwin1123| |maintainer-emjay0921| + +This module is part of the `OpenSPP/OpenSPP2 `_ project on GitHub. + +You are welcome to contribute. \ No newline at end of file diff --git a/spp_api_v2_programs/__init__.py b/spp_api_v2_programs/__init__.py new file mode 100644 index 000000000..57cf5d725 --- /dev/null +++ b/spp_api_v2_programs/__init__.py @@ -0,0 +1,6 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + +from . import models +from . import routers +from . import schemas +from . import services diff --git a/spp_api_v2_programs/__manifest__.py b/spp_api_v2_programs/__manifest__.py new file mode 100644 index 000000000..5becc04d0 --- /dev/null +++ b/spp_api_v2_programs/__manifest__.py @@ -0,0 +1,29 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +{ # pylint: disable=pointless-statement + "name": "OpenSPP API V2 - Programs", + "category": "OpenSPP/Integration", + "version": "19.0.1.0.0", + "sequence": 1, + "author": "OpenSPP.org", + "website": "https://github.com/OpenSPP/OpenSPP2", + "license": "LGPL-3", + "development_status": "Production/Stable", + "maintainers": ["jeremi", "gonzalesedwin1123", "emjay0921"], + "summary": "REST API endpoints for Programs and Program Memberships.", + "depends": [ + "spp_api_v2", + "spp_programs", + ], + "data": [ + "security/ir.model.access.csv", + "data/api_path_data.xml", + "data/filter_config_program.xml", + "data/filter_config_program_membership.xml", + ], + "assets": {}, + "demo": [], + "images": [], + "application": False, + "installable": True, + "auto_install": ["spp_api_v2", "spp_programs"], +} diff --git a/spp_api_v2_programs/data/api_path_data.xml b/spp_api_v2_programs/data/api_path_data.xml new file mode 100644 index 000000000..3acdd48e7 --- /dev/null +++ b/spp_api_v2_programs/data/api_path_data.xml @@ -0,0 +1,24 @@ + + + + + + + 30 + Program + + Social protection programs + False + 10 + + + + + 40 + ProgramMembership + + Beneficiary enrollments in programs + False + 10 + + diff --git a/spp_api_v2/data/filter_config_program.xml b/spp_api_v2_programs/data/filter_config_program.xml similarity index 100% rename from spp_api_v2/data/filter_config_program.xml rename to spp_api_v2_programs/data/filter_config_program.xml diff --git a/spp_api_v2/data/filter_config_program_membership.xml b/spp_api_v2_programs/data/filter_config_program_membership.xml similarity index 100% rename from spp_api_v2/data/filter_config_program_membership.xml rename to spp_api_v2_programs/data/filter_config_program_membership.xml diff --git a/spp_api_v2_programs/models/__init__.py b/spp_api_v2_programs/models/__init__.py new file mode 100644 index 000000000..371088cf6 --- /dev/null +++ b/spp_api_v2_programs/models/__init__.py @@ -0,0 +1,3 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + +from . import fastapi_endpoint_programs diff --git a/spp_api_v2_programs/models/fastapi_endpoint_programs.py b/spp_api_v2_programs/models/fastapi_endpoint_programs.py new file mode 100644 index 000000000..7f66c8f9e --- /dev/null +++ b/spp_api_v2_programs/models/fastapi_endpoint_programs.py @@ -0,0 +1,37 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""Register the Program and ProgramMembership routers on the API V2 app.""" + +import logging + +from odoo import models + +from fastapi import APIRouter + +_logger = logging.getLogger(__name__) + + +class SppApiV2ProgramsEndpoint(models.Model): + """Extend the API V2 FastAPI endpoint with program routers.""" + + _inherit = "fastapi.endpoint" + + def _get_fastapi_routers(self) -> list[APIRouter]: + """Add Program / ProgramMembership routers to API V2.""" + routers = super()._get_fastapi_routers() + if self.app == "api_v2": + from ..routers.program import program_router + from ..routers.program_filters import ( + program_filter_router, + program_membership_filter_router, + ) + from ..routers.program_membership import program_membership_router + + routers.extend( + [ + program_router, + program_filter_router, + program_membership_router, + program_membership_filter_router, + ] + ) + return routers diff --git a/spp_api_v2_programs/pyproject.toml b/spp_api_v2_programs/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/spp_api_v2_programs/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/spp_api_v2_programs/readme/DESCRIPTION.md b/spp_api_v2_programs/readme/DESCRIPTION.md new file mode 100644 index 000000000..fa97a4bd5 --- /dev/null +++ b/spp_api_v2_programs/readme/DESCRIPTION.md @@ -0,0 +1,21 @@ +Extends OpenSPP API V2 with REST endpoints for **Programs** and **Program Memberships**. This module was split out of `spp_api_v2` so that the base API module no longer depends on `spp_programs` — deployments that don't use programs (e.g. a registry-only Social Registry) can ship the API without installing the Programs stack. + +It auto-installs whenever both `spp_api_v2` and `spp_programs` are present, so program API functionality is unchanged for deployments that use programs. + +### Key Capabilities + +- **Program endpoints**: read and search programs (`/api/v2/spp/Program`) +- **Program Membership endpoints**: read, search, create, and update beneficiary enrollments (`/api/v2/spp/ProgramMembership`) +- **Advanced filtering**: `/_filters` and `/_search` endpoints for both resources, registered into the shared API V2 filter framework +- **OAuth scopes**: the `program` and `program_membership` client-scope resources (defined in `spp_api_v2`) gate access + +### UI Location + +No standalone menu. Endpoints are available under the API V2 app: + +- `/api/v2/spp/Program`, `/api/v2/spp/Program/{identifier}` +- `/api/v2/spp/ProgramMembership`, `/api/v2/spp/ProgramMembership/{identifier}` + +### Dependencies + +`spp_api_v2`, `spp_programs` diff --git a/spp_api_v2_programs/readme/HISTORY.md b/spp_api_v2_programs/readme/HISTORY.md new file mode 100644 index 000000000..a87d1ffe4 --- /dev/null +++ b/spp_api_v2_programs/readme/HISTORY.md @@ -0,0 +1,3 @@ +### 19.0.1.0.0 + +- Split program and program-membership REST endpoints out of `spp_api_v2` into this companion module (#1081), so `spp_api_v2` no longer depends on `spp_programs`. diff --git a/spp_api_v2_programs/routers/__init__.py b/spp_api_v2_programs/routers/__init__.py new file mode 100644 index 000000000..d250089cb --- /dev/null +++ b/spp_api_v2_programs/routers/__init__.py @@ -0,0 +1,5 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + +from . import program +from . import program_filters +from . import program_membership diff --git a/spp_api_v2/routers/program.py b/spp_api_v2_programs/routers/program.py similarity index 97% rename from spp_api_v2/routers/program.py rename to spp_api_v2_programs/routers/program.py index c397ff2a2..98d431fb5 100644 --- a/spp_api_v2/routers/program.py +++ b/spp_api_v2_programs/routers/program.py @@ -8,6 +8,8 @@ from odoo.osv import expression from odoo.addons.fastapi.dependencies import odoo_env +from odoo.addons.spp_api_v2.middleware.auth import get_authenticated_client +from odoo.addons.spp_api_v2.schemas.search_result import SearchResult, create_search_result from fastapi import ( APIRouter, @@ -20,9 +22,7 @@ status, ) -from ..middleware.auth import get_authenticated_client from ..schemas.program import Program -from ..schemas.search_result import SearchResult, create_search_result from ..services.program_service import ProgramService _logger = logging.getLogger(__name__) diff --git a/spp_api_v2_programs/routers/program_filters.py b/spp_api_v2_programs/routers/program_filters.py new file mode 100644 index 000000000..6bee47d87 --- /dev/null +++ b/spp_api_v2_programs/routers/program_filters.py @@ -0,0 +1,80 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""Filter discovery + advanced search endpoints for Program and ProgramMembership. + +Registers the Program / ProgramMembership resources into the shared +``RESOURCE_SERVICES`` registry defined in ``spp_api_v2`` and exposes their +``/_filters`` and ``/_search`` endpoints. The endpoint factories are reused +from the base filter module so behaviour is identical to the pre-split +implementation (OP#1081). +""" + +from odoo.addons.spp_api_v2.routers.filter import ( + RESOURCE_SERVICES, + _create_filter_metadata_endpoint, + _create_search_endpoint, +) +from odoo.addons.spp_api_v2.schemas.bundle import Bundle +from odoo.addons.spp_api_v2.schemas.filter import FilterMetadataResponse + +from fastapi import APIRouter + +from ..services.program_membership_service import ProgramMembershipService +from ..services.program_service import ProgramService + +# Register program resources into the shared filter registry so the generic +# search / filter factories resolve them at request time. +RESOURCE_SERVICES["Program"] = { + "service_class": ProgramService, + "model": "spp.program", + "base_domain": [], + "consent_type": None, +} +RESOURCE_SERVICES["ProgramMembership"] = { + "service_class": ProgramMembershipService, + "model": "spp.program.membership", + "base_domain": [], + "consent_type": "program_membership", +} + +program_filter_router = APIRouter(tags=["Program"], prefix="/Program") +program_membership_filter_router = APIRouter(tags=["ProgramMembership"], prefix="/ProgramMembership") + +# Register endpoints for Program +program_filter_router.add_api_route( + "/_filters", + _create_filter_metadata_endpoint("Program"), + methods=["GET"], + response_model=FilterMetadataResponse, + response_model_exclude_none=True, + summary="Get Program Filters", + description="Get available filters and presets for Program resource", +) +program_filter_router.add_api_route( + "/_search", + _create_search_endpoint("Program"), + methods=["POST"], + response_model=Bundle, + response_model_exclude_none=True, + summary="Advanced Program Search", + description="Search programs with complex filter conditions", +) + +# Register endpoints for ProgramMembership +program_membership_filter_router.add_api_route( + "/_filters", + _create_filter_metadata_endpoint("ProgramMembership"), + methods=["GET"], + response_model=FilterMetadataResponse, + response_model_exclude_none=True, + summary="Get ProgramMembership Filters", + description="Get available filters and presets for ProgramMembership resource", +) +program_membership_filter_router.add_api_route( + "/_search", + _create_search_endpoint("ProgramMembership"), + methods=["POST"], + response_model=Bundle, + response_model_exclude_none=True, + summary="Advanced ProgramMembership Search", + description="Search program memberships with complex filter conditions", +) diff --git a/spp_api_v2/routers/program_membership.py b/spp_api_v2_programs/routers/program_membership.py similarity index 97% rename from spp_api_v2/routers/program_membership.py rename to spp_api_v2_programs/routers/program_membership.py index 4830ca502..0a3b34e6c 100644 --- a/spp_api_v2/routers/program_membership.py +++ b/spp_api_v2_programs/routers/program_membership.py @@ -7,6 +7,10 @@ from odoo.api import Environment from odoo.addons.fastapi.dependencies import odoo_env +from odoo.addons.spp_api_v2.middleware.auth import get_authenticated_client +from odoo.addons.spp_api_v2.schemas.search_result import SearchResult, create_search_result +from odoo.addons.spp_api_v2.services.consent_service import ConsentService +from odoo.addons.spp_api_v2.utils.pagination import fetch_with_consent from fastapi import ( APIRouter, @@ -20,12 +24,8 @@ status, ) -from ..middleware.auth import get_authenticated_client from ..schemas.program_membership import ProgramMembership -from ..schemas.search_result import SearchResult, create_search_result -from ..services.consent_service import ConsentService from ..services.program_membership_service import ProgramMembershipService -from ..utils.pagination import fetch_with_consent _logger = logging.getLogger(__name__) diff --git a/spp_api_v2_programs/schemas/__init__.py b/spp_api_v2_programs/schemas/__init__.py new file mode 100644 index 000000000..aeece600d --- /dev/null +++ b/spp_api_v2_programs/schemas/__init__.py @@ -0,0 +1,4 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + +from . import program +from . import program_membership diff --git a/spp_api_v2/schemas/program.py b/spp_api_v2_programs/schemas/program.py similarity index 96% rename from spp_api_v2/schemas/program.py rename to spp_api_v2_programs/schemas/program.py index 173009de5..f81252bc1 100644 --- a/spp_api_v2/schemas/program.py +++ b/spp_api_v2_programs/schemas/program.py @@ -5,7 +5,7 @@ from pydantic import BaseModel, ConfigDict, Field -from .base import CodeableConcept, Identifier, Period, ResourceMeta +from odoo.addons.spp_api_v2.schemas.base import CodeableConcept, Identifier, Period, ResourceMeta class Program(BaseModel): diff --git a/spp_api_v2/schemas/program_membership.py b/spp_api_v2_programs/schemas/program_membership.py similarity index 96% rename from spp_api_v2/schemas/program_membership.py rename to spp_api_v2_programs/schemas/program_membership.py index 710e5e84a..29810fc69 100644 --- a/spp_api_v2/schemas/program_membership.py +++ b/spp_api_v2_programs/schemas/program_membership.py @@ -6,7 +6,7 @@ from pydantic import BaseModel, ConfigDict, Field -from .base import CodeableConcept, Identifier, Reference, ResourceMeta +from odoo.addons.spp_api_v2.schemas.base import CodeableConcept, Identifier, Reference, ResourceMeta class ProgramMembership(BaseModel): diff --git a/spp_api_v2_programs/security/ir.model.access.csv b/spp_api_v2_programs/security/ir.model.access.csv new file mode 100644 index 000000000..97dd8b917 --- /dev/null +++ b/spp_api_v2_programs/security/ir.model.access.csv @@ -0,0 +1 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink diff --git a/spp_api_v2_programs/services/__init__.py b/spp_api_v2_programs/services/__init__.py new file mode 100644 index 000000000..1dcbdd006 --- /dev/null +++ b/spp_api_v2_programs/services/__init__.py @@ -0,0 +1,4 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + +from . import program_service +from . import program_membership_service diff --git a/spp_api_v2/services/program_membership_service.py b/spp_api_v2_programs/services/program_membership_service.py similarity index 100% rename from spp_api_v2/services/program_membership_service.py rename to spp_api_v2_programs/services/program_membership_service.py diff --git a/spp_api_v2/services/program_service.py b/spp_api_v2_programs/services/program_service.py similarity index 100% rename from spp_api_v2/services/program_service.py rename to spp_api_v2_programs/services/program_service.py diff --git a/spp_api_v2_programs/static/description/icon.png b/spp_api_v2_programs/static/description/icon.png new file mode 100644 index 000000000..c7dbdaaf1 Binary files /dev/null and b/spp_api_v2_programs/static/description/icon.png differ diff --git a/spp_api_v2_programs/static/description/index.html b/spp_api_v2_programs/static/description/index.html new file mode 100644 index 000000000..27b961dcd --- /dev/null +++ b/spp_api_v2_programs/static/description/index.html @@ -0,0 +1,450 @@ + + + + + +OpenSPP API V2 - Programs + + + +
+

OpenSPP API V2 - Programs

+ + +

Production/Stable License: LGPL-3 OpenSPP/OpenSPP2

+

Extends OpenSPP API V2 with REST endpoints for Programs and +Program Memberships. This module was split out of spp_api_v2 so +that the base API module no longer depends on spp_programs — +deployments that don’t use programs (e.g. a registry-only Social +Registry) can ship the API without installing the Programs stack.

+

It auto-installs whenever both spp_api_v2 and spp_programs are +present, so program API functionality is unchanged for deployments that +use programs.

+
+

Key Capabilities

+
    +
  • Program endpoints: read and search programs +(/api/v2/spp/Program)
  • +
  • Program Membership endpoints: read, search, create, and update +beneficiary enrollments (/api/v2/spp/ProgramMembership)
  • +
  • Advanced filtering: /_filters and /_search endpoints for +both resources, registered into the shared API V2 filter framework
  • +
  • OAuth scopes: the program and program_membership +client-scope resources (defined in spp_api_v2) gate access
  • +
+
+
+

UI Location

+

No standalone menu. Endpoints are available under the API V2 app:

+
    +
  • /api/v2/spp/Program, /api/v2/spp/Program/{identifier}
  • +
  • /api/v2/spp/ProgramMembership, +/api/v2/spp/ProgramMembership/{identifier}
  • +
+
+
+

Dependencies

+

spp_api_v2, spp_programs

+

Table of contents

+
+ +
+ +
+
+

19.0.1.0.0

+
    +
  • Split program and program-membership REST endpoints out of +spp_api_v2 into this companion module (#1081), so spp_api_v2 +no longer depends on spp_programs.
  • +
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • OpenSPP.org
  • +
+
+
+

Maintainers

+

Current maintainers:

+

jeremi gonzalesedwin1123 emjay0921

+

This module is part of the OpenSPP/OpenSPP2 project on GitHub.

+

You are welcome to contribute.

+
+
+
+
+ + diff --git a/spp_api_v2_programs/tests/__init__.py b/spp_api_v2_programs/tests/__init__.py new file mode 100644 index 000000000..c8d2b6f67 --- /dev/null +++ b/spp_api_v2_programs/tests/__init__.py @@ -0,0 +1,7 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + +from . import test_program_api +from . import test_program_membership_api +from . import test_program_service +from . import test_program_membership_service +from . import test_scope_enforcement_program diff --git a/spp_api_v2/tests/test_program_api.py b/spp_api_v2_programs/tests/test_program_api.py similarity index 99% rename from spp_api_v2/tests/test_program_api.py rename to spp_api_v2_programs/tests/test_program_api.py index cbe108f71..98300f7c6 100644 --- a/spp_api_v2/tests/test_program_api.py +++ b/spp_api_v2_programs/tests/test_program_api.py @@ -3,7 +3,7 @@ import json -from .common import ApiV2HttpTestCase +from odoo.addons.spp_api_v2.tests.common import ApiV2HttpTestCase class TestProgramAPIEndpoints(ApiV2HttpTestCase): diff --git a/spp_api_v2/tests/test_program_membership_api.py b/spp_api_v2_programs/tests/test_program_membership_api.py similarity index 77% rename from spp_api_v2/tests/test_program_membership_api.py rename to spp_api_v2_programs/tests/test_program_membership_api.py index 17612904a..f06c3231a 100644 --- a/spp_api_v2/tests/test_program_membership_api.py +++ b/spp_api_v2_programs/tests/test_program_membership_api.py @@ -4,7 +4,7 @@ import json from datetime import date -from .common import ApiV2HttpTestCase +from odoo.addons.spp_api_v2.tests.common import ApiV2HttpTestCase class TestProgramMembershipAPIEndpoints(ApiV2HttpTestCase): @@ -304,51 +304,52 @@ def test_create_program_membership_validation_error(self): self.assertEqual(response.status_code, 422) def test_update_program_membership_success(self): - """PUT /ProgramMembership/{id} updates membership""" + """PUT /ProgramMembership/{id} updates membership status""" url = f"{self.api_base_url}/urn:openspp:vocab:id-type%23test_national_id|ENROLL-001" - # Get current version + # Use the current representation as the update payload get_response = self.url_open(url, headers=self._get_headers()) - current_data = json.loads(get_response.content) - version_id = current_data["meta"]["versionId"] - - # Update to paused status - payload = current_data.copy() + payload = json.loads(get_response.content) payload["status"] = "paused" - headers = self._get_headers() - headers["If-Match"] = f'"{version_id}"' + response = self.url_put(url, data=json.dumps(payload), headers=self._get_headers()) - response = self.url_open( - url, - data=json.dumps(payload), - headers=headers, - ) - - # Note: PUT method needs special handling in url_open - # For now, check that endpoint exists (200/405 acceptable) - self.assertIn(response.status_code, [200, 405]) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content) + self.assertEqual(data["status"], "paused") + # Persisted on the record + self.membership.invalidate_recordset() + self.assertEqual(self.membership.state, "paused") - def test_update_program_membership_without_if_match_still_works(self): - """PUT without If-Match still works (optional optimistic locking)""" + def test_update_program_membership_wrong_if_match_returns_409(self): + """PUT with a stale If-Match version returns 409 Conflict""" url = f"{self.api_base_url}/urn:openspp:vocab:id-type%23test_national_id|ENROLL-001" - # Get current data get_response = self.url_open(url, headers=self._get_headers()) - current_data = json.loads(get_response.content) - - # Update without If-Match header - payload = current_data.copy() + payload = json.loads(get_response.content) payload["status"] = "paused" - response = self.url_open( - url, - data=json.dumps(payload), - headers=self._get_headers(), - ) + headers = self._get_headers() + headers["If-Match"] = '"stale-version-0"' + + response = self.url_put(url, data=json.dumps(payload), headers=headers) + self.assertEqual(response.status_code, 409) - # Should still work (200/405 acceptable in test environment) - self.assertIn(response.status_code, [200, 405]) + def test_update_program_membership_not_found_returns_404(self): + """PUT to a non-existent membership returns 404""" + url = f"{self.api_base_url}/urn:openspp:vocab:id-type%23test_national_id|NONEXISTENT-PUT" + + payload = { + "type": "ProgramMembership", + "program": {"reference": "Program/urn:openspp:program|test-enrollment-program"}, + "beneficiary": { + "reference": "Individual/urn:openspp:vocab:id-type%23test_national_id|NONEXISTENT-PUT", + }, + "status": "paused", + } + + response = self.url_put(url, data=json.dumps(payload), headers=self._get_headers()) + self.assertEqual(response.status_code, 404) def test_update_program_membership_no_scope(self): """PUT without update scope returns 403""" @@ -373,14 +374,93 @@ def test_update_program_membership_no_scope(self): "status": "paused", } - response = self.url_open( + response = self.url_put( url, data=json.dumps(payload), headers=self._get_headers(token=read_only_token), ) - # 403 or 405 (method not allowed in test mode) - self.assertIn(response.status_code, [403, 405]) + self.assertEqual(response.status_code, 403) + + def test_create_program_membership_unknown_program_returns_422(self): + """POST referencing a non-existent program hits the create error path (422)""" + new_individual = self.create_test_individual(identifier_value="ERR-ENROLL-001") + self.create_consent( + registrant=new_individual, + grantee_partner=self.client.partner_id, + resource_type="all", + field_access="all", + ) + + payload = { + "type": "ProgramMembership", + "program": {"reference": "Program/urn:openspp:program|does-not-exist-program"}, + "beneficiary": { + "reference": "Individual/urn:openspp:vocab:id-type%23test_national_id|ERR-ENROLL-001", + }, + "status": "enrolled", + } + + response = self.url_open( + self.api_base_url, + data=json.dumps(payload), + headers=self._get_headers(), + ) + + self.assertEqual(response.status_code, 422) + + def test_update_program_membership_bad_identifier_returns_400(self): + """PUT with an identifier lacking the 'system|value' separator returns 400""" + url = f"{self.api_base_url}/no-pipe-identifier" + + payload = { + "type": "ProgramMembership", + "program": {"reference": "Program/urn:openspp:program|test-enrollment-program"}, + "beneficiary": { + "reference": "Individual/urn:openspp:vocab:id-type%23test_national_id|ENROLL-001", + }, + "status": "paused", + } + + response = self.url_put(url, data=json.dumps(payload), headers=self._get_headers()) + self.assertEqual(response.status_code, 400) + + def test_update_program_membership_unknown_program_returns_422(self): + """PUT whose payload references a non-existent program hits the update error path (422)""" + url = f"{self.api_base_url}/urn:openspp:vocab:id-type%23test_national_id|ENROLL-001" + + payload = { + "type": "ProgramMembership", + "program": {"reference": "Program/urn:openspp:program|does-not-exist-program"}, + "beneficiary": { + "reference": "Individual/urn:openspp:vocab:id-type%23test_national_id|ENROLL-001", + }, + "status": "paused", + } + + response = self.url_put(url, data=json.dumps(payload), headers=self._get_headers()) + self.assertEqual(response.status_code, 422) + + def test_search_program_memberships_prev_link(self): + """Search with a non-zero _offset builds a previous-page link""" + # Seed a few more memberships so paging is meaningful + for i in range(3): + ind = self.create_test_individual(identifier_value=f"PREV-{i}") + self.create_test_membership(partner=ind, program=self.program) + self.create_consent( + registrant=ind, + grantee_partner=self.client.partner_id, + resource_type="all", + field_access="all", + ) + + url = f"{self.api_base_url}?_count=2&_offset=2" + response = self.url_open(url, headers=self._get_headers()) + + self.assertEqual(response.status_code, 200) + data = json.loads(response.content) + self.assertIn("links", data) + self.assertIsNotNone(data["links"].get("prev")) def test_consent_filtering_applied(self): """Without consent, read returns 403 (same as individual endpoint pattern)""" diff --git a/spp_api_v2/tests/test_program_membership_service.py b/spp_api_v2_programs/tests/test_program_membership_service.py similarity index 99% rename from spp_api_v2/tests/test_program_membership_service.py rename to spp_api_v2_programs/tests/test_program_membership_service.py index e245140a2..a536b2f25 100644 --- a/spp_api_v2/tests/test_program_membership_service.py +++ b/spp_api_v2_programs/tests/test_program_membership_service.py @@ -5,10 +5,11 @@ from odoo.exceptions import ValidationError -from ..schemas.base import Reference +from odoo.addons.spp_api_v2.schemas.base import Reference +from odoo.addons.spp_api_v2.tests.common import ApiV2TestCase + from ..schemas.program_membership import ProgramMembership from ..services.program_membership_service import ProgramMembershipService -from .common import ApiV2TestCase class TestProgramMembershipService(ApiV2TestCase): diff --git a/spp_api_v2/tests/test_program_service.py b/spp_api_v2_programs/tests/test_program_service.py similarity index 99% rename from spp_api_v2/tests/test_program_service.py rename to spp_api_v2_programs/tests/test_program_service.py index e1e00f338..f364fae15 100644 --- a/spp_api_v2/tests/test_program_service.py +++ b/spp_api_v2_programs/tests/test_program_service.py @@ -1,8 +1,9 @@ # Part of OpenSPP. See LICENSE file for full copyright and licensing details. """Tests for ProgramService""" +from odoo.addons.spp_api_v2.tests.common import ApiV2TestCase + from ..services.program_service import ProgramService -from .common import ApiV2TestCase class TestProgramService(ApiV2TestCase): diff --git a/spp_api_v2_programs/tests/test_scope_enforcement_program.py b/spp_api_v2_programs/tests/test_scope_enforcement_program.py new file mode 100644 index 000000000..0feced987 --- /dev/null +++ b/spp_api_v2_programs/tests/test_scope_enforcement_program.py @@ -0,0 +1,140 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""Scope enforcement tests for the Program Membership endpoints. + +Moved out of spp_api_v2 together with the program API (OP#1081). +""" + +import json + +from odoo.addons.spp_api_v2.tests.common import ApiV2HttpTestCase + + +class TestScopeEnforcementProgramMembership(ApiV2HttpTestCase): + """Test scope enforcement on Program Membership endpoints""" + + def setUp(self): + super().setUp() + # Create test data + self.program = self.create_test_program(name="Test Program", target_type="individual") + self.individual = self.create_test_individual(identifier_value="SCOPE-PM-001") + + # Create test membership + self.membership = self.create_test_membership( + partner=self.individual, + program=self.program, + state="enrolled", + ) + + # Base URL for program membership + self.pm_url = "/api/v2/spp/ProgramMembership" + self.pm_id_url = f"{self.pm_url}/urn:openspp:vocab:id-type%23test_national_id|SCOPE-PM-001" + + def _make_client_without_scope(self, excluded_resource, excluded_action): + """Create a client that has all scopes EXCEPT the specified one""" + scopes = [] + # Give it a different resource scope to prove it's not resource-agnostic + other_resource = "individual" if excluded_resource == "program_membership" else "program_membership" + scopes.append({"resource": other_resource, "action": "all"}) + client = self.create_api_client( + name=f"No {excluded_resource}:{excluded_action}", + scopes=scopes, + require_consent=False, + ) + return client, self.generate_jwt_token(client) + + def test_program_membership_create_requires_scope(self): + """POST /ProgramMembership returns 403 without program_membership:create scope""" + client, token = self._make_client_without_scope("program_membership", "create") + + # Create new individual for enrollment + self.create_test_individual(identifier_value="SCOPE-PM-NEW") + + payload = { + "resourceType": "ProgramMembership", + "status": "enrolled", + "beneficiary": { + "reference": "Individual/urn:openspp:vocab:id-type#test_national_id|SCOPE-PM-NEW", + }, + "program": { + "reference": "Program/urn:openspp:program|test-program", + }, + } + + response = self.url_open( + self.pm_url, + data=json.dumps(payload), + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {token}", + }, + ) + + self.assertEqual(response.status_code, 403) + data = json.loads(response.content) + self.assertIn("Missing required scope", data["detail"]) + self.assertIn("program_membership:create", data["detail"]) + + def test_program_membership_update_requires_scope(self): + """PUT /ProgramMembership/{id} returns 403 without program_membership:update scope""" + client, token = self._make_client_without_scope("program_membership", "update") + + payload = { + "resourceType": "ProgramMembership", + "status": "paused", + "beneficiary": { + "reference": "Individual/urn:openspp:vocab:id-type#test_national_id|SCOPE-PM-001", + }, + "program": { + "reference": "Program/urn:openspp:program|test-program", + }, + } + + response = self.url_put( + self.pm_id_url, + data=json.dumps(payload), + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {token}", + }, + ) + + self.assertEqual(response.status_code, 403) + data = json.loads(response.content) + self.assertIn("Missing required scope", data["detail"]) + self.assertIn("program_membership:update", data["detail"]) + + def test_individual_scope_does_not_grant_program_membership_access(self): + """individual:create scope does not grant access to program_membership endpoints""" + # Create client with only individual:create scope + client = self.create_api_client( + name="Individual Create Only Client", + scopes=[{"resource": "individual", "action": "create"}], + require_consent=False, + ) + token = self.generate_jwt_token(client) + + # Try to create program membership + payload = { + "resourceType": "ProgramMembership", + "status": "enrolled", + "beneficiary": { + "reference": "Individual/urn:openspp:vocab:id-type#test_national_id|SCOPE-PM-001", + }, + "program": { + "reference": "Program/urn:openspp:program|test-program", + }, + } + + response = self.url_open( + self.pm_url, + data=json.dumps(payload), + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {token}", + }, + ) + + self.assertEqual(response.status_code, 403) + data = json.loads(response.content) + self.assertIn("Missing required scope", data["detail"]) + self.assertIn("program_membership:create", data["detail"]) diff --git a/spp_audit/__manifest__.py b/spp_audit/__manifest__.py index 5b5bd4bf0..bef9d14a0 100644 --- a/spp_audit/__manifest__.py +++ b/spp_audit/__manifest__.py @@ -15,7 +15,6 @@ "mail", "spp_registry", "spp_security", - "spp_programs", "spp_service_points", ], "external_dependencies": { diff --git a/spp_audit/data/audit_rule_data.xml b/spp_audit/data/audit_rule_data.xml index 366a174bb..5c2ecbc4f 100644 --- a/spp_audit/data/audit_rule_data.xml +++ b/spp_audit/data/audit_rule_data.xml @@ -1,22 +1,5 @@ - - - Program Rule - spp.program - - - - - - Cycle Rule - spp.cycle - - - Registry Rule @@ -36,106 +19,4 @@ eval="['name', 'agent_home_address', 'agent_number', 'area_id', 'country_id', 'disabled_date', 'disabled_reason', 'display_name', 'dms_directory_ids', 'final_agent_number', 'id', 'individual_ids', 'is_contract_active', 'is_disabled', 'is_tamawwon_onboarded', 'message_attachment_count', 'message_follower_ids', 'message_has_error', 'message_has_error_counter', 'message_has_sms_error', 'message_is_follower', 'message_main_attachment_id', 'message_needaction', 'message_needaction_counter', 'message_partner_ids', 'message_unread', 'message_unread_counter', 'phone_no', 'phone_sanitized', 'res_partner_company_id', 'service_type_id', 'service_type_ids', 'shop_address', 'write_uid', 'website_message_ids', ]" /> - - - - Default Eligibility Manager Rule - spp.program.membership.manager.default - - - Program Rule - program_id - - - - - Deduplication Manager Rule - spp.deduplication.manager.default - - - Program Rule - program_id - - - - - Notification Manager Rule - spp.program.notification.manager.sms - - - Program Rule - program_id - - - - - Program Manager Rule - spp.program.manager.default - - - Program Rule - program_id - - - - - Cycle Manager Rule - spp.cycle.manager.default - - - Program Rule - program_id - - - - - Entitlement Manager Rule - spp.program.entitlement.manager.default - - - Program Rule - program_id - - - - - Payment Manager Rule - spp.program.payment.manager.default - - - Program Rule - program_id - - - - - Cash Entitlement Manager Rule - spp.program.entitlement.manager.cash - - - Program Rule - program_id - - - - - Basket Entitlement Manager Rule - spp.program.entitlement.manager.basket - - - Program Rule - program_id - diff --git a/spp_audit/tests/test_audit_backend.py b/spp_audit/tests/test_audit_backend.py index 2b36f80f2..9c050cc83 100644 --- a/spp_audit/tests/test_audit_backend.py +++ b/spp_audit/tests/test_audit_backend.py @@ -184,7 +184,7 @@ def setUpClass(cls): def test_is_post_to_thread_default_false(self): """Test that is_post_to_thread defaults to False.""" - model = self.env["ir.model"].search([("model", "=", "spp.program")], limit=1) + model = self.env["ir.model"].search([("model", "=", "res.users")], limit=1) # Delete any existing rule for this model to avoid uniqueness constraint existing_rule = self.env["spp.audit.rule"].search([("model_id", "=", model.id)], limit=1) if existing_rule: @@ -249,7 +249,7 @@ def test_rule_creation_logged(self): "OPENSPP_AUDIT_FILE_PATH": self.temp_dir, }, ): - model = self.env["ir.model"].search([("model", "=", "spp.cycle")], limit=1) + model = self.env["ir.model"].search([("model", "=", "res.users")], limit=1) # Delete any existing rule for this model to avoid uniqueness constraint existing_rule = self.env["spp.audit.rule"].search([("model_id", "=", model.id)], limit=1) if existing_rule: @@ -289,7 +289,7 @@ def test_rule_deletion_logged(self): "OPENSPP_AUDIT_FILE_PATH": self.temp_dir, }, ): - model = self.env["ir.model"].search([("model", "=", "spp.cycle")], limit=1) + model = self.env["ir.model"].search([("model", "=", "res.users")], limit=1) # Delete any existing rule for this model to avoid uniqueness constraint existing_rule = self.env["spp.audit.rule"].search([("model_id", "=", model.id)], limit=1) if existing_rule: diff --git a/spp_audit_programs/README.rst b/spp_audit_programs/README.rst new file mode 100644 index 000000000..ffc72d6e8 --- /dev/null +++ b/spp_audit_programs/README.rst @@ -0,0 +1,91 @@ +======================== +OpenSPP Audit - Programs +======================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:3d8ce2b5ae598962e2e52452909f0ed639414e808c903dbbef7f8a984981496a + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png + :target: https://odoo-community.org/page/development-status + :alt: Production/Stable +.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OpenSPP%2FOpenSPP2-lightgray.png?logo=github + :target: https://github.com/OpenSPP/OpenSPP2/tree/19.0/spp_audit_programs + :alt: OpenSPP/OpenSPP2 + +|badge1| |badge2| |badge3| + +Ships the audit rules for program and cycle models. Split out of +``spp_audit`` so the base audit module no longer depends on +``spp_programs`` — modules that only need registry/service-point +auditing (and everything that depends on ``spp_audit``, e.g. +``spp_studio``) no longer pull in the Programs stack. + +Auto-installs whenever both ``spp_audit`` and ``spp_programs`` are +present, so program auditing is unchanged where programs are used. + +Dependencies +~~~~~~~~~~~~ + +``spp_audit``, ``spp_programs`` + +**Table of contents** + +.. contents:: + :local: + +Changelog +========= + +19.0.1.0.0 +~~~~~~~~~~ + +- Split the program/cycle audit rules out of ``spp_audit`` into this + companion (#1085), so ``spp_audit`` no longer depends on + ``spp_programs``. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* OpenSPP.org + +Maintainers +----------- + +.. |maintainer-jeremi| image:: https://github.com/jeremi.png?size=40px + :target: https://github.com/jeremi + :alt: jeremi +.. |maintainer-gonzalesedwin1123| image:: https://github.com/gonzalesedwin1123.png?size=40px + :target: https://github.com/gonzalesedwin1123 + :alt: gonzalesedwin1123 +.. |maintainer-emjay0921| image:: https://github.com/emjay0921.png?size=40px + :target: https://github.com/emjay0921 + :alt: emjay0921 + +Current maintainers: + +|maintainer-jeremi| |maintainer-gonzalesedwin1123| |maintainer-emjay0921| + +This module is part of the `OpenSPP/OpenSPP2 `_ project on GitHub. + +You are welcome to contribute. \ No newline at end of file diff --git a/spp_audit_programs/__init__.py b/spp_audit_programs/__init__.py new file mode 100644 index 000000000..441611e10 --- /dev/null +++ b/spp_audit_programs/__init__.py @@ -0,0 +1 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. diff --git a/spp_audit_programs/__manifest__.py b/spp_audit_programs/__manifest__.py new file mode 100644 index 000000000..9d5ac4fb2 --- /dev/null +++ b/spp_audit_programs/__manifest__.py @@ -0,0 +1,26 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +{ # pylint: disable=pointless-statement + "name": "OpenSPP Audit - Programs", + "category": "OpenSPP/Core", + "version": "19.0.1.0.0", + "sequence": 1, + "author": "OpenSPP.org", + "website": "https://github.com/OpenSPP/OpenSPP2", + "license": "LGPL-3", + "development_status": "Production/Stable", + "maintainers": ["jeremi", "gonzalesedwin1123", "emjay0921"], + "summary": "Audit rules for program and cycle models.", + "depends": [ + "spp_audit", + "spp_programs", + ], + "data": [ + "data/audit_rule_data.xml", + ], + "assets": {}, + "demo": [], + "images": [], + "application": False, + "installable": True, + "auto_install": ["spp_audit", "spp_programs"], +} diff --git a/spp_audit_programs/data/audit_rule_data.xml b/spp_audit_programs/data/audit_rule_data.xml new file mode 100644 index 000000000..5d7eadcde --- /dev/null +++ b/spp_audit_programs/data/audit_rule_data.xml @@ -0,0 +1,125 @@ + + + + + + + Program Rule + spp.program + + + + + + Cycle Rule + spp.cycle + + + + + + Default Eligibility Manager Rule + spp.program.membership.manager.default + + + Program Rule + program_id + + + + + Deduplication Manager Rule + spp.deduplication.manager.default + + + Program Rule + program_id + + + + + Notification Manager Rule + spp.program.notification.manager.sms + + + Program Rule + program_id + + + + + Program Manager Rule + spp.program.manager.default + + + Program Rule + program_id + + + + + Cycle Manager Rule + spp.cycle.manager.default + + + Program Rule + program_id + + + + + Entitlement Manager Rule + spp.program.entitlement.manager.default + + + Program Rule + program_id + + + + + Payment Manager Rule + spp.program.payment.manager.default + + + Program Rule + program_id + + + + + Cash Entitlement Manager Rule + spp.program.entitlement.manager.cash + + + Program Rule + program_id + + + + + Basket Entitlement Manager Rule + spp.program.entitlement.manager.basket + + + Program Rule + program_id + + diff --git a/spp_audit_programs/pyproject.toml b/spp_audit_programs/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/spp_audit_programs/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/spp_audit_programs/readme/DESCRIPTION.md b/spp_audit_programs/readme/DESCRIPTION.md new file mode 100644 index 000000000..8a08292a3 --- /dev/null +++ b/spp_audit_programs/readme/DESCRIPTION.md @@ -0,0 +1,7 @@ +Ships the audit rules for program and cycle models. Split out of `spp_audit` so the base audit module no longer depends on `spp_programs` — modules that only need registry/service-point auditing (and everything that depends on `spp_audit`, e.g. `spp_studio`) no longer pull in the Programs stack. + +Auto-installs whenever both `spp_audit` and `spp_programs` are present, so program auditing is unchanged where programs are used. + +### Dependencies + +`spp_audit`, `spp_programs` diff --git a/spp_audit_programs/readme/HISTORY.md b/spp_audit_programs/readme/HISTORY.md new file mode 100644 index 000000000..9301ca773 --- /dev/null +++ b/spp_audit_programs/readme/HISTORY.md @@ -0,0 +1,3 @@ +### 19.0.1.0.0 + +- Split the program/cycle audit rules out of `spp_audit` into this companion (#1085), so `spp_audit` no longer depends on `spp_programs`. diff --git a/spp_audit_programs/static/description/icon.png b/spp_audit_programs/static/description/icon.png new file mode 100644 index 000000000..b45bba65c Binary files /dev/null and b/spp_audit_programs/static/description/icon.png differ diff --git a/spp_audit_programs/static/description/index.html b/spp_audit_programs/static/description/index.html new file mode 100644 index 000000000..c24e977c1 --- /dev/null +++ b/spp_audit_programs/static/description/index.html @@ -0,0 +1,427 @@ + + + + + +OpenSPP Audit - Programs + + + +
+

OpenSPP Audit - Programs

+ + +

Production/Stable License: LGPL-3 OpenSPP/OpenSPP2

+

Ships the audit rules for program and cycle models. Split out of +spp_audit so the base audit module no longer depends on +spp_programs — modules that only need registry/service-point +auditing (and everything that depends on spp_audit, e.g. +spp_studio) no longer pull in the Programs stack.

+

Auto-installs whenever both spp_audit and spp_programs are +present, so program auditing is unchanged where programs are used.

+
+

Dependencies

+

spp_audit, spp_programs

+

Table of contents

+
+ +
+ +
+
+

19.0.1.0.0

+
    +
  • Split the program/cycle audit rules out of spp_audit into this +companion (#1085), so spp_audit no longer depends on +spp_programs.
  • +
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • OpenSPP.org
  • +
+
+
+

Maintainers

+

Current maintainers:

+

jeremi gonzalesedwin1123 emjay0921

+

This module is part of the OpenSPP/OpenSPP2 project on GitHub.

+

You are welcome to contribute.

+
+
+
+
+ + diff --git a/spp_audit_programs/tests/__init__.py b/spp_audit_programs/tests/__init__.py new file mode 100644 index 000000000..95a1d07ec --- /dev/null +++ b/spp_audit_programs/tests/__init__.py @@ -0,0 +1,3 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + +from . import test_program_audit_rules diff --git a/spp_audit_programs/tests/test_program_audit_rules.py b/spp_audit_programs/tests/test_program_audit_rules.py new file mode 100644 index 000000000..5edbe2c46 --- /dev/null +++ b/spp_audit_programs/tests/test_program_audit_rules.py @@ -0,0 +1,13 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""Program/cycle audit rules are created by this companion (OP#1085).""" + +from odoo.tests import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestProgramAuditRules(TransactionCase): + def test_program_audit_rules_created(self): + """The Program and Cycle audit rules exist once this module is installed.""" + Rule = self.env["spp.audit.rule"] + self.assertTrue(Rule.search([("name", "=", "Program Rule")], limit=1)) + self.assertTrue(Rule.search([("name", "=", "Cycle Rule")], limit=1)) diff --git a/spp_source_tracking/__manifest__.py b/spp_source_tracking/__manifest__.py index 372616de9..39f7cd3cc 100644 --- a/spp_source_tracking/__manifest__.py +++ b/spp_source_tracking/__manifest__.py @@ -11,7 +11,7 @@ "license": "LGPL-3", "development_status": "Production/Stable", "maintainers": ["OpenSPP"], - "depends": ["base", "spp_security", "spp_registry", "spp_programs"], + "depends": ["base", "spp_security", "spp_registry"], "data": [ "security/ir.model.access.csv", "views/merge_provenance_views.xml", diff --git a/spp_source_tracking/migrations/1.0/post-migrate.py b/spp_source_tracking/migrations/1.0/post-migrate.py index e4f45c5af..bde5d7d8b 100644 --- a/spp_source_tracking/migrations/1.0/post-migrate.py +++ b/spp_source_tracking/migrations/1.0/post-migrate.py @@ -44,20 +44,24 @@ def migrate(cr, version): reg_id_count = cr.rowcount _logger.info("Updated %s existing registry IDs with migration source tracking", reg_id_count) - # Update existing program memberships - cr.execute( + # Update existing program memberships — only when the programs stack is + # installed (the source-tracking extension now lives in + # spp_source_tracking_programs; the table may be absent otherwise). + cr.execute("SELECT 1 FROM information_schema.tables WHERE table_name = 'spp_program_membership'") + if cr.fetchone(): + cr.execute( + """ + UPDATE spp_program_membership + SET source_system = 'v1-migration', + collection_method = 'migration', + collection_date = create_date + WHERE source_system IS NULL """ - UPDATE spp_program_membership - SET source_system = 'v1-migration', - collection_method = 'migration', - collection_date = create_date - WHERE source_system IS NULL - """ - ) - membership_count = cr.rowcount - _logger.info( - "Updated %s existing program memberships with migration source tracking", - membership_count, - ) + ) + membership_count = cr.rowcount + _logger.info( + "Updated %s existing program memberships with migration source tracking", + membership_count, + ) _logger.info("Source tracking migration completed successfully") diff --git a/spp_source_tracking/models/__init__.py b/spp_source_tracking/models/__init__.py index bc2576fb5..d9b357a8f 100644 --- a/spp_source_tracking/models/__init__.py +++ b/spp_source_tracking/models/__init__.py @@ -4,4 +4,3 @@ from . import merge_provenance from . import res_partner from . import registry_id -from . import program_membership diff --git a/spp_source_tracking/tests/test_merge_provenance.py b/spp_source_tracking/tests/test_merge_provenance.py index 4c385001c..a320ff6c1 100644 --- a/spp_source_tracking/tests/test_merge_provenance.py +++ b/spp_source_tracking/tests/test_merge_provenance.py @@ -17,8 +17,12 @@ def setUpClass(cls): cls.RegistryId = cls.env["spp.registry.id"] cls.VocabularyCode = cls.env["spp.vocabulary.code"] cls.Vocabulary = cls.env["spp.vocabulary"] - cls.Program = cls.env["spp.program"] - cls.Membership = cls.env["spp.program.membership"] + # spp.program lives in spp_programs; program-membership merge handling + # is exercised only when the programs stack is installed. + cls.has_programs = "spp.program" in cls.env + if cls.has_programs: + cls.Program = cls.env["spp.program"] + cls.Membership = cls.env["spp.program.membership"] # Create a vocabulary for ID types (id_type_id expects spp.vocabulary.code) cls.test_vocab = cls.Vocabulary.create( @@ -36,13 +40,14 @@ def setUpClass(cls): } ) - # Create a test program with unique name - cls.program = cls.Program.create( - { - "name": f"Test Social Program {uuid.uuid4().hex[:8]}", - "target_type": "individual", - } - ) + # Create a test program with unique name (only when programs installed) + if cls.has_programs: + cls.program = cls.Program.create( + { + "name": f"Test Social Program {uuid.uuid4().hex[:8]}", + "target_type": "individual", + } + ) def _create_registrant(self, name, source_system="test-source", **kwargs): """Helper to create a registrant with source tracking.""" @@ -106,6 +111,8 @@ def test_merge_into_transfers_identifiers(self): def test_merge_into_transfers_memberships(self): """Test that merge_into transfers program memberships.""" + if not self.has_programs: + self.skipTest("spp.program not installed (see spp_source_tracking_programs)") source = self._create_registrant("Source Partner") target = self._create_registrant("Target Partner") @@ -123,6 +130,8 @@ def test_merge_into_transfers_memberships(self): def test_merge_into_handles_duplicate_memberships(self): """Test that duplicate memberships are archived during merge.""" + if not self.has_programs: + self.skipTest("spp.program not installed (see spp_source_tracking_programs)") source = self._create_registrant("Source Partner") target = self._create_registrant("Target Partner") diff --git a/spp_source_tracking/tests/test_source_tracking_mixin.py b/spp_source_tracking/tests/test_source_tracking_mixin.py index 38a869da7..4c3498664 100644 --- a/spp_source_tracking/tests/test_source_tracking_mixin.py +++ b/spp_source_tracking/tests/test_source_tracking_mixin.py @@ -17,8 +17,12 @@ def setUpClass(cls): cls.RegistryId = cls.env["spp.registry.id"] cls.VocabularyCode = cls.env["spp.vocabulary.code"] cls.Vocabulary = cls.env["spp.vocabulary"] - cls.Program = cls.env["spp.program"] - cls.Membership = cls.env["spp.program.membership"] + # spp.program lives in spp_programs; the program-membership source + # tracking now lives in the spp_source_tracking_programs companion. + cls.has_programs = "spp.program" in cls.env + if cls.has_programs: + cls.Program = cls.env["spp.program"] + cls.Membership = cls.env["spp.program.membership"] # Create a vocabulary for ID types (id_type_id expects spp.vocabulary.code) cls.test_vocab = cls.Vocabulary.create( @@ -36,13 +40,14 @@ def setUpClass(cls): } ) - # Create a test program with unique name - cls.program = cls.Program.create( - { - "name": f"Test Program {uuid.uuid4().hex[:8]}", - "target_type": "individual", - } - ) + # Create a test program with unique name (only when programs installed) + if cls.has_programs: + cls.program = cls.Program.create( + { + "name": f"Test Program {uuid.uuid4().hex[:8]}", + "target_type": "individual", + } + ) def test_create_sets_default_source_system(self): """Test that create sets default source_system to 'odoo-ui'.""" @@ -189,6 +194,8 @@ def test_registry_id_has_source_tracking(self): def test_program_membership_has_source_tracking(self): """Test that spp.program.membership inherits source tracking mixin.""" + if not self.has_programs: + self.skipTest("spp.program not installed (see spp_source_tracking_programs)") partner = self.Partner.create( { "name": "Test Partner", diff --git a/spp_source_tracking_programs/README.rst b/spp_source_tracking_programs/README.rst new file mode 100644 index 000000000..658d7850c --- /dev/null +++ b/spp_source_tracking_programs/README.rst @@ -0,0 +1,92 @@ +================================== +OpenSPP Source Tracking - Programs +================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:5193f25e638a9f42e8c7e55afd969616845d435904fc553977fa64b7fea0121a + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png + :target: https://odoo-community.org/page/development-status + :alt: Production/Stable +.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OpenSPP%2FOpenSPP2-lightgray.png?logo=github + :target: https://github.com/OpenSPP/OpenSPP2/tree/19.0/spp_source_tracking_programs + :alt: OpenSPP/OpenSPP2 + +|badge1| |badge2| |badge3| + +Adds source/provenance tracking to program memberships +(``spp.program.membership``). Split out of ``spp_source_tracking`` so +the base source-tracking module no longer depends on ``spp_programs`` — +registry-only deployments get source tracking without the Programs +stack. + +Auto-installs whenever both ``spp_source_tracking`` and ``spp_programs`` +are present, so program-membership source tracking is unchanged where +programs are used. + +Dependencies +~~~~~~~~~~~~ + +``spp_source_tracking``, ``spp_programs`` + +**Table of contents** + +.. contents:: + :local: + +Changelog +========= + +19.0.1.0.0 +~~~~~~~~~~ + +- Split the program-membership source-tracking extension out of + ``spp_source_tracking`` into this companion (#1084), so + ``spp_source_tracking`` no longer depends on ``spp_programs``. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* OpenSPP.org + +Maintainers +----------- + +.. |maintainer-jeremi| image:: https://github.com/jeremi.png?size=40px + :target: https://github.com/jeremi + :alt: jeremi +.. |maintainer-gonzalesedwin1123| image:: https://github.com/gonzalesedwin1123.png?size=40px + :target: https://github.com/gonzalesedwin1123 + :alt: gonzalesedwin1123 +.. |maintainer-emjay0921| image:: https://github.com/emjay0921.png?size=40px + :target: https://github.com/emjay0921 + :alt: emjay0921 + +Current maintainers: + +|maintainer-jeremi| |maintainer-gonzalesedwin1123| |maintainer-emjay0921| + +This module is part of the `OpenSPP/OpenSPP2 `_ project on GitHub. + +You are welcome to contribute. \ No newline at end of file diff --git a/spp_source_tracking_programs/__init__.py b/spp_source_tracking_programs/__init__.py new file mode 100644 index 000000000..c4ccea794 --- /dev/null +++ b/spp_source_tracking_programs/__init__.py @@ -0,0 +1,3 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + +from . import models diff --git a/spp_source_tracking_programs/__manifest__.py b/spp_source_tracking_programs/__manifest__.py new file mode 100644 index 000000000..bcef3fc30 --- /dev/null +++ b/spp_source_tracking_programs/__manifest__.py @@ -0,0 +1,26 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +{ # pylint: disable=pointless-statement + "name": "OpenSPP Source Tracking - Programs", + "category": "OpenSPP/Core", + "version": "19.0.1.0.0", + "sequence": 1, + "author": "OpenSPP.org", + "website": "https://github.com/OpenSPP/OpenSPP2", + "license": "LGPL-3", + "development_status": "Production/Stable", + "maintainers": ["jeremi", "gonzalesedwin1123", "emjay0921"], + "summary": "Source tracking for program memberships.", + "depends": [ + "spp_source_tracking", + "spp_programs", + ], + "data": [ + "security/ir.model.access.csv", + ], + "assets": {}, + "demo": [], + "images": [], + "application": False, + "installable": True, + "auto_install": ["spp_source_tracking", "spp_programs"], +} diff --git a/spp_source_tracking_programs/models/__init__.py b/spp_source_tracking_programs/models/__init__.py new file mode 100644 index 000000000..7cfbebd1a --- /dev/null +++ b/spp_source_tracking_programs/models/__init__.py @@ -0,0 +1,3 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + +from . import program_membership diff --git a/spp_source_tracking/models/program_membership.py b/spp_source_tracking_programs/models/program_membership.py similarity index 100% rename from spp_source_tracking/models/program_membership.py rename to spp_source_tracking_programs/models/program_membership.py diff --git a/spp_source_tracking_programs/pyproject.toml b/spp_source_tracking_programs/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/spp_source_tracking_programs/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/spp_source_tracking_programs/readme/DESCRIPTION.md b/spp_source_tracking_programs/readme/DESCRIPTION.md new file mode 100644 index 000000000..a255b6759 --- /dev/null +++ b/spp_source_tracking_programs/readme/DESCRIPTION.md @@ -0,0 +1,7 @@ +Adds source/provenance tracking to program memberships (`spp.program.membership`). Split out of `spp_source_tracking` so the base source-tracking module no longer depends on `spp_programs` — registry-only deployments get source tracking without the Programs stack. + +Auto-installs whenever both `spp_source_tracking` and `spp_programs` are present, so program-membership source tracking is unchanged where programs are used. + +### Dependencies + +`spp_source_tracking`, `spp_programs` diff --git a/spp_source_tracking_programs/readme/HISTORY.md b/spp_source_tracking_programs/readme/HISTORY.md new file mode 100644 index 000000000..30b26bfa7 --- /dev/null +++ b/spp_source_tracking_programs/readme/HISTORY.md @@ -0,0 +1,3 @@ +### 19.0.1.0.0 + +- Split the program-membership source-tracking extension out of `spp_source_tracking` into this companion (#1084), so `spp_source_tracking` no longer depends on `spp_programs`. diff --git a/spp_source_tracking_programs/security/ir.model.access.csv b/spp_source_tracking_programs/security/ir.model.access.csv new file mode 100644 index 000000000..97dd8b917 --- /dev/null +++ b/spp_source_tracking_programs/security/ir.model.access.csv @@ -0,0 +1 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink diff --git a/spp_source_tracking_programs/static/description/icon.png b/spp_source_tracking_programs/static/description/icon.png new file mode 100644 index 000000000..c7dbdaaf1 Binary files /dev/null and b/spp_source_tracking_programs/static/description/icon.png differ diff --git a/spp_source_tracking_programs/static/description/index.html b/spp_source_tracking_programs/static/description/index.html new file mode 100644 index 000000000..11dafb3ae --- /dev/null +++ b/spp_source_tracking_programs/static/description/index.html @@ -0,0 +1,428 @@ + + + + + +OpenSPP Source Tracking - Programs + + + +
+

OpenSPP Source Tracking - Programs

+ + +

Production/Stable License: LGPL-3 OpenSPP/OpenSPP2

+

Adds source/provenance tracking to program memberships +(spp.program.membership). Split out of spp_source_tracking so +the base source-tracking module no longer depends on spp_programs — +registry-only deployments get source tracking without the Programs +stack.

+

Auto-installs whenever both spp_source_tracking and spp_programs +are present, so program-membership source tracking is unchanged where +programs are used.

+
+

Dependencies

+

spp_source_tracking, spp_programs

+

Table of contents

+
+ +
+ +
+
+

19.0.1.0.0

+
    +
  • Split the program-membership source-tracking extension out of +spp_source_tracking into this companion (#1084), so +spp_source_tracking no longer depends on spp_programs.
  • +
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • OpenSPP.org
  • +
+
+
+

Maintainers

+

Current maintainers:

+

jeremi gonzalesedwin1123 emjay0921

+

This module is part of the OpenSPP/OpenSPP2 project on GitHub.

+

You are welcome to contribute.

+
+
+
+
+ + diff --git a/spp_source_tracking_programs/tests/__init__.py b/spp_source_tracking_programs/tests/__init__.py new file mode 100644 index 000000000..8e7ed3c81 --- /dev/null +++ b/spp_source_tracking_programs/tests/__init__.py @@ -0,0 +1,3 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + +from . import test_program_membership_source_tracking diff --git a/spp_source_tracking_programs/tests/test_program_membership_source_tracking.py b/spp_source_tracking_programs/tests/test_program_membership_source_tracking.py new file mode 100644 index 000000000..02ec50b4c --- /dev/null +++ b/spp_source_tracking_programs/tests/test_program_membership_source_tracking.py @@ -0,0 +1,22 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""spp.program.membership gains source tracking via this companion (OP#1084).""" + +import uuid + +from odoo.tests import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestProgramMembershipSourceTracking(TransactionCase): + def test_program_membership_has_source_tracking(self): + """spp.program.membership inherits the source-tracking mixin.""" + program = self.env["spp.program"].create( + {"name": f"Test Program {uuid.uuid4().hex[:8]}", "target_type": "individual"} + ) + partner = self.env["res.partner"].create({"name": "ST Partner", "is_registrant": True}) + membership = ( + self.env["spp.program.membership"] + .with_context(source_system="enrollment-api") + .create({"partner_id": partner.id, "program_id": program.id}) + ) + self.assertEqual(membership.source_system, "enrollment-api") diff --git a/spp_studio/__manifest__.py b/spp_studio/__manifest__.py index ea14eb2ce..dab2e9e6d 100644 --- a/spp_studio/__manifest__.py +++ b/spp_studio/__manifest__.py @@ -14,7 +14,6 @@ "spp_security", "spp_registry", "spp_base_common", - "spp_programs", "spp_user_roles", "spp_custom_field", "spp_cel_domain", # Unified variable system diff --git a/spp_studio/models/logic.py b/spp_studio/models/logic.py index a6356622d..da40d358f 100644 --- a/spp_studio/models/logic.py +++ b/spp_studio/models/logic.py @@ -152,19 +152,28 @@ def write(self, vals): # LIFECYCLE (override state from parent for Studio governance workflow) # ═══════════════════════════════════════════════════════════════════════ - # Override state field with Studio-specific values - # Parent has: draft, active, inactive - # Studio needs: draft, pending, published, archived + # Extend the shared spp.cel.expression.state with Studio's governance + # states. Parent (spp_cel_domain) has: draft, active, inactive. + # Studio adds: pending, published, archived. + # + # Use selection_add (NOT a full selection= override) so the base + # lifecycle values are preserved. Replacing the selection drops "active" + # /"inactive", which then breaks dependent modules whose data relies on + # them (e.g. spp_programs eligibility templates created with + # state="active") depending on module load order. selection_add unions + # the two sets, so the result is load-order independent. Studio's own + # views restrict the visible workflow states via statusbar_visible. state = fields.Selection( - selection=[ - ("draft", "Draft"), + selection_add=[ ("pending", "Pending Approval"), ("published", "Published"), ("archived", "Archived"), ], - string="Status", - default="draft", - required=True, + ondelete={ + "pending": "set default", + "published": "set default", + "archived": "set default", + }, tracking=True, help="Current lifecycle state of the logic (Studio governance workflow)", ) diff --git a/spp_studio/models/studio_mixin.py b/spp_studio/models/studio_mixin.py index 2c6c11de9..8afbc159e 100644 --- a/spp_studio/models/studio_mixin.py +++ b/spp_studio/models/studio_mixin.py @@ -78,13 +78,6 @@ class StudioMixin(models.AbstractModel): copy=False, ) - # Link to programs (optional, for program-specific configs) - program_ids = fields.Many2many( - "spp.program", - string="Programs", - help="If set, this configuration is only visible in these programs. Leave empty for global visibility.", - ) - def action_activate(self): """Activate the configuration, making it available for use.""" self.ensure_one() diff --git a/spp_studio/tests/test_deferred_resolution.py b/spp_studio/tests/test_deferred_resolution.py index babde4c54..13e16ca92 100644 --- a/spp_studio/tests/test_deferred_resolution.py +++ b/spp_studio/tests/test_deferred_resolution.py @@ -9,6 +9,7 @@ """ import logging +import unittest from odoo.tests import TransactionCase, tagged @@ -486,6 +487,12 @@ class TestDeferredResolutionProgramOverrides(TransactionCase, CELTestDataMixin): def setUpClass(cls): """Set up test data.""" super().setUpClass() + # Program-specific overrides require spp_programs (the + # spp.cel.program.parameter model lives there, surfaced via + # spp_studio_programs). When spp_studio is installed without programs, + # skip the whole class instead of erroring in setUpClass. + if "spp.cel.program.parameter" not in cls.env: + raise unittest.SkipTest("spp.cel.program.parameter not available (spp_programs not installed)") cls._test_id = cls._get_unique_test_id() cls.LogicVariable = cls.env["spp.cel.variable"] cls.LogicVariableResolver = cls.env["spp.cel.variable.resolver"] diff --git a/spp_studio/tests/test_studio_mixin.py b/spp_studio/tests/test_studio_mixin.py index 091e2d3c2..1a3cd3b28 100644 --- a/spp_studio/tests/test_studio_mixin.py +++ b/spp_studio/tests/test_studio_mixin.py @@ -41,9 +41,3 @@ def test_audit_fields_defined(self): self.assertIn("activated_date", fields) self.assertIn("deactivated_by_id", fields) self.assertIn("deactivated_date", fields) - - def test_program_ids_field(self): - """Test program_ids field for program-specific configurations.""" - self.assertIn("program_ids", self.StudioField._fields) - field = self.StudioField._fields["program_ids"] - self.assertEqual(field.type, "many2many") diff --git a/spp_studio/wizard/pack_install_wizard.py b/spp_studio/wizard/pack_install_wizard.py index 08458d6ef..eeb28eaab 100644 --- a/spp_studio/wizard/pack_install_wizard.py +++ b/spp_studio/wizard/pack_install_wizard.py @@ -64,11 +64,6 @@ class PackInstallWizard(models.TransientModel): string="Show Preview", default=False, ) - program_id = fields.Many2one( - "spp.program", - string="Program", - help="Optional: Program for constant value lookups", - ) # Results result_message = fields.Text(string="Result", readonly=True) @@ -115,6 +110,15 @@ def _compute_missing_variables(self): wizard.missing_variables = _("All required variables are available.") wizard.has_missing_variables = False + def _get_pack_program_id(self): + """Program id used for constant-value lookups during pack expansion. + + None in the base module. The ``spp_studio_programs`` companion adds a + ``program_id`` field and returns it here, so program-scoped lookups work + without ``spp_studio`` depending on ``spp_programs`` (OP#1083). + """ + return None + def action_preview(self): """Generate preview showing original expressions and runtime resolution preview. @@ -136,7 +140,7 @@ def action_preview(self): previews = [] for item in self.item_ids: - result = resolver.expand_pack_item(item, program_id=self.program_id.id if self.program_id else None) + result = resolver.expand_pack_item(item, program_id=self._get_pack_program_id()) previews.append( Command.create( diff --git a/spp_studio_change_requests/views/studio_cr_type_views.xml b/spp_studio_change_requests/views/studio_cr_type_views.xml index 2600fb555..603d7187d 100644 --- a/spp_studio_change_requests/views/studio_cr_type_views.xml +++ b/spp_studio_change_requests/views/studio_cr_type_views.xml @@ -258,13 +258,6 @@ /> - - - diff --git a/spp_studio_change_requests_programs/README.rst b/spp_studio_change_requests_programs/README.rst new file mode 100644 index 000000000..7792c52e1 --- /dev/null +++ b/spp_studio_change_requests_programs/README.rst @@ -0,0 +1,98 @@ +========================================= +OpenSPP Studio Change Requests - Programs +========================================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:f584665a9d5994d41a99e015339eab8d20335ea5d909cb5efd1ea7fb173aa221 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png + :target: https://odoo-community.org/page/development-status + :alt: Production/Stable +.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OpenSPP%2FOpenSPP2-lightgray.png?logo=github + :target: https://github.com/OpenSPP/OpenSPP2/tree/19.0/spp_studio_change_requests_programs + :alt: OpenSPP/OpenSPP2 + +|badge1| |badge2| |badge3| + +Re-adds program scoping to Studio change request types. Split out of +``spp_studio_change_requests`` so the base module no longer depends +(transitively) on ``spp_programs`` — deployments without programs can +use Studio change requests without installing the Programs stack. + +Auto-installs whenever both ``spp_studio_change_requests`` and +``spp_studio_programs`` are present, so the program-scoping field +reappears on the change request type form exactly where programs are +used. + +Key Capabilities +~~~~~~~~~~~~~~~~ + +- Injects the ``program_ids`` field back into the change request type + form (Technical Info → Programs) + +Dependencies +~~~~~~~~~~~~ + +``spp_studio_change_requests``, ``spp_studio_programs`` + +**Table of contents** + +.. contents:: + :local: + +Changelog +========= + +19.0.1.0.0 +~~~~~~~~~~ + +- Split program scoping (the change request type's ``program_ids`` + field) out of ``spp_studio_change_requests`` into this companion + (#1083), so the base module loads without ``spp_programs``. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* OpenSPP.org + +Maintainers +----------- + +.. |maintainer-jeremi| image:: https://github.com/jeremi.png?size=40px + :target: https://github.com/jeremi + :alt: jeremi +.. |maintainer-gonzalesedwin1123| image:: https://github.com/gonzalesedwin1123.png?size=40px + :target: https://github.com/gonzalesedwin1123 + :alt: gonzalesedwin1123 +.. |maintainer-emjay0921| image:: https://github.com/emjay0921.png?size=40px + :target: https://github.com/emjay0921 + :alt: emjay0921 + +Current maintainers: + +|maintainer-jeremi| |maintainer-gonzalesedwin1123| |maintainer-emjay0921| + +This module is part of the `OpenSPP/OpenSPP2 `_ project on GitHub. + +You are welcome to contribute. \ No newline at end of file diff --git a/spp_studio_change_requests_programs/__init__.py b/spp_studio_change_requests_programs/__init__.py new file mode 100644 index 000000000..441611e10 --- /dev/null +++ b/spp_studio_change_requests_programs/__init__.py @@ -0,0 +1 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. diff --git a/spp_studio_change_requests_programs/__manifest__.py b/spp_studio_change_requests_programs/__manifest__.py new file mode 100644 index 000000000..b59c95ad4 --- /dev/null +++ b/spp_studio_change_requests_programs/__manifest__.py @@ -0,0 +1,26 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +{ # pylint: disable=pointless-statement + "name": "OpenSPP Studio Change Requests - Programs", + "category": "OpenSPP/Configuration", + "version": "19.0.1.0.0", + "sequence": 1, + "author": "OpenSPP.org", + "website": "https://github.com/OpenSPP/OpenSPP2", + "license": "LGPL-3", + "development_status": "Production/Stable", + "maintainers": ["jeremi", "gonzalesedwin1123", "emjay0921"], + "summary": "Program scoping for Studio change request types.", + "depends": [ + "spp_studio_change_requests", + "spp_studio_programs", + ], + "data": [ + "views/studio_cr_type_views.xml", + ], + "assets": {}, + "demo": [], + "images": [], + "application": False, + "installable": True, + "auto_install": ["spp_studio_change_requests", "spp_studio_programs"], +} diff --git a/spp_studio_change_requests_programs/pyproject.toml b/spp_studio_change_requests_programs/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/spp_studio_change_requests_programs/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/spp_studio_change_requests_programs/readme/DESCRIPTION.md b/spp_studio_change_requests_programs/readme/DESCRIPTION.md new file mode 100644 index 000000000..0c02f91d3 --- /dev/null +++ b/spp_studio_change_requests_programs/readme/DESCRIPTION.md @@ -0,0 +1,11 @@ +Re-adds program scoping to Studio change request types. Split out of `spp_studio_change_requests` so the base module no longer depends (transitively) on `spp_programs` — deployments without programs can use Studio change requests without installing the Programs stack. + +Auto-installs whenever both `spp_studio_change_requests` and `spp_studio_programs` are present, so the program-scoping field reappears on the change request type form exactly where programs are used. + +### Key Capabilities + +- Injects the `program_ids` field back into the change request type form (Technical Info → Programs) + +### Dependencies + +`spp_studio_change_requests`, `spp_studio_programs` diff --git a/spp_studio_change_requests_programs/readme/HISTORY.md b/spp_studio_change_requests_programs/readme/HISTORY.md new file mode 100644 index 000000000..89f59abef --- /dev/null +++ b/spp_studio_change_requests_programs/readme/HISTORY.md @@ -0,0 +1,3 @@ +### 19.0.1.0.0 + +- Split program scoping (the change request type's ``program_ids`` field) out of `spp_studio_change_requests` into this companion (#1083), so the base module loads without `spp_programs`. diff --git a/spp_studio_change_requests_programs/static/description/icon.png b/spp_studio_change_requests_programs/static/description/icon.png new file mode 100644 index 000000000..c7dbdaaf1 Binary files /dev/null and b/spp_studio_change_requests_programs/static/description/icon.png differ diff --git a/spp_studio_change_requests_programs/static/description/index.html b/spp_studio_change_requests_programs/static/description/index.html new file mode 100644 index 000000000..d86d3365f --- /dev/null +++ b/spp_studio_change_requests_programs/static/description/index.html @@ -0,0 +1,435 @@ + + + + + +OpenSPP Studio Change Requests - Programs + + + +
+

OpenSPP Studio Change Requests - Programs

+ + +

Production/Stable License: LGPL-3 OpenSPP/OpenSPP2

+

Re-adds program scoping to Studio change request types. Split out of +spp_studio_change_requests so the base module no longer depends +(transitively) on spp_programs — deployments without programs can +use Studio change requests without installing the Programs stack.

+

Auto-installs whenever both spp_studio_change_requests and +spp_studio_programs are present, so the program-scoping field +reappears on the change request type form exactly where programs are +used.

+
+

Key Capabilities

+
    +
  • Injects the program_ids field back into the change request type +form (Technical Info → Programs)
  • +
+
+
+

Dependencies

+

spp_studio_change_requests, spp_studio_programs

+

Table of contents

+
+ +
+ +
+
+

19.0.1.0.0

+
    +
  • Split program scoping (the change request type’s program_ids +field) out of spp_studio_change_requests into this companion +(#1083), so the base module loads without spp_programs.
  • +
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • OpenSPP.org
  • +
+
+
+

Maintainers

+

Current maintainers:

+

jeremi gonzalesedwin1123 emjay0921

+

This module is part of the OpenSPP/OpenSPP2 project on GitHub.

+

You are welcome to contribute.

+
+
+
+
+ + diff --git a/spp_studio_change_requests_programs/tests/__init__.py b/spp_studio_change_requests_programs/tests/__init__.py new file mode 100644 index 000000000..98fb16733 --- /dev/null +++ b/spp_studio_change_requests_programs/tests/__init__.py @@ -0,0 +1,2 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +from . import test_cr_type_programs_view diff --git a/spp_studio_change_requests_programs/tests/test_cr_type_programs_view.py b/spp_studio_change_requests_programs/tests/test_cr_type_programs_view.py new file mode 100644 index 000000000..18f143d63 --- /dev/null +++ b/spp_studio_change_requests_programs/tests/test_cr_type_programs_view.py @@ -0,0 +1,16 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""This companion re-adds program_ids to the change request type form.""" + +from odoo.tests import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestCrTypeProgramsView(TransactionCase): + def test_program_ids_in_form_view(self): + """The change request type form exposes program_ids when installed.""" + CrType = self.env["spp.studio.change.request.type"] + # Field is provided by spp_studio_programs (a dependency) + self.assertIn("program_ids", CrType._fields) + # ...and this companion injects it back into the form arch + arch = CrType.get_view(view_type="form")["arch"] + self.assertIn("program_ids", arch) diff --git a/spp_studio_change_requests_programs/views/studio_cr_type_views.xml b/spp_studio_change_requests_programs/views/studio_cr_type_views.xml new file mode 100644 index 000000000..599e0b587 --- /dev/null +++ b/spp_studio_change_requests_programs/views/studio_cr_type_views.xml @@ -0,0 +1,23 @@ + + + + + spp.studio.change.request.type.form.programs + spp.studio.change.request.type + + + + + + + + + + diff --git a/spp_studio_events/views/studio_event_type_views.xml b/spp_studio_events/views/studio_event_type_views.xml index ef35b6ca9..57788fc3e 100644 --- a/spp_studio_events/views/studio_event_type_views.xml +++ b/spp_studio_events/views/studio_event_type_views.xml @@ -395,23 +395,6 @@
- - - - - - - - - diff --git a/spp_studio_events_programs/README.rst b/spp_studio_events_programs/README.rst new file mode 100644 index 000000000..bebae9ab4 --- /dev/null +++ b/spp_studio_events_programs/README.rst @@ -0,0 +1,97 @@ +================================ +OpenSPP Studio Events - Programs +================================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:e9883cde3c90d81d3ab0ab8cd5bdad09e45118ef076af07c7802f901e390c0e9 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png + :target: https://odoo-community.org/page/development-status + :alt: Production/Stable +.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OpenSPP%2FOpenSPP2-lightgray.png?logo=github + :target: https://github.com/OpenSPP/OpenSPP2/tree/19.0/spp_studio_events_programs + :alt: OpenSPP/OpenSPP2 + +|badge1| |badge2| |badge3| + +Re-adds program scoping to Studio event types. Split out of +``spp_studio_events`` so the base module no longer depends +(transitively) on ``spp_programs`` — deployments without programs can +use Studio events without installing the Programs stack. + +Auto-installs whenever both ``spp_studio_events`` and +``spp_studio_programs`` are present, so the program-scoping page +reappears on the event type form exactly where programs are used. + +Key Capabilities +~~~~~~~~~~~~~~~~ + +- Injects the Programs page (``program_ids``) back into the event type + form + +Dependencies +~~~~~~~~~~~~ + +``spp_studio_events``, ``spp_studio_programs`` + +**Table of contents** + +.. contents:: + :local: + +Changelog +========= + +19.0.1.0.0 +~~~~~~~~~~ + +- Split program scoping (the event type's ``program_ids`` page) out of + ``spp_studio_events`` into this companion (#1083), so the base module + loads without ``spp_programs``. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* OpenSPP.org + +Maintainers +----------- + +.. |maintainer-jeremi| image:: https://github.com/jeremi.png?size=40px + :target: https://github.com/jeremi + :alt: jeremi +.. |maintainer-gonzalesedwin1123| image:: https://github.com/gonzalesedwin1123.png?size=40px + :target: https://github.com/gonzalesedwin1123 + :alt: gonzalesedwin1123 +.. |maintainer-emjay0921| image:: https://github.com/emjay0921.png?size=40px + :target: https://github.com/emjay0921 + :alt: emjay0921 + +Current maintainers: + +|maintainer-jeremi| |maintainer-gonzalesedwin1123| |maintainer-emjay0921| + +This module is part of the `OpenSPP/OpenSPP2 `_ project on GitHub. + +You are welcome to contribute. \ No newline at end of file diff --git a/spp_studio_events_programs/__init__.py b/spp_studio_events_programs/__init__.py new file mode 100644 index 000000000..441611e10 --- /dev/null +++ b/spp_studio_events_programs/__init__.py @@ -0,0 +1 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. diff --git a/spp_studio_events_programs/__manifest__.py b/spp_studio_events_programs/__manifest__.py new file mode 100644 index 000000000..59ffa889e --- /dev/null +++ b/spp_studio_events_programs/__manifest__.py @@ -0,0 +1,26 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +{ # pylint: disable=pointless-statement + "name": "OpenSPP Studio Events - Programs", + "category": "OpenSPP/Configuration", + "version": "19.0.1.0.0", + "sequence": 1, + "author": "OpenSPP.org", + "website": "https://github.com/OpenSPP/OpenSPP2", + "license": "LGPL-3", + "development_status": "Production/Stable", + "maintainers": ["jeremi", "gonzalesedwin1123", "emjay0921"], + "summary": "Program scoping for Studio event types.", + "depends": [ + "spp_studio_events", + "spp_studio_programs", + ], + "data": [ + "views/studio_event_type_views.xml", + ], + "assets": {}, + "demo": [], + "images": [], + "application": False, + "installable": True, + "auto_install": ["spp_studio_events", "spp_studio_programs"], +} diff --git a/spp_studio_events_programs/pyproject.toml b/spp_studio_events_programs/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/spp_studio_events_programs/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/spp_studio_events_programs/readme/DESCRIPTION.md b/spp_studio_events_programs/readme/DESCRIPTION.md new file mode 100644 index 000000000..8290968a7 --- /dev/null +++ b/spp_studio_events_programs/readme/DESCRIPTION.md @@ -0,0 +1,11 @@ +Re-adds program scoping to Studio event types. Split out of `spp_studio_events` so the base module no longer depends (transitively) on `spp_programs` — deployments without programs can use Studio events without installing the Programs stack. + +Auto-installs whenever both `spp_studio_events` and `spp_studio_programs` are present, so the program-scoping page reappears on the event type form exactly where programs are used. + +### Key Capabilities + +- Injects the Programs page (`program_ids`) back into the event type form + +### Dependencies + +`spp_studio_events`, `spp_studio_programs` diff --git a/spp_studio_events_programs/readme/HISTORY.md b/spp_studio_events_programs/readme/HISTORY.md new file mode 100644 index 000000000..f5426c314 --- /dev/null +++ b/spp_studio_events_programs/readme/HISTORY.md @@ -0,0 +1,3 @@ +### 19.0.1.0.0 + +- Split program scoping (the event type's ``program_ids`` page) out of `spp_studio_events` into this companion (#1083), so the base module loads without `spp_programs`. diff --git a/spp_studio_events_programs/static/description/icon.png b/spp_studio_events_programs/static/description/icon.png new file mode 100644 index 000000000..c7dbdaaf1 Binary files /dev/null and b/spp_studio_events_programs/static/description/icon.png differ diff --git a/spp_studio_events_programs/static/description/index.html b/spp_studio_events_programs/static/description/index.html new file mode 100644 index 000000000..87a8b12f2 --- /dev/null +++ b/spp_studio_events_programs/static/description/index.html @@ -0,0 +1,434 @@ + + + + + +OpenSPP Studio Events - Programs + + + +
+

OpenSPP Studio Events - Programs

+ + +

Production/Stable License: LGPL-3 OpenSPP/OpenSPP2

+

Re-adds program scoping to Studio event types. Split out of +spp_studio_events so the base module no longer depends +(transitively) on spp_programs — deployments without programs can +use Studio events without installing the Programs stack.

+

Auto-installs whenever both spp_studio_events and +spp_studio_programs are present, so the program-scoping page +reappears on the event type form exactly where programs are used.

+
+

Key Capabilities

+
    +
  • Injects the Programs page (program_ids) back into the event type +form
  • +
+
+
+

Dependencies

+

spp_studio_events, spp_studio_programs

+

Table of contents

+
+ +
+ +
+
+

19.0.1.0.0

+
    +
  • Split program scoping (the event type’s program_ids page) out of +spp_studio_events into this companion (#1083), so the base module +loads without spp_programs.
  • +
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • OpenSPP.org
  • +
+
+
+

Maintainers

+

Current maintainers:

+

jeremi gonzalesedwin1123 emjay0921

+

This module is part of the OpenSPP/OpenSPP2 project on GitHub.

+

You are welcome to contribute.

+
+
+
+
+ + diff --git a/spp_studio_events_programs/tests/__init__.py b/spp_studio_events_programs/tests/__init__.py new file mode 100644 index 000000000..4b4a9681b --- /dev/null +++ b/spp_studio_events_programs/tests/__init__.py @@ -0,0 +1,2 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +from . import test_event_type_programs_view diff --git a/spp_studio_events_programs/tests/test_event_type_programs_view.py b/spp_studio_events_programs/tests/test_event_type_programs_view.py new file mode 100644 index 000000000..d54e89376 --- /dev/null +++ b/spp_studio_events_programs/tests/test_event_type_programs_view.py @@ -0,0 +1,16 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""This companion re-adds program_ids to the event type form.""" + +from odoo.tests import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestEventTypeProgramsView(TransactionCase): + def test_program_ids_in_form_view(self): + """The event type form exposes program_ids when installed.""" + EventType = self.env["spp.studio.event.type"] + # Field is provided by spp_studio_programs (a dependency) + self.assertIn("program_ids", EventType._fields) + # ...and this companion injects it back into the form arch + arch = EventType.get_view(view_type="form")["arch"] + self.assertIn("program_ids", arch) diff --git a/spp_studio_events_programs/views/studio_event_type_views.xml b/spp_studio_events_programs/views/studio_event_type_views.xml new file mode 100644 index 000000000..3216af93f --- /dev/null +++ b/spp_studio_events_programs/views/studio_event_type_views.xml @@ -0,0 +1,30 @@ + + + + + spp.studio.event.type.form.programs + spp.studio.event.type + + + + + + + + + + + + + + + + diff --git a/spp_studio_programs/README.rst b/spp_studio_programs/README.rst new file mode 100644 index 000000000..1f7bce2e4 --- /dev/null +++ b/spp_studio_programs/README.rst @@ -0,0 +1,100 @@ +========================= +OpenSPP Studio - Programs +========================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:e722af971f449aa84db11c19b7ea7ef4463d86350e8972b00c0d4beb086586a1 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png + :target: https://odoo-community.org/page/development-status + :alt: Production/Stable +.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OpenSPP%2FOpenSPP2-lightgray.png?logo=github + :target: https://github.com/OpenSPP/OpenSPP2/tree/19.0/spp_studio_programs + :alt: OpenSPP/OpenSPP2 + +|badge1| |badge2| |badge3| + +Adds program scoping to OpenSPP Studio (no-code) configurations. Split +out of ``spp_studio`` so the base Studio module no longer depends on +``spp_programs`` — deployments without programs can use Studio without +installing the Programs stack. + +Auto-installs whenever both ``spp_studio`` and ``spp_programs`` are +present, so program-scoping behaviour is unchanged where programs are +used. + +Key Capabilities +~~~~~~~~~~~~~~~~ + +- Adds the optional ``program_ids`` link on Studio configurations + (``spp.studio.mixin``), so fields / logic variables can be scoped to + specific programs +- Adds the optional ``program_id`` on the Logic Pack install wizard for + program-specific constant-value lookups + +Dependencies +~~~~~~~~~~~~ + +``spp_studio``, ``spp_programs`` + +**Table of contents** + +.. contents:: + :local: + +Changelog +========= + +19.0.1.0.0 +~~~~~~~~~~ + +- Split program scoping (config ``program_ids`` and the pack-install + wizard's ``program_id``) out of ``spp_studio`` into this companion + (#1083), so ``spp_studio`` no longer depends on ``spp_programs``. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* OpenSPP.org + +Maintainers +----------- + +.. |maintainer-jeremi| image:: https://github.com/jeremi.png?size=40px + :target: https://github.com/jeremi + :alt: jeremi +.. |maintainer-gonzalesedwin1123| image:: https://github.com/gonzalesedwin1123.png?size=40px + :target: https://github.com/gonzalesedwin1123 + :alt: gonzalesedwin1123 +.. |maintainer-emjay0921| image:: https://github.com/emjay0921.png?size=40px + :target: https://github.com/emjay0921 + :alt: emjay0921 + +Current maintainers: + +|maintainer-jeremi| |maintainer-gonzalesedwin1123| |maintainer-emjay0921| + +This module is part of the `OpenSPP/OpenSPP2 `_ project on GitHub. + +You are welcome to contribute. \ No newline at end of file diff --git a/spp_studio_programs/__init__.py b/spp_studio_programs/__init__.py new file mode 100644 index 000000000..c4ccea794 --- /dev/null +++ b/spp_studio_programs/__init__.py @@ -0,0 +1,3 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + +from . import models diff --git a/spp_studio_programs/__manifest__.py b/spp_studio_programs/__manifest__.py new file mode 100644 index 000000000..a57a5c3a3 --- /dev/null +++ b/spp_studio_programs/__manifest__.py @@ -0,0 +1,26 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +{ # pylint: disable=pointless-statement + "name": "OpenSPP Studio - Programs", + "category": "OpenSPP/Configuration", + "version": "19.0.1.0.0", + "sequence": 1, + "author": "OpenSPP.org", + "website": "https://github.com/OpenSPP/OpenSPP2", + "license": "LGPL-3", + "development_status": "Production/Stable", + "maintainers": ["jeremi", "gonzalesedwin1123", "emjay0921"], + "summary": "Program scoping for OpenSPP Studio configurations.", + "depends": [ + "spp_studio", + "spp_programs", + ], + "data": [ + "security/ir.model.access.csv", + ], + "assets": {}, + "demo": [], + "images": [], + "application": False, + "installable": True, + "auto_install": ["spp_studio", "spp_programs"], +} diff --git a/spp_studio_programs/models/__init__.py b/spp_studio_programs/models/__init__.py new file mode 100644 index 000000000..1f0fcb24c --- /dev/null +++ b/spp_studio_programs/models/__init__.py @@ -0,0 +1,4 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + +from . import studio_mixin +from . import pack_install_wizard diff --git a/spp_studio_programs/models/pack_install_wizard.py b/spp_studio_programs/models/pack_install_wizard.py new file mode 100644 index 000000000..ebcba5941 --- /dev/null +++ b/spp_studio_programs/models/pack_install_wizard.py @@ -0,0 +1,24 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""Add program selection to the Logic Pack install wizard. + +Re-opens ``spp.studio.pack.install.wizard`` to add the optional ``program_id`` +used for constant-value lookups during pack expansion, and supplies it through +the ``_get_pack_program_id`` hook defined on the base wizard (OP#1083). +""" + +from odoo import fields, models + + +class PackInstallWizard(models.TransientModel): + _inherit = "spp.studio.pack.install.wizard" + + program_id = fields.Many2one( + "spp.program", + string="Program", + help="Optional: Program for constant value lookups", + ) + + def _get_pack_program_id(self): + """Use the selected program for constant-value lookups.""" + self.ensure_one() + return self.program_id.id if self.program_id else None diff --git a/spp_studio_programs/models/studio_mixin.py b/spp_studio_programs/models/studio_mixin.py new file mode 100644 index 000000000..2caaf8a41 --- /dev/null +++ b/spp_studio_programs/models/studio_mixin.py @@ -0,0 +1,20 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""Add program-scoping to Studio configurations. + +Re-opens ``spp.studio.mixin`` to add the optional ``program_ids`` link, so +Studio configs (fields, logic variables) can be scoped to specific programs. +This lives in the companion so the base ``spp_studio`` module does not depend +on ``spp_programs`` (OP#1083). +""" + +from odoo import fields, models + + +class StudioMixin(models.AbstractModel): + _inherit = "spp.studio.mixin" + + program_ids = fields.Many2many( + "spp.program", + string="Programs", + help="If set, this configuration is only visible in these programs. Leave empty for global visibility.", + ) diff --git a/spp_studio_programs/pyproject.toml b/spp_studio_programs/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/spp_studio_programs/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/spp_studio_programs/readme/DESCRIPTION.md b/spp_studio_programs/readme/DESCRIPTION.md new file mode 100644 index 000000000..6298a2b90 --- /dev/null +++ b/spp_studio_programs/readme/DESCRIPTION.md @@ -0,0 +1,12 @@ +Adds program scoping to OpenSPP Studio (no-code) configurations. Split out of `spp_studio` so the base Studio module no longer depends on `spp_programs` — deployments without programs can use Studio without installing the Programs stack. + +Auto-installs whenever both `spp_studio` and `spp_programs` are present, so program-scoping behaviour is unchanged where programs are used. + +### Key Capabilities + +- Adds the optional `program_ids` link on Studio configurations (`spp.studio.mixin`), so fields / logic variables can be scoped to specific programs +- Adds the optional `program_id` on the Logic Pack install wizard for program-specific constant-value lookups + +### Dependencies + +`spp_studio`, `spp_programs` diff --git a/spp_studio_programs/readme/HISTORY.md b/spp_studio_programs/readme/HISTORY.md new file mode 100644 index 000000000..d6a37b0bd --- /dev/null +++ b/spp_studio_programs/readme/HISTORY.md @@ -0,0 +1,3 @@ +### 19.0.1.0.0 + +- Split program scoping (config ``program_ids`` and the pack-install wizard's ``program_id``) out of `spp_studio` into this companion (#1083), so `spp_studio` no longer depends on `spp_programs`. diff --git a/spp_studio_programs/security/ir.model.access.csv b/spp_studio_programs/security/ir.model.access.csv new file mode 100644 index 000000000..97dd8b917 --- /dev/null +++ b/spp_studio_programs/security/ir.model.access.csv @@ -0,0 +1 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink diff --git a/spp_studio_programs/static/description/icon.png b/spp_studio_programs/static/description/icon.png new file mode 100644 index 000000000..c7dbdaaf1 Binary files /dev/null and b/spp_studio_programs/static/description/icon.png differ diff --git a/spp_studio_programs/static/description/index.html b/spp_studio_programs/static/description/index.html new file mode 100644 index 000000000..85c642465 --- /dev/null +++ b/spp_studio_programs/static/description/index.html @@ -0,0 +1,437 @@ + + + + + +OpenSPP Studio - Programs + + + +
+

OpenSPP Studio - Programs

+ + +

Production/Stable License: LGPL-3 OpenSPP/OpenSPP2

+

Adds program scoping to OpenSPP Studio (no-code) configurations. Split +out of spp_studio so the base Studio module no longer depends on +spp_programs — deployments without programs can use Studio without +installing the Programs stack.

+

Auto-installs whenever both spp_studio and spp_programs are +present, so program-scoping behaviour is unchanged where programs are +used.

+
+

Key Capabilities

+
    +
  • Adds the optional program_ids link on Studio configurations +(spp.studio.mixin), so fields / logic variables can be scoped to +specific programs
  • +
  • Adds the optional program_id on the Logic Pack install wizard for +program-specific constant-value lookups
  • +
+
+
+

Dependencies

+

spp_studio, spp_programs

+

Table of contents

+
+ +
+ +
+
+

19.0.1.0.0

+
    +
  • Split program scoping (config program_ids and the pack-install +wizard’s program_id) out of spp_studio into this companion +(#1083), so spp_studio no longer depends on spp_programs.
  • +
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • OpenSPP.org
  • +
+
+
+

Maintainers

+

Current maintainers:

+

jeremi gonzalesedwin1123 emjay0921

+

This module is part of the OpenSPP/OpenSPP2 project on GitHub.

+

You are welcome to contribute.

+
+
+
+
+ + diff --git a/spp_studio_programs/tests/__init__.py b/spp_studio_programs/tests/__init__.py new file mode 100644 index 000000000..4459c4581 --- /dev/null +++ b/spp_studio_programs/tests/__init__.py @@ -0,0 +1,4 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + +from . import test_studio_mixin_programs +from . import test_pack_install_wizard_programs diff --git a/spp_studio_programs/tests/test_pack_install_wizard_programs.py b/spp_studio_programs/tests/test_pack_install_wizard_programs.py new file mode 100644 index 000000000..a45612e00 --- /dev/null +++ b/spp_studio_programs/tests/test_pack_install_wizard_programs.py @@ -0,0 +1,33 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""The pack-install wizard's program_id and _get_pack_program_id hook.""" + +from odoo.tests import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestPackInstallWizardPrograms(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.Wizard = cls.env["spp.studio.pack.install.wizard"] + cls.program = cls.env["spp.program"].create( + { + "name": "Pack Lookup Program", + "target_type": "individual", + } + ) + + def test_program_id_field_added(self): + """The companion adds the optional program_id field to the wizard.""" + self.assertIn("program_id", self.Wizard._fields) + self.assertEqual(self.Wizard._fields["program_id"].comodel_name, "spp.program") + + def test_get_pack_program_id_returns_selected_program(self): + """_get_pack_program_id returns the selected program id.""" + wizard = self.Wizard.new({"program_id": self.program.id}) + self.assertEqual(wizard._get_pack_program_id(), self.program.id) + + def test_get_pack_program_id_none_when_unset(self): + """_get_pack_program_id returns None when no program is selected.""" + wizard = self.Wizard.new({}) + self.assertIsNone(wizard._get_pack_program_id()) diff --git a/spp_studio_programs/tests/test_studio_mixin_programs.py b/spp_studio_programs/tests/test_studio_mixin_programs.py new file mode 100644 index 000000000..06bcaa895 --- /dev/null +++ b/spp_studio_programs/tests/test_studio_mixin_programs.py @@ -0,0 +1,13 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""The program_ids scoping field is added to Studio configs by this companion.""" + +from odoo.tests import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestStudioMixinPrograms(TransactionCase): + def test_program_ids_field(self): + """program_ids many2many is present on Studio configurations.""" + StudioField = self.env["spp.studio.field"] + self.assertIn("program_ids", StudioField._fields) + self.assertEqual(StudioField._fields["program_ids"].type, "many2many")