Full Working static multi tenant
This commit is contained in:
@@ -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", )
|
||||
|
||||
@@ -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"]
|
||||
@@ -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")
|
||||
|
||||
|
||||
70
back/app/contract/routes_contract.py
Normal file
70
back/app/contract/routes_contract.py
Normal 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")
|
||||
@@ -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)
|
||||
|
||||
35
back/app/contract/routes_signature.py
Normal file
35
back/app/contract/routes_signature.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user