From 59cc709ed59aef957eee9388d3f56389900f65d5 Mon Sep 17 00:00:00 2001 From: ewandor Date: Tue, 1 Apr 2025 00:29:43 +0200 Subject: [PATCH] Full Working static multi tenant --- back/app/contract/__init__.py | 105 +----------------- back/app/contract/models.py | 85 +++++++++----- back/app/contract/print/__init__.py | 47 ++++---- back/app/contract/routes_contract.py | 70 ++++++++++++ back/app/contract/routes_draft.py | 41 +++---- back/app/contract/routes_signature.py | 35 ++++++ back/app/contract/schemas.py | 24 ++-- back/app/core/models.py | 15 ++- back/app/core/routes.py | 37 +++--- back/app/core/schemas.py | 8 +- back/app/entity/models.py | 3 +- back/app/entity/schemas.py | 2 +- back/app/main.py | 9 +- back/app/template/models.py | 37 ++++-- back/app/template/routes_contract.py | 4 +- back/app/template/routes_provision.py | 4 +- back/app/template/schemas.py | 6 +- back/app/user/manager.py | 3 - .../views/contract-drafts/drafts.component.ts | 4 +- .../views/contracts/contracts.component.ts | 6 +- .../src/common/crud/card/card.component.ts | 8 +- front/app/src/common/crud/crud.service.ts | 8 +- .../src/common/crud/list/list.component.html | 2 +- .../src/common/crud/types/foreignkey.type.ts | 8 +- 24 files changed, 327 insertions(+), 244 deletions(-) create mode 100644 back/app/contract/routes_contract.py create mode 100644 back/app/contract/routes_signature.py diff --git a/back/app/contract/__init__.py b/back/app/contract/__init__.py index 3556929e..b4610462 100644 --- a/back/app/contract/__init__.py +++ b/back/app/contract/__init__.py @@ -1,105 +1,12 @@ -import uuid -from fastapi import Depends, HTTPException, File, UploadFile -import shutil +from fastapi import APIRouter -from ..core.routes import get_crud_router +from .routes_contract import contract_router as contract_subrouter +from .routes_signature import signature_router from .routes_draft import draft_router from .print import print_router -from .models import Contract, ContractDraft, ContractDraftStatus, Party, replace_variables_in_value -from .schemas import ContractCreate, ContractRead, ContractUpdate - -from ..entity.models import Entity -from ..template.models import ProvisionTemplate -from ..user.manager import get_current_user, get_current_superuser - -contract_router = get_crud_router(Contract, ContractCreate, ContractRead, ContractUpdate) -del(contract_router.routes[0]) -del(contract_router.routes[2]) -del(contract_router.routes[2]) - +contract_router = APIRouter() contract_router.include_router(draft_router, prefix="/draft", ) +contract_router.include_router(contract_subrouter, ) contract_router.include_router(print_router, prefix="/print", ) - - -@contract_router.post("/", response_description="Contract Successfully created") -async def create(item: ContractCreate, user=Depends(get_current_user)) -> dict: - await item.validate_foreign_key() - - draft = await ContractDraft.get(item.draft_id) - - for v in draft.variables: - if not v.key or not v.value: - raise HTTPException(status_code=400, detail="Variable {} is invalid".format(v)) - - contract_dict = item.dict() - del(contract_dict['draft_id']) - - contract_dict['lawyer'] = await Entity.get(user.entity_id) - - contract_dict['name'] = draft.name - contract_dict['title'] = draft.title - parties = [] - for p in draft.parties: - parties.append({ - 'entity': await Entity.get(p.entity_id), - 'part': p.part, - 'representative': await Entity.get(p.representative_id) if p.representative_id else None, - 'signature_uuid': str(uuid.uuid4()) - }) - - contract_dict['parties'] = parties - - provisions = [] - for p in draft.provisions: - p = p.provision - provision = await ProvisionTemplate.get(p.provision_template_id) if p.type == 'template' \ - else p - - provisions.append({ - 'title': replace_variables_in_value(draft.variables, provision.title), - 'body': replace_variables_in_value(draft.variables, provision.body) - }) - - contract_dict['provisions'] = provisions - - o = await Contract(**contract_dict).create() - - await draft.update({"$set": {"status": ContractDraftStatus.published}}) - return {"message": "Contract Successfully created", "id": o.id} - - -@contract_router.put("/{id}", response_description="") -async def update(id: str, contract_form: ContractUpdate, user=Depends(get_current_superuser)) -> ContractRead: - raise HTTPException(status_code=400, detail="No modification on contract") - - -@contract_router.get("/signature/{signature_id}", response_description="") -async def get_signature(signature_id: str) -> Party: - contract = await Contract.find_by_signature_id(signature_id) - signature = contract.get_signature(signature_id) - return signature - - -@contract_router.post("/signature/{signature_id}", response_description="") -async def affix_signature(signature_id: str, signature_file: UploadFile = File(...)) -> bool: - contract = await Contract.find_by_signature_id(signature_id) - - signature_index = contract.get_signature_index(signature_id) - signature = contract.parties[signature_index] - - if signature.signature_affixed: - raise HTTPException(status_code=400, detail="Signature already affixed") - - with open(f'media/signatures/{signature_id}.png', "wb") as buffer: - shutil.copyfileobj(signature_file.file, buffer) - - update_query = {"$set": { - f'parties.{signature_index}.signature_affixed': True - }} - signature.signature_affixed = True - if contract.is_signed(): - update_query["$set"]['status'] = 'signed' - await contract.update(update_query) - - return True +contract_router.include_router(signature_router, prefix="/signature", ) diff --git a/back/app/contract/models.py b/back/app/contract/models.py index c468546a..e7530081 100644 --- a/back/app/contract/models.py +++ b/back/app/contract/models.py @@ -1,11 +1,12 @@ import datetime from typing import List, Literal, Optional from enum import Enum +from uuid import UUID -from pydantic import BaseModel, Field, validator -from beanie.operators import ElemMatch +from pydantic import BaseModel, Field from ..core.models import CrudDocument, RichtextSingleline, RichtextMultiline, DictionaryEntry +from ..core.filter import Filter from ..entity.models import Entity @@ -57,9 +58,12 @@ class Party(BaseModel): signature_affixed: bool = False signature_png: Optional[str] = None +class ContractProvisionType(Enum): + genuine = 'genuine' + template = 'template' class ProvisionGenuine(BaseModel): - type: Literal['genuine'] = 'genuine' + type: Literal['genuine'] = ContractProvisionType.genuine title: str = RichtextSingleline(props={"parametrized": True}, default="", title="Titre") body: str = RichtextMultiline(props={"parametrized": True}, default="", title="Corps") @@ -68,7 +72,7 @@ class ProvisionGenuine(BaseModel): class ContractProvisionTemplateReference(BaseModel): - type: Literal['template'] = 'template' + type: Literal['template'] = ContractProvisionType.template provision_template_id: str = Field( foreignKey={ "reference": { @@ -97,6 +101,9 @@ class Provision(BaseModel): title: str = RichtextSingleline(title="Titre") body: str = RichtextMultiline(title="Corps") +class ContractDraftUpdateStatus(BaseModel): + status: str = Field() + todo: List[str] = Field(default=[]) class ContractDraft(CrudDocument): """ @@ -130,7 +137,7 @@ class ContractDraft(CrudDocument): class Config: title = 'Brouillon de contrat' - async def check_is_ready(self): + async def check_is_ready(self, db): if self.status == ContractDraftStatus.published: return @@ -152,18 +159,18 @@ class ContractDraft(CrudDocument): for v in self.variables: if not (v.key and v.value): - self.todo.append('Empty variable') + self.todo.append(f'Empty variable: {v.key}') if self.todo: self.status = ContractDraftStatus.in_progress else: self.status = ContractDraftStatus.ready - await self.update({"$set": { - "status": self.status, - "todo": self.todo - }}) + await self.update(db, self, ContractDraftUpdateStatus(status=self.status, todo=self.todo)) + async def update_status(self, db, status): + update = ContractDraftUpdateStatus(status=status) + await self.update(db, self, update) class Contract(CrudDocument): """ @@ -181,19 +188,14 @@ class Contract(CrudDocument): lawyer: Entity = Field(title="Avocat en charge") location: str = Field(title="Lieu") date: datetime.date = Field(title="Date") - label: Optional[str] = None - @validator("label", always=True) - def generate_label(cls, v, values, **kwargs): - if not v: - contract_label = values['title'] - for p in values['parties']: - contract_label = contract_label + f" - {p.entity.label}" + def compute_label(self) -> str: + contract_label = self.title + for p in self.parties: + contract_label = f"{contract_label} - {p.entity.label}" - contract_label = contract_label + f" - {values['date'].strftime('%m/%d/%Y')}" - return contract_label - - return v + contract_label = f"{contract_label} - {self.date.strftime('%m/%d/%Y')}" + return contract_label class Settings(CrudDocument.Settings): fulltext_search = ['name', 'title'] @@ -205,18 +207,19 @@ class Contract(CrudDocument): } @classmethod - def find_by_signature_id(cls, signature_id: str): - crit = ElemMatch(cls.parties, {"signature_uuid": signature_id}) - return cls.find_one(crit) + async def find_by_signature_id(cls, db, signature_id: UUID): + request = {'parties': {"$elemMatch": {"signature_uuid": str(signature_id) }}} + value = await cls._get_collection(db).find_one(request) + return cls.model_validate(value) if value else None def get_signature(self, signature_id: str): for p in self.parties: - if p.signature_uuid == signature_id: + if p.signature_uuid == str(signature_id): return p def get_signature_index(self, signature_id: str): for i, p in enumerate(self.parties): - if p.signature_uuid == signature_id: + if p.signature_uuid == str(signature_id): return i def is_signed(self): @@ -225,9 +228,39 @@ class Contract(CrudDocument): return False return True + async def affix_signature(self, db, signature_index): + update_query = {"$set": { + f'parties.{signature_index}.signature_affixed': True + }} + + self.parties[signature_index].signature_affixed = True + if self.is_signed(): + update_query["$set"]['status'] = 'signed' + + await self._get_collection(db).update_one({"_id": self.id}, update_query) + return await self.get(db, self.id) + def replace_variables_in_value(variables, value: str): for v in variables: if v.value: value = value.replace('%{}%'.format(v.key), v.value) return value + +class ContractDraftFilters(Filter): + name__like: Optional[str] = None + + order_by: Optional[list[str]] = None + + class Constants(Filter.Constants): + model = ContractDraft + search_model_fields = ["name"] + +class ContractFilters(Filter): + name__like: Optional[str] = None + + order_by: Optional[list[str]] = None + + class Constants(Filter.Constants): + model = Contract + search_model_fields = ["name"] \ No newline at end of file diff --git a/back/app/contract/print/__init__.py b/back/app/contract/print/__init__.py index 10800110..a7c23b8f 100644 --- a/back/app/contract/print/__init__.py +++ b/back/app/contract/print/__init__.py @@ -1,16 +1,19 @@ import datetime import os import base64 +from uuid import UUID -from fastapi import APIRouter, HTTPException, Request +from fastapi import APIRouter, HTTPException, Request, Depends from fastapi.responses import HTMLResponse, FileResponse from fastapi.templating import Jinja2Templates +from pydantic import BaseModel from weasyprint import HTML, CSS from weasyprint.text.fonts import FontConfiguration from pathlib import Path +from app.core.routes import get_tenant_db_cursor from app.entity.models import Entity from app.template.models import ProvisionTemplate from ..models import ContractDraft, Contract, ContractStatus, replace_variables_in_value @@ -77,16 +80,24 @@ async def render_css(root_url, contract): }) +def retrieve_signature_png(filepath): + with open(filepath, "rb") as f: + b_content = f.read() + base64_utf8_str = base64.b64encode(b_content).decode('utf-8') + ext = filepath.split('.')[-1] + return f'data:image/{ext};base64,{base64_utf8_str}' + + @print_router.get("/preview/draft/{draft_id}", response_class=HTMLResponse) -async def preview_draft(draft_id: str, request: Request) -> str: - draft = await build_model(await ContractDraft.get(draft_id)) +async def preview_draft(draft_id: str, db=Depends(get_tenant_db_cursor)) -> str: + draft = await build_model(await ContractDraft.get(db, draft_id)) return await render_print('', draft) @print_router.get("/preview/signature/{signature_id}", response_class=HTMLResponse) -async def preview_contract_by_signature(signature_id: str, request: Request) -> str: - contract = await Contract.find_by_signature_id(signature_id) +async def preview_contract_by_signature(signature_id: UUID, db=Depends(get_tenant_db_cursor)) -> str: + contract = await Contract.find_by_signature_id(db, signature_id) for p in contract.parties: if p.signature_affixed: p.signature_png = retrieve_signature_png(f'media/signatures/{p.signature_uuid}.png') @@ -95,8 +106,8 @@ async def preview_contract_by_signature(signature_id: str, request: Request) -> @print_router.get("/preview/{contract_id}", response_class=HTMLResponse) -async def preview_contract(contract_id: str, request: Request) -> str: - contract = await Contract.get(contract_id) +async def preview_contract(contract_id: str, db=Depends(get_tenant_db_cursor)) -> str: + contract = await Contract.get(db, contract_id) for p in contract.parties: if p.signature_affixed: p.signature_png = retrieve_signature_png(f'media/signatures/{p.signature_uuid}.png') @@ -105,8 +116,8 @@ async def preview_contract(contract_id: str, request: Request) -> str: @print_router.get("/pdf/{contract_id}", response_class=FileResponse) -async def create_pdf(contract_id: str) -> str: - contract = await Contract.get(contract_id) +async def create_pdf(contract_id: str, db=Depends(get_tenant_db_cursor)) -> str: + contract = await Contract.get(db, contract_id) contract_path = "media/contracts/{}.pdf".format(contract_id) if not os.path.isfile(contract_path): if contract.status != ContractStatus.signed: @@ -122,10 +133,8 @@ async def create_pdf(contract_id: str) -> str: css = CSS(string=await render_css('http://nginx', contract), font_config=font_config) html.write_pdf(contract_path, stylesheets=[css], font_config=font_config) - update_query = {"$set": { - 'status': 'printed' - }} - await contract.update(update_query) + + await contract.update_status(db, 'printed') return FileResponse( contract_path, @@ -133,17 +142,9 @@ async def create_pdf(contract_id: str) -> str: filename=contract.label) -def retrieve_signature_png(filepath): - with open(filepath, "rb") as f: - b_content = f.read() - base64_utf8_str = base64.b64encode(b_content).decode('utf-8') - ext = filepath.split('.')[-1] - return f'data:image/{ext};base64,{base64_utf8_str}' - - @print_router.get("/opengraph/{signature_id}", response_class=HTMLResponse) -async def get_signature_opengraph(signature_id: str, request: Request) -> str: - contract = await Contract.find_by_signature_id(signature_id) +async def get_signature_opengraph(signature_id: str, request: Request, db=Depends(get_tenant_db_cursor)) -> str: + contract = await Contract.find_by_signature_id(db, signature_id) signature = contract.get_signature(signature_id) template = templates.get_template("opengraph.html") diff --git a/back/app/contract/routes_contract.py b/back/app/contract/routes_contract.py new file mode 100644 index 00000000..14d2f705 --- /dev/null +++ b/back/app/contract/routes_contract.py @@ -0,0 +1,70 @@ +import uuid +from fastapi import Depends, HTTPException + +from ..core.routes import get_crud_router, get_logged_tenant_db_cursor + +from .models import Contract, ContractDraft, ContractDraftStatus, replace_variables_in_value, ContractFilters +from .schemas import ContractCreate, ContractRead, ContractUpdate, ContractInit + +from ..entity.models import Entity +from ..template.models import ProvisionTemplate +from ..user.manager import get_current_user + + +contract_router = get_crud_router(Contract, ContractCreate, ContractRead, ContractUpdate, ContractFilters) +del(contract_router.routes[4]) #delete +del(contract_router.routes[3]) #update +del(contract_router.routes[1]) #create + +@contract_router.post("/", response_description="Contract Successfully created") +async def create(schema: ContractCreate, db=Depends(get_logged_tenant_db_cursor), user=Depends(get_current_user)) -> ContractRead: + await schema.validate_foreign_key(db) + + draft = await ContractDraft.get(db, schema.draft_id) + if not draft: + raise HTTPException(status_code=404, detail=f"Contract draft not found!") + + for v in draft.variables: + if not v.key or not v.value: + raise HTTPException(status_code=400, detail="Variable {} is invalid".format(v)) + + contract_dict = schema.model_dump() + del(contract_dict['draft_id']) + + lawyer = await Entity.get(db, user.entity_id) + contract_dict['lawyer'] = lawyer.model_dump() + + contract_dict['name'] = draft.name + contract_dict['title'] = draft.title + parties = [] + for p in draft.parties: + parties.append({ + 'entity': await Entity.get(db, p.entity_id), + 'part': p.part, + 'representative': await Entity.get(db, p.representative_id) if p.representative_id else None, + 'signature_uuid': str(uuid.uuid4()) + }) + + contract_dict['parties'] = parties + + provisions = [] + for p in draft.provisions: + p = p.provision + provision = await ProvisionTemplate.get(db, p.provision_template_id) if p.type == "template" \ + else p + + provisions.append({ + 'title': replace_variables_in_value(draft.variables, provision.title), + 'body': replace_variables_in_value(draft.variables, provision.body) + }) + + contract_dict['provisions'] = provisions + + record = await Contract.create(db, ContractInit(**contract_dict)) + await draft.update_status(db, ContractDraftStatus.published) + + return ContractRead.from_model(record) + +@contract_router.put("/{record_id}", response_description="") +async def update(record_id: str, contract_form: ContractUpdate, db=Depends(get_logged_tenant_db_cursor)) -> ContractRead: + raise HTTPException(status_code=400, detail="No modification on contract") diff --git a/back/app/contract/routes_draft.py b/back/app/contract/routes_draft.py index 97635af1..6f874862 100644 --- a/back/app/contract/routes_draft.py +++ b/back/app/contract/routes_draft.py @@ -1,47 +1,42 @@ from beanie import PydanticObjectId from fastapi import HTTPException, Depends -from ..core.routes import get_crud_router +from ..core.routes import get_crud_router, get_logged_tenant_db_cursor from ..user.manager import get_current_user -from .models import ContractDraft, ContractDraftStatus +from .models import ContractDraft, ContractDraftStatus, ContractDraftFilters from .schemas import ContractDraftCreate, ContractDraftRead, ContractDraftUpdate -draft_router = get_crud_router(ContractDraft, ContractDraftCreate, ContractDraftRead, ContractDraftUpdate) +draft_router = get_crud_router(ContractDraft, ContractDraftCreate, ContractDraftRead, ContractDraftUpdate, ContractDraftFilters) -del(draft_router.routes[0]) -del(draft_router.routes[2]) +del(draft_router.routes[3]) #update route +del(draft_router.routes[1]) #post route @draft_router.post("/", response_description="Contract Draft added to the database") -async def create(item: ContractDraftCreate, user=Depends(get_current_user)) -> dict: - await item.validate_foreign_key() - o = await ContractDraft(**item.dict()).create() - await o.check_is_ready() +async def create(schema: ContractDraftCreate, db=Depends(get_logged_tenant_db_cursor)) -> ContractDraftRead: + await schema.validate_foreign_key(db) + record = await ContractDraft.create(db, schema) + await record.check_is_ready(db) - return {"message": "Contract Draft added successfully", "id": o.id} + return ContractDraftRead.from_model(record) -@draft_router.put("/{id}", response_description="Contract Draft record updated") -async def update(id: PydanticObjectId, req: ContractDraftUpdate, user=Depends(get_current_user)) -> ContractDraftRead: - req = {k: v for k, v in req.dict().items() if v is not None} - update_query = {"$set": { - field: value for field, value in req.items() - }} - - item = await ContractDraft.get(id) - if not item: +@draft_router.put("/{record_id}", response_description="Contract Draft record updated") +async def update(record_id: PydanticObjectId, schema: ContractDraftUpdate, db=Depends(get_logged_tenant_db_cursor)) -> ContractDraftRead: + record = await ContractDraft.get(db, record_id) + if not record: raise HTTPException( status_code=404, detail="Contract Draft record not found!" ) - if item.status == ContractDraftStatus.published: + if record.status == ContractDraftStatus.published: raise HTTPException( status_code=400, detail="Contract Draft has already been published" ) - await item.update(update_query) - await item.check_is_ready() + record = await ContractDraft.update(db, record, schema) + await record.check_is_ready(db) - return ContractDraftRead(**item.dict()) + return ContractDraftRead.from_model(record) diff --git a/back/app/contract/routes_signature.py b/back/app/contract/routes_signature.py new file mode 100644 index 00000000..1d338344 --- /dev/null +++ b/back/app/contract/routes_signature.py @@ -0,0 +1,35 @@ +from fastapi import Depends, HTTPException, File, UploadFile, APIRouter +import shutil + +from uuid import UUID + +from .models import Contract, Party +from ..core.routes import get_tenant_db_cursor + + +signature_router = APIRouter() + +@signature_router.get("/{signature_id}", response_description="") +async def get_signature(signature_id: UUID, db=Depends(get_tenant_db_cursor)) -> Party: + contract = await Contract.find_by_signature_id(db, signature_id) + signature = contract.get_signature(signature_id) + return signature + +@signature_router.post("/{signature_id}", response_description="") +async def affix_signature(signature_id: UUID, signature_file: UploadFile = File(...), db=Depends(get_tenant_db_cursor)) -> bool: + contract = await Contract.find_by_signature_id(db, signature_id) + + if not contract: + raise HTTPException(status_code=404, detail="Contract record not found!") + + signature_index = contract.get_signature_index(signature_id) + signature = contract.parties[signature_index] + + if signature.signature_affixed: + raise HTTPException(status_code=400, detail="Signature already affixed") + + with open(f'media/signatures/{signature_id}.png', "wb") as buffer: + shutil.copyfileobj(signature_file.file, buffer) + + await contract.affix_signature(db, signature_index) + return True diff --git a/back/app/contract/schemas.py b/back/app/contract/schemas.py index e70bfdaa..2afb9914 100644 --- a/back/app/contract/schemas.py +++ b/back/app/contract/schemas.py @@ -6,11 +6,11 @@ from pydantic import BaseModel, Field from .models import ContractDraft, DraftProvision, DraftParty, Contract from ..entity.models import Entity -from ..core.schemas import Writer +from ..core.schemas import Writer, Reader from ..core.models import DictionaryEntry -class ContractDraftRead(ContractDraft): +class ContractDraftRead(Reader, ContractDraft): pass @@ -28,12 +28,12 @@ class ContractDraftCreate(Writer): title='Variables' ) - async def validate_foreign_key(self): - return + async def validate_foreign_key(self, db): for p in self.parties: - p.entity = await Entity.get(p.entity) - if p.entity is None: - raise ValueError + if p.entity_id: + p.entity = await Entity.get(db, p.entity_id) + if p.entity is None: + raise ValueError class ContractDraftUpdate(ContractDraftCreate): @@ -57,7 +57,7 @@ class PartyRead(BaseModel): title = "Partie" -class ContractRead(Contract): +class ContractRead(Reader, Contract): parties: List[PartyRead] lawyer: ForeignEntityRead @@ -70,6 +70,14 @@ class ContractCreate(Writer): location: str draft_id: str +class ContractInit(BaseModel): + date: datetime.date + location: str + lawyer: dict + name: str + title: str + parties: List[dict] + provisions: List[dict] class ContractUpdate(BaseModel): pass diff --git a/back/app/core/models.py b/back/app/core/models.py index 2b77a09c..cfb28c47 100644 --- a/back/app/core/models.py +++ b/back/app/core/models.py @@ -6,12 +6,16 @@ from pydantic import BaseModel, Field, computed_field class CrudDocument(BaseModel): - id: Optional[PydanticObjectId] = Field(alias="_id", default=None) + id: Optional[PydanticObjectId] = Field(default=None) created_at: datetime = Field(default=datetime.now(UTC), nullable=False, title="Créé le") # created_by: str updated_at: datetime = Field(default_factory=datetime.utcnow, nullable=False, title="Modifié le") # updated_by: str + @property + def _id(self): + return self.id + @computed_field def label(self) -> str: return self.compute_label() @@ -45,12 +49,17 @@ class CrudDocument(BaseModel): @classmethod async def get(cls, db, model_id): - return cls.model_validate(await cls._get_collection(db).find_one({"_id": model_id})) + value = await cls._get_collection(db).find_one({"_id": model_id}) + if not value: + return None + + value["id"] = value.pop("_id") + return cls.model_validate(value) @classmethod async def update(cls, db, model, update_schema): update_query = { - "$set": {field: value for field, value in update_schema.model_dump(mode="json").items()} + "$set": {field: value for field, value in update_schema.model_dump(mode="json").items() if field!= "id" } } await cls._get_collection(db).update_one({"_id": model.id}, update_query) diff --git a/back/app/core/routes.py b/back/app/core/routes.py index 3904726e..07ddb09b 100644 --- a/back/app/core/routes.py +++ b/back/app/core/routes.py @@ -7,8 +7,9 @@ from fastapi_filter import FilterDepends from fastapi_pagination import Page, add_pagination from fastapi_pagination.ext.motor import paginate +from .models import CrudDocument +from .schemas import Writer, Reader from ..db import get_db_client -from ..user.manager import get_current_user def parse_sort(sort_by): @@ -60,23 +61,35 @@ def parse_query(query: str, model): return And(*and_array) if len(and_array) > 1 else and_array[0] else: return {} - -#user=Depends(get_current_user) -def get_tenant_db_cursor(instance: str="westside", firm: str="cht", db_client=Depends(get_db_client), user=None): +#instance: str="westside", firm: str="cht", +def get_tenant_db_cursor(db_client=Depends(get_db_client)): + instance = "westside" + firm = "cht" return db_client[f"tenant_{instance}_{firm}"] -def get_crud_router(model, model_create, model_read, model_update, model_filter): +#instance: str="westside", firm: str="cht", +#user=Depends(get_current_user) +def get_logged_tenant_db_cursor(db_client=Depends(get_db_client), user=None): + instance = "westside" + firm = "cht" + return db_client[f"tenant_{instance}_{firm}"] + +def get_crud_router(model: CrudDocument, model_create: Writer, model_read: Reader, model_update: Writer, model_filter): model_name = model.__name__ router = APIRouter() + @router.get("/", response_model=Page[model_read], response_description=f"{model_name} records retrieved") + async def read_list(filters: model_filter=FilterDepends(model_filter), db=Depends(get_logged_tenant_db_cursor)) -> Page[model_read]: + return await paginate(model.list(db, filters)) + @router.post("/", response_description=f"{model_name} added to the database") - async def create(schema: model_create, db=Depends(get_tenant_db_cursor)) -> model_read: + async def create(schema: model_create, db=Depends(get_logged_tenant_db_cursor)) -> model_read: await schema.validate_foreign_key(db) record = await model.create(db, schema) - return model_read.from_model(record) + return model_read.validate_model(record) @router.get("/{record_id}", response_description=f"{model_name} record retrieved") - async def read_one(record_id: PydanticObjectId, db=Depends(get_tenant_db_cursor)) -> model_read: + async def read_one(record_id: PydanticObjectId, db=Depends(get_logged_tenant_db_cursor)) -> model_read: record = await model.get(db, record_id) if not record: raise HTTPException( @@ -86,12 +99,8 @@ def get_crud_router(model, model_create, model_read, model_update, model_filter) return model_read.from_model(record) - @router.get("/", response_model=Page[model_read], response_description=f"{model_name} records retrieved") - async def read_list(filters: model_filter=FilterDepends(model_filter), db=Depends(get_tenant_db_cursor)) -> Page[model_read]: - return await paginate(model.list(db, filters)) - @router.put("/{record_id}", response_description=f"{model_name} record updated") - async def update(record_id: PydanticObjectId, schema: model_update, db=Depends(get_tenant_db_cursor)) -> model_read: + async def update(record_id: PydanticObjectId, schema: model_update, db=Depends(get_logged_tenant_db_cursor)) -> model_read: record = await model.get(db, record_id) if not record: raise HTTPException( @@ -103,7 +112,7 @@ def get_crud_router(model, model_create, model_read, model_update, model_filter) return model_read.from_model(record) @router.delete("/{record_id}", response_description=f"{model_name} record deleted from the database") - async def delete(record_id: PydanticObjectId, db=Depends(get_tenant_db_cursor)) -> dict: + async def delete(record_id: PydanticObjectId, db=Depends(get_logged_tenant_db_cursor)) -> dict: record = await model.get(db, record_id) if not record: raise HTTPException( diff --git a/back/app/core/schemas.py b/back/app/core/schemas.py index a521124c..76c78301 100644 --- a/back/app/core/schemas.py +++ b/back/app/core/schemas.py @@ -1,13 +1,15 @@ +from typing import Optional + +from beanie import PydanticObjectId from pydantic import BaseModel, Field class Reader(BaseModel): - id: str = Field() + id: Optional[PydanticObjectId] = Field(default=None, validation_alias="_id") @classmethod def from_model(cls, model): - schema = cls.model_validate(model.model_dump()) - schema.id = model.id + schema = cls.model_validate(model, from_attributes=True) return schema diff --git a/back/app/entity/models.py b/back/app/entity/models.py index 8c154543..4875372d 100644 --- a/back/app/entity/models.py +++ b/back/app/entity/models.py @@ -31,8 +31,7 @@ class Individual(EntityType): def label(self) -> str: # if len(self.surnames) > 0: # return '{} "{}" {}'.format(self.firstname, self.surnames[0], self.lastname) - - return '{} {}'.format(self.firstname, self.lastname) + return f"{self.firstname} {self.lastname}" class Config: title = 'Particulier' diff --git a/back/app/entity/schemas.py b/back/app/entity/schemas.py index f3b9e674..9abb87f8 100644 --- a/back/app/entity/schemas.py +++ b/back/app/entity/schemas.py @@ -3,7 +3,7 @@ from pydantic import Field from .models import Entity, Institution, Individual, Corporation from ..core.schemas import Writer, Reader -class EntityRead(Entity, Reader): +class EntityRead(Reader, Entity): pass class EntityCreate(Writer): diff --git a/back/app/main.py b/back/app/main.py index 34e9c1d0..24f23136 100644 --- a/back/app/main.py +++ b/back/app/main.py @@ -1,11 +1,12 @@ from contextlib import asynccontextmanager from fastapi import FastAPI -#from .contract import contract_router from .db import init_db, stop_db from .user import user_router, get_auth_router + from .entity import entity_router -#from .template import template_router +from .template import template_router +from .contract import contract_router @asynccontextmanager @@ -22,8 +23,8 @@ app.include_router(user_router, prefix="/users", tags=["users"], ) multitenant_prefix = "/{instance}/{firm}" app.include_router(entity_router, prefix=f"{multitenant_prefix}/entity", tags=["entity"], ) -#app.include_router(template_router, prefix=f"{multitenant_prefix}/template", tags=["template"], ) -#app.include_router(contract_router, prefix=f"{multitenant_prefix}/contract", tags=["contract"], ) +app.include_router(template_router, prefix=f"{multitenant_prefix}/template", tags=["template"], ) +app.include_router(contract_router, prefix=f"{multitenant_prefix}/contract", tags=["contract"], ) if __name__ == '__main__': import uvicorn diff --git a/back/app/template/models.py b/back/app/template/models.py index 3f6f10fa..98445c53 100644 --- a/back/app/template/models.py +++ b/back/app/template/models.py @@ -1,9 +1,10 @@ -from typing import List +from typing import List, Optional from html import unescape -from pydantic import BaseModel, Field, validator +from pydantic import BaseModel, Field from ..core.models import CrudDocument, RichtextMultiline, RichtextSingleline, DictionaryEntry +from ..core.filter import Filter class PartyTemplate(BaseModel): @@ -47,12 +48,10 @@ class ProvisionTemplate(CrudDocument): name: str = Field(title="Nom") title: str = RichtextSingleline(title="Titre") - label: str = "" body: str = RichtextMultiline(title="Corps") - @validator("label", always=True) - def generate_label(cls, v, values, **kwargs): - return "{} - \"{}\"".format(values['name'], unescape(remove_html_tags(values['title']))) + def compute_label(self) -> str: + return f"{self.name} - \"{unescape(remove_html_tags(self.title))}\"" class Settings(CrudDocument.Settings): fulltext_search = ['name', 'title', 'body'] @@ -84,7 +83,6 @@ class ContractTemplate(CrudDocument): """ name: str = Field(title="Nom") title: str = Field(title="Titre") - label: str = "" parties: List[PartyTemplate] = Field(default=[], title="Parties") provisions: List[ProvisionTemplateReference] = Field( default=[], @@ -97,12 +95,31 @@ class ContractTemplate(CrudDocument): title="Variables" ) - @validator("label", always=True) - def generate_label(cls, v, values, **kwargs): - return "{} - \"{}\"".format(values['name'], unescape(remove_html_tags(values['title']))) + def compute_label(self) -> str: + return f"{self.name} - \"{unescape(remove_html_tags(self.title))}\"" class Settings(CrudDocument.Settings): fulltext_search = ['name', 'title'] class Config: title = 'Template de contrat' + + +class ContractTemplateFilters(Filter): + name__like: Optional[str] = None + + order_by: Optional[list[str]] = None + + class Constants(Filter.Constants): + model = ContractTemplate + search_model_fields = ["name"] + + +class ProvisionTemplateFilters(Filter): + name__like: Optional[str] = None + + order_by: Optional[list[str]] = None + + class Constants(Filter.Constants): + model = ProvisionTemplate + search_model_fields = ["name"] diff --git a/back/app/template/routes_contract.py b/back/app/template/routes_contract.py index b5463ea5..13bff033 100644 --- a/back/app/template/routes_contract.py +++ b/back/app/template/routes_contract.py @@ -1,5 +1,5 @@ from ..core.routes import get_crud_router -from .models import ContractTemplate +from .models import ContractTemplate, ContractTemplateFilters from .schemas import ContractTemplateCreate, ContractTemplateRead, ContractTemplateUpdate -router = get_crud_router(ContractTemplate, ContractTemplateCreate, ContractTemplateRead, ContractTemplateUpdate) +router = get_crud_router(ContractTemplate, ContractTemplateCreate, ContractTemplateRead, ContractTemplateUpdate, ContractTemplateFilters) diff --git a/back/app/template/routes_provision.py b/back/app/template/routes_provision.py index 6f18f262..d78b528e 100644 --- a/back/app/template/routes_provision.py +++ b/back/app/template/routes_provision.py @@ -1,5 +1,5 @@ from ..core.routes import get_crud_router -from .models import ProvisionTemplate +from .models import ProvisionTemplate, ProvisionTemplateFilters from .schemas import ProvisionTemplateCreate, ProvisionTemplateRead, ProvisionTemplateUpdate -router = get_crud_router(ProvisionTemplate, ProvisionTemplateCreate, ProvisionTemplateRead, ProvisionTemplateUpdate) +router = get_crud_router(ProvisionTemplate, ProvisionTemplateCreate, ProvisionTemplateRead, ProvisionTemplateUpdate, ProvisionTemplateFilters) diff --git a/back/app/template/schemas.py b/back/app/template/schemas.py index 9afbf1d0..adc56e57 100644 --- a/back/app/template/schemas.py +++ b/back/app/template/schemas.py @@ -2,11 +2,11 @@ from pydantic import Field from typing import List from .models import ContractTemplate, ProvisionTemplate, PartyTemplate, ProvisionTemplateReference, DictionaryEntry -from ..core.schemas import Writer +from ..core.schemas import Writer, Reader from ..core.models import RichtextMultiline, RichtextSingleline -class ContractTemplateRead(ContractTemplate): +class ContractTemplateRead(Reader, ContractTemplate): pass @@ -34,7 +34,7 @@ class ContractTemplateUpdate(ContractTemplateCreate): pass -class ProvisionTemplateRead(ProvisionTemplate): +class ProvisionTemplateRead(Reader, ProvisionTemplate): pass diff --git a/back/app/user/manager.py b/back/app/user/manager.py index 44778641..daa8e0c5 100644 --- a/back/app/user/manager.py +++ b/back/app/user/manager.py @@ -109,9 +109,6 @@ fastapi_users = FastAPIUsers[User, uuid.UUID]( get_current_user = fastapi_users.current_user(active=True) get_current_superuser = fastapi_users.current_user(active=True, superuser=True) -def get_current_user_and_firm(user=Depends(get_current_user)): - return user - def get_auth_router(): return fastapi_users.get_auth_router(auth_backend) diff --git a/front/app/src/app/views/contract-drafts/drafts.component.ts b/front/app/src/app/views/contract-drafts/drafts.component.ts index 7686958b..90f0bef5 100644 --- a/front/app/src/app/views/contract-drafts/drafts.component.ts +++ b/front/app/src/app/views/contract-drafts/drafts.component.ts @@ -70,7 +70,7 @@ export class DraftsNewComponent extends BaseDraftsComponent implements OnInit { this.temaplateForm.valueChanges.subscribe((values) => { if (values.template_id !== undefined) { this.crudService.get("template/contract", values.template_id).subscribe((templateModel) => { - delete templateModel._id; + delete templateModel.id; delete templateModel.created_at; delete templateModel.updated_at; delete templateModel.label; @@ -98,7 +98,7 @@ export class DraftsNewComponent extends BaseDraftsComponent implements OnInit { (resourceReceived)="this.onResourceReceived($event)" > - Preview + Preview diff --git a/front/app/src/app/views/contracts/contracts.component.ts b/front/app/src/app/views/contracts/contracts.component.ts index 0c0ae053..8a65a1e4 100644 --- a/front/app/src/app/views/contracts/contracts.component.ts +++ b/front/app/src/app/views/contracts/contracts.component.ts @@ -71,8 +71,8 @@ export class ContractsCardComponent extends BaseContractsComponent{ onResourceReceived(model: any): void { this.resourceReadyToPrint = model.status != "published"; - this.contractPrintLink = `${location.origin}/api/v1/contract/print/pdf/${this.resource_id}` - this.contractPreviewLink = `${location.origin}/api/v1/contract/print/preview/${this.resource_id}` + this.contractPrintLink = `${location.origin}/api/v1/westside/cht/contract/print/pdf/${this.resource_id}` + this.contractPreviewLink = `${location.origin}/api/v1/westside/cht/contract/print/preview/${this.resource_id}` } } @@ -124,7 +124,7 @@ export class ContractsSignatureComponent implements OnInit { } getPreview() { - return this.sanitizer.bypassSecurityTrustResourceUrl("/api/v1/contract/print/preview/signature/" + this.signature_id); + return this.sanitizer.bypassSecurityTrustResourceUrl("/api/v1/westside/cht/contract/print/preview/signature/" + this.signature_id); } postSignature(image: string) { diff --git a/front/app/src/common/crud/card/card.component.ts b/front/app/src/common/crud/card/card.component.ts index 3d78445b..ce527729 100644 --- a/front/app/src/common/crud/card/card.component.ts +++ b/front/app/src/common/crud/card/card.component.ts @@ -24,7 +24,7 @@ export class CardComponent implements OnInit { @Input() set model(value: any) { this._model = value; if (Object.keys(this.form.controls).length) { - delete value._id; + delete value.id; this.form.patchValue(value); } } @@ -109,10 +109,10 @@ export class CardComponent implements OnInit { error: (err) => this.error.emit("Error creating the entity:" + err) }); } else { - model._id = this.resource_id; + model.id = this.resource_id; this.crudService.update(this.resource!, model).subscribe( { next: (model: any) => { - this.resourceUpdated.emit(model._id); + this.resourceUpdated.emit(model.id); this.resourceReceived.emit(model); this.model = model; this._modelLoading$.next(false); @@ -124,7 +124,7 @@ export class CardComponent implements OnInit { onDelete() { this._modelLoading$.next(true); - this.model._id = this.resource_id; + this.model.id = this.resource_id; this.crudService.delete(this.resource!, this.model).subscribe({ next: (model: any) => { this._modelLoading$.next(false); diff --git a/front/app/src/common/crud/crud.service.ts b/front/app/src/common/crud/crud.service.ts index 3e97c9ff..f73466f0 100644 --- a/front/app/src/common/crud/crud.service.ts +++ b/front/app/src/common/crud/crud.service.ts @@ -10,10 +10,10 @@ import {SortDirection} from "./list/sortable.directive"; export class ApiService { constructor(protected http: HttpClient) {} - protected api_root: string = '/api/v1' + protected api_root: string = '/api/v1/westside/cht' public getSchema() { - return this.http.get(`${this.api_root}/openapi.json`); + return this.http.get("/api/v1/openapi.json"); } } @@ -98,7 +98,7 @@ export class CrudService extends ApiService { public update(resource: string, model: any) { return this.http.put<{ menu: [{}] }>( - `${this.api_root}/${resource.toLowerCase()}/${model._id}`, + `${this.api_root}/${resource.toLowerCase()}/${model.id}`, model ); } @@ -112,7 +112,7 @@ export class CrudService extends ApiService { public delete(resource: string, model: any) { return this.http.delete<{ menu: [{}] }>( - `${this.api_root}/${resource.toLowerCase()}/${model._id}` + `${this.api_root}/${resource.toLowerCase()}/${model.id}` ); } } diff --git a/front/app/src/common/crud/list/list.component.html b/front/app/src/common/crud/list/list.component.html index ad8d5d4c..86b31b81 100644 --- a/front/app/src/common/crud/list/list.component.html +++ b/front/app/src/common/crud/list/list.component.html @@ -33,7 +33,7 @@ Loading... - + diff --git a/front/app/src/common/crud/types/foreignkey.type.ts b/front/app/src/common/crud/types/foreignkey.type.ts index 9b309e56..3fd3e982 100644 --- a/front/app/src/common/crud/types/foreignkey.type.ts +++ b/front/app/src/common/crud/types/foreignkey.type.ts @@ -53,7 +53,7 @@ import {DictionaryService} from "./dictionary.service";