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

View File

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

View File

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

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

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