Full Working static multi tenant

This commit is contained in:
2025-04-01 00:29:43 +02:00
parent 50fdf22afc
commit 59cc709ed5
24 changed files with 327 additions and 244 deletions

View File

@@ -1,105 +1,12 @@
import uuid from fastapi import APIRouter
from fastapi import Depends, HTTPException, File, UploadFile
import shutil
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 .routes_draft import draft_router
from .print import print_router from .print import print_router
from .models import Contract, ContractDraft, ContractDraftStatus, Party, replace_variables_in_value contract_router = APIRouter()
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.include_router(draft_router, prefix="/draft", ) contract_router.include_router(draft_router, prefix="/draft", )
contract_router.include_router(contract_subrouter, )
contract_router.include_router(print_router, prefix="/print", ) contract_router.include_router(print_router, prefix="/print", )
contract_router.include_router(signature_router, prefix="/signature", )
@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

View File

@@ -1,11 +1,12 @@
import datetime import datetime
from typing import List, Literal, Optional from typing import List, Literal, Optional
from enum import Enum from enum import Enum
from uuid import UUID
from pydantic import BaseModel, Field, validator from pydantic import BaseModel, Field
from beanie.operators import ElemMatch
from ..core.models import CrudDocument, RichtextSingleline, RichtextMultiline, DictionaryEntry from ..core.models import CrudDocument, RichtextSingleline, RichtextMultiline, DictionaryEntry
from ..core.filter import Filter
from ..entity.models import Entity from ..entity.models import Entity
@@ -57,9 +58,12 @@ class Party(BaseModel):
signature_affixed: bool = False signature_affixed: bool = False
signature_png: Optional[str] = None signature_png: Optional[str] = None
class ContractProvisionType(Enum):
genuine = 'genuine'
template = 'template'
class ProvisionGenuine(BaseModel): class ProvisionGenuine(BaseModel):
type: Literal['genuine'] = 'genuine' type: Literal['genuine'] = ContractProvisionType.genuine
title: str = RichtextSingleline(props={"parametrized": True}, default="", title="Titre") title: str = RichtextSingleline(props={"parametrized": True}, default="", title="Titre")
body: str = RichtextMultiline(props={"parametrized": True}, default="", title="Corps") body: str = RichtextMultiline(props={"parametrized": True}, default="", title="Corps")
@@ -68,7 +72,7 @@ class ProvisionGenuine(BaseModel):
class ContractProvisionTemplateReference(BaseModel): class ContractProvisionTemplateReference(BaseModel):
type: Literal['template'] = 'template' type: Literal['template'] = ContractProvisionType.template
provision_template_id: str = Field( provision_template_id: str = Field(
foreignKey={ foreignKey={
"reference": { "reference": {
@@ -97,6 +101,9 @@ class Provision(BaseModel):
title: str = RichtextSingleline(title="Titre") title: str = RichtextSingleline(title="Titre")
body: str = RichtextMultiline(title="Corps") body: str = RichtextMultiline(title="Corps")
class ContractDraftUpdateStatus(BaseModel):
status: str = Field()
todo: List[str] = Field(default=[])
class ContractDraft(CrudDocument): class ContractDraft(CrudDocument):
""" """
@@ -130,7 +137,7 @@ class ContractDraft(CrudDocument):
class Config: class Config:
title = 'Brouillon de contrat' title = 'Brouillon de contrat'
async def check_is_ready(self): async def check_is_ready(self, db):
if self.status == ContractDraftStatus.published: if self.status == ContractDraftStatus.published:
return return
@@ -152,18 +159,18 @@ class ContractDraft(CrudDocument):
for v in self.variables: for v in self.variables:
if not (v.key and v.value): if not (v.key and v.value):
self.todo.append('Empty variable') self.todo.append(f'Empty variable: {v.key}')
if self.todo: if self.todo:
self.status = ContractDraftStatus.in_progress self.status = ContractDraftStatus.in_progress
else: else:
self.status = ContractDraftStatus.ready self.status = ContractDraftStatus.ready
await self.update({"$set": { await self.update(db, self, ContractDraftUpdateStatus(status=self.status, todo=self.todo))
"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): class Contract(CrudDocument):
""" """
@@ -181,20 +188,15 @@ class Contract(CrudDocument):
lawyer: Entity = Field(title="Avocat en charge") lawyer: Entity = Field(title="Avocat en charge")
location: str = Field(title="Lieu") location: str = Field(title="Lieu")
date: datetime.date = Field(title="Date") date: datetime.date = Field(title="Date")
label: Optional[str] = None
@validator("label", always=True) def compute_label(self) -> str:
def generate_label(cls, v, values, **kwargs): contract_label = self.title
if not v: for p in self.parties:
contract_label = values['title'] contract_label = f"{contract_label} - {p.entity.label}"
for p in values['parties']:
contract_label = contract_label + f" - {p.entity.label}"
contract_label = contract_label + f" - {values['date'].strftime('%m/%d/%Y')}" contract_label = f"{contract_label} - {self.date.strftime('%m/%d/%Y')}"
return contract_label return contract_label
return v
class Settings(CrudDocument.Settings): class Settings(CrudDocument.Settings):
fulltext_search = ['name', 'title'] fulltext_search = ['name', 'title']
@@ -205,18 +207,19 @@ class Contract(CrudDocument):
} }
@classmethod @classmethod
def find_by_signature_id(cls, signature_id: str): async def find_by_signature_id(cls, db, signature_id: UUID):
crit = ElemMatch(cls.parties, {"signature_uuid": signature_id}) request = {'parties': {"$elemMatch": {"signature_uuid": str(signature_id) }}}
return cls.find_one(crit) 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): def get_signature(self, signature_id: str):
for p in self.parties: for p in self.parties:
if p.signature_uuid == signature_id: if p.signature_uuid == str(signature_id):
return p return p
def get_signature_index(self, signature_id: str): def get_signature_index(self, signature_id: str):
for i, p in enumerate(self.parties): for i, p in enumerate(self.parties):
if p.signature_uuid == signature_id: if p.signature_uuid == str(signature_id):
return i return i
def is_signed(self): def is_signed(self):
@@ -225,9 +228,39 @@ class Contract(CrudDocument):
return False return False
return True 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): def replace_variables_in_value(variables, value: str):
for v in variables: for v in variables:
if v.value: if v.value:
value = value.replace('%{}%'.format(v.key), v.value) value = value.replace('%{}%'.format(v.key), v.value)
return 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"]

View File

@@ -1,16 +1,19 @@
import datetime import datetime
import os import os
import base64 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.responses import HTMLResponse, FileResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from pydantic import BaseModel
from weasyprint import HTML, CSS from weasyprint import HTML, CSS
from weasyprint.text.fonts import FontConfiguration from weasyprint.text.fonts import FontConfiguration
from pathlib import Path from pathlib import Path
from app.core.routes import get_tenant_db_cursor
from app.entity.models import Entity from app.entity.models import Entity
from app.template.models import ProvisionTemplate from app.template.models import ProvisionTemplate
from ..models import ContractDraft, Contract, ContractStatus, replace_variables_in_value 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) @print_router.get("/preview/draft/{draft_id}", response_class=HTMLResponse)
async def preview_draft(draft_id: str, request: Request) -> str: async def preview_draft(draft_id: str, db=Depends(get_tenant_db_cursor)) -> str:
draft = await build_model(await ContractDraft.get(draft_id)) draft = await build_model(await ContractDraft.get(db, draft_id))
return await render_print('', draft) return await render_print('', draft)
@print_router.get("/preview/signature/{signature_id}", response_class=HTMLResponse) @print_router.get("/preview/signature/{signature_id}", response_class=HTMLResponse)
async def preview_contract_by_signature(signature_id: str, request: Request) -> str: async def preview_contract_by_signature(signature_id: UUID, db=Depends(get_tenant_db_cursor)) -> str:
contract = await Contract.find_by_signature_id(signature_id) contract = await Contract.find_by_signature_id(db, signature_id)
for p in contract.parties: for p in contract.parties:
if p.signature_affixed: if p.signature_affixed:
p.signature_png = retrieve_signature_png(f'media/signatures/{p.signature_uuid}.png') 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) @print_router.get("/preview/{contract_id}", response_class=HTMLResponse)
async def preview_contract(contract_id: str, request: Request) -> str: async def preview_contract(contract_id: str, db=Depends(get_tenant_db_cursor)) -> str:
contract = await Contract.get(contract_id) contract = await Contract.get(db, contract_id)
for p in contract.parties: for p in contract.parties:
if p.signature_affixed: if p.signature_affixed:
p.signature_png = retrieve_signature_png(f'media/signatures/{p.signature_uuid}.png') 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) @print_router.get("/pdf/{contract_id}", response_class=FileResponse)
async def create_pdf(contract_id: str) -> str: async def create_pdf(contract_id: str, db=Depends(get_tenant_db_cursor)) -> str:
contract = await Contract.get(contract_id) contract = await Contract.get(db, contract_id)
contract_path = "media/contracts/{}.pdf".format(contract_id) contract_path = "media/contracts/{}.pdf".format(contract_id)
if not os.path.isfile(contract_path): if not os.path.isfile(contract_path):
if contract.status != ContractStatus.signed: 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) css = CSS(string=await render_css('http://nginx', contract), font_config=font_config)
html.write_pdf(contract_path, stylesheets=[css], font_config=font_config) html.write_pdf(contract_path, stylesheets=[css], font_config=font_config)
update_query = {"$set": {
'status': 'printed' await contract.update_status(db, 'printed')
}}
await contract.update(update_query)
return FileResponse( return FileResponse(
contract_path, contract_path,
@@ -133,17 +142,9 @@ async def create_pdf(contract_id: str) -> str:
filename=contract.label) 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) @print_router.get("/opengraph/{signature_id}", response_class=HTMLResponse)
async def get_signature_opengraph(signature_id: str, request: Request) -> str: async def get_signature_opengraph(signature_id: str, request: Request, db=Depends(get_tenant_db_cursor)) -> str:
contract = await Contract.find_by_signature_id(signature_id) contract = await Contract.find_by_signature_id(db, signature_id)
signature = contract.get_signature(signature_id) signature = contract.get_signature(signature_id)
template = templates.get_template("opengraph.html") template = templates.get_template("opengraph.html")

View File

@@ -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")

View File

@@ -1,47 +1,42 @@
from beanie import PydanticObjectId from beanie import PydanticObjectId
from fastapi import HTTPException, Depends 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 ..user.manager import get_current_user
from .models import ContractDraft, ContractDraftStatus from .models import ContractDraft, ContractDraftStatus, ContractDraftFilters
from .schemas import ContractDraftCreate, ContractDraftRead, ContractDraftUpdate 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[3]) #update route
del(draft_router.routes[2]) del(draft_router.routes[1]) #post route
@draft_router.post("/", response_description="Contract Draft added to the database") @draft_router.post("/", response_description="Contract Draft added to the database")
async def create(item: ContractDraftCreate, user=Depends(get_current_user)) -> dict: async def create(schema: ContractDraftCreate, db=Depends(get_logged_tenant_db_cursor)) -> ContractDraftRead:
await item.validate_foreign_key() await schema.validate_foreign_key(db)
o = await ContractDraft(**item.dict()).create() record = await ContractDraft.create(db, schema)
await o.check_is_ready() 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") @draft_router.put("/{record_id}", response_description="Contract Draft record updated")
async def update(id: PydanticObjectId, req: ContractDraftUpdate, user=Depends(get_current_user)) -> ContractDraftRead: async def update(record_id: PydanticObjectId, schema: ContractDraftUpdate, db=Depends(get_logged_tenant_db_cursor)) -> ContractDraftRead:
req = {k: v for k, v in req.dict().items() if v is not None} record = await ContractDraft.get(db, record_id)
update_query = {"$set": { if not record:
field: value for field, value in req.items()
}}
item = await ContractDraft.get(id)
if not item:
raise HTTPException( raise HTTPException(
status_code=404, status_code=404,
detail="Contract Draft record not found!" detail="Contract Draft record not found!"
) )
if item.status == ContractDraftStatus.published: if record.status == ContractDraftStatus.published:
raise HTTPException( raise HTTPException(
status_code=400, status_code=400,
detail="Contract Draft has already been published" detail="Contract Draft has already been published"
) )
await item.update(update_query) record = await ContractDraft.update(db, record, schema)
await item.check_is_ready() await record.check_is_ready(db)
return ContractDraftRead(**item.dict()) return ContractDraftRead.from_model(record)

View File

@@ -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

View File

@@ -6,11 +6,11 @@ from pydantic import BaseModel, Field
from .models import ContractDraft, DraftProvision, DraftParty, Contract from .models import ContractDraft, DraftProvision, DraftParty, Contract
from ..entity.models import Entity from ..entity.models import Entity
from ..core.schemas import Writer from ..core.schemas import Writer, Reader
from ..core.models import DictionaryEntry from ..core.models import DictionaryEntry
class ContractDraftRead(ContractDraft): class ContractDraftRead(Reader, ContractDraft):
pass pass
@@ -28,10 +28,10 @@ class ContractDraftCreate(Writer):
title='Variables' title='Variables'
) )
async def validate_foreign_key(self): async def validate_foreign_key(self, db):
return
for p in self.parties: for p in self.parties:
p.entity = await Entity.get(p.entity) if p.entity_id:
p.entity = await Entity.get(db, p.entity_id)
if p.entity is None: if p.entity is None:
raise ValueError raise ValueError
@@ -57,7 +57,7 @@ class PartyRead(BaseModel):
title = "Partie" title = "Partie"
class ContractRead(Contract): class ContractRead(Reader, Contract):
parties: List[PartyRead] parties: List[PartyRead]
lawyer: ForeignEntityRead lawyer: ForeignEntityRead
@@ -70,6 +70,14 @@ class ContractCreate(Writer):
location: str location: str
draft_id: 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): class ContractUpdate(BaseModel):
pass pass

View File

@@ -6,12 +6,16 @@ from pydantic import BaseModel, Field, computed_field
class CrudDocument(BaseModel): 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_at: datetime = Field(default=datetime.now(UTC), nullable=False, title="Créé le")
# created_by: str # created_by: str
updated_at: datetime = Field(default_factory=datetime.utcnow, nullable=False, title="Modifié le") updated_at: datetime = Field(default_factory=datetime.utcnow, nullable=False, title="Modifié le")
# updated_by: str # updated_by: str
@property
def _id(self):
return self.id
@computed_field @computed_field
def label(self) -> str: def label(self) -> str:
return self.compute_label() return self.compute_label()
@@ -45,12 +49,17 @@ class CrudDocument(BaseModel):
@classmethod @classmethod
async def get(cls, db, model_id): 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 @classmethod
async def update(cls, db, model, update_schema): async def update(cls, db, model, update_schema):
update_query = { 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) await cls._get_collection(db).update_one({"_id": model.id}, update_query)

View File

@@ -7,8 +7,9 @@ from fastapi_filter import FilterDepends
from fastapi_pagination import Page, add_pagination from fastapi_pagination import Page, add_pagination
from fastapi_pagination.ext.motor import paginate from fastapi_pagination.ext.motor import paginate
from .models import CrudDocument
from .schemas import Writer, Reader
from ..db import get_db_client from ..db import get_db_client
from ..user.manager import get_current_user
def parse_sort(sort_by): 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] return And(*and_array) if len(and_array) > 1 else and_array[0]
else: else:
return {} return {}
#instance: str="westside", firm: str="cht",
#user=Depends(get_current_user) def get_tenant_db_cursor(db_client=Depends(get_db_client)):
def get_tenant_db_cursor(instance: str="westside", firm: str="cht", db_client=Depends(get_db_client), user=None): instance = "westside"
firm = "cht"
return db_client[f"tenant_{instance}_{firm}"] 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__ model_name = model.__name__
router = APIRouter() 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") @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) await schema.validate_foreign_key(db)
record = await model.create(db, schema) 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") @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) record = await model.get(db, record_id)
if not record: if not record:
raise HTTPException( 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) 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") @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) record = await model.get(db, record_id)
if not record: if not record:
raise HTTPException( 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) return model_read.from_model(record)
@router.delete("/{record_id}", response_description=f"{model_name} record deleted from the database") @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) record = await model.get(db, record_id)
if not record: if not record:
raise HTTPException( raise HTTPException(

View File

@@ -1,13 +1,15 @@
from typing import Optional
from beanie import PydanticObjectId
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
class Reader(BaseModel): class Reader(BaseModel):
id: str = Field() id: Optional[PydanticObjectId] = Field(default=None, validation_alias="_id")
@classmethod @classmethod
def from_model(cls, model): def from_model(cls, model):
schema = cls.model_validate(model.model_dump()) schema = cls.model_validate(model, from_attributes=True)
schema.id = model.id
return schema return schema

View File

@@ -31,8 +31,7 @@ class Individual(EntityType):
def label(self) -> str: def label(self) -> str:
# if len(self.surnames) > 0: # if len(self.surnames) > 0:
# return '{} "{}" {}'.format(self.firstname, self.surnames[0], self.lastname) # return '{} "{}" {}'.format(self.firstname, self.surnames[0], self.lastname)
return f"{self.firstname} {self.lastname}"
return '{} {}'.format(self.firstname, self.lastname)
class Config: class Config:
title = 'Particulier' title = 'Particulier'

View File

@@ -3,7 +3,7 @@ from pydantic import Field
from .models import Entity, Institution, Individual, Corporation from .models import Entity, Institution, Individual, Corporation
from ..core.schemas import Writer, Reader from ..core.schemas import Writer, Reader
class EntityRead(Entity, Reader): class EntityRead(Reader, Entity):
pass pass
class EntityCreate(Writer): class EntityCreate(Writer):

View File

@@ -1,11 +1,12 @@
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
#from .contract import contract_router
from .db import init_db, stop_db from .db import init_db, stop_db
from .user import user_router, get_auth_router from .user import user_router, get_auth_router
from .entity import entity_router from .entity import entity_router
#from .template import template_router from .template import template_router
from .contract import contract_router
@asynccontextmanager @asynccontextmanager
@@ -22,8 +23,8 @@ app.include_router(user_router, prefix="/users", tags=["users"], )
multitenant_prefix = "/{instance}/{firm}" multitenant_prefix = "/{instance}/{firm}"
app.include_router(entity_router, prefix=f"{multitenant_prefix}/entity", tags=["entity"], ) 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(template_router, prefix=f"{multitenant_prefix}/template", tags=["template"], )
#app.include_router(contract_router, prefix=f"{multitenant_prefix}/contract", tags=["contract"], ) app.include_router(contract_router, prefix=f"{multitenant_prefix}/contract", tags=["contract"], )
if __name__ == '__main__': if __name__ == '__main__':
import uvicorn import uvicorn

View File

@@ -1,9 +1,10 @@
from typing import List from typing import List, Optional
from html import unescape 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.models import CrudDocument, RichtextMultiline, RichtextSingleline, DictionaryEntry
from ..core.filter import Filter
class PartyTemplate(BaseModel): class PartyTemplate(BaseModel):
@@ -47,12 +48,10 @@ class ProvisionTemplate(CrudDocument):
name: str = Field(title="Nom") name: str = Field(title="Nom")
title: str = RichtextSingleline(title="Titre") title: str = RichtextSingleline(title="Titre")
label: str = ""
body: str = RichtextMultiline(title="Corps") body: str = RichtextMultiline(title="Corps")
@validator("label", always=True) def compute_label(self) -> str:
def generate_label(cls, v, values, **kwargs): return f"{self.name} - \"{unescape(remove_html_tags(self.title))}\""
return "{} - \"{}\"".format(values['name'], unescape(remove_html_tags(values['title'])))
class Settings(CrudDocument.Settings): class Settings(CrudDocument.Settings):
fulltext_search = ['name', 'title', 'body'] fulltext_search = ['name', 'title', 'body']
@@ -84,7 +83,6 @@ class ContractTemplate(CrudDocument):
""" """
name: str = Field(title="Nom") name: str = Field(title="Nom")
title: str = Field(title="Titre") title: str = Field(title="Titre")
label: str = ""
parties: List[PartyTemplate] = Field(default=[], title="Parties") parties: List[PartyTemplate] = Field(default=[], title="Parties")
provisions: List[ProvisionTemplateReference] = Field( provisions: List[ProvisionTemplateReference] = Field(
default=[], default=[],
@@ -97,12 +95,31 @@ class ContractTemplate(CrudDocument):
title="Variables" title="Variables"
) )
@validator("label", always=True) def compute_label(self) -> str:
def generate_label(cls, v, values, **kwargs): return f"{self.name} - \"{unescape(remove_html_tags(self.title))}\""
return "{} - \"{}\"".format(values['name'], unescape(remove_html_tags(values['title'])))
class Settings(CrudDocument.Settings): class Settings(CrudDocument.Settings):
fulltext_search = ['name', 'title'] fulltext_search = ['name', 'title']
class Config: class Config:
title = 'Template de contrat' 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"]

View File

@@ -1,5 +1,5 @@
from ..core.routes import get_crud_router from ..core.routes import get_crud_router
from .models import ContractTemplate from .models import ContractTemplate, ContractTemplateFilters
from .schemas import ContractTemplateCreate, ContractTemplateRead, ContractTemplateUpdate from .schemas import ContractTemplateCreate, ContractTemplateRead, ContractTemplateUpdate
router = get_crud_router(ContractTemplate, ContractTemplateCreate, ContractTemplateRead, ContractTemplateUpdate) router = get_crud_router(ContractTemplate, ContractTemplateCreate, ContractTemplateRead, ContractTemplateUpdate, ContractTemplateFilters)

View File

@@ -1,5 +1,5 @@
from ..core.routes import get_crud_router from ..core.routes import get_crud_router
from .models import ProvisionTemplate from .models import ProvisionTemplate, ProvisionTemplateFilters
from .schemas import ProvisionTemplateCreate, ProvisionTemplateRead, ProvisionTemplateUpdate from .schemas import ProvisionTemplateCreate, ProvisionTemplateRead, ProvisionTemplateUpdate
router = get_crud_router(ProvisionTemplate, ProvisionTemplateCreate, ProvisionTemplateRead, ProvisionTemplateUpdate) router = get_crud_router(ProvisionTemplate, ProvisionTemplateCreate, ProvisionTemplateRead, ProvisionTemplateUpdate, ProvisionTemplateFilters)

View File

@@ -2,11 +2,11 @@ from pydantic import Field
from typing import List from typing import List
from .models import ContractTemplate, ProvisionTemplate, PartyTemplate, ProvisionTemplateReference, DictionaryEntry 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 from ..core.models import RichtextMultiline, RichtextSingleline
class ContractTemplateRead(ContractTemplate): class ContractTemplateRead(Reader, ContractTemplate):
pass pass
@@ -34,7 +34,7 @@ class ContractTemplateUpdate(ContractTemplateCreate):
pass pass
class ProvisionTemplateRead(ProvisionTemplate): class ProvisionTemplateRead(Reader, ProvisionTemplate):
pass pass

View File

@@ -109,9 +109,6 @@ fastapi_users = FastAPIUsers[User, uuid.UUID](
get_current_user = fastapi_users.current_user(active=True) get_current_user = fastapi_users.current_user(active=True)
get_current_superuser = fastapi_users.current_user(active=True, superuser=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(): def get_auth_router():
return fastapi_users.get_auth_router(auth_backend) return fastapi_users.get_auth_router(auth_backend)

View File

@@ -70,7 +70,7 @@ export class DraftsNewComponent extends BaseDraftsComponent implements OnInit {
this.temaplateForm.valueChanges.subscribe((values) => { this.temaplateForm.valueChanges.subscribe((values) => {
if (values.template_id !== undefined) { if (values.template_id !== undefined) {
this.crudService.get("template/contract", values.template_id).subscribe((templateModel) => { this.crudService.get("template/contract", values.template_id).subscribe((templateModel) => {
delete templateModel._id; delete templateModel.id;
delete templateModel.created_at; delete templateModel.created_at;
delete templateModel.updated_at; delete templateModel.updated_at;
delete templateModel.label; delete templateModel.label;
@@ -98,7 +98,7 @@ export class DraftsNewComponent extends BaseDraftsComponent implements OnInit {
(resourceReceived)="this.onResourceReceived($event)" (resourceReceived)="this.onResourceReceived($event)"
> >
</base-card> </base-card>
<a class="btn btn-link" href="/api/v1/contract/print/preview/draft/{{this.resource_id}}" target="_blank" i18n>Preview</a> <a class="btn btn-link" href="/api/v1/westside/cht/contract/print/preview/draft/{{this.resource_id}}" target="_blank" i18n>Preview</a>
<ng-container *ngIf="this.isReadyForPublication;"> <ng-container *ngIf="this.isReadyForPublication;">
<formly-form [fields]="newContractFormfields" [form]="newContractForm" [model]="newContractModel"></formly-form> <formly-form [fields]="newContractFormfields" [form]="newContractForm" [model]="newContractModel"></formly-form>
<button class="btn btn-success" (click)="publish()" i18n>Publish</button> <button class="btn btn-success" (click)="publish()" i18n>Publish</button>

View File

@@ -71,8 +71,8 @@ export class ContractsCardComponent extends BaseContractsComponent{
onResourceReceived(model: any): void { onResourceReceived(model: any): void {
this.resourceReadyToPrint = model.status != "published"; this.resourceReadyToPrint = model.status != "published";
this.contractPrintLink = `${location.origin}/api/v1/contract/print/pdf/${this.resource_id}` this.contractPrintLink = `${location.origin}/api/v1/westside/cht/contract/print/pdf/${this.resource_id}`
this.contractPreviewLink = `${location.origin}/api/v1/contract/print/preview/${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() { 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) { postSignature(image: string) {

View File

@@ -24,7 +24,7 @@ export class CardComponent implements OnInit {
@Input() set model(value: any) { @Input() set model(value: any) {
this._model = value; this._model = value;
if (Object.keys(this.form.controls).length) { if (Object.keys(this.form.controls).length) {
delete value._id; delete value.id;
this.form.patchValue(value); this.form.patchValue(value);
} }
} }
@@ -109,10 +109,10 @@ export class CardComponent implements OnInit {
error: (err) => this.error.emit("Error creating the entity:" + err) error: (err) => this.error.emit("Error creating the entity:" + err)
}); });
} else { } else {
model._id = this.resource_id; model.id = this.resource_id;
this.crudService.update(this.resource!, model).subscribe( { this.crudService.update(this.resource!, model).subscribe( {
next: (model: any) => { next: (model: any) => {
this.resourceUpdated.emit(model._id); this.resourceUpdated.emit(model.id);
this.resourceReceived.emit(model); this.resourceReceived.emit(model);
this.model = model; this.model = model;
this._modelLoading$.next(false); this._modelLoading$.next(false);
@@ -124,7 +124,7 @@ export class CardComponent implements OnInit {
onDelete() { onDelete() {
this._modelLoading$.next(true); this._modelLoading$.next(true);
this.model._id = this.resource_id; this.model.id = this.resource_id;
this.crudService.delete(this.resource!, this.model).subscribe({ this.crudService.delete(this.resource!, this.model).subscribe({
next: (model: any) => { next: (model: any) => {
this._modelLoading$.next(false); this._modelLoading$.next(false);

View File

@@ -10,10 +10,10 @@ import {SortDirection} from "./list/sortable.directive";
export class ApiService { export class ApiService {
constructor(protected http: HttpClient) {} constructor(protected http: HttpClient) {}
protected api_root: string = '/api/v1' protected api_root: string = '/api/v1/westside/cht'
public getSchema() { public getSchema() {
return this.http.get<Schema>(`${this.api_root}/openapi.json`); return this.http.get<Schema>("/api/v1/openapi.json");
} }
} }
@@ -98,7 +98,7 @@ export class CrudService extends ApiService {
public update(resource: string, model: any) { public update(resource: string, model: any) {
return this.http.put<{ menu: [{}] }>( return this.http.put<{ menu: [{}] }>(
`${this.api_root}/${resource.toLowerCase()}/${model._id}`, `${this.api_root}/${resource.toLowerCase()}/${model.id}`,
model model
); );
} }
@@ -112,7 +112,7 @@ export class CrudService extends ApiService {
public delete(resource: string, model: any) { public delete(resource: string, model: any) {
return this.http.delete<{ menu: [{}] }>( return this.http.delete<{ menu: [{}] }>(
`${this.api_root}/${resource.toLowerCase()}/${model._id}` `${this.api_root}/${resource.toLowerCase()}/${model.id}`
); );
} }
} }

View File

@@ -33,7 +33,7 @@
<tr *ngIf="loading$ | async"> <tr *ngIf="loading$ | async">
<td class="text-center" [attr.colspan]="this.displayedColumns.length" i18n>Loading...</td> <td class="text-center" [attr.colspan]="this.displayedColumns.length" i18n>Loading...</td>
</tr> </tr>
<tr *ngFor="let row of listData$ | async" (click)="onRowClick(row._id)" (auxclick)="onRowMiddleClick(row._id);" class="table-row-link"> <tr *ngFor="let row of listData$ | async" (click)="onRowClick(row.id)" (auxclick)="onRowMiddleClick(row.id);" class="table-row-link">
<td class="text-truncate" *ngFor="let col of this.displayedColumns" style="max-width: 150px;"> <td class="text-truncate" *ngFor="let col of this.displayedColumns" style="max-width: 150px;">
<ngb-highlight [result]="getColumnValue(row, col.path)" [term]="searchTerm"></ngb-highlight> <ngb-highlight [result]="getColumnValue(row, col.path)" [term]="searchTerm"></ngb-highlight>
</td> </td>

View File

@@ -53,7 +53,7 @@ import {DictionaryService} from "./dictionary.service";
<div class="modal-body"> <div class="modal-body">
<crud-card *ngIf="this.hasValue()" <crud-card *ngIf="this.hasValue()"
[resource]="this.foreignResource" [resource]="this.foreignResource"
[resource_id]="this.foreignModel._id" [resource_id]="this.foreignModel.id"
[schema]="this.foreignSchema" [schema]="this.foreignSchema"
[is_modal]="true" [is_modal]="true"
(resourceDeleted)="onResourceDeleted($event)" (resourceDeleted)="onResourceDeleted($event)"
@@ -106,7 +106,7 @@ export class ForeignkeyTypeComponent extends FieldType<FieldTypeConfig> implemen
if (!v) { if (!v) {
this.foreignModel = {}; this.foreignModel = {};
this.foreignLabel = ""; this.foreignLabel = "";
} else if (v != this.foreignModel._id) { } else if (v != this.foreignModel.id) {
this.loadModel(v); this.loadModel(v);
} }
}) })
@@ -123,7 +123,7 @@ export class ForeignkeyTypeComponent extends FieldType<FieldTypeConfig> implemen
loadModel(id:string) { loadModel(id:string) {
this.getResource(id).pipe( this.getResource(id).pipe(
map(v => { this.setModel(v); return v; }), map(v => { this.setModel(v); return v; }),
map( v => this.formControl.patchValue(v._id)) map( v => this.formControl.patchValue(v.id))
).subscribe() ).subscribe()
} }
@@ -150,7 +150,7 @@ export class ForeignkeyTypeComponent extends FieldType<FieldTypeConfig> implemen
selectedItem(event: any) { selectedItem(event: any) {
this.setModel(event.item); this.setModel(event.item);
this.formControl.setValue(event.item._id); this.formControl.setValue(event.item.id);
} }
setModel(model: any) { setModel(model: any) {