From e1ec833fdb0fea62bec83bd66c4c3b28a5fbc69b Mon Sep 17 00:00:00 2001 From: mkumar-02 Date: Sat, 4 May 2024 13:45:45 +0530 Subject: [PATCH] Migrated Registry and Bank API module. Signed-off-by: mkumar-02 --- .pre-commit-config.yaml | 2 - README.md | 4 +- g2p_bank_rest_api/__init__.py | 4 +- g2p_bank_rest_api/__manifest__.py | 2 +- g2p_bank_rest_api/models/bank_details.py | 28 ---- g2p_bank_rest_api/models/group_membership.py | 7 - g2p_bank_rest_api/models/registrant.py | 11 -- .../{services => routers}/__init__.py | 0 .../process_group_mixin.py | 4 +- .../process_individual_mixin.py | 10 +- .../{models => schemas}/__init__.py | 0 g2p_bank_rest_api/schemas/bank_details.py | 11 ++ g2p_bank_rest_api/schemas/group_membership.py | 9 + g2p_bank_rest_api/schemas/registrant.py | 13 ++ .../static/description/index.html | 7 +- .../static/description/index.html | 7 +- g2p_registry_rest_api/README.rst | 8 +- g2p_registry_rest_api/__init__.py | 6 +- g2p_registry_rest_api/__manifest__.py | 7 +- g2p_registry_rest_api/controllers/__init__.py | 2 - g2p_registry_rest_api/controllers/main.py | 8 - .../data/fastapi_endpoint_registry.xml | 16 ++ g2p_registry_rest_api/http.py | 79 --------- g2p_registry_rest_api/models/__init__.py | 7 +- .../models/fastapi_endpoint_registry.py | 25 +++ g2p_registry_rest_api/models/group.py | 39 ----- .../models/group_membership.py | 65 -------- .../models/group_search_param.py | 8 - g2p_registry_rest_api/models/individual.py | 61 ------- .../models/individual_search_param.py | 7 - .../models/naive_orm_model.py | 13 -- g2p_registry_rest_api/models/registrant.py | 142 ---------------- g2p_registry_rest_api/readme/CONTRIBUTORS.rst | 8 +- .../{services => routers}/__init__.py | 4 +- g2p_registry_rest_api/routers/group.py | 134 +++++++++++++++ g2p_registry_rest_api/routers/individual.py | 91 ++++++++++ .../process_group_mixin.py | 11 +- .../process_individual_mixin.py | 20 ++- g2p_registry_rest_api/schemas/__init__.py | 6 + .../{models => schemas}/error_response.py | 0 g2p_registry_rest_api/schemas/group.py | 22 +++ .../schemas/group_membership.py | 34 ++++ g2p_registry_rest_api/schemas/individual.py | 24 +++ .../schemas/naive_orm_model.py | 6 + g2p_registry_rest_api/schemas/registrant.py | 58 +++++++ g2p_registry_rest_api/services/group.py | 157 ------------------ g2p_registry_rest_api/services/individual.py | 157 ------------------ .../static/description/index.html | 13 +- g2p_registry_rest_api/tests/__init__.py | 0 requirements.txt | 2 + 50 files changed, 505 insertions(+), 854 deletions(-) delete mode 100644 g2p_bank_rest_api/models/bank_details.py delete mode 100644 g2p_bank_rest_api/models/group_membership.py delete mode 100644 g2p_bank_rest_api/models/registrant.py rename g2p_bank_rest_api/{services => routers}/__init__.py (100%) rename g2p_bank_rest_api/{services => routers}/process_group_mixin.py (72%) rename g2p_bank_rest_api/{services => routers}/process_individual_mixin.py (69%) rename g2p_bank_rest_api/{models => schemas}/__init__.py (100%) create mode 100644 g2p_bank_rest_api/schemas/bank_details.py create mode 100644 g2p_bank_rest_api/schemas/group_membership.py create mode 100644 g2p_bank_rest_api/schemas/registrant.py delete mode 100644 g2p_registry_rest_api/controllers/__init__.py delete mode 100644 g2p_registry_rest_api/controllers/main.py create mode 100644 g2p_registry_rest_api/data/fastapi_endpoint_registry.xml delete mode 100644 g2p_registry_rest_api/http.py create mode 100644 g2p_registry_rest_api/models/fastapi_endpoint_registry.py delete mode 100644 g2p_registry_rest_api/models/group.py delete mode 100644 g2p_registry_rest_api/models/group_membership.py delete mode 100644 g2p_registry_rest_api/models/group_search_param.py delete mode 100644 g2p_registry_rest_api/models/individual.py delete mode 100644 g2p_registry_rest_api/models/individual_search_param.py delete mode 100644 g2p_registry_rest_api/models/naive_orm_model.py delete mode 100644 g2p_registry_rest_api/models/registrant.py rename g2p_registry_rest_api/{services => routers}/__init__.py (100%) create mode 100644 g2p_registry_rest_api/routers/group.py create mode 100644 g2p_registry_rest_api/routers/individual.py rename g2p_registry_rest_api/{services => routers}/process_group_mixin.py (79%) rename g2p_registry_rest_api/{services => routers}/process_individual_mixin.py (82%) create mode 100644 g2p_registry_rest_api/schemas/__init__.py rename g2p_registry_rest_api/{models => schemas}/error_response.py (100%) create mode 100644 g2p_registry_rest_api/schemas/group.py create mode 100644 g2p_registry_rest_api/schemas/group_membership.py create mode 100644 g2p_registry_rest_api/schemas/individual.py create mode 100644 g2p_registry_rest_api/schemas/naive_orm_model.py create mode 100644 g2p_registry_rest_api/schemas/registrant.py delete mode 100644 g2p_registry_rest_api/services/group.py delete mode 100644 g2p_registry_rest_api/services/individual.py create mode 100644 g2p_registry_rest_api/tests/__init__.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 02f019bc..18fc0c06 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,9 +1,7 @@ exclude: | (?x) # NOT INSTALLABLE ADDONS - ^g2p_bank_rest_api/| ^g2p_registry_addl_info_rest_api/| - ^g2p_registry_rest_api/| ^g2p_registry_rest_api_extension_demo/| # END NOT INSTALLABLE ADDONS # Files and folders generated by bots, to avoid loops diff --git a/README.md b/README.md index cb04a618..776f2ccd 100644 --- a/README.md +++ b/README.md @@ -22,20 +22,20 @@ Available addons addon | version | maintainers | summary --- | --- | --- | --- [g2p_bank](g2p_bank/) | 17.0.1.0.0 | | G2P Registry: Bank Details +[g2p_bank_rest_api](g2p_bank_rest_api/) | 17.0.1.0.0 | | G2P Registry: Bank Details Rest API [g2p_registry_addl_info](g2p_registry_addl_info/) | 17.0.1.0.0 | | G2P Registry: Additional Info [g2p_registry_base](g2p_registry_base/) | 17.0.1.0.0 | | G2P Registry: Base [g2p_registry_group](g2p_registry_group/) | 17.0.1.0.0 | | G2P Registry: Groups [g2p_registry_individual](g2p_registry_individual/) | 17.0.1.0.0 | | G2P Registry: Individual [g2p_registry_membership](g2p_registry_membership/) | 17.0.1.0.0 | | G2P Registry: Membership +[g2p_registry_rest_api](g2p_registry_rest_api/) | 17.0.1.0.0 | | G2P Registry: Rest API Unported addons --------------- addon | version | maintainers | summary --- | --- | --- | --- -[g2p_bank_rest_api](g2p_bank_rest_api/) | 17.0.1.0.0 (unported) | | G2P Registry: Bank Details Rest API [g2p_registry_addl_info_rest_api](g2p_registry_addl_info_rest_api/) | 17.0.1.0.0 (unported) | | G2P Registry: Additional Info REST API -[g2p_registry_rest_api](g2p_registry_rest_api/) | 17.0.1.0.0 (unported) | | G2P Registry: Rest API [g2p_registry_rest_api_extension_demo](g2p_registry_rest_api_extension_demo/) | 17.0.1.0.0 (unported) | | G2P Registry: Rest API Extension Demo [//]: # (end addons) diff --git a/g2p_bank_rest_api/__init__.py b/g2p_bank_rest_api/__init__.py index 3333faa0..b4a22ad3 100644 --- a/g2p_bank_rest_api/__init__.py +++ b/g2p_bank_rest_api/__init__.py @@ -1,3 +1,3 @@ # Part of OpenG2P Registry. See LICENSE file for full copyright and licensing details. -from . import models -from . import services +from . import schemas +from . import routers diff --git a/g2p_bank_rest_api/__manifest__.py b/g2p_bank_rest_api/__manifest__.py index ba23062f..288d8e70 100644 --- a/g2p_bank_rest_api/__manifest__.py +++ b/g2p_bank_rest_api/__manifest__.py @@ -17,6 +17,6 @@ "demo": [], "images": [], "application": False, - "installable": False, + "installable": True, "auto_install": False, } diff --git a/g2p_bank_rest_api/models/bank_details.py b/g2p_bank_rest_api/models/bank_details.py deleted file mode 100644 index 9702ba88..00000000 --- a/g2p_bank_rest_api/models/bank_details.py +++ /dev/null @@ -1,28 +0,0 @@ -from pydantic import validator - -from odoo.http import request - -from odoo.addons.g2p_registry_rest_api.exceptions import base_exception, error_codes -from odoo.addons.g2p_registry_rest_api.models import naive_orm_model - - -class BankDetailsIn(naive_orm_model.NaiveOrmModel): - bank_name: str = None - acc_number: str = None - - @validator("acc_number") - def validate_acc_number_duplicate(cls, value): # noqa: B902 - if value: - acc_num = request.env["res.partner.bank"].search([("acc_number", "=", value)]) - if acc_num: - raise base_exception.G2PApiValidationError( - error_message=error_codes.G2PErrorCodes.G2P_REQ_009.get_error_message(), - error_code=error_codes.G2PErrorCodes.G2P_REQ_009.get_error_code(), - error_description="Account number - %s cannot be duplicate." % value, - ) - return value - - -class BankDetailsOut(naive_orm_model.NaiveOrmModel): - bank_name: str = None - acc_number: str = None diff --git a/g2p_bank_rest_api/models/group_membership.py b/g2p_bank_rest_api/models/group_membership.py deleted file mode 100644 index 0a35f5a1..00000000 --- a/g2p_bank_rest_api/models/group_membership.py +++ /dev/null @@ -1,7 +0,0 @@ -from odoo.addons.g2p_registry_rest_api.models import group_membership - -from . import bank_details - - -class GroupMembersInfoIn(group_membership.GroupMembersInfoIn, extends=group_membership.GroupMembersInfoIn): - bank_ids: list[bank_details.BankDetailsIn] | None diff --git a/g2p_bank_rest_api/models/registrant.py b/g2p_bank_rest_api/models/registrant.py deleted file mode 100644 index 09d3b6de..00000000 --- a/g2p_bank_rest_api/models/registrant.py +++ /dev/null @@ -1,11 +0,0 @@ -from odoo.addons.g2p_registry_rest_api.models import registrant - -from . import bank_details - - -class RegistrantAddlInfoIn(registrant.RegistrantInfoIn, extends=registrant.RegistrantInfoIn): - bank_ids: list[bank_details.BankDetailsIn] | None - - -class RegistrantAddlInfoOut(registrant.RegistrantInfoOut, extends=registrant.RegistrantInfoOut): - bank_ids: list[bank_details.BankDetailsOut] | None diff --git a/g2p_bank_rest_api/services/__init__.py b/g2p_bank_rest_api/routers/__init__.py similarity index 100% rename from g2p_bank_rest_api/services/__init__.py rename to g2p_bank_rest_api/routers/__init__.py diff --git a/g2p_bank_rest_api/services/process_group_mixin.py b/g2p_bank_rest_api/routers/process_group_mixin.py similarity index 72% rename from g2p_bank_rest_api/services/process_group_mixin.py rename to g2p_bank_rest_api/routers/process_group_mixin.py index 4ea773bf..238177cd 100644 --- a/g2p_bank_rest_api/services/process_group_mixin.py +++ b/g2p_bank_rest_api/routers/process_group_mixin.py @@ -1,7 +1,7 @@ -from odoo.addons.component.core import AbstractComponent +from odoo import models -class ProcessGroupMixin(AbstractComponent): +class ProcessGroupMixin(models.AbstractModel): _inherit = "process_group.rest.mixin" def _process_group(self, group_info): diff --git a/g2p_bank_rest_api/services/process_individual_mixin.py b/g2p_bank_rest_api/routers/process_individual_mixin.py similarity index 69% rename from g2p_bank_rest_api/services/process_individual_mixin.py rename to g2p_bank_rest_api/routers/process_individual_mixin.py index 6e34dbe1..277a6301 100644 --- a/g2p_bank_rest_api/services/process_individual_mixin.py +++ b/g2p_bank_rest_api/routers/process_individual_mixin.py @@ -1,23 +1,23 @@ -from odoo.addons.component.core import AbstractComponent +from odoo import models -class ProcessIndividualMixin(AbstractComponent): +class ProcessIndividualMixin(models.AbstractModel): _inherit = "process_individual.rest.mixin" def _process_individual(self, individual): res = super()._process_individual(individual) - if individual.dict().get("bank_ids", None): + if individual.model_dump().get("bank_ids", None): res["bank_ids"] = self._process_bank_ids(individual) return res def _process_bank_ids(self, registrant_info): bank_ids = [] for rec in registrant_info.bank_ids: - bank_name = self.env["res.bank"].search([("name", "=", rec.bank_name)]) + bank_name = self.env["res.bank"].sudo().search([("name", "=", rec.bank_name)]) if bank_name: bank_name = bank_name[0] else: - bank_name = self.env["res.bank"].create({"name": rec.bank_name}) + bank_name = self.env["res.bank"].sudo().create({"name": rec.bank_name}) bank_ids.append( ( 0, diff --git a/g2p_bank_rest_api/models/__init__.py b/g2p_bank_rest_api/schemas/__init__.py similarity index 100% rename from g2p_bank_rest_api/models/__init__.py rename to g2p_bank_rest_api/schemas/__init__.py diff --git a/g2p_bank_rest_api/schemas/bank_details.py b/g2p_bank_rest_api/schemas/bank_details.py new file mode 100644 index 00000000..880a55a5 --- /dev/null +++ b/g2p_bank_rest_api/schemas/bank_details.py @@ -0,0 +1,11 @@ +from odoo.addons.g2p_registry_rest_api.schemas import naive_orm_model + + +class BankDetailsRequest(naive_orm_model.NaiveOrmModel): + bank_name: str = None + acc_number: str = None + + +class BankDetailsResponse(naive_orm_model.NaiveOrmModel): + bank_name: str = None + acc_number: str = None diff --git a/g2p_bank_rest_api/schemas/group_membership.py b/g2p_bank_rest_api/schemas/group_membership.py new file mode 100644 index 00000000..46dc19b5 --- /dev/null +++ b/g2p_bank_rest_api/schemas/group_membership.py @@ -0,0 +1,9 @@ +from odoo.addons.g2p_registry_rest_api.schemas import group_membership + +from . import bank_details + + +class GroupMembersInfoRequest( + group_membership.GroupMembersInfoRequest, extends=group_membership.GroupMembersInfoRequest +): + bank_ids: list[bank_details.BankDetailsRequest] | None diff --git a/g2p_bank_rest_api/schemas/registrant.py b/g2p_bank_rest_api/schemas/registrant.py new file mode 100644 index 00000000..f37c05c6 --- /dev/null +++ b/g2p_bank_rest_api/schemas/registrant.py @@ -0,0 +1,13 @@ +from odoo.addons.g2p_registry_rest_api.schemas import registrant + +from . import bank_details + + +class RegistrantAddlInfoRequest(registrant.RegistrantInfoRequest, extends=registrant.RegistrantInfoRequest): + bank_ids: list[bank_details.BankDetailsRequest] | None = None + + +class RegistrantAddlInfoResponse( + registrant.RegistrantInfoResponse, extends=registrant.RegistrantInfoResponse +): + bank_ids: list[bank_details.BankDetailsResponse] | None = None diff --git a/g2p_bank_rest_api/static/description/index.html b/g2p_bank_rest_api/static/description/index.html index ce439934..925ef11f 100644 --- a/g2p_bank_rest_api/static/description/index.html +++ b/g2p_bank_rest_api/static/description/index.html @@ -8,10 +8,11 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ +:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. +Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -274,7 +275,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: grey; } /* line numbers */ +pre.code .ln { color: gray; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -300,7 +301,7 @@ span.pre { white-space: pre } -span.problematic { +span.problematic, pre.problematic { color: red } span.section-subtitle { diff --git a/g2p_registry_base/static/description/index.html b/g2p_registry_base/static/description/index.html index bb8cc4bb..23de9f5a 100644 --- a/g2p_registry_base/static/description/index.html +++ b/g2p_registry_base/static/description/index.html @@ -8,10 +8,11 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ +:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. +Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -274,7 +275,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: grey; } /* line numbers */ +pre.code .ln { color: gray; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -300,7 +301,7 @@ span.pre { white-space: pre } -span.problematic { +span.problematic, pre.problematic { color: red } span.section-subtitle { diff --git a/g2p_registry_rest_api/README.rst b/g2p_registry_rest_api/README.rst index 53b0610c..089d5d79 100644 --- a/g2p_registry_rest_api/README.rst +++ b/g2p_registry_rest_api/README.rst @@ -52,13 +52,7 @@ Authors Contributors ~~~~~~~~~~~~ -`OpenSPP `__ donated the original code to the project. - -Contributors include: - -* Edwin Gonzales (`Newlogic `__) -* Jeremi Joslin (`Newlogic `__) -* Michael Gonzales (`Newlogic `__) +* Manoj Kumar (``__) Maintainers ~~~~~~~~~~~ diff --git a/g2p_registry_rest_api/__init__.py b/g2p_registry_rest_api/__init__.py index ab1d6c7a..23b33128 100644 --- a/g2p_registry_rest_api/__init__.py +++ b/g2p_registry_rest_api/__init__.py @@ -1,6 +1,6 @@ # Part of OpenG2P Registry. See LICENSE file for full copyright and licensing details. from . import models -from . import services -from . import controllers -from . import http +from . import routers + +# from . import http from . import exceptions diff --git a/g2p_registry_rest_api/__manifest__.py b/g2p_registry_rest_api/__manifest__.py index 8f40d6e6..ed4c39fa 100644 --- a/g2p_registry_rest_api/__manifest__.py +++ b/g2p_registry_rest_api/__manifest__.py @@ -9,14 +9,13 @@ "license": "Other OSI approved licence", "development_status": "Alpha", "depends": [ - "g2p_registry_base", - "g2p_registry_group", - "g2p_registry_individual", + "g2p_registry_membership", "fastapi", "extendable_fastapi", ], "external_dependencies": {"python": ["extendable-pydantic", "pydantic"]}, "data": [ + "data/fastapi_endpoint_registry.xml", "security/g2p_security.xml", "security/ir.model.access.csv", ], @@ -24,6 +23,6 @@ "demo": [], "images": [], "application": False, - "installable": False, + "installable": True, "auto_install": False, } diff --git a/g2p_registry_rest_api/controllers/__init__.py b/g2p_registry_rest_api/controllers/__init__.py deleted file mode 100644 index 4159ea5e..00000000 --- a/g2p_registry_rest_api/controllers/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Part of OpenG2P Registry. See LICENSE file for full copyright and licensing details. -from . import main diff --git a/g2p_registry_rest_api/controllers/main.py b/g2p_registry_rest_api/controllers/main.py deleted file mode 100644 index 4944fc6b..00000000 --- a/g2p_registry_rest_api/controllers/main.py +++ /dev/null @@ -1,8 +0,0 @@ -# Part of OpenG2P Registry. See LICENSE file for full copyright and licensing details. -from odoo.addons.base_rest.controllers import main - - -class RegistryApiController(main.RestController): - _root_path = "/api/v1/registry/" - _collection_name = "base.rest.registry.services" - _default_auth = "user" diff --git a/g2p_registry_rest_api/data/fastapi_endpoint_registry.xml b/g2p_registry_rest_api/data/fastapi_endpoint_registry.xml new file mode 100644 index 00000000..138f86b2 --- /dev/null +++ b/g2p_registry_rest_api/data/fastapi_endpoint_registry.xml @@ -0,0 +1,16 @@ + + + + OpenG2P FastAPI Endpoint + This module implements OpenG2P APIs. + registry + /api/v1/registry + + + + + diff --git a/g2p_registry_rest_api/http.py b/g2p_registry_rest_api/http.py deleted file mode 100644 index 70997a75..00000000 --- a/g2p_registry_rest_api/http.py +++ /dev/null @@ -1,79 +0,0 @@ -import json - -from werkzeug.exceptions import BadRequest, HTTPException, InternalServerError - -import odoo -from odoo.http import Root -from odoo.tools.config import config - -from odoo.addons.base_rest.core import _rest_services_routes -from odoo.addons.base_rest.http import HttpRestRequest, JSONEncoder, wrapJsonException - -from .exceptions.base_exception import G2PApiException, G2PApiValidationError -from .models.error_response import G2PErrorResponse - - -def g2pFixException(exception, original_exception=None): - get_original_headers = HTTPException(exception).get_headers - - def get_body(environ=None, scope=None): - if original_exception and isinstance(original_exception, G2PApiException): - res = G2PErrorResponse( - errorCode=original_exception.error_code, - errorMessage=original_exception.error_message, - errorDescription=original_exception.error_description or "", - ).dict() - else: - extra_info = getattr(exception, "rest_json_info", None) - extra_info = json.dumps(extra_info) if extra_info else "" - res = G2PErrorResponse( - errorCode=exception.code, - errorMessage=exception.get_description(environ), - errorDescription=extra_info, - ).dict() - if config.get_misc("base_rest", "dev_mode"): - # return exception info only if base_rest is in dev_mode - res.update({"traceback": exception.traceback}) - return JSONEncoder().encode(res) - - def get_headers(environ=None, scope=None): - """Get a list of headers.""" - _headers = [("Content-Type", "application/json")] - for key, value in get_original_headers(environ=environ, scope=scope): - if key != "Content-Type": - _headers.append(key, value) - return _headers - - exception.get_headers = get_headers - exception.get_body = get_body - return exception - - -class G2PHttpRestRequest(HttpRestRequest): - def _handle_exception(self, exception): - res = super()._handle_exception(exception) - if isinstance(exception, G2PApiValidationError): - res = wrapJsonException(BadRequest(exception.args[0])) - g2pFixException(res, exception) - elif isinstance(exception, G2PApiException): - res = wrapJsonException(InternalServerError(exception.args[0])) - g2pFixException(res, exception) - - return res - - -ori_get_request = Root.get_request - - -def get_request(self, httprequest): - db = httprequest.session.db - if db and odoo.service.db.exp_db_exist(db): - odoo.registry(db) - rest_routes = _rest_services_routes.get(db, []) - for root_path in rest_routes: - if httprequest.path.startswith(root_path): - return G2PHttpRestRequest(httprequest) - return ori_get_request(self, httprequest) - - -Root.get_request = get_request diff --git a/g2p_registry_rest_api/models/__init__.py b/g2p_registry_rest_api/models/__init__.py index 2abc03a3..7d7f5819 100644 --- a/g2p_registry_rest_api/models/__init__.py +++ b/g2p_registry_rest_api/models/__init__.py @@ -1,6 +1,3 @@ # Part of OpenG2P Registry. See LICENSE file for full copyright and licensing details. -from . import registrant -from . import group -from . import individual -from . import group_membership -from . import error_response + +from . import fastapi_endpoint_registry diff --git a/g2p_registry_rest_api/models/fastapi_endpoint_registry.py b/g2p_registry_rest_api/models/fastapi_endpoint_registry.py new file mode 100644 index 00000000..281366bd --- /dev/null +++ b/g2p_registry_rest_api/models/fastapi_endpoint_registry.py @@ -0,0 +1,25 @@ +from fastapi import APIRouter + +from odoo import api, fields, models + +from ..routers.group import group_router +from ..routers.individual import individual_router + + +class G2PRegistryEndpoint(models.Model): + _inherit = "fastapi.endpoint" + + app: str = fields.Selection( + selection_add=[("registry", "Registry Endpoint")], ondelete={"registry": "cascade"} + ) + + def _get_fastapi_routers(self) -> list[APIRouter]: + routers = super()._get_fastapi_routers() + if self.app == "registry": + routers.append(group_router) + routers.append(individual_router) + return routers + + @api.model + def sync_endpoint_id_with_registry(self, endpoint_id): + return self.browse(endpoint_id).action_sync_registry() diff --git a/g2p_registry_rest_api/models/group.py b/g2p_registry_rest_api/models/group.py deleted file mode 100644 index ccd75c5d..00000000 --- a/g2p_registry_rest_api/models/group.py +++ /dev/null @@ -1,39 +0,0 @@ -import pydantic -from pydantic import validator - -from ..exceptions.base_exception import G2PApiValidationError -from ..exceptions.error_codes import G2PErrorCodes -from .group_membership import GroupMembersInfoIn, GroupMembersInfoOut -from .registrant import RegistrantInfoIn, RegistrantInfoOut - - -class GroupShortInfoOut(RegistrantInfoOut): - pass - - -class GroupInfoOut(RegistrantInfoOut): - is_group = True - members: list[GroupMembersInfoOut] = pydantic.Field(..., alias="group_membership_ids") - kind: str | None = pydantic.Field(..., alias="kind_as_str") - is_partial_group: bool - - -class GroupInfoIn(RegistrantInfoIn): - is_group = True - members: list[GroupMembersInfoIn] - kind: str = None - is_partial_group: bool = None - - @validator("kind") - def validate_kind_no_spaces(cls, value): # noqa: B902 - # Using lstrip() to remove leading spaces from the value - new_val = value.lstrip() if value else value - - # Checking if the length of the cleaned value is less than 1 - if value and len(new_val) < 1: - raise G2PApiValidationError( - error_message=G2PErrorCodes.G2P_REQ_001.get_error_message(), - error_code=G2PErrorCodes.G2P_REQ_001.get_error_code(), - error_description="Group type (kind) field cannot be empty.", - ) - return value diff --git a/g2p_registry_rest_api/models/group_membership.py b/g2p_registry_rest_api/models/group_membership.py deleted file mode 100644 index d70b6969..00000000 --- a/g2p_registry_rest_api/models/group_membership.py +++ /dev/null @@ -1,65 +0,0 @@ -from datetime import date, datetime - -from pydantic import validator - -from odoo.http import request - -from ..exceptions.base_exception import G2PApiValidationError -from ..exceptions.error_codes import G2PErrorCodes -from .individual import IndividualInfoOut -from .naive_orm_model import NaiveOrmModel -from .registrant import PhoneNumberIn, RegistrantIDIn - - -class GroupMembershipKindInfo(NaiveOrmModel): - name: str - - @validator("name") - def validate_name_no_spaces(cls, value): # noqa: B902 - # Using lstrip() to remove leading spaces from the value - new_value = value.lstrip() if value else value - - # Checking if the length of the cleaned value is less than 1 - if value and len(new_value) < 1: - raise G2PApiValidationError( - error_message=G2PErrorCodes.G2P_REQ_001.get_error_message(), - error_code=G2PErrorCodes.G2P_REQ_001.get_error_code(), - error_description="Member's kind field cannot be empty.", - ) - return value - - -class GroupMembersInfoOut(NaiveOrmModel): - id: int - individual: IndividualInfoOut - kind: list[GroupMembershipKindInfo] | None = None # TODO: Would be nicer to have it as a list of str - create_date: datetime = None - write_date: datetime = None - - -class GroupMembersInfoIn(NaiveOrmModel): - name: str - given_name: str = None - addl_name: str = None - family_name: str = None - ids: list[RegistrantIDIn] = None - registration_date: date = None - phone_numbers: list[PhoneNumberIn] = None - email: str = None - address: str = None - gender: str = None - birthdate: date = None - birth_place: str = None - is_group = False - kind: list[GroupMembershipKindInfo] = None # TODO: Would be nicer to have it as a list of str - - @validator("gender") - def validate_gender(cls, value): # noqa: B902 - options = request.env["gender.type"].search([("active", "=", True)]) - if value and not options.search([("code", "=", value)]): - raise G2PApiValidationError( - error_message=G2PErrorCodes.G2P_REQ_008.get_error_message(), - error_code=G2PErrorCodes.G2P_REQ_008.get_error_code(), - error_description=f"Invalid gender-{value}. It should be {[option.code for option in options]}", - ) - return value diff --git a/g2p_registry_rest_api/models/group_search_param.py b/g2p_registry_rest_api/models/group_search_param.py deleted file mode 100644 index dd995230..00000000 --- a/g2p_registry_rest_api/models/group_search_param.py +++ /dev/null @@ -1,8 +0,0 @@ -from extendable_pydantic import ExtendableModelMeta -from pydantic import BaseModel - - -class GroupSearchParam(BaseModel, metaclass=ExtendableModelMeta): - id: int = None - name: str = None - include_members_full: bool = False diff --git a/g2p_registry_rest_api/models/individual.py b/g2p_registry_rest_api/models/individual.py deleted file mode 100644 index 70c27df2..00000000 --- a/g2p_registry_rest_api/models/individual.py +++ /dev/null @@ -1,61 +0,0 @@ -from datetime import date - -from pydantic import validator - -from odoo.http import request - -from ..exceptions.base_exception import G2PApiValidationError -from ..exceptions.error_codes import G2PErrorCodes -from .registrant import RegistrantInfoIn, RegistrantInfoOut - - -class IndividualInfoOut(RegistrantInfoOut): - given_name: str = None - addl_name: str = None - family_name: str = None - gender: str = None - birthdate: date = None - age: str - birth_place: str = None - is_group = False - - -class IndividualInfoIn(RegistrantInfoIn): - given_name: str - addl_name: str = None - family_name: str - gender: str = None - birthdate: date = None - birth_place: str = None - is_group = False - - @validator("given_name") - def validate_given_name(cls, v): # noqa: B902 - if v is None or v.strip() == "": - raise G2PApiValidationError( - error_message=G2PErrorCodes.G2P_REQ_002.get_error_message(), - error_code=G2PErrorCodes.G2P_REQ_002.get_error_code(), - error_description="Given name is mandatory", - ) - return v - - @validator("family_name") - def validate_family_name(cls, v): # noqa: B902 - if v is None or v.strip() == "": - raise G2PApiValidationError( - error_message=G2PErrorCodes.G2P_REQ_002.get_error_message(), - error_code=G2PErrorCodes.G2P_REQ_002.get_error_code(), - error_description="Family name is mandatory", - ) - return v - - @validator("gender") - def validate_gender(cls, value): # noqa: B902 - options = request.env["gender.type"].search([("active", "=", True)]) - if value and not options.search([("code", "=", value)]): - raise G2PApiValidationError( - error_message=G2PErrorCodes.G2P_REQ_008.get_error_message(), - error_code=G2PErrorCodes.G2P_REQ_008.get_error_code(), - error_description=f"Invalid gender-{value}. It should be {[option.code for option in options]}", - ) - return value diff --git a/g2p_registry_rest_api/models/individual_search_param.py b/g2p_registry_rest_api/models/individual_search_param.py deleted file mode 100644 index 5ab4be44..00000000 --- a/g2p_registry_rest_api/models/individual_search_param.py +++ /dev/null @@ -1,7 +0,0 @@ -from extendable_pydantic import ExtendableModelMeta -from pydantic import BaseModel - - -class IndividualSearchParam(BaseModel, metaclass=ExtendableModelMeta): - id: int = None - name: str = None diff --git a/g2p_registry_rest_api/models/naive_orm_model.py b/g2p_registry_rest_api/models/naive_orm_model.py deleted file mode 100644 index ce48934b..00000000 --- a/g2p_registry_rest_api/models/naive_orm_model.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright 2021 ACSONE SA/NV -# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). - -from extendable_pydantic import ExtendableModelMeta -from pydantic import BaseModel - -from odoo.addons.pydantic import utils - - -class NaiveOrmModel(BaseModel, metaclass=ExtendableModelMeta): - class Config: - orm_mode = True - getter_dict = utils.GenericOdooGetter diff --git a/g2p_registry_rest_api/models/registrant.py b/g2p_registry_rest_api/models/registrant.py deleted file mode 100644 index f30fa0b1..00000000 --- a/g2p_registry_rest_api/models/registrant.py +++ /dev/null @@ -1,142 +0,0 @@ -import re -from datetime import date, datetime - -import pydantic -from pydantic import Field, validator - -from odoo import tools -from odoo.http import request - -from ..exceptions.base_exception import G2PApiValidationError -from ..exceptions.error_codes import G2PErrorCodes -from .naive_orm_model import NaiveOrmModel - - -class IDType(NaiveOrmModel): - name: str - - -class RegistrantIDOut(NaiveOrmModel): - id: int - id_type: str = pydantic.Field(..., alias="id_type_as_str") - value: str = None - expiry_date: date = None - - -class PhoneNumberOut(NaiveOrmModel): - id: int - phone_no: str - phone_sanitized: str - date_collected: date = None - disabled: date = None - - -class PhoneNumberIn(NaiveOrmModel): - phone_no: str - date_collected: date = None - - @validator("phone_no") - def validate_phone_number(cls, value): # noqa: B902 - phone_number_pattern = request.env["ir.config_parameter"].get_param("g2p_registry.phone_regex") - if value and not re.match(phone_number_pattern, value): - raise G2PApiValidationError( - error_message=G2PErrorCodes.G2P_REQ_006.get_error_message(), - error_code=G2PErrorCodes.G2P_REQ_006.get_error_code(), - error_description=("Please provide a valid phone number"), - ) - return value - - -class RegistrantInfoOut(NaiveOrmModel): - id: int - name: str - ids: list[RegistrantIDOut] = pydantic.Field(..., alias="reg_ids") - is_group: bool - registration_date: date = None - phone_numbers: list[PhoneNumberOut] = pydantic.Field(..., alias="phone_number_ids") - email: str = None - address: str = None - create_date: datetime = None - write_date: datetime = None - - -class RegistrantIDIn(NaiveOrmModel): - id_type: str = None - value: str = None - expiry_date: date = None - - @validator("id_type") - def validate_id_type_no_spaces(cls, value): # noqa: B902 - # Using lstrip() to remove leading spaces from the value - new_val = value.lstrip() if value else value - - # Checking if the length of the cleaned value is less than 1 - if value and len(new_val) < 1: - raise G2PApiValidationError( - error_message=G2PErrorCodes.G2P_REQ_005.get_error_message(), - error_code=G2PErrorCodes.G2P_REQ_005.get_error_code(), - error_description="ID type field cannot be empty.", - ) - return value - - @validator("value") - def validate_id_value(cls, value, values): - id_type = values.get("id_type") - if id_type: - id_type_id = request.env["g2p.id.type"].search([("name", "=", id_type)], limit=1) - if id_type_id.id_validation and not re.match(id_type_id.id_validation, value): - raise G2PApiValidationError( - error_message=G2PErrorCodes.G2P_REQ_005.get_error_message(), - error_code=G2PErrorCodes.G2P_REQ_005.get_error_code(), - error_description=f"The provided {id_type_id.name} ID '{value}' is invalid.", - ) - - return value - - -class RegistrantInfoIn(NaiveOrmModel): - name: str = Field(..., description="Mandatory field") - ids: list[RegistrantIDIn] = None - registration_date: date = None - is_group: bool - phone_numbers: list[PhoneNumberIn] = None - email: str = None - address: str = None - - @validator("email") - def validate_email(cls, value): # noqa: B902 - if value and not tools.single_email_re.match(value): - raise G2PApiValidationError( - error_message=G2PErrorCodes.G2P_REQ_007.get_error_message(), - error_code=G2PErrorCodes.G2P_REQ_007.get_error_code(), - error_description=("Please provide a valid email address"), - ) - return value - - @validator("registration_date") - def validate_registration_date(cls, value): - if value is not None and value > date.today(): - raise G2PApiValidationError( - error_message=G2PErrorCodes.G2P_REQ_011.get_error_message(), - error_code=G2PErrorCodes.G2P_REQ_011.get_error_code(), - error_description="Registration date cannot be in the future", - ) - return value - - @validator("name") - def validate_name_presence(cls, value): - if value is None or value.strip() == "": - raise G2PApiValidationError( - error_message=G2PErrorCodes.G2P_REQ_012.get_error_message(), - error_code=G2PErrorCodes.G2P_REQ_012.get_error_code(), - error_description="Name should not be empty. Please provide a valid name.", - ) - return value - - -class RegistrantUpdateIDIn(RegistrantIDIn): - partner_id: int - - -class RegistrantUpdateIDOut(RegistrantIDOut): - partner_id: int diff --git a/g2p_registry_rest_api/readme/CONTRIBUTORS.rst b/g2p_registry_rest_api/readme/CONTRIBUTORS.rst index 34e34526..6f611ce9 100644 --- a/g2p_registry_rest_api/readme/CONTRIBUTORS.rst +++ b/g2p_registry_rest_api/readme/CONTRIBUTORS.rst @@ -1,7 +1 @@ -`OpenSPP `__ donated the original code to the project. - -Contributors include: - -* Edwin Gonzales (`Newlogic `__) -* Jeremi Joslin (`Newlogic `__) -* Michael Gonzales (`Newlogic `__) +* Manoj Kumar (``__) diff --git a/g2p_registry_rest_api/services/__init__.py b/g2p_registry_rest_api/routers/__init__.py similarity index 100% rename from g2p_registry_rest_api/services/__init__.py rename to g2p_registry_rest_api/routers/__init__.py index 4d1ba3ef..c9dd6a56 100644 --- a/g2p_registry_rest_api/services/__init__.py +++ b/g2p_registry_rest_api/routers/__init__.py @@ -1,4 +1,4 @@ -from . import process_individual_mixin -from . import process_group_mixin from . import group from . import individual +from . import process_group_mixin +from . import process_individual_mixin diff --git a/g2p_registry_rest_api/routers/group.py b/g2p_registry_rest_api/routers/group.py new file mode 100644 index 00000000..12436417 --- /dev/null +++ b/g2p_registry_rest_api/routers/group.py @@ -0,0 +1,134 @@ +import logging +from typing import Annotated + +from fastapi import APIRouter, Depends + +from odoo.api import Environment + +from odoo.addons.fastapi.dependencies import odoo_env + +from ..exceptions.base_exception import G2PApiValidationError +from ..exceptions.error_codes import G2PErrorCodes +from ..schemas.group import GroupInfoRequest, GroupInfoResponse, GroupShortInfoOut + +group_router = APIRouter(prefix="/group", tags=["group"]) + + +@group_router.get("/{_id}", responses={200: {"model": GroupInfoResponse}}) +def get_group(_id, env: Annotated[Environment, Depends(odoo_env)]): + """ + Get partner's information by ID + """ + partner = _get_group(env, _id) + + if partner: + return GroupInfoResponse.model_validate(partner) + else: + raise G2PApiValidationError( + error_message=G2PErrorCodes.G2P_REQ_010.get_error_message(), + error_code=G2PErrorCodes.G2P_REQ_010.get_error_code(), + error_description=("Record is not present in the database."), + ) + + +@group_router.get( + "", +) +def search_groups( + env: Annotated[Environment, Depends(odoo_env)], + _id: int | None = None, + name: str | None = None, + include_members_full: bool = False, +): + """ + Search for groups by ID or name + """ + domain = [("is_registrant", "=", True), ("is_group", "=", True)] + error_description = "" + + if _id: + domain.append(("id", "=", _id)) + error_description = "This ID does not exist. Please enter a valid ID." + + if name: + domain.append(("name", "like", name)) + error_description = "This Name does not exist. Please enter a valid Name." + + res = [] + + for p in env["res.partner"].sudo().search(domain): + if include_members_full: + res.append(GroupInfoResponse.model_validate(p)) + else: + res.append(GroupShortInfoOut.model_validate(p)) + if not len(res): + if name and _id: + error_description = "Entered Name and ID does not exist." + raise G2PApiValidationError( + error_message=G2PErrorCodes.G2P_REQ_010.get_error_message(), + error_code=G2PErrorCodes.G2P_REQ_010.get_error_code(), + error_description=error_description, + ) + return res + + +@group_router.post("/", responses={200: {"model": GroupInfoResponse}}) +def create_group(request: GroupInfoRequest, env: Annotated[Environment, Depends(odoo_env)]): + """ + Create a new Group + """ + # Create the individual Objects + grp_membership_rec = [] + logging.info("INDIVIDUALS:") + + for membership_info in request.members: + individual = membership_info + + indv_rec = env["process_individual.rest.mixin"]._process_individual(individual) + + logging.info("Creating Individual Record") + indv_id = env["res.partner"].sudo().create(indv_rec) + + # Add individual's membership kind fields + membership_kind = membership_info.kind + + indv_membership_kinds = [] + if membership_kind: + for kind in membership_kind: + # Search Kind + kind_id = env["g2p.group.membership.kind"].sudo().search([("name", "=", kind.name)]) + if kind_id: + indv_membership_kinds.append((4, kind_id[0].id)) + elif kind.name: + raise G2PApiValidationError( + error_message=G2PErrorCodes.G2P_REQ_004.get_error_message(), + error_code=G2PErrorCodes.G2P_REQ_004.get_error_code(), + error_description="Membership kind - %s is not present in the database." % kind.name, + ) + grp_membership_rec.append({"individual": indv_id.id, "kind": indv_membership_kinds}) + + # TODO: create the group object + logging.info("GROUP:") + + grp_rec = env["process_group.rest.mixin"]._process_group(request) + + logging.info("Creating Group Record") + grp_id = env["res.partner"].sudo().create(grp_rec) + for mbr in grp_membership_rec: + mbr_rec = mbr + mbr_rec.update({"group": grp_id.id}) + + env["g2p.group.membership"].sudo().create(mbr_rec) + + # TODO: Reload the new object from the DB + partner = _get_group(env, grp_id.id) + + return GroupInfoResponse.model_validate(partner) + + +def _get_group(env: Environment, _id: int): + return ( + env["res.partner"] + .sudo() + .search([("id", "=", _id), ("is_registrant", "=", True), ("is_group", "=", True)]) + ) diff --git a/g2p_registry_rest_api/routers/individual.py b/g2p_registry_rest_api/routers/individual.py new file mode 100644 index 00000000..5ae41f61 --- /dev/null +++ b/g2p_registry_rest_api/routers/individual.py @@ -0,0 +1,91 @@ +import logging +from typing import Annotated + +from fastapi import APIRouter, Depends + +from odoo.api import Environment + +from odoo.addons.fastapi.dependencies import odoo_env + +from ..exceptions.base_exception import G2PApiValidationError +from ..exceptions.error_codes import G2PErrorCodes +from ..schemas.individual import IndividualInfoRequest, IndividualInfoResponse + +individual_router = APIRouter(prefix="/individual", tags=["individual"]) + + +@individual_router.get("/{_id}", responses={200: {"model": IndividualInfoResponse}}) +async def get_individual(_id, env: Annotated[Environment, Depends(odoo_env)]): + """ + Get partner's information by ID + """ + partner = _get_individual(env, _id) + if partner: + return IndividualInfoResponse.model_validate(partner) + else: + _handle_error(G2PErrorCodes.G2P_REQ_010, "Record is not present in the database.") + + +@individual_router.get( + "", + responses={200: {"model": list[IndividualInfoResponse]}}, +) +def search_individuals( + env: Annotated[Environment, Depends(odoo_env)], + _id: int | None = None, + name: str | None = None, +) -> list[IndividualInfoResponse]: + """ + Search for individuals by ID or name + """ + + domain = [("is_registrant", "=", True), ("is_group", "=", False)] + + if _id: + domain.append(("id", "=", _id)) + if name: + domain.append(("name", "like", name)) + + partners = env["res.partner"].sudo().search(domain) + if not partners: + error_message = "The specified criteria did not match any records." + _handle_error(G2PErrorCodes.G2P_REQ_010, error_message) + + return [IndividualInfoResponse.model_validate(partner) for partner in partners] + + +@individual_router.post( + "/", + responses={200: {"model": IndividualInfoResponse}}, +) +def create_individual( + request: IndividualInfoRequest, env: Annotated[Environment, Depends(odoo_env)] +) -> IndividualInfoResponse: + """ + Create a new individual + """ + # Create the individual Object + indv_rec = env["process_individual.rest.mixin"]._process_individual(request) + + logging.info("Individual Api: Creating Individual Record") + indv_id = env["res.partner"].sudo().create(indv_rec) + + partner = _get_individual(env, indv_id.id) + + return IndividualInfoResponse.model_validate(partner) + + +def _get_individual(env: Environment, _id: int): + return ( + env["res.partner"] + .sudo() + .search([("id", "=", _id), ("is_registrant", "=", True), ("is_group", "=", False)]) + ) + + +def _handle_error(error_code, error_description): + raise G2PApiValidationError( + error_message=error_code.get_error_message(), + error_code=error_code.get_error_code(), + error_description=error_description, + ) diff --git a/g2p_registry_rest_api/services/process_group_mixin.py b/g2p_registry_rest_api/routers/process_group_mixin.py similarity index 79% rename from g2p_registry_rest_api/services/process_group_mixin.py rename to g2p_registry_rest_api/routers/process_group_mixin.py index a037470e..748892ef 100644 --- a/g2p_registry_rest_api/services/process_group_mixin.py +++ b/g2p_registry_rest_api/routers/process_group_mixin.py @@ -1,10 +1,11 @@ -from odoo.addons.component.core import AbstractComponent +from odoo import models from ..exceptions.base_exception import G2PApiValidationError from ..exceptions.error_codes import G2PErrorCodes +from .process_individual_mixin import ProcessIndividualMixin -class ProcessGroupMixin(AbstractComponent): +class ProcessGroupMixin(models.AbstractModel): _name = "process_group.rest.mixin" _inherit = "process_individual.rest.mixin" _description = """ @@ -24,7 +25,7 @@ def _process_group(self, group_info): # Add group's kind field if group_info.kind: # Search Kind - kind_id = self.env["g2p.group.kind"].search([("name", "=", group_info.kind)]) + kind_id = self.env["g2p.group.kind"].sudo().search([("name", "=", group_info.kind)]) if kind_id: grp_rec.update({"kind": kind_id[0].id}) elif group_info.kind: @@ -36,12 +37,12 @@ def _process_group(self, group_info): ids = [] ids_info = group_info - ids = self._process_ids(ids_info) + ids = ProcessIndividualMixin._process_ids(self, ids_info) if ids: grp_rec.update({"reg_ids": ids}) phone_numbers = [] - phone_numbers, primary_phone = self._process_phones(ids_info) + phone_numbers, primary_phone = ProcessIndividualMixin._process_phones(self, ids_info) if primary_phone: grp_rec.update({"phone": primary_phone}) if phone_numbers: diff --git a/g2p_registry_rest_api/services/process_individual_mixin.py b/g2p_registry_rest_api/routers/process_individual_mixin.py similarity index 82% rename from g2p_registry_rest_api/services/process_individual_mixin.py rename to g2p_registry_rest_api/routers/process_individual_mixin.py index 6b98bd33..f85bac80 100644 --- a/g2p_registry_rest_api/services/process_individual_mixin.py +++ b/g2p_registry_rest_api/routers/process_individual_mixin.py @@ -1,10 +1,10 @@ -from odoo.addons.component.core import AbstractComponent +from odoo import models from ..exceptions.base_exception import G2PApiValidationError from ..exceptions.error_codes import G2PErrorCodes -class ProcessIndividualMixin(AbstractComponent): +class ProcessIndividualMixin(models.AbstractModel): _name = "process_individual.rest.mixin" _description = """ Process Individual REST API Mixin @@ -22,25 +22,25 @@ def _process_individual(self, individual): "addl_name": individual.addl_name, "birthdate": individual.birthdate or False, "birth_place": individual.birth_place or False, - "address": individual.address or None, + "address": individual.address if individual.address else None, } ids = [] ids_info = individual - ids = self._process_ids(ids_info) + ids = ProcessIndividualMixin._process_ids(self, ids_info) if ids: indv_rec.update({"reg_ids": ids}) phone_numbers = [] - phone_numbers, primary_phone = self._process_phones(ids_info) + phone_numbers, primary_phone = ProcessIndividualMixin._process_phones(self, ids_info) if primary_phone: indv_rec.update({"phone": primary_phone}) if phone_numbers: indv_rec.update({"phone_number_ids": phone_numbers}) - gender = self._process_gender(ids_info) + gender = ProcessIndividualMixin._process_gender(self, ids_info) if gender: indv_rec.update({"gender": gender}) return indv_rec @@ -50,7 +50,7 @@ def _process_ids(self, ids_info): if ids_info.ids: for rec in ids_info.ids: # Search ID Type - id_type_id = self.env["g2p.id.type"].search([("name", "=", rec.id_type)]) + id_type_id = self.env["g2p.id.type"].sudo().search([("name", "=", rec.id_type)]) if id_type_id: ids.append( ( @@ -94,8 +94,10 @@ def _process_phones(self, ids_info): def _process_gender(self, ids_info): if ids_info.gender: - gender = self.env["gender.type"].search( - [("active", "=", True), ("code", "=", ids_info.gender)], limit=1 + gender = ( + self.env["gender.type"] + .sudo() + .search([("active", "=", True), ("code", "=", ids_info.gender)], limit=1) ) if gender: return gender.value diff --git a/g2p_registry_rest_api/schemas/__init__.py b/g2p_registry_rest_api/schemas/__init__.py new file mode 100644 index 00000000..2abc03a3 --- /dev/null +++ b/g2p_registry_rest_api/schemas/__init__.py @@ -0,0 +1,6 @@ +# Part of OpenG2P Registry. See LICENSE file for full copyright and licensing details. +from . import registrant +from . import group +from . import individual +from . import group_membership +from . import error_response diff --git a/g2p_registry_rest_api/models/error_response.py b/g2p_registry_rest_api/schemas/error_response.py similarity index 100% rename from g2p_registry_rest_api/models/error_response.py rename to g2p_registry_rest_api/schemas/error_response.py diff --git a/g2p_registry_rest_api/schemas/group.py b/g2p_registry_rest_api/schemas/group.py new file mode 100644 index 00000000..4a1dc852 --- /dev/null +++ b/g2p_registry_rest_api/schemas/group.py @@ -0,0 +1,22 @@ +import pydantic + +from .group_membership import GroupMembersInfoRequest, GroupMembersInfoResponse +from .registrant import RegistrantInfoRequest, RegistrantInfoResponse + + +class GroupShortInfoOut(RegistrantInfoResponse): + pass + + +class GroupInfoResponse(RegistrantInfoResponse): + is_group: bool = True + members: list[GroupMembersInfoResponse] = pydantic.Field([], alias="group_membership_ids") + # kind: str | None = pydantic.Field(..., alias="kind_as_str") + is_partial_group: bool + + +class GroupInfoRequest(RegistrantInfoRequest): + is_group: bool = True + members: list[GroupMembersInfoRequest] + kind: str | None + is_partial_group: bool | None diff --git a/g2p_registry_rest_api/schemas/group_membership.py b/g2p_registry_rest_api/schemas/group_membership.py new file mode 100644 index 00000000..f06b2585 --- /dev/null +++ b/g2p_registry_rest_api/schemas/group_membership.py @@ -0,0 +1,34 @@ +from datetime import date, datetime + +from .individual import IndividualInfoResponse +from .naive_orm_model import NaiveOrmModel +from .registrant import PhoneNumberRequest, RegistrantIDRequest + + +class GroupMembershipKindInfo(NaiveOrmModel): + name: str | None + + +class GroupMembersInfoResponse(NaiveOrmModel): + id: int + individual: IndividualInfoResponse | None = [] + kind: list[GroupMembershipKindInfo] | None = None # TODO: Would be nicer to have it as a list of str + create_date: datetime = None + write_date: datetime = None + + +class GroupMembersInfoRequest(NaiveOrmModel): + name: str + given_name: str = None + addl_name: str = None + family_name: str = None + ids: list[RegistrantIDRequest] = None + registration_date: date = None + phone_numbers: list[PhoneNumberRequest] = None + email: str | None + address: str | None + gender: str | None + birthdate: date = None + birth_place: str | None + is_group: bool = False + kind: list[GroupMembershipKindInfo] = None # TODO: Would be nicer to have it as a list of str diff --git a/g2p_registry_rest_api/schemas/individual.py b/g2p_registry_rest_api/schemas/individual.py new file mode 100644 index 00000000..ad54869a --- /dev/null +++ b/g2p_registry_rest_api/schemas/individual.py @@ -0,0 +1,24 @@ +from datetime import date + +from .registrant import RegistrantInfoRequest, RegistrantInfoResponse + + +class IndividualInfoResponse(RegistrantInfoResponse): + given_name: str + addl_name: str | None + family_name: str + gender: str = None + birthdate: date = None + age: str | None = None + birth_place: str | None = None + is_group: bool = False + + +class IndividualInfoRequest(RegistrantInfoRequest): + given_name: str + addl_name: str | None + family_name: str + gender: str | None + birthdate: date | None + birth_place: str | None + is_group: bool = False diff --git a/g2p_registry_rest_api/schemas/naive_orm_model.py b/g2p_registry_rest_api/schemas/naive_orm_model.py new file mode 100644 index 00000000..5495c9b1 --- /dev/null +++ b/g2p_registry_rest_api/schemas/naive_orm_model.py @@ -0,0 +1,6 @@ +from extendable_pydantic import ExtendableModelMeta +from pydantic import BaseModel, ConfigDict + + +class NaiveOrmModel(BaseModel, metaclass=ExtendableModelMeta): + model_config = ConfigDict(from_attributes=True) diff --git a/g2p_registry_rest_api/schemas/registrant.py b/g2p_registry_rest_api/schemas/registrant.py new file mode 100644 index 00000000..3df55148 --- /dev/null +++ b/g2p_registry_rest_api/schemas/registrant.py @@ -0,0 +1,58 @@ +from datetime import date, datetime + +import pydantic +from pydantic import Field + +from .naive_orm_model import NaiveOrmModel + + +class IDType(NaiveOrmModel): + name: str + + +class RegistrantIDResponse(NaiveOrmModel): + id: int + id_type: str = pydantic.Field(..., alias="id_type_as_str") + value: str | None + expiry_date: date | None = None + + +class PhoneNumberResponse(NaiveOrmModel): + id: int + phone_no: str + phone_sanitized: str + date_collected: date = None + # disabled: date | None = None + + +class PhoneNumberRequest(NaiveOrmModel): + phone_no: str + date_collected: date = None + + +class RegistrantInfoResponse(NaiveOrmModel): + id: int + name: str + ids: list[RegistrantIDResponse] | None = pydantic.Field([], alias="reg_ids") + is_group: bool + registration_date: date = None + phone_numbers: list[PhoneNumberResponse] | None = pydantic.Field([], alias="phone_number_ids") + email: str | None + address: str | None + create_date: datetime = None + write_date: datetime = None + + +class RegistrantIDRequest(NaiveOrmModel): + id_type: str + value: str + expiry_date: date + + +class RegistrantInfoRequest(NaiveOrmModel): + name: str = Field(..., description="Mandatory field") + ids: list[RegistrantIDRequest] + registration_date: date = None + phone_numbers: list[PhoneNumberRequest] + email: str | None = None + address: str | None = None diff --git a/g2p_registry_rest_api/services/group.py b/g2p_registry_rest_api/services/group.py deleted file mode 100644 index 670bfc76..00000000 --- a/g2p_registry_rest_api/services/group.py +++ /dev/null @@ -1,157 +0,0 @@ -import logging - -from odoo.addons.base_rest import restapi -from odoo.addons.base_rest_pydantic.restapi import PydanticModel, PydanticModelList -from odoo.addons.component.core import Component - -from ..exceptions.base_exception import G2PApiValidationError -from ..exceptions.error_codes import G2PErrorCodes -from ..models.group import GroupInfoIn, GroupInfoOut, GroupShortInfoOut -from ..models.group_search_param import GroupSearchParam - - -class GroupApiService(Component): - _inherit = ["base.rest.service", "process_group.rest.mixin"] - _name = "registrant_group.rest.service" - _usage = "group" - _collection = "base.rest.registry.services" - _description = """ - Registrant Group API Services - """ - - @restapi.method( - [ - ( - [ - "/", - ], - "GET", - ) - ], - output_param=PydanticModel(GroupInfoOut), - auth="user", - ) - def get(self, _id): - """ - Get partner's information - """ - partner = self._get(_id) - if partner: - return GroupInfoOut.from_orm(partner) - else: - raise G2PApiValidationError( - error_message=G2PErrorCodes.G2P_REQ_010.get_error_message(), - error_code=G2PErrorCodes.G2P_REQ_010.get_error_code(), - error_description=("Record is not present in the database."), - ) - - @restapi.method( - [(["/", "/search"], "GET")], - input_param=PydanticModel(GroupSearchParam), - output_param=PydanticModelList(GroupShortInfoOut), - auth="user", - ) - def search(self, partner_search_param): - """ - Search for partners - :param partner_search_param: An instance of partner.search.param - :return: List of partner.short.info - """ - domain = [] - error_description = "" - if partner_search_param.name: - domain.append(("name", "like", partner_search_param.name)) - error_description = "This Name does not exist. Please enter a valid Name." - - if partner_search_param.id: - domain.append(("id", "=", partner_search_param.id)) - error_description = ( - "The ID Number you have entered does not exist. Please enter a valid ID Number." - ) - - domain.append(("is_registrant", "=", True)) - domain.append(("is_group", "=", True)) - res = [] - - for p in self.env["res.partner"].search(domain): - if partner_search_param.include_members_full: - res.append(GroupInfoOut.from_orm(p)) - else: - res.append(GroupShortInfoOut.from_orm(p)) - if not len(res): - if partner_search_param.name and partner_search_param.id: - error_description = "The ID Number or Name entered does not exist." - raise G2PApiValidationError( - error_message=G2PErrorCodes.G2P_REQ_010.get_error_message(), - error_code=G2PErrorCodes.G2P_REQ_010.get_error_code(), - error_description=error_description, - ) - return res - - @restapi.method( - [(["/"], "POST")], - input_param=PydanticModel(GroupInfoIn), - output_param=PydanticModel(GroupInfoOut), - auth="user", - ) - def createGroup(self, group_info): - """ - Create a new Group - :param group_info: An instance of the group info - :return: An instance of partner.info - """ - # Create the individual Objects - grp_membership_rec = [] - logging.info("INDIVIDUALS:") - for membership_info in group_info.members: - individual = membership_info - - indv_rec = self._process_individual(individual) - - logging.info("Creating Individual Record") - indv_id = self.env["res.partner"].create(indv_rec) - - # Add individual's membership kind fields - membership_kind = membership_info.kind - - indv_membership_kinds = [] - if membership_kind: - for kind in membership_kind: - # Search Kind - kind_id = self.env["g2p.group.membership.kind"].search([("name", "=", kind.name)]) - if kind_id: - indv_membership_kinds.append((4, kind_id[0].id)) - elif kind.name: - raise G2PApiValidationError( - error_message=G2PErrorCodes.G2P_REQ_004.get_error_message(), - error_code=G2PErrorCodes.G2P_REQ_004.get_error_code(), - error_description="Membership kind - %s is not present in the database." - % kind.name, - ) - grp_membership_rec.append({"individual": indv_id.id, "kind": indv_membership_kinds}) - - # TODO: create the group object - logging.info("GROUP:") - - grp_rec = self._process_group(group_info) - - logging.info("Creating Group Record") - grp_id = self.env["res.partner"].create(grp_rec) - for mbr in grp_membership_rec: - mbr_rec = mbr - mbr_rec.update({"group": grp_id.id}) - - self.env["g2p.group.membership"].create(mbr_rec) - - # TODO: Reload the new object from the DB - partner = self._get(grp_id.id) - return GroupInfoOut.from_orm(partner) - - # The following method are 'private' and should be never never NEVER call - # from the controller. - - def _get(self, _id): - partner = self.env["res.partner"].search([("id", "=", _id)]) - if partner and partner.is_group: - return partner - return None diff --git a/g2p_registry_rest_api/services/individual.py b/g2p_registry_rest_api/services/individual.py deleted file mode 100644 index d8fa5fe6..00000000 --- a/g2p_registry_rest_api/services/individual.py +++ /dev/null @@ -1,157 +0,0 @@ -import logging - -from odoo.addons.base_rest import restapi -from odoo.addons.base_rest_pydantic.restapi import PydanticModel, PydanticModelList -from odoo.addons.component.core import Component - -from ..exceptions.base_exception import G2PApiValidationError -from ..exceptions.error_codes import G2PErrorCodes -from ..models.individual import IndividualInfoIn, IndividualInfoOut -from ..models.individual_search_param import IndividualSearchParam -from ..models.registrant import RegistrantUpdateIDIn, RegistrantUpdateIDOut - - -class IndividualApiService(Component): - _inherit = ["base.rest.service", "process_individual.rest.mixin"] - _name = "registrant_individual.rest.service" - _usage = "individual" - _collection = "base.rest.registry.services" - _description = """ - Registrant Individual API Services - """ - - @restapi.method( - [ - ( - [ - "/", - ], - "GET", - ) - ], - output_param=PydanticModel(IndividualInfoOut), - auth="user", - ) - def get(self, _id): - """ - Get partner's information - """ - partner = self._get(_id) - if partner: - return IndividualInfoOut.from_orm(partner) - else: - raise G2PApiValidationError( - error_message=G2PErrorCodes.G2P_REQ_010.get_error_message(), - error_code=G2PErrorCodes.G2P_REQ_010.get_error_code(), - error_description=("Record is not present in the database."), - ) - - @restapi.method( - [(["/", "/search"], "GET")], - input_param=PydanticModel(IndividualSearchParam), - output_param=PydanticModelList(IndividualInfoOut), - auth="user", - ) - def search(self, partner_search_param): - """ - Search for partners - :param partner_search_param: An instance of partner.search.param - :return: List of partner.info - """ - domain = [] - error_description = "" - if partner_search_param.name: - domain.append(("name", "like", partner_search_param.name)) - error_description = "This Name does not exist. Please enter a valid Name." - - if partner_search_param.id: - domain.append(("id", "=", partner_search_param.id)) - error_description = ( - "The ID Number you have entered does not exist. Please enter a valid ID Number." - ) - - domain.append(("is_registrant", "=", True)) - domain.append(("is_group", "=", False)) - res = [] - - for p in self.env["res.partner"].search(domain): - res.append(IndividualInfoOut.from_orm(p)) - if not len(res): - if partner_search_param.name and partner_search_param.id: - error_description = "The ID Number or Name entered does not exist." - raise G2PApiValidationError( - error_message=G2PErrorCodes.G2P_REQ_010.get_error_message(), - error_code=G2PErrorCodes.G2P_REQ_010.get_error_code(), - error_description=error_description, - ) - return res - - @restapi.method( - [(["/"], "POST")], - input_param=PydanticModel(IndividualInfoIn), - output_param=PydanticModel(IndividualInfoOut), - auth="user", - ) - def createIndividual(self, individual_info): - """ - Create a new Individual - :param individual_info: An instance of the individual info - :return: An instance of partner info - """ - # Create the individual Object - indv_rec = self._process_individual(individual_info) - - logging.info("Individual Api: Creating Individual Record") - indv_id = self.env["res.partner"].create(indv_rec) - - # TODO: Reload the new object from the DB - partner = self._get(indv_id.id) - return IndividualInfoOut.from_orm(partner) - - def _get(self, _id): - partner = self.env["res.partner"].search([("id", "=", _id)]) - if partner and partner.is_registrant and not partner.is_group: - return partner - return None - - @restapi.method( - [("/updateIdentification", "PATCH")], - input_param=PydanticModel(RegistrantUpdateIDIn), - output_param=PydanticModel(RegistrantUpdateIDOut), - auth="user", - ) - def updateIdentification(self, reg_id): - """ - Update Individual Identification - :param reg_id: An instance of the partner.reg_id - :return: An instance of partner.reg_id - """ - id_type_id = self.env["g2p.id.type"].search([("name", "=", reg_id.id_type)], limit=1) - if id_type_id: - registrant = self.env["res.partner"].search( - [ - ("id", "=", reg_id.partner_id), - ("is_registrant", "=", True), - ("is_group", "=", False), - ] - ) - if registrant: - reg_id_dict = reg_id.dict() - reg_id_dict["id_type"] = id_type_id.id - for each_reg_id in registrant.reg_ids: - if each_reg_id.id_type.id == id_type_id.id: - each_reg_id.update(reg_id_dict) - return RegistrantUpdateIDOut.from_orm(each_reg_id) - return RegistrantUpdateIDOut.from_orm(self.env["g2p.reg.id"].create(reg_id_dict)) - else: - raise G2PApiValidationError( - error_message=G2PErrorCodes.G2P_REQ_013.get_error_message(), - error_code=G2PErrorCodes.G2P_REQ_013.get_error_code(), - error_description=f"The partner ID - {reg_id.partner_id} does not exist.", - ) - - raise G2PApiValidationError( - error_message=G2PErrorCodes.G2P_REQ_005.get_error_message(), - error_code=G2PErrorCodes.G2P_REQ_005.get_error_code(), - error_description=f"The provided ID type - {reg_id.id_type} is Invalid.", - ) diff --git a/g2p_registry_rest_api/static/description/index.html b/g2p_registry_rest_api/static/description/index.html index 85f67d06..2d381784 100644 --- a/g2p_registry_rest_api/static/description/index.html +++ b/g2p_registry_rest_api/static/description/index.html @@ -8,10 +8,11 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ +:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. +Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -274,7 +275,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: grey; } /* line numbers */ +pre.code .ln { color: gray; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -300,7 +301,7 @@ span.pre { white-space: pre } -span.problematic { +span.problematic, pre.problematic { color: red } span.section-subtitle { @@ -406,12 +407,8 @@

Authors

Contributors

-

OpenSPP donated the original code to the project.

-

Contributors include:

diff --git a/g2p_registry_rest_api/tests/__init__.py b/g2p_registry_rest_api/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/requirements.txt b/requirements.txt index 835fce58..493cd5d6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,4 @@ # generated from manifests external_dependencies +extendable-pydantic +pydantic schwifty