diff --git a/mozaik_event_rest/__manifest__.py b/mozaik_event_rest/__manifest__.py index 3e04a39bb..aa3394062 100644 --- a/mozaik_event_rest/__manifest__.py +++ b/mozaik_event_rest/__manifest__.py @@ -10,8 +10,10 @@ "author": "ACSONE SA/NV", "website": "https://github.com/OCA/mozaik", "depends": [ - "event_rest_api", + "base_rest", + "base_rest_pydantic", "event_sale", + "extendable", "mozaik_address", "mozaik_ama_indexed_on_website", "mozaik_event_image", diff --git a/mozaik_event_rest/pydantic_models/__init__.py b/mozaik_event_rest/pydantic_models/__init__.py index 25990eb37..2021ae9b2 100644 --- a/mozaik_event_rest/pydantic_models/__init__.py +++ b/mozaik_event_rest/pydantic_models/__init__.py @@ -1,11 +1,14 @@ from . import event_info from . import event_question_answer from . import event_question_info +from . import event_registration_info from . import event_registration_request from . import event_search_filter from . import event_stage_info from . import event_stage_search_filter from . import event_ticket_info +from . import event_type_info +from . import event_type_search_filter from . import event_website_domain_info from . import event_website_domain_search_filter from . import partner_address_info diff --git a/mozaik_event_rest/pydantic_models/event_info.py b/mozaik_event_rest/pydantic_models/event_info.py index f0d398a25..e1ac3dd34 100644 --- a/mozaik_event_rest/pydantic_models/event_info.py +++ b/mozaik_event_rest/pydantic_models/event_info.py @@ -1,26 +1,37 @@ # Copyright 2021 ACSONE SA/NV # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -from datetime import date +from datetime import date, datetime from typing import List import pydantic +from extendable_pydantic import ExtendableModelMeta +from pydantic import BaseModel -from odoo.addons.event_rest_api.pydantic_models.event_info import ( - EventInfo as BaseEventInfo, - EventShortInfo as BaseEventShortInfo, -) from odoo.addons.mozaik_thesaurus_api.pydantic_models.thesaurus_term_info import ( ThesaurusTermInfo, ) +from odoo.addons.pydantic import utils from .event_question_info import EventQuestionInfo +from .event_stage_info import EventStageInfo +from .event_ticket_info import EventTicketInfo +from .event_type_info import EventTypeInfo from .event_website_domain_info import EventWebsiteDomainInfo from .partner_address_info import PartnerAddressInfo from .partner_minimum_info import PartnerMinimumInfo -class EventShortInfo(BaseEventShortInfo, extends=BaseEventShortInfo): +class EventShortInfo(BaseModel, metaclass=ExtendableModelMeta): + + id: int + name: str + date_begin: datetime + date_end: datetime + event_type: EventTypeInfo = pydantic.Field(None, alias="event_type_id") + stage: EventStageInfo = pydantic.Field(None, alias="stage_id") + note: str = None + write_date: datetime image_url: str interests: List[ThesaurusTermInfo] = pydantic.Field([], alias="interest_ids") address: PartnerAddressInfo = pydantic.Field(None, alias="address_id") @@ -35,8 +46,17 @@ class EventShortInfo(BaseEventShortInfo, extends=BaseEventShortInfo): is_published: bool = None is_headline: bool = None + class Config: + orm_mode = True + getter_dict = utils.GenericOdooGetter + + +class EventInfo(EventShortInfo): -class EventInfo(BaseEventInfo, extends=BaseEventInfo): + event_tickets: List[EventTicketInfo] = pydantic.Field([], alias="event_ticket_ids") + seats_limited: bool + seats_max: int = None + seats_expected: int = None publish_date: date = None questions: List[EventQuestionInfo] = pydantic.Field([], alias="question_ids") menu_register_cta: bool = None diff --git a/mozaik_event_rest/pydantic_models/event_registration_info.py b/mozaik_event_rest/pydantic_models/event_registration_info.py new file mode 100644 index 000000000..f11287c45 --- /dev/null +++ b/mozaik_event_rest/pydantic_models/event_registration_info.py @@ -0,0 +1,29 @@ +# Copyright 2021 ACSONE SA/NV +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from datetime import datetime + +import pydantic +from extendable_pydantic import ExtendableModelMeta +from pydantic import BaseModel + +from odoo.addons.pydantic import utils + +from .event_info import EventInfo +from .event_ticket_info import EventTicketInfo + + +class EventRegistrationInfo(BaseModel, metaclass=ExtendableModelMeta): + + id: int + partner_id: int = None + firstname: str = None + lastname: str = None + email: str = None + event: EventInfo = pydantic.Field(..., alias="event_id") + event_ticket: EventTicketInfo = pydantic.Field(None, alias="event_ticket_id") + write_date: datetime + + class Config: + orm_mode = True + getter_dict = utils.GenericOdooGetter diff --git a/mozaik_event_rest/pydantic_models/event_registration_request.py b/mozaik_event_rest/pydantic_models/event_registration_request.py index 51cc20047..f602e9c9c 100644 --- a/mozaik_event_rest/pydantic_models/event_registration_request.py +++ b/mozaik_event_rest/pydantic_models/event_registration_request.py @@ -4,24 +4,26 @@ from typing import List import pydantic - -from odoo.addons.event_rest_api.pydantic_models.event_registration_request import ( - EventRegistrationRequest as BaseEventRegistrationRequest, -) +from extendable_pydantic import ExtendableModelMeta +from pydantic import BaseModel from .event_question_answer import EventQuestionAnswer -class EventRegistrationRequest( - BaseEventRegistrationRequest, extends=BaseEventRegistrationRequest -): - # firstname, lastname and email are not mandatory anymore since we - # can give registered_partner_id +class EventRegistrationRequest(BaseModel, metaclass=ExtendableModelMeta): + firstname: str = None lastname: str = None email: str = None - + phone: str = None + mobile: str = None + event_ticket_id: int = None registered_partner_id: int = None zip: str = None answers: List[EventQuestionAnswer] = pydantic.Field([]) force_autoval: bool = False + + +class EventRegistrationRequestList(BaseModel, metaclass=ExtendableModelMeta): + + event_registration_requests: List[EventRegistrationRequest] = [] diff --git a/mozaik_event_rest/pydantic_models/event_search_filter.py b/mozaik_event_rest/pydantic_models/event_search_filter.py index eaf3bdecb..e133c3899 100644 --- a/mozaik_event_rest/pydantic_models/event_search_filter.py +++ b/mozaik_event_rest/pydantic_models/event_search_filter.py @@ -1,15 +1,21 @@ # Copyright 2021 ACSONE SA/NV # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +from datetime import datetime from typing import List -from odoo.addons.event_rest_api.pydantic_models.event_search_filter import ( - EventSearchFilter as BaseEventSearchFilter, -) +from extendable_pydantic import ExtendableModelMeta +from pydantic import BaseModel -class EventSearchFilter(BaseEventSearchFilter, extends=BaseEventSearchFilter): +class EventSearchFilter(BaseModel, metaclass=ExtendableModelMeta): + id: int = None + name: str = None + start_after: datetime = None + end_before: datetime = None + event_type_ids: List[int] = None + stage_ids: List[int] = None website_domain_ids: List[int] = None interest_ids: List[int] = None is_headline: bool = None diff --git a/mozaik_event_rest/pydantic_models/event_stage_info.py b/mozaik_event_rest/pydantic_models/event_stage_info.py index 8c98cba45..b27c3fda4 100644 --- a/mozaik_event_rest/pydantic_models/event_stage_info.py +++ b/mozaik_event_rest/pydantic_models/event_stage_info.py @@ -2,10 +2,22 @@ # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -from odoo.addons.event_rest_api.pydantic_models.event_stage_info import ( - EventStageInfo as BaseEventStageInfo, -) +from datetime import datetime +from extendable_pydantic import ExtendableModelMeta +from pydantic import BaseModel -class EventStageInfo(BaseEventStageInfo, extends=BaseEventStageInfo): +from odoo.addons.pydantic import utils + + +class EventStageInfo(BaseModel, metaclass=ExtendableModelMeta): + id: int + name: str + sequence: int = None + pipe_end: bool = None + write_date: datetime draft_stage: bool + + class Config: + orm_mode = True + getter_dict = utils.GenericOdooGetter diff --git a/mozaik_event_rest/pydantic_models/event_stage_search_filter.py b/mozaik_event_rest/pydantic_models/event_stage_search_filter.py index 7eb01ae4d..e3b26fd77 100644 --- a/mozaik_event_rest/pydantic_models/event_stage_search_filter.py +++ b/mozaik_event_rest/pydantic_models/event_stage_search_filter.py @@ -1,13 +1,13 @@ # Copyright 2021 ACSONE SA/NV # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -from odoo.addons.event_rest_api.pydantic_models.event_stage_search_filter import ( - EventStageSearchFilter as BaseEventStageSearchFilter, -) +from extendable_pydantic import ExtendableModelMeta +from pydantic import BaseModel -class EventStageSearchFilter( - BaseEventStageSearchFilter, extends=BaseEventStageSearchFilter -): +class EventStageSearchFilter(BaseModel, metaclass=ExtendableModelMeta): + id: int = None + name: str = None + pipe_end: bool = None draft_stage: bool = None diff --git a/mozaik_event_rest/pydantic_models/event_ticket_info.py b/mozaik_event_rest/pydantic_models/event_ticket_info.py index f0b212d7f..81489a6fd 100644 --- a/mozaik_event_rest/pydantic_models/event_ticket_info.py +++ b/mozaik_event_rest/pydantic_models/event_ticket_info.py @@ -2,10 +2,25 @@ # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -from odoo.addons.event_rest_api.pydantic_models.event_ticket_info import ( - EventTicketInfo as BaseEventTicketInfo, -) +from datetime import date, datetime +from extendable_pydantic import ExtendableModelMeta +from pydantic import BaseModel -class EventTicketInfo(BaseEventTicketInfo, extends=BaseEventTicketInfo): +from odoo.addons.pydantic import utils + + +class EventTicketInfo(BaseModel, metaclass=ExtendableModelMeta): + id: int + event_id: int + name: str + description: str = None + start_sale_date: date = None + end_sale_date: date = None + seats_available: int = None + write_date: datetime price: float = None + + class Config: + orm_mode = True + getter_dict = utils.GenericOdooGetter diff --git a/mozaik_event_rest/pydantic_models/event_type_info.py b/mozaik_event_rest/pydantic_models/event_type_info.py new file mode 100644 index 000000000..e61ae6974 --- /dev/null +++ b/mozaik_event_rest/pydantic_models/event_type_info.py @@ -0,0 +1,19 @@ +# Copyright 2021 ACSONE SA/NV +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from datetime import datetime + +from extendable_pydantic import ExtendableModelMeta +from pydantic import BaseModel + +from odoo.addons.pydantic import utils + + +class EventTypeInfo(BaseModel, metaclass=ExtendableModelMeta): + id: int + name: str + write_date: datetime + + class Config: + orm_mode = True + getter_dict = utils.GenericOdooGetter diff --git a/mozaik_event_rest/pydantic_models/event_type_search_filter.py b/mozaik_event_rest/pydantic_models/event_type_search_filter.py new file mode 100644 index 000000000..ae35a3fa9 --- /dev/null +++ b/mozaik_event_rest/pydantic_models/event_type_search_filter.py @@ -0,0 +1,11 @@ +# 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 + + +class EventTypeSearchFilter(BaseModel, metaclass=ExtendableModelMeta): + + id: int = None + name: str = None diff --git a/mozaik_event_rest/services/__init__.py b/mozaik_event_rest/services/__init__.py index 211e6a57a..00b3f7eba 100644 --- a/mozaik_event_rest/services/__init__.py +++ b/mozaik_event_rest/services/__init__.py @@ -1,3 +1,5 @@ +from . import service from . import event_stage +from . import event_type from . import event_website_domain from . import event diff --git a/mozaik_event_rest/services/event.py b/mozaik_event_rest/services/event.py index 9b87a7fbd..ae59a1b5a 100644 --- a/mozaik_event_rest/services/event.py +++ b/mozaik_event_rest/services/event.py @@ -1,20 +1,53 @@ # Copyright 2021 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from typing import List + from odoo import _ from odoo.exceptions import ValidationError +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 odoo.addons.event_rest_api.pydantic_models.event_registration_request import ( + +from ..pydantic_models.event_info import EventInfo, EventShortInfo +from ..pydantic_models.event_registration_info import EventRegistrationInfo +from ..pydantic_models.event_registration_request import ( EventRegistrationRequest, + EventRegistrationRequestList, ) +from ..pydantic_models.event_search_filter import EventSearchFilter class EventService(Component): - _inherit = "event.rest.service" + _inherit = "base.event.rest.service" + _name = "event.rest.service" + _usage = "event" + _expose_model = "event.event" + _description = __doc__ + + @restapi.method( + routes=[(["/"], "GET")], + output_param=PydanticModel(EventInfo), + ) + def get(self, _id: int) -> EventInfo: + event = self._get(_id) + return EventInfo.from_orm(event) def _get_search_domain(self, filters): - domain = super()._get_search_domain(filters) + domain = [] + if filters.name: + domain.append(("name", "like", filters.name)) + if filters.id: + domain.append(("id", "=", filters.id)) + if filters.start_after: + domain.append(("date_begin", ">", filters.start_after)) + if filters.end_before: + domain.append(("date_end", "<", filters.end_before)) + if filters.event_type_ids: + domain.append(("event_type_id", "in", filters.event_type_ids)) + if filters.stage_ids: + domain.append(("stage_id", "in", filters.stage_ids)) if filters.website_domain_ids: domain.append(("website_domain_ids", "in", filters.website_domain_ids)) if filters.interest_ids: @@ -23,6 +56,18 @@ def _get_search_domain(self, filters): domain.append(("is_headline", "=", filters.is_headline)) return domain + @restapi.method( + routes=[(["/", "/search"], "GET")], + input_param=PydanticModel(EventSearchFilter), + output_param=PydanticModelList(EventShortInfo), + ) + def search(self, event_search_filter: EventSearchFilter) -> List[EventShortInfo]: + domain = self._get_search_domain(event_search_filter) + res: List[EventShortInfo] = [] + for e in self.env["event.event"].sudo().search(domain): + res.append(EventShortInfo.from_orm(e)) + return res + def _prepare_event_registration_values( self, event, event_registration_request: EventRegistrationRequest ) -> dict: @@ -44,9 +89,6 @@ def _prepare_event_registration_values( ) ) - res = super()._prepare_event_registration_values( - event, event_registration_request - ) answers = [] for answer in event_registration_request.answers: answers.append( @@ -61,10 +103,48 @@ def _prepare_event_registration_values( }, ) ) - res["registration_answer_ids"] = answers - res["associated_partner_id"] = ( - event_registration_request.registered_partner_id or False - ) - res["zip"] = event_registration_request.zip - res["force_autoval"] = event_registration_request.force_autoval + return { + "event_id": event.id, + "partner_id": self.env.context.get("authenticated_partner_id", False), + "firstname": event_registration_request.firstname, + "lastname": event_registration_request.lastname, + "email": event_registration_request.email, + "phone": event_registration_request.phone, + "mobile": event_registration_request.mobile, + "event_ticket_id": event_registration_request.event_ticket_id, + "registration_answer_ids": answers, + "associated_partner_id": event_registration_request.registered_partner_id + or False, + "zip": event_registration_request.zip, + "force_autoval": event_registration_request.force_autoval, + } + + @restapi.method( + routes=[(["//registration"], "POST")], + input_param=PydanticModel(EventRegistrationRequestList), + output_param=PydanticModelList(EventRegistrationInfo), + ) + def registration( + self, _id: int, event_registration_request_list: EventRegistrationRequestList + ) -> List[EventRegistrationInfo]: + event = self._get(_id) + if event.seats_limited: + ordered_seats = len( + event_registration_request_list.event_registration_requests + ) + if event.seats_available < ordered_seats: + raise ValidationError( + _("Not enough seats available: %s") % (event.seats_available) + ) + res: List[EventRegistrationInfo] = [] + for ( + event_registration_request + ) in event_registration_request_list.event_registration_requests: + event_registration_values = self._prepare_event_registration_values( + event, event_registration_request + ) + event_registration = self.env["event.registration"].create( + event_registration_values + ) + res.append(EventRegistrationInfo.from_orm(event_registration)) return res diff --git a/mozaik_event_rest/services/event_stage.py b/mozaik_event_rest/services/event_stage.py index b013ea639..d349b5341 100644 --- a/mozaik_event_rest/services/event_stage.py +++ b/mozaik_event_rest/services/event_stage.py @@ -1,14 +1,53 @@ # Copyright 2021 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from typing import List + +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 ..pydantic_models.event_stage_info import EventStageInfo +from ..pydantic_models.event_stage_search_filter import EventStageSearchFilter + class EventStageService(Component): - _inherit = "event.stage.rest.service" + _inherit = "base.event.rest.service" + _name = "event.stage.rest.service" + _usage = "event_stage" + _expose_model = "event.stage" + _description = __doc__ + + @restapi.method( + routes=[(["/"], "GET")], + output_param=PydanticModel(EventStageInfo), + ) + def get(self, _id: int) -> EventStageInfo: + event_stage = self._get(_id) + return EventStageInfo.from_orm(event_stage) def _get_search_domain(self, filters): - domain = super()._get_search_domain(filters) + domain = [] + if filters.name: + domain.append(("name", "like", filters.name)) + if filters.id: + domain.append(("id", "=", filters.id)) + if filters.pipe_end is not None: + domain.append(("pipe_end", "=", filters.pipe_end)) if filters.draft_stage is not None: domain.append(("draft_stage", "=", filters.draft_stage)) return domain + + @restapi.method( + routes=[(["/", "/search"], "GET")], + input_param=PydanticModel(EventStageSearchFilter), + output_param=PydanticModelList(EventStageInfo), + ) + def search( + self, event_stage_search_filter: EventStageSearchFilter + ) -> List[EventStageInfo]: + domain = self._get_search_domain(event_stage_search_filter) + res: List[EventStageInfo] = [] + for e in self.env["event.stage"].sudo().search(domain): + res.append(EventStageInfo.from_orm(e)) + return res diff --git a/mozaik_event_rest/services/event_type.py b/mozaik_event_rest/services/event_type.py new file mode 100644 index 000000000..6374161cd --- /dev/null +++ b/mozaik_event_rest/services/event_type.py @@ -0,0 +1,49 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from typing import List + +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 ..pydantic_models.event_type_info import EventTypeInfo +from ..pydantic_models.event_type_search_filter import EventTypeSearchFilter + + +class EventTypeService(Component): + _inherit = "base.event.rest.service" + _name = "event.type.rest.service" + _usage = "event_type" + _expose_model = "event.type" + _description = __doc__ + + @restapi.method( + routes=[(["/"], "GET")], + output_param=PydanticModel(EventTypeInfo), + ) + def get(self, _id: int) -> EventTypeInfo: + event_type = self._get(_id) + return EventTypeInfo.from_orm(event_type) + + def _get_search_domain(self, filters): + domain = [] + if filters.name: + domain.append(("name", "like", filters.name)) + if filters.id: + domain.append(("id", "=", filters.id)) + return domain + + @restapi.method( + routes=[(["/", "/search"], "GET")], + input_param=PydanticModel(EventTypeSearchFilter), + output_param=PydanticModelList(EventTypeInfo), + ) + def search( + self, event_type_search_filter: EventTypeSearchFilter + ) -> List[EventTypeInfo]: + domain = self._get_search_domain(event_type_search_filter) + res: List[EventTypeInfo] = [] + for e in self.env["event.type"].sudo().search(domain): + res.append(EventTypeInfo.from_orm(e)) + return res diff --git a/mozaik_event_rest/services/service.py b/mozaik_event_rest/services/service.py new file mode 100644 index 000000000..0ee9690bd --- /dev/null +++ b/mozaik_event_rest/services/service.py @@ -0,0 +1,24 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _ +from odoo.exceptions import MissingError + +from odoo.addons.component.core import AbstractComponent + + +class BaseEventService(AbstractComponent): + _inherit = "base.rest.service" + _name = "base.event.rest.service" + _collection = "event.rest.services" + _expose_model = None + + def _get(self, _id): + domain = [("id", "=", _id)] + record = self.env[self._expose_model].search(domain) + if not record: + raise MissingError( + _("The record %s %s does not exist") % (self._expose_model, _id) + ) + else: + return record diff --git a/mozaik_event_rest/tests/__init__.py b/mozaik_event_rest/tests/__init__.py new file mode 100644 index 000000000..8d73378b6 --- /dev/null +++ b/mozaik_event_rest/tests/__init__.py @@ -0,0 +1 @@ +from . import test_event diff --git a/mozaik_event_rest/tests/test_event.py b/mozaik_event_rest/tests/test_event.py new file mode 100644 index 000000000..6607eee26 --- /dev/null +++ b/mozaik_event_rest/tests/test_event.py @@ -0,0 +1,43 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from datetime import datetime + +from odoo.http import request + +from odoo.addons.base_rest.controllers.main import _PseudoCollection +from odoo.addons.base_rest.tests.common import BaseRestCase +from odoo.addons.component.core import WorkContext +from odoo.addons.extendable.tests.common import ExtendableMixin + + +class EventCase(BaseRestCase, ExtendableMixin): + @classmethod + def setUpClass(cls): + super().setUpClass() + collection = _PseudoCollection("event.rest.services", cls.env) + cls.services_env = WorkContext( + model_name="rest.service.registration", + collection=collection, + request=request, + ) + cls.service = cls.services_env.component(usage="event") + cls.event = cls.env["event.event"].create( + { + "name": "Test Event", + "date_begin": datetime.now(), + "date_end": datetime.now(), + } + ) + cls.setUpExtendable() + + # pylint: disable=W8106 + def setUp(self): + # resolve an inheritance issue (common.TransactionCase does not call + # super) + BaseRestCase.setUp(self) + ExtendableMixin.setUp(self) + + def test_get_event(self): + res = self.service.dispatch("get", self.event.id) + self.assertEqual(res["name"], "Test Event")