41 Commits

Author SHA1 Message Date
56ca5156c4 Implementing filter and sort in back 2025-04-01 18:40:45 +02:00
59cc709ed5 Full Working static multi tenant 2025-04-01 00:29:43 +02:00
50fdf22afc Initializing multitenant 2025-03-30 17:13:14 +02:00
1a247f14ce Merge branch 'master' into multi-tenant 2025-03-28 13:42:12 +01:00
c7335af514 Replacing reverse proxy from nginx to traefik 2025-03-28 13:41:58 +01:00
706b3dc275 WIP - Adding multi tenant 2025-03-27 00:11:53 +01:00
ff78f9da54 Migrating to fastapi-pagination 2025-03-17 17:46:04 +01:00
3a14528402 upgrading libraries 2025-03-17 16:58:15 +01:00
5c276faf78 Folding lists and opened variables 2023-03-27 01:29:56 +02:00
95b17947b2 Removing accordion on contract signature page 2023-03-27 01:00:11 +02:00
f20635e10e Spacing printed lis 2023-03-27 00:55:24 +02:00
8d4d64076a Correcting list navigation 2023-03-26 21:00:38 +02:00
f9867b3e2b Changing the contract file name to the contract label. Updating label content with a dash 2023-03-24 13:42:07 +01:00
b89d043880 Updating favicon 2023-03-21 21:33:22 +01:00
0ecbb99423 Square border radii 2023-03-20 16:02:27 +01:00
61bd5589ff Adding static meta to index.html 2023-03-20 16:00:46 +01:00
72065c0d0d Adding static meta data on signatures 2023-03-20 14:30:43 +01:00
cd8bb58dfb Revert "Adding a label to signature read entities"
This reverts commit 191a3d0018.
2023-03-20 14:05:25 +01:00
9ab1571067 Revert "Adding metadata for social networks on signature page"
This reverts commit 2948e9b961.
2023-03-20 14:04:27 +01:00
2948e9b961 Adding metadata for social networks on signature page 2023-03-19 13:55:57 +01:00
191a3d0018 Adding a label to signature read entities 2023-03-19 13:55:13 +01:00
8885969a07 Removing nickname from individual's labels 2023-03-19 13:54:38 +01:00
a997e54891 Adding an autogenerated label to contracts 2023-03-19 13:53:07 +01:00
0b12f8718a Decorating the sidenav 2023-03-18 18:29:37 +01:00
d5d2f31178 Correcting signature translation bug 2023-03-18 17:35:06 +01:00
1bd2774cdd Correcting redirection error after contract creation 2023-03-18 17:27:35 +01:00
e5375edda8 Changing website title 2023-03-18 17:20:08 +01:00
ae306bb89e Correcting overselecting menue 2023-03-18 17:13:36 +01:00
e877c36a3d Moving contracts/drafts to contract-drafts 2023-03-18 17:07:29 +01:00
7f639fc5a0 Correcting overselecting menue 2023-03-18 17:05:58 +01:00
7fbba953cd separating logoutbutton from rest of menu 2023-03-18 16:28:18 +01:00
4e52999b5d Progression translations 2023-03-18 16:21:55 +01:00
392d66b03e Correcting bug caused by not deep enough clones 2023-03-18 16:12:54 +01:00
dc6616bba6 Use address bar to remember list params 2023-03-17 17:45:50 +01:00
f3f0ddc004 Implementing enus is jsonschema utilities 2023-03-17 17:39:20 +01:00
0b9d5d42cb Increasing flashmessage display duration to human-readable length 2023-03-17 15:23:58 +01:00
bb0ecb5c13 Sending an error message on contract failure 2023-03-17 15:23:21 +01:00
5a5f1b3519 Correcting foreignKey not updated on update 2023-03-17 15:15:53 +01:00
2adecb99d2 Removing htmltags in list values 2023-03-17 15:06:31 +01:00
3db7c62b09 Filtering out htmlentities in rich text field 2023-03-17 14:36:40 +01:00
78c4ee119a Improving table list display 2023-03-17 14:35:55 +01:00
64 changed files with 1106 additions and 559 deletions

View File

@@ -1,4 +1,4 @@
FROM python:3.10 FROM python:3.13
RUN apt update && apt install -y xfonts-base xfonts-75dpi python3-pip python3-cffi python3-brotli libpango-1.0-0 libpangoft2-1.0-0 \ RUN apt update && apt install -y xfonts-base xfonts-75dpi python3-pip python3-cffi python3-brotli libpango-1.0-0 libpangoft2-1.0-0 \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*

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 from typing import List, Literal, Optional
from enum import Enum from enum import Enum
from uuid import UUID
from pydantic import BaseModel, Field 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, FilterSchema
from ..entity.models import Entity from ..entity.models import Entity
@@ -52,14 +53,17 @@ class DraftParty(BaseModel):
class Party(BaseModel): class Party(BaseModel):
entity: Entity entity: Entity
part: str part: str
representative: Entity = None representative: Optional[Entity] = None
signature_uuid: str signature_uuid: str
signature_affixed: bool = False signature_affixed: bool = False
signature_png: 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": {
@@ -90,13 +94,16 @@ class DraftProvision(BaseModel):
provision: ContractProvisionTemplateReference | ProvisionGenuine = Field(..., discriminator='type') provision: ContractProvisionTemplateReference | ProvisionGenuine = Field(..., discriminator='type')
class Config: class Config:
title = 'Provision' title = 'Clause'
class Provision(BaseModel): 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,20 +159,24 @@ 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):
"""
Contrat publié. Les contrats ne peuvent pas être modifiés.
Ils peuvent seulement être signés par les parties et imprimés par l'avocat
"""
name: str = Field(title="Nom") name: str = Field(title="Nom")
title: str = Field(title="Titre") title: str = Field(title="Titre")
parties: List[Party] = Field(title="Parties") parties: List[Party] = Field(title="Parties")
@@ -178,6 +189,14 @@ class Contract(CrudDocument):
location: str = Field(title="Lieu") location: str = Field(title="Lieu")
date: datetime.date = Field(title="Date") date: datetime.date = Field(title="Date")
def compute_label(self) -> str:
contract_label = self.title
for p in self.parties:
contract_label = f"{contract_label} - {p.entity.label}"
contract_label = f"{contract_label} - {self.date.strftime('%m/%d/%Y')}"
return contract_label
class Settings(CrudDocument.Settings): class Settings(CrudDocument.Settings):
fulltext_search = ['name', 'title'] fulltext_search = ['name', 'title']
@@ -188,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):
@@ -208,9 +228,36 @@ 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(FilterSchema):
status: Optional[str] = None
class Constants(Filter.Constants):
model = ContractDraft
search_model_fields = ["label", "status"]
class ContractFilters(FilterSchema):
status: Optional[str] = None
class Constants(Filter.Constants):
model = Contract
search_model_fields = ["label", "status"]

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,20 +133,25 @@ 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,
media_type="application/pdf", media_type="application/pdf",
filename=contract.name) filename=contract.label)
def retrieve_signature_png(filepath): @print_router.get("/opengraph/{signature_id}", response_class=HTMLResponse)
with open(filepath, "rb") as f: async def get_signature_opengraph(signature_id: str, request: Request, db=Depends(get_tenant_db_cursor)) -> str:
b_content = f.read() contract = await Contract.find_by_signature_id(db, signature_id)
base64_utf8_str = base64.b64encode(b_content).decode('utf-8') signature = contract.get_signature(signature_id)
ext = filepath.split('.')[-1] template = templates.get_template("opengraph.html")
return f'data:image/{ext};base64,{base64_utf8_str}'
signatory = signature.representative.label if signature.representative else signature.entity.label
return template.render({
"signatory": signatory,
"title": contract.label,
"origin_url": f"{request.url.scheme}://{request.url.hostname}"
})

View File

@@ -0,0 +1,10 @@
<html>
<head>
<meta property="og:title" content="{{ title }}" />
<meta property="og:description" content="Cette page est à la destination exclusive de {{ signatory }}
Si vous n'êtes pas {{ signatory }}, veuillez fermer cette page immédiatement et surpprimer tous les liens en votre possession menant vers celle-ci.
En vous maintenant et/ou en interagissant avec cette page, vous enfreignez l'article L.229 du code pénal de l'Etat de San Andreas pour usurpation d'identité et vous vous exposez ainsi à une amende de 20 000$ ainsi qu'à des poursuites civiles.
Le cabinet Cooper, Hillman & Toshi LLC" />
<meta property="og:image" content="{{ origin_url }}/assets/logo.png" />
</head>
</html>

View File

@@ -109,6 +109,11 @@ h2 {
text-indent: 2em; text-indent: 2em;
} }
.content td p {
text-indent: 0;
text-align: left;
}
.provision { .provision {
page-break-inside: avoid; page-break-inside: avoid;
} }
@@ -117,13 +122,17 @@ p {
text-align: justify; text-align: justify;
} }
li {
margin: 16px 0;
}
.footer { .footer {
margin-top: 30px; margin-top: 30px;
page-break-inside: avoid; page-break-inside: avoid;
} }
.mention { .mention {
margin: 0px; margin: 0;
font-size: 0.9em; font-size: 0.9em;
} }

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,12 +28,12 @@ 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:
if p.entity is None: p.entity = await Entity.get(db, p.entity_id)
raise ValueError if p.entity is None:
raise ValueError
class ContractDraftUpdate(ContractDraftCreate): class ContractDraftUpdate(ContractDraftCreate):
@@ -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

135
back/app/core/filter.py Normal file
View File

@@ -0,0 +1,135 @@
from collections.abc import Callable, Mapping
from typing import Any, Optional, Union
from pydantic import ValidationInfo, field_validator
from fastapi_filter.base.filter import BaseFilterModel
_odm_operator_transformer: dict[str, Callable[[Optional[str]], Optional[dict[str, Any]]]] = {
"neq": lambda value: {"$ne": value},
"gt": lambda value: {"$gt": value},
"gte": lambda value: {"$gte": value},
"in": lambda value: {"$in": value},
"isnull": lambda value: None if value else {"$ne": None},
"lt": lambda value: {"$lt": value},
"lte": lambda value: {"$lte": value},
"not": lambda value: {"$ne": value},
"ne": lambda value: {"$ne": value},
"not_in": lambda value: {"$nin": value},
"nin": lambda value: {"$nin": value},
"like": lambda value: {"$regex": f".*{value}.*"},
"ilike": lambda value: {"$regex": f".*{value}.*", "$options": "i"},
"exists": lambda value: {"$exists": value},
}
class Filter(BaseFilterModel):
"""Base filter for beanie related filters.
Example:
```python
class MyModel:
id: PrimaryKey()
name: StringField(null=True)
count: IntField()
created_at: DatetimeField()
class MyModelFilter(Filter):
id: Optional[int]
id__in: Optional[str]
count: Optional[int]
count__lte: Optional[int]
created_at__gt: Optional[datetime]
name__ne: Optional[str]
name__nin: Optional[list[str]]
name__isnull: Optional[bool]
```
"""
def sort(self):
if not self.ordering_values:
return None
sort = {}
for column in self.ordering_values:
direction = 1
if column[0] in ["+", "-"]:
if column[0] == "-":
direction = -1
column = column[1:]
sort[column] = direction
return sort
@field_validator("*", mode="before")
@classmethod
def split_str(
cls: type["BaseFilterModel"], value: Optional[str], field: ValidationInfo
) -> Optional[Union[list[str], str]]:
if (
field.field_name is not None
and (
field.field_name == cls.Constants.ordering_field_name
or field.field_name.endswith("__in")
or field.field_name.endswith("__nin")
)
and isinstance(value, str)
):
if not value:
# Empty string should return [] not ['']
return []
return list(value.split(","))
return value
def _get_filter_conditions(self, nesting_depth: int = 1) -> list[tuple[Mapping[str, Any], Mapping[str, Any]]]:
filter_conditions: list[tuple[Mapping[str, Any], Mapping[str, Any]]] = []
for field_name, value in self.filtering_fields:
field_value = getattr(self, field_name)
if isinstance(field_value, Filter):
if not field_value.model_dump(exclude_none=True, exclude_unset=True):
continue
filter_conditions.append(
(
{field_name: _odm_operator_transformer["neq"](None)},
{"fetch_links": True, "nesting_depth": nesting_depth},
)
)
for part, part_options in field_value._get_filter_conditions(nesting_depth=nesting_depth + 1): # noqa: SLF001
for sub_field_name, sub_value in part.items():
filter_conditions.append(
(
{f"{field_name}.{sub_field_name}": sub_value},
{"fetch_links": True, "nesting_depth": nesting_depth, **part_options},
)
)
elif "__" in field_name:
stripped_field_name, operator = field_name.split("__")
search_criteria = _odm_operator_transformer[operator](value)
filter_conditions.append(({stripped_field_name: search_criteria}, {}))
elif field_name == self.Constants.search_field_name and hasattr(self.Constants, "search_model_fields"):
search_conditions = [
{search_field: _odm_operator_transformer["ilike"](value)}
for search_field in self.Constants.search_model_fields
]
filter_conditions.append(({"$or": search_conditions}, {}))
else:
filter_conditions.append(({field_name: value}, {}))
return filter_conditions
def filter(self, query):
data = self._get_filter_conditions()
for filter_condition, filter_kwargs in data:
for field_name, value in filter_condition.items():
if field_name in query:
query[field_name] = query[field_name] | value
else:
query[field_name] = value
return query
class FilterSchema(Filter):
label__ilike: Optional[str] = None
order_by: Optional[list[str]] = None

View File

@@ -1,21 +1,82 @@
from datetime import datetime from datetime import datetime, UTC
from typing import Optional
from beanie import Document from beanie import PydanticObjectId
from pydantic import BaseModel, Field, validator from motor.motor_asyncio import AsyncIOMotorCollection
from pydantic import BaseModel, Field, computed_field
class CrudDocument(Document): class CrudDocument(BaseModel):
_id: str id: Optional[PydanticObjectId] = Field(default=None)
created_at: datetime = Field(default=datetime.utcnow(), nullable=False, title="Créé le") 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_at: datetime = Field(default_factory=datetime.utcnow, nullable=False, title="Modifié le")
# updated_by: str
@validator("label", always=True, check_fields=False) @property
def generate_label(cls, v, values, **kwargs): def _id(self):
return v return self.id
@computed_field
def label(self) -> str:
return self.compute_label()
def compute_label(self) -> str:
return ""
class Settings: class Settings:
fulltext_search = [] fulltext_search = []
@classmethod
def _collection_name(cls):
return cls.__name__
@classmethod
def _get_collection(cls, db) -> AsyncIOMotorCollection:
return db.get_collection(cls._collection_name())
@classmethod
async def create(cls, db, create_schema):
values = cls.model_validate(create_schema.model_dump()).model_dump(mode="json")
result = await cls._get_collection(db).insert_one(values)
return await cls.get(db, result.inserted_id)
@classmethod
def find(cls, db, filters):
return {
"collection": cls._get_collection(db),
"query_filter": filters.filter({}),
"sort": filters.sort(),
}
@classmethod
def list(cls, db):
return cls._get_collection(db).find({})
@classmethod
async def get(cls, db, 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() if field!= "id" }
}
await cls._get_collection(db).update_one({"_id": model.id}, update_query)
return await cls.get(db, model.id)
@classmethod
async def delete(cls, db, model):
await cls._get_collection(db).delete_one({"_id": model.id})
def text_area(*args, **kwargs): def text_area(*args, **kwargs):
kwargs['widget'] = { kwargs['widget'] = {

View File

@@ -3,116 +3,77 @@ from beanie.odm.operators.find.comparison import In
from beanie.operators import And, RegEx, Eq from beanie.operators import And, RegEx, Eq
from fastapi import APIRouter, HTTPException, Depends from fastapi import APIRouter, HTTPException, Depends
from fastapi_paginate import Page, Params, add_pagination from fastapi_filter import FilterDepends
from fastapi_paginate.ext.motor import paginate from fastapi_pagination import Page, add_pagination
from fastapi_pagination.ext.motor import paginate
from ..user.manager import get_current_user, get_current_superuser from .models import CrudDocument
from .schemas import Writer, Reader
from ..db import get_db_client
def parse_sort(sort_by): #instance: str="westside", firm: str="cht",
if not sort_by: def get_tenant_db_cursor(db_client=Depends(get_db_client)):
return [] instance = "westside"
firm = "cht"
return db_client[f"tenant_{instance}_{firm}"]
fields = [] #instance: str="westside", firm: str="cht",
for field in sort_by.split(','): #user=Depends(get_current_user)
dir, col = field.split('(') def get_logged_tenant_db_cursor(db_client=Depends(get_db_client), user=None):
fields.append((col[:-1], 1 if dir == 'asc' else -1)) instance = "westside"
firm = "cht"
return fields return db_client[f"tenant_{instance}_{firm}"]
def Or(filters):
return {'$or': filters}
def parse_query(query: str, model):
if query is None:
return {}
and_array = []
for criterion in query.split(' AND '):
[column, operator, value] = criterion.split(' ', 2)
column = column.lower()
if column == 'fulltext':
if not model.Settings.fulltext_search:
continue
or_array = []
for field in model.Settings.fulltext_search:
words_and_array = []
for word in value.split(' '):
words_and_array.append(RegEx(field, word, 'i'))
or_array.append(And(*words_and_array) if len(words_and_array) > 1 else words_and_array[0])
operand = Or(or_array) if len(or_array) > 1 else or_array[0]
elif operator == 'eq':
operand = Eq(column, value)
elif operator == 'in':
operand = In(column, value.split(','))
and_array.append(operand)
if and_array:
return And(*and_array) if len(and_array) > 1 else and_array[0]
else:
return {}
def get_crud_router(model, model_create, model_read, model_update):
def get_crud_router(model: CrudDocument, model_create: Writer, model_read: Reader, model_update: Writer, model_filter):
model_name = model.__name__
router = APIRouter() router = APIRouter()
@router.post("/", response_description="{} added to the database".format(model.__name__)) @router.get("/", response_model=Page[model_read], response_description=f"{model_name} records retrieved")
async def create(item: model_create, user=Depends(get_current_user)) -> dict: async def read_list(filters: model_filter=FilterDepends(model_filter), db=Depends(get_logged_tenant_db_cursor)) -> Page[model_read]:
await item.validate_foreign_key() return await paginate(**model.find(db, filters))
o = await model(**item.dict()).create()
return {"message": "{} added successfully".format(model.__name__), "id": o.id}
@router.get("/{id}", response_description="{} record retrieved".format(model.__name__)) @router.post("/", response_description=f"{model_name} added to the database")
async def read_id(id: PydanticObjectId, user=Depends(get_current_user)) -> model_read: async def create(schema: model_create, db=Depends(get_logged_tenant_db_cursor)) -> model_read:
item = await model.get(id) await schema.validate_foreign_key(db)
return model_read(**item.dict()) record = await model.create(db, schema)
return model_read.validate_model(record)
@router.get("/", response_model=Page[model_read], response_description="{} records retrieved".format(model.__name__)) @router.get("/{record_id}", response_description=f"{model_name} record retrieved")
async def read_list(size: int = 50, page: int = 1, sort_by: str = None, query: str = None, async def read_one(record_id: PydanticObjectId, db=Depends(get_logged_tenant_db_cursor)) -> model_read:
user=Depends(get_current_user)) -> Page[model_read]: record = await model.get(db, record_id)
sort = parse_sort(sort_by) if not record:
query = parse_query(query, model_read)
collection = model.get_motor_collection()
items = paginate(collection, query, Params(**{'size': size, 'page': page}), sort=sort)
return await items
@router.put("/{id}", response_description="{} record updated".format(model.__name__))
async def update(id: PydanticObjectId, req: model_update, user=Depends(get_current_user)) -> model_read:
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 model.get(id)
if not item:
raise HTTPException( raise HTTPException(
status_code=404, status_code=404,
detail="{} record not found!".format(model.__name__) detail=f"{model_name} record not found!"
) )
await item.update(update_query) return model_read.from_model(record)
return model_read(**item.dict())
@router.delete("/{id}", response_description="{} record deleted from the database".format(model.__name__)) @router.put("/{record_id}", response_description=f"{model_name} record updated")
async def delete(id: PydanticObjectId, user=Depends(get_current_superuser)) -> dict: async def update(record_id: PydanticObjectId, schema: model_update, db=Depends(get_logged_tenant_db_cursor)) -> model_read:
item = await model.get(id) record = await model.get(db, record_id)
if not record:
if not item:
raise HTTPException( raise HTTPException(
status_code=404, status_code=404,
detail="{} record not found!".format(model.__name__) detail=f"{model_name} record not found!"
) )
await item.delete() record = await model.update(db, record, schema)
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_logged_tenant_db_cursor)) -> dict:
record = await model.get(db, record_id)
if not record:
raise HTTPException(
status_code=404,
detail=f"{model_name} record not found!"
)
await model.delete(db, record)
return { return {
"message": "{} deleted successfully".format(model.__name__) "message": f"{model_name} deleted successfully"
} }
add_pagination(router) add_pagination(router)

View File

@@ -1,12 +1,18 @@
from pydantic import BaseModel from typing import Optional
from beanie import PydanticObjectId
from pydantic import BaseModel, Field
class Reader(BaseModel): class Reader(BaseModel):
pass id: Optional[PydanticObjectId] = Field(default=None, validation_alias="_id")
# class Config:
# fields = {'id': '_id'} @classmethod
def from_model(cls, model):
schema = cls.model_validate(model, from_attributes=True)
return schema
class Writer(BaseModel): class Writer(BaseModel):
async def validate_foreign_key(self): async def validate_foreign_key(self, db):
pass pass

View File

@@ -3,21 +3,22 @@ import motor.motor_asyncio
from beanie import init_beanie from beanie import init_beanie
from .user import User, AccessToken from .user import User, AccessToken
from .entity.models import Entity
from .template.models import ContractTemplate, ProvisionTemplate
from .contract.models import ContractDraft, Contract
# from .order.models import Order
DB_PASSWORD = "IBO3eber0mdw2R9pnInLdtFykQFY2f06" DB_PASSWORD = "IBO3eber0mdw2R9pnInLdtFykQFY2f06"
DATABASE_URL = f"mongodb://root:{DB_PASSWORD}@mongo:27017/" DATABASE_URL = f"mongodb://root:{DB_PASSWORD}@mongo:27017/"
client = motor.motor_asyncio.AsyncIOMotorClient(
DATABASE_URL, uuidRepresentation="standard"
)
async def init_db(): async def init_db():
client = motor.motor_asyncio.AsyncIOMotorClient( await init_beanie(database=client.core,
DATABASE_URL, uuidRepresentation="standard" document_models=[User, AccessToken, ], # Entity, ContractTemplate, ProvisionTemplate, ContractDraft, Contract,
)
await init_beanie(database=client.db_name,
document_models=[User, AccessToken, Entity, ContractTemplate, ProvisionTemplate, ContractDraft,
Contract, ],
allow_index_dropping=True) allow_index_dropping=True)
async def stop_db():
client.close()
def get_db_client():
yield client

View File

@@ -1,10 +1,11 @@
from datetime import date, datetime from datetime import date, datetime
from typing import List, Literal, Optional from typing import List, Literal, Optional
from pydantic import Field, BaseModel, validator from pydantic import Field, BaseModel
from beanie import Indexed from beanie import Indexed
from ..core.models import CrudDocument from ..core.models import CrudDocument
from ..core.filter import Filter, FilterSchema
class EntityType(BaseModel): class EntityType(BaseModel):
@@ -23,15 +24,14 @@ class Individual(EntityType):
props={"items-per-row": "4", "numbered": True}, props={"items-per-row": "4", "numbered": True},
title="Surnoms" title="Surnoms"
) )
day_of_birth: date = Field(default=None, title='Date de naissance') day_of_birth: Optional[date] = Field(default=None, title='Date de naissance')
place_of_birth: str = Field(default="", title='Lieu de naissance') place_of_birth: Optional[str] = Field(default="", title='Lieu de naissance')
@property @property
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'
@@ -75,14 +75,12 @@ class Entity(CrudDocument):
Fiche d'un client Fiche d'un client
""" """
entity_data: Individual | Corporation | Institution = Field(..., discriminator='type') entity_data: Individual | Corporation | Institution = Field(..., discriminator='type')
label: str = None
address: str = Field(default="", title='Adresse') address: str = Field(default="", title='Adresse')
@validator("label", always=True) def compute_label(self) -> str:
def generate_label(cls, v, values, **kwargs): if not self.entity_data:
if 'entity_data' not in values: return ""
return v return self.entity_data.label
return values['entity_data'].label
class Settings(CrudDocument.Settings): class Settings(CrudDocument.Settings):
fulltext_search = ['label'] fulltext_search = ['label']
@@ -96,6 +94,8 @@ class Entity(CrudDocument):
class Config: class Config:
title = 'Client' title = 'Client'
@classmethod
def get_create_resource(cls): class EntityFilters(FilterSchema):
print('coucou') class Constants(Filter.Constants):
model = Entity
search_model_fields = ["label"]

View File

@@ -1,7 +1,5 @@
from ..core.routes import get_crud_router from ..core.routes import get_crud_router
from .models import Entity from .models import Entity, EntityFilters
from .schemas import EntityCreate, EntityRead, EntityUpdate from .schemas import EntityCreate, EntityRead, EntityUpdate
router = get_crud_router(Entity, EntityCreate, EntityRead, EntityUpdate) router = get_crud_router(Entity, EntityCreate, EntityRead, EntityUpdate, EntityFilters)

View File

@@ -1,14 +1,11 @@
from typing import Optional from pydantic import Field
from pydantic import BaseModel, Field
from .models import Entity, Institution, Individual, Corporation from .models import Entity, Institution, Individual, Corporation
from ..core.schemas import Writer from ..core.schemas import Writer, Reader
class EntityRead(Reader, Entity):
class EntityRead(Entity):
pass pass
class EntityCreate(Writer): class EntityCreate(Writer):
entity_data: Individual | Corporation | Institution = Field(..., discriminator='type') entity_data: Individual | Corporation | Institution = Field(..., discriminator='type')
address: str = Field(default="", title='Adresse') address: str = Field(default="", title='Adresse')
@@ -16,6 +13,5 @@ class EntityCreate(Writer):
class Config: class Config:
title = "Création d'un client" title = "Création d'un client"
class EntityUpdate(EntityCreate): class EntityUpdate(EntityCreate):
pass pass

View File

@@ -1,26 +1,30 @@
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
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 .order import order_router from .contract import contract_router
app = FastAPI(root_path="/api/v1")
@app.on_event("startup") @asynccontextmanager
async def on_startup(): async def lifespan(app: FastAPI):
await init_db() await init_db()
yield
await stop_db()
app = FastAPI(root_path="/api/v1", lifespan=lifespan)
app.include_router(get_auth_router(), prefix="/auth", tags=["auth"], ) app.include_router(get_auth_router(), prefix="/auth", tags=["auth"], )
app.include_router(user_router, prefix="/users", tags=["users"], ) app.include_router(user_router, prefix="/users", tags=["users"], )
app.include_router(entity_router, prefix="/entity", tags=["entity"], )
app.include_router(template_router, prefix="/template", tags=["template"], ) multitenant_prefix = "/{instance}/{firm}"
app.include_router(contract_router, prefix="/contract", tags=["contract"], )
# app.include_router(order_router, prefix="/order", tags=["order"], ) 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"], )
if __name__ == '__main__': if __name__ == '__main__':
import uvicorn import uvicorn

View File

@@ -1 +0,0 @@
from .routes import router as order_router

View File

@@ -1,13 +0,0 @@
from datetime import datetime
from beanie import Document
class Order(Document):
id: str
client: str
created_at: datetime
updated_at: datetime
class Settings:
name = "order_collection"

View File

@@ -1,5 +0,0 @@
from ..core.routes import get_crud_router
from .models import Order
from .schemas import OrderCreate, OrderRead, OrderUpdate
router = get_crud_router(Order, OrderCreate, OrderRead, OrderUpdate)

View File

@@ -1,14 +0,0 @@
import uuid
from pydantic import BaseModel
class OrderRead(BaseModel):
pass
class OrderCreate(BaseModel):
login: str
class OrderUpdate(BaseModel):
pass

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, FilterSchema
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,23 @@ 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(FilterSchema):
class Constants(Filter.Constants):
model = ContractTemplate
search_model_fields = ["label"]
class ProvisionTemplateFilters(FilterSchema):
class Constants(Filter.Constants):
model = ProvisionTemplate
search_model_fields = ["label"]

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

@@ -1,7 +1,7 @@
from typing import Optional, TypeVar from typing import Optional, TypeVar
from datetime import datetime from datetime import datetime
from pydantic import Field from pydantic import Field
from beanie import PydanticObjectId from beanie import Document
from fastapi_users.db import BeanieBaseUser, BeanieUserDatabase from fastapi_users.db import BeanieBaseUser, BeanieUserDatabase
from fastapi_users_db_beanie.access_token import BeanieAccessTokenDatabase, BeanieBaseAccessToken from fastapi_users_db_beanie.access_token import BeanieAccessTokenDatabase, BeanieBaseAccessToken
@@ -9,11 +9,11 @@ from fastapi_users_db_beanie.access_token import BeanieAccessTokenDatabase, Bean
from pymongo import IndexModel from pymongo import IndexModel
class AccessToken(BeanieBaseAccessToken[PydanticObjectId]): class AccessToken(BeanieBaseAccessToken, Document):
pass pass
class User(BeanieBaseUser[PydanticObjectId]): class User(BeanieBaseUser, Document):
login: str login: str
entity_id: str entity_id: str
created_at: datetime = Field(default=datetime.utcnow(), nullable=False) created_at: datetime = Field(default=datetime.utcnow(), nullable=False)

View File

@@ -1,4 +1,6 @@
from pydantic import BaseModel from typing import Annotated
from pydantic import BaseModel, Field
from fastapi_users import schemas from fastapi_users import schemas
from .models import User from .models import User
@@ -9,12 +11,8 @@ class UserBase(schemas.CreateUpdateDictModel):
class UserRead(User): class UserRead(User):
class Config: _id: Annotated[str, Field(alias='id')]
fields = { hashed_password: Annotated[str, Field(exclude=True)]
'_id': {'alias': 'id'},
'hashed_password': {'exclude': True}
}
class UserCreate(UserBase): class UserCreate(UserBase):
login: str login: str

View File

@@ -1,8 +1,8 @@
fastapi==0.88.0 fastapi
fastapi_users==10.2.1 fastapi_users
fastapi_users_db_beanie==1.1.2 fastapi_users_db_beanie
motor==3.1.1 fastapi_pagination
fastapi-paginate==0.1.0 fastapi_filter
uvicorn uvicorn
jinja2 jinja2
weasyprint weasyprint

View File

@@ -1,4 +1,3 @@
version: "3.9"
services: services:
back: back:
build: build:
@@ -10,6 +9,11 @@ services:
volumes: volumes:
- ./back/app:/code/app - ./back/app:/code/app
- ./back/media:/code/media - ./back/media:/code/media
labels:
- "traefik.enable=true"
- "traefik.http.routers.back.entrypoints=web"
- "traefik.http.routers.back.rule=PathPrefix(`/api/v1/`)"
- "traefik.http.services.back.loadbalancer.server.port=8000"
front: front:
build: build:
@@ -21,12 +25,23 @@ services:
volumes: volumes:
- ./front/app/src:/app/src - ./front/app/src:/app/src
- ./front/app/public:/app/public - ./front/app/public:/app/public
labels:
- "traefik.enable=true"
- "traefik.http.routers.front.entrypoints=web"
- "traefik.http.routers.front.rule=PathPrefix(`/`)"
- "traefik.http.services.front.loadbalancer.server.port=4200"
nginx: proxy:
build: image: traefik
context: ./nginx
image: cht-lawfirm-nginx-dev
restart: always restart: always
command:
- --providers.docker
- --providers.docker.watch=true
- --providers.docker.exposedByDefault=false
- --entrypoints.web.address=:80
- --log.level=DEBUG
volumes:
- /var/run/docker.sock:/var/run/docker.sock
ports: ports:
- "80:80" - "80:80"

View File

@@ -1,6 +1,5 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import {ContractsModule} from "./views/contracts/contracts.module";
const routes: Routes = [ const routes: Routes = [
{ {
@@ -30,6 +29,11 @@ const routes: Routes = [
loadChildren: () => loadChildren: () =>
import('./views/templates/templates.module').then((m) => m.TemplatesModule) import('./views/templates/templates.module').then((m) => m.TemplatesModule)
}, },
{
path: 'contract-drafts',
loadChildren: () =>
import('./views/contract-drafts/contract-drafts.module').then((m) => m.ContractDraftsModule)
},
{ {
path: 'contracts', path: 'contracts',
loadChildren: () => loadChildren: () =>

View File

@@ -0,0 +1,5 @@
.sidenav {
background-image: url("/assets/leather_texture.png");
background-size: 220px;
max-width: 220px;
}

View File

@@ -3,7 +3,7 @@
<main style="margin-top: 0px;"> <main style="margin-top: 0px;">
<div class="container-fluid"> <div class="container-fluid">
<div class="row flex-nowrap"> <div class="row flex-nowrap">
<div class="col-auto col-md-3 col-xl-2 px-sm-2 px-0 bg-dark" style="max-width: 200px;"> <div class="sidenav col-auto col-md-3 col-xl-2 px-sm-2 px-0 bg-dark">
<sidenav class="sticky-top"></sidenav> <sidenav class="sticky-top"></sidenav>
</div> </div>
<div class="col py-3"> <div class="col py-3">

View File

@@ -1,4 +1,5 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import {Title} from "@angular/platform-browser";
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@@ -8,5 +9,7 @@ import { Component } from '@angular/core';
export class AppComponent { export class AppComponent {
title = 'Cooper, Hillman & Toshi'; title = 'Cooper, Hillman & Toshi';
constructor() {} constructor(private titleService: Title) {
titleService.setTitle(this.title)
}
} }

View File

@@ -1,7 +1,7 @@
<div class="toast-container position-fixed bottom-0 end-0 p-3"> <div class="toast-container position-fixed bottom-0 end-0 p-3">
<ngb-toast <ngb-toast
*ngFor="let flashmessage of flashmessagesService.toasts" *ngFor="let flashmessage of flashmessagesService.toasts"
[header]="flashmessage.type" [autohide]="true" [delay]="flashmessage.delay || 1500" [header]="flashmessage.type" [autohide]="true" [delay]="flashmessage.delay || 5000"
(hiddden)="flashmessagesService.remove(flashmessage)" (hiddden)="flashmessagesService.remove(flashmessage)"
> >
<ng-container [ngSwitch]="flashmessage.type"> <ng-container [ngSwitch]="flashmessage.type">

View File

@@ -0,0 +1,3 @@
.logout {
margin-top: 25px;
}

View File

@@ -11,6 +11,6 @@
</a> </a>
</li> </li>
</ng-container> </ng-container>
<li><logout></logout></li> <li class="logout"><logout></logout></li>
</ul> </ul>
</div> </div>

View File

@@ -3,13 +3,21 @@ import { Router } from '@angular/router';
import { IconNamesEnum } from "ngx-bootstrap-icons"; import { IconNamesEnum } from "ngx-bootstrap-icons";
import { AuthService } from "../auth/auth.service"; import { AuthService } from "../auth/auth.service";
interface MenuItem {
title: string,
link: string,
icon: IconNamesEnum
}
@Component({ @Component({
selector: "sidenav", selector: "sidenav",
templateUrl: "./sidenav.component.html", templateUrl: "./sidenav.component.html",
styleUrls: ["./sidenav.component.css"] styleUrls: ["./sidenav.component.css"]
}) })
export class SidenavComponent { export class SidenavComponent {
Menu = [ Menu: MenuItem[] = [
{ {
title: $localize`Dashboard`, title: $localize`Dashboard`,
link: "/dashboard", link: "/dashboard",
@@ -32,7 +40,7 @@ export class SidenavComponent {
}, },
{ {
title: $localize`Contracts&nbsp;Drafts`, title: $localize`Contracts&nbsp;Drafts`,
link: "/contracts/drafts", link: "/contract-drafts",
icon: IconNamesEnum.PencilSquare icon: IconNamesEnum.PencilSquare
}, },
{ {
@@ -54,7 +62,7 @@ export class SidenavComponent {
}) })
} }
is_current_page(menu_item: any) { is_current_page(menu_item: MenuItem) {
return this.router.url.indexOf(menu_item.link) > -1; return this.router.url.startsWith(menu_item.link);
} }
} }

View File

@@ -0,0 +1,44 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { DraftsCardComponent, DraftsListComponent, DraftsNewComponent } from "./drafts.component";
const routes: Routes = [
{
path: '',
data: {
title: 'Contract Drafts',
},
children: [
{ path: '', redirectTo: 'list', pathMatch: 'full' },
{
path: 'list',
component: DraftsListComponent,
data: {
title: 'List',
},
},
{
path: 'new',
component: DraftsNewComponent,
data: {
title: 'New',
},
},
{
path: ':id',
component: DraftsCardComponent,
data: {
title: 'Card',
},
},
],
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ContractDraftsRoutingModule {}

View File

@@ -0,0 +1,42 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { BaseViewModule } from "../base-view/base-view.module";
import { ContractDraftsRoutingModule } from './contract-drafts-routing.module';
import { DraftsCardComponent, DraftsListComponent, DraftsNewComponent, DraftsNewFormComponent } from "./drafts.component";
import { FormlyModule } from "@ngx-formly/core";
import { FormlyBootstrapModule } from "@ngx-formly/bootstrap";
import { ForeignkeyTypeComponent } from "@common/crud/types/foreignkey.type";
import { CrudService, ImageUploaderCrudService } from "@common/crud/crud.service";
import { NgbAccordionModule, NgbCollapseModule } from "@ng-bootstrap/ng-bootstrap";
import { allIcons, NgxBootstrapIconsModule } from "ngx-bootstrap-icons";
import { ClipboardModule } from "@angular/cdk/clipboard";
@NgModule({
imports: [
CommonModule,
BaseViewModule,
ContractDraftsRoutingModule,
NgbAccordionModule,
NgbCollapseModule,
NgxBootstrapIconsModule.pick(allIcons),
FormlyModule.forRoot({
types: [
{ name: 'foreign-key', component: ForeignkeyTypeComponent }
]
}),
FormlyBootstrapModule,
ClipboardModule,
],
declarations: [
DraftsListComponent,
DraftsNewComponent,
DraftsCardComponent,
DraftsNewFormComponent
],
providers: [CrudService, ImageUploaderCrudService]
})
export class ContractDraftsModule {
}

View File

@@ -6,6 +6,7 @@ import { CrudService } from "@common/crud/crud.service";
import { ActivatedRoute, ParamMap, Router } from "@angular/router"; import { ActivatedRoute, ParamMap, Router } from "@angular/router";
import { formatDate } from "@angular/common"; import { formatDate } from "@angular/common";
import {FlashmessagesService} from "../../layout/flashmessages/flashmessages.service";
export class BaseDraftsComponent { export class BaseDraftsComponent {
@@ -69,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;
@@ -97,10 +98,10 @@ 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">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()">Publish</button> <button class="btn btn-success" (click)="publish()" i18n>Publish</button>
</ng-container> </ng-container>
` `
}) })
@@ -142,6 +143,7 @@ export class DraftsCardComponent extends BaseDraftsComponent implements OnInit {
private formlyJsonschema: CrudFormlyJsonschemaService, private formlyJsonschema: CrudFormlyJsonschemaService,
private crudService: CrudService, private crudService: CrudService,
private router: Router, private router: Router,
private flashService: FlashmessagesService,
) { ) {
super(); super();
} }
@@ -159,8 +161,9 @@ export class DraftsCardComponent extends BaseDraftsComponent implements OnInit {
} }
publish() { publish() {
this.crudService.create('contract', this.newContractModel).subscribe((response: any) => { this.crudService.create('contract', this.newContractModel).subscribe({
this.router.navigate([`../../${response.id}`], {relativeTo: this.route}); next: (response: any) => this.router.navigate([`../../contracts/${response.id}`], {relativeTo: this.route}),
error: (err) => this.flashService.error(err)
}); });
} }

View File

@@ -1,7 +1,6 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router'; import { Routes, RouterModule } from '@angular/router';
import { DraftsCardComponent, DraftsListComponent, DraftsNewComponent } from "./drafts.component";
import { ContractsCardComponent, ContractsListComponent, ContractsNewComponent, ContractsSignatureComponent} from "./contracts.component"; import { ContractsCardComponent, ContractsListComponent, ContractsNewComponent, ContractsSignatureComponent} from "./contracts.component";
@@ -13,6 +12,7 @@ const routes: Routes = [
}, },
children: [ children: [
{ path: '', redirectTo: 'list', pathMatch: 'full' }, { path: '', redirectTo: 'list', pathMatch: 'full' },
{ path: 'drafts', redirectTo: '/contract-drafts/list' },
{ {
path: 'list', path: 'list',
component: ContractsListComponent, component: ContractsListComponent,
@@ -34,36 +34,6 @@ const routes: Routes = [
title: 'New', title: 'New',
}, },
}, },
{
path: 'drafts',
data: {
title: 'Drafts',
},
children: [
{ path: '', redirectTo: 'list', pathMatch: 'full' },
{
path: 'list',
component: DraftsListComponent,
data: {
title: 'List',
},
},
{
path: 'new',
component: DraftsNewComponent,
data: {
title: 'New',
},
},
{
path: ':id',
component: DraftsCardComponent,
data: {
title: 'Card',
},
},
],
},
{ {
path: ':id', path: ':id',
component: ContractsCardComponent, component: ContractsCardComponent,

View File

@@ -71,44 +71,29 @@ 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}`
} }
} }
@Component({ @Component({
template: ` template: `
<ngb-accordion #acc="ngbAccordion" activeIds="ngb-panel-1"> <div>
<ngb-panel> <iframe width="100%"
<ng-template ngbPanelTitle> [src]="getPreview()"
<span>Preview</span> onload='javascript:(function(o){o.style.height=o.contentWindow.document.body.scrollHeight+"px";o.style.width=o.contentWindow.document.body.scrollWidth+"px";}(this));' style="height:200px;width:100%;border:none;overflow:hidden;">
</ng-template> </iframe>
<ng-template ngbPanelContent> </div>
<iframe width="100%" <div class="row" *ngIf="!this.affixed">
[src]="getPreview()" <signature-drawer class="col-7"
onload='javascript:(function(o){o.style.height=o.contentWindow.document.body.scrollHeight+"px";o.style.width=o.contentWindow.document.body.scrollWidth+"px";}(this));' style="height:200px;width:100%;border:none;overflow:hidden;"></iframe> (signatureDrawn$)="postSignature($event)"></signature-drawer>
</ng-template> <div class="col-5" i18n>
</ngb-panel> <p>Cette page est à la destination exclusive de <strong>{{ this.signatory }}</strong></p>
<ngb-panel> <p>Si vous n'êtes <strong>pas</strong> {{ this.signatory }}, veuillez <strong>fermer cette page immédiatement</strong> et surpprimer tous les liens en votre possession menant vers celle-ci.</p>
<ng-template ngbPanelTitle> <p>En vous maintenant et/ou en interagissant avec cette page, vous enfreignez l'article L.229 du code pénal de l'Etat de San Andreas pour <strong>usurpation d'identité</strong> et vous vous exposez ainsi à une amende de 20 000$ ainsi qu'à des poursuites civiles.</p>
<span>Signature</span> <p>Le cabinet Cooper, Hillman & Toshi LLC</p>
</ng-template> </div>
<ng-template ngbPanelContent> </div>`
<ng-container *ngIf="this.affixed">This Contract has already been signed by {{ this.signatory }}</ng-container>
<div class="row" *ngIf="!this.affixed">
<signature-drawer class="col-7"
(signatureDrawn$)="postSignature($event)"></signature-drawer>
<div class="col-5">
<p>Cette page est à la destination exclusive de <strong>{{ this.signatory }}</strong></p>
<p>Si vous n'êtes <strong>pas</strong> {{ this.signatory }}, veuillez <strong>fermer cette page immédiatement</strong> et surpprimer tous les liens en votre possession menant vers celle-ci.</p>
<p>En vous maintenant et/ou en interagissant avec cette page, vous enfreignez l'article L.229 du code pénal de l'Etat de San Andreas pour <strong>usurpation d'identité</strong> et vous vous exposez ainsi à une amende de 20 000$ ainsi qu'à des poursuites civiles.</p>
<p>Le cabinet Cooper, Hillman & Toshi LLC</p>
</div>
</div>
</ng-template>
</ngb-panel>
</ngb-accordion>
`
}) })
export class ContractsSignatureComponent implements OnInit { export class ContractsSignatureComponent implements OnInit {
signature_id: string | null = null; signature_id: string | null = null;
@@ -139,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

@@ -3,7 +3,6 @@ import { NgModule } from '@angular/core';
import { BaseViewModule } from "../base-view/base-view.module"; import { BaseViewModule } from "../base-view/base-view.module";
import { ContractsRoutingModule } from './contracts-routing.module'; import { ContractsRoutingModule } from './contracts-routing.module';
import { DraftsCardComponent, DraftsListComponent, DraftsNewComponent, DraftsNewFormComponent } from "./drafts.component";
import { FormlyModule } from "@ngx-formly/core"; import { FormlyModule } from "@ngx-formly/core";
import { FormlyBootstrapModule } from "@ngx-formly/bootstrap"; import { FormlyBootstrapModule } from "@ngx-formly/bootstrap";
import { ForeignkeyTypeComponent } from "@common/crud/types/foreignkey.type"; import { ForeignkeyTypeComponent } from "@common/crud/types/foreignkey.type";
@@ -33,10 +32,6 @@ import { ClipboardModule } from "@angular/cdk/clipboard";
ClipboardModule, ClipboardModule,
], ],
declarations: [ declarations: [
DraftsListComponent,
DraftsNewComponent,
DraftsCardComponent,
DraftsNewFormComponent,
ContractsListComponent, ContractsListComponent,
ContractsNewComponent, ContractsNewComponent,
ContractsCardComponent, ContractsCardComponent,

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

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,13 +109,13 @@ 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.resourceReceived.emit(model);
this.model = model; this.model = model;
this._modelLoading$.next(false); this._modelLoading$.next(false);
this.resourceUpdated.emit(model._id);
this.resourceReceived.emit(model);
}, },
error: (err) => this.error.emit("Error updating the entity:" + err) error: (err) => this.error.emit("Error updating the entity:" + err)
}); });
@@ -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

@@ -30,7 +30,7 @@ export class JsonschemasService {
buildResource(resourceName: string) { buildResource(resourceName: string) {
let resource; let resource;
resource = { ... this.rawSchemas.components.schemas[resourceName]}; resource = structuredClone(this.rawSchemas.components.schemas[resourceName]);
resource.components = { schemas: {} }; resource.components = { schemas: {} };
for (let prop_name in resource.properties) { for (let prop_name in resource.properties) {
let prop = resource.properties[prop_name]; let prop = resource.properties[prop_name];
@@ -162,6 +162,14 @@ export class JsonschemasService {
} }
return false; return false;
} }
} else if (this.is_enum(resource)) {
for (const ref of resource.allOf!) {
// @ts-ignore
if (this.has_descendant(ref, property_name)) {
return true;
}
return false;
}
} }
throw new Error("Jsonschema format not implemented in property finder"); throw new Error("Jsonschema format not implemented in property finder");
return false; return false;
@@ -186,6 +194,14 @@ export class JsonschemasService {
return this.get_descendant(ref, property_name); return this.get_descendant(ref, property_name);
} }
} }
} else if (this.is_enum(resource)) {
for (const ref of resource.allOf!) {
// @ts-ignore
if (this.has_descendant(ref, property_name)) {
// @ts-ignore
return this.get_descendant(ref, property_name);
}
}
} }
throw new Error("property not found or Jsonschema format not implemented"); throw new Error("property not found or Jsonschema format not implemented");
} }

View File

@@ -14,13 +14,14 @@ import {CrudFormlyJsonschemaService} from "@common/crud/crud-formly-jsonschema.s
export class FilterListComponent implements OnInit { export class FilterListComponent implements OnInit {
@Input() filters: string[] = []; @Input() filters: string[] = [];
@Input() schema = ""; @Input() schema = "";
@Input() values = {};
@Output() filterChange: EventEmitter<{[key: string]: any}> = new EventEmitter(); @Output() filterChange: EventEmitter<{[key: string]: any}> = new EventEmitter();
form = new FormGroup({}); form = new FormGroup({});
fields: FormlyFieldConfig[] = []; fields: FormlyFieldConfig[] = [];
searchTerms = {} searchTerms: {[key: string]: string | {}} = {}
public fieldJson = { public fieldJson = {
components: {}, components: {},
@@ -48,6 +49,16 @@ export class FilterListComponent implements OnInit {
prop = schema.components.schemas[prop.allOf![0]['$ref'].replace('#/components/schemas/', '')]; prop = schema.components.schemas[prop.allOf![0]['$ref'].replace('#/components/schemas/', '')];
prop.type = "array"; prop.type = "array";
prop.items = {"type": "string", "enum": prop.enum}; prop.items = {"type": "string", "enum": prop.enum};
if (filter in this.values) {
this.searchTerms[filter] = {};
// @ts-ignore
for (let val of this.values[filter]) {
// @ts-ignore
this.searchTerms[filter][val] = true;
}
}
} else if(true) {
} }
if (prop.hasOwnProperty('readOnly') && prop.readOnly) { if (prop.hasOwnProperty('readOnly') && prop.readOnly) {

View File

@@ -0,0 +1,3 @@
.table-row-link {
cursor: pointer;
}

View File

@@ -14,20 +14,27 @@
/> />
</div> </div>
<div class="col-xs-3 col-sm-auto"> <div class="col-xs-3 col-sm-auto">
<crud-list-filter-list [filters]="this.filters" [schema]="this.schema!" (filterChange)="onFilterChange($event)"></crud-list-filter-list> <crud-list-filter-list
[filters]="this.filters"
[schema]="this.schema!"
[values]="this.searchFilters"
(filterChange)="onFilterChange($event)"
></crud-list-filter-list>
</div> </div>
<span class="col col-form-label" i18n *ngIf="loading$ | async">Loading...</span>
</div> </div>
<div class="table-responsive-md"> <div class="table-responsive-md">
<table class="table table-striped"> <table class="table table-striped table-hover">
<thead> <thead>
<tr> <tr>
<th *ngFor="let col of this.displayedColumns" scope="col" sortable="name" (sort)="onSort($event)">{{ col.title }}</th> <th *ngFor="let col of this.displayedColumns" scope="col" sortable="name" (sort)="onSort($event)">{{ col.title }}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let row of listData$ | async" (click)="onSelect(row._id)"> <tr *ngIf="loading$ | async">
<td *ngFor="let col of this.displayedColumns"> <td class="text-center" [attr.colspan]="this.displayedColumns.length" i18n>Loading...</td>
</tr>
<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;">
<ngb-highlight [result]="getColumnValue(row, col.path)" [term]="searchTerm"></ngb-highlight> <ngb-highlight [result]="getColumnValue(row, col.path)" [term]="searchTerm"></ngb-highlight>
</td> </td>
</tr> </tr>
@@ -35,7 +42,7 @@
</table> </table>
</div> </div>
<div class="d-flex justify-content-between p-2"> <div class="d-flex justify-content-between p-2" *ngIf="! (loading$ | async)" >
<ngb-pagination [collectionSize]="(total$ | async)!" [(page)]="page" [pageSize]="pageSize"> <ngb-pagination [collectionSize]="(total$ | async)!" [(page)]="page" [pageSize]="pageSize">
</ngb-pagination> </ngb-pagination>

View File

@@ -40,7 +40,6 @@ export class ListComponent implements OnInit {
public displayedColumns: Column[] = []; public displayedColumns: Column[] = [];
private _loading$ = new BehaviorSubject<boolean>(true); private _loading$ = new BehaviorSubject<boolean>(true);
//private _search$ = new Subject<void>();
private _listData$ = new BehaviorSubject<any[]>([]); private _listData$ = new BehaviorSubject<any[]>([]);
private _total$ = new BehaviorSubject<number>(0); private _total$ = new BehaviorSubject<number>(0);
@@ -64,7 +63,13 @@ export class ListComponent implements OnInit {
next: (schema: any) => this.getColumnDefinition(schema), next: (schema: any) => this.getColumnDefinition(schema),
error: (err) => this.error.emit("Error loading the schema:" + err) error: (err) => this.error.emit("Error loading the schema:" + err)
}); });
this._search(); this.route.queryParams.subscribe(params => {
let parsedParams = {...params};
if (parsedParams.hasOwnProperty('searchFilters')) {
parsedParams['searchFilters'] = JSON.parse(parsedParams['searchFilters']);
}
this._set(parsedParams)
});
} }
getColumnDefinition(schema: JSONSchema7) { getColumnDefinition(schema: JSONSchema7) {
@@ -100,13 +105,13 @@ export class ListComponent implements OnInit {
parent = parent[key]; parent = parent[key];
} }
} }
return parent; return parent.replace(/<[^>]*>/g, '');
} }
private _search() { private _search() {
this._loading$.next(true); this._loading$.next(true);
let sortBy = new SortBy(this.sortColumn, this.sortDirection) let sortBy = new SortBy(this.sortColumn, this.sortDirection)
let filters = this.searchTerm ? [new Filters('fulltext', 'eq', this.searchTerm)] : []; let filters = this.searchTerm ? [new Filters('label', 'ilike', this.searchTerm)] : [];
for (let f in this.searchFilters) { for (let f in this.searchFilters) {
if (Array.isArray(this.searchFilters[f])) { if (Array.isArray(this.searchFilters[f])) {
filters.push(new Filters(f, 'in', this.searchFilters[f])) filters.push(new Filters(f, 'in', this.searchFilters[f]))
@@ -144,10 +149,15 @@ export class ListComponent implements OnInit {
this.sortDirection = direction; this.sortDirection = direction;
} }
onSelect(id: string) { onRowClick(id: string) {
this.router.navigate([`../${id}`], {relativeTo: this.route}); this.router.navigate([`../${id}`], {relativeTo: this.route});
} }
onRowMiddleClick(id: string) {
let newUrl = window.location.href.replace('list', id).split('?')[0]
window.open(newUrl, '_blank');
}
onCreate() { onCreate() {
this.router.navigate([`../new`], {relativeTo: this.route}); this.router.navigate([`../new`], {relativeTo: this.route});
} }
@@ -177,30 +187,43 @@ export class ListComponent implements OnInit {
return this._state.searchTerm; return this._state.searchTerm;
} }
get searchFilters() { get searchFilters() {
return this._state.searchFilters; return this._state.searchFilters;
} }
set page(page: number) { set page(page: number) {
this._set({ page }); this.updateState({ page });
} }
set pageSize(pageSize: number) {
this._set({ pageSize });
}
set searchTerm(searchTerm: string) {
this._set({ searchTerm });
}
set searchFilters(searchFilters: {[key: string]: any}) {
this._set({ searchFilters });
}
set sortColumn(sortColumn: SortColumn) {
this._set({ sortColumn });
}
set sortDirection(sortDirection: SortDirection) {
this._set({ sortDirection });
}
private _set(patch: Partial<State>) { set pageSize(pageSize: number) {
Object.assign(this._state, patch); this.updateState({ pageSize });
this._search(); }
}
set searchTerm(searchTerm: string) {
this.updateState({ searchTerm });
}
set searchFilters(searchFilters: {[key: string]: any}) {
this.updateState({ searchFilters: JSON.stringify(searchFilters) });
}
set sortColumn(sortColumn: SortColumn) {
this.updateState({ sortColumn });
}
set sortDirection(sortDirection: SortDirection) {
this.updateState({ sortDirection });
}
private updateState(patch: any) {
this.router.navigate([], {
relativeTo: this.route,
queryParams: patch ,
queryParamsHandling: 'merge'
});
}
private _set(patch: Partial<State>) {
Object.assign(this._state, patch);
this._search();
}
} }

View File

@@ -2,36 +2,44 @@ import {Component, OnInit} from '@angular/core';
import { FieldArrayType } from '@ngx-formly/core'; import { FieldArrayType } from '@ngx-formly/core';
@Component({ @Component({
selector: 'formly-array-type', selector: 'formly-array-type',
template: ` template: `
<div> <div class="mb-3">
<label *ngIf="props.label" class="form-label">{{ props.label }}</label> <ngb-accordion #acc="ngbAccordion" activeIds="ngb-panel-0">
<p *ngIf="props.description">{{ props.description }}</p> <ngb-panel id="ngb-panel-0">
<div class="alert alert-danger" role="alert" *ngIf="showError && formControl.errors"> <ng-template ngbPanelTitle>
<formly-validation-message [field]="field"></formly-validation-message> <label *ngIf="props.label" class="form-label">{{ props.label }}</label>
</div> <p *ngIf="props.description">{{ props.description }}</p>
<div class="row row-cols-1 row-cols-md-{{this.itemsPerRow}} g-1"> <div class="alert alert-danger" role="alert" *ngIf="showError && formControl.errors">
<div *ngFor="let entry of field.fieldGroup; let i = index" class="col"> <formly-validation-message [field]="field"></formly-validation-message>
<div class="card">
<div class="card-header">
<div *ngIf="props['numbered']" class="float-start">
<strong>{{ i + 1 }}</strong>
</div> </div>
<div *ngIf="! this.field.props.readonly" class="btn-group float-end"> </ng-template>
<button class="btn btn-primary btn-sm" [attr.disabled]="i == 0 ? 'disabled' : null" type="button" (click)="move(i, i-1)"><i-bs name="caret-up-fill"></i-bs></button> <ng-template ngbPanelContent>
<button class="btn btn-primary btn-sm" [attr.disabled]="i == this.field.fieldGroup!.length-1 ? 'disabled' : null" type="button" (click)="move(i, i+1)"><i-bs name="caret-down-fill"></i-bs></button> <div class="row row-cols-1 row-cols-md-{{this.itemsPerRow}} g-1">
<button class="btn btn-danger btn-sm" [attr.disabled]="field.props!['removable'] === false ? 'disabled' : null" type="button" (click)="remove(i)"><i-bs name="x-octagon-fill"></i-bs></button> <div *ngFor="let entry of field.fieldGroup; let i = index" class="col">
<div class="card">
<div class="card-header">
<div *ngIf="props['numbered']" class="float-start">
<strong>{{ i + 1 }}</strong>
</div>
<div *ngIf="! this.field.props.readonly" class="btn-group float-end">
<button class="btn btn-primary btn-sm" [attr.disabled]="i == 0 ? 'disabled' : null" type="button" (click)="move(i, i-1)"><i-bs name="caret-up-fill"></i-bs></button>
<button class="btn btn-primary btn-sm" [attr.disabled]="i == this.field.fieldGroup!.length-1 ? 'disabled' : null" type="button" (click)="move(i, i+1)"><i-bs name="caret-down-fill"></i-bs></button>
<button class="btn btn-danger btn-sm" [attr.disabled]="field.props!['removable'] === false ? 'disabled' : null" type="button" (click)="remove(i)"><i-bs name="x-octagon-fill"></i-bs></button>
</div>
</div>
<div class="card-body">
<formly-field class="col" [field]="entry"></formly-field>
</div>
</div>
</div>
</div> </div>
</div> <button *ngIf="! this.field.props.readonly" class="btn btn-success col-sm-12 gap-3" type="button" (click)="add()"><i-bs name="plus-square-fill"></i-bs></button>
<div class="card-body"> </ng-template>
<formly-field class="col" [field]="entry"></formly-field> </ngb-panel>
</div> </ngb-accordion>
</div>
</div>
</div> </div>
<button *ngIf="! this.field.props.readonly" class="btn btn-success col-sm-12" type="button" (click)="add()"><i-bs name="plus-square-fill"></i-bs></button> `,
</div>
`,
}) })
export class ArrayTypeComponent extends FieldArrayType implements OnInit { export class ArrayTypeComponent extends FieldArrayType implements OnInit {
colSm: string = "col-sm-6" colSm: string = "col-sm-6"

View File

@@ -10,7 +10,7 @@ import {FormlyJsonschema} from "@ngx-formly/core/json-schema";
template: ` template: `
<div class="mb-3"> <div class="mb-3">
<ngb-accordion #acc="ngbAccordion" activeIds="ngb-panel-0"> <ngb-accordion #acc="ngbAccordion" activeIds="ngb-panel-0">
<ngb-panel> <ngb-panel id="ngb-panel-0">
<ng-template ngbPanelTitle> <ng-template ngbPanelTitle>
<label *ngIf="props.label && props['hideLabel'] !== true" [attr.for]="id" <label *ngIf="props.label && props['hideLabel'] !== true" [attr.for]="id"
class="form-label">{{ props.label }} class="form-label">{{ props.label }}

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()
} }
@@ -142,7 +142,7 @@ export class ForeignkeyTypeComponent extends FieldType<FieldTypeConfig> implemen
1, 1,
10, 10,
[], [],
[new Filters('fulltext', 'eq', term)] [new Filters('label', 'ilike', term)]
).pipe( ).pipe(
map((result: any) => result["items"]), map((result: any) => result["items"]),
); );
@@ -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) {

View File

@@ -44,6 +44,7 @@ export class RichtextTypeComponent extends FormlyFieldInput implements OnInit {
autoresize_bottom_margin: 0, autoresize_bottom_margin: 0,
body_class: "contract-body", body_class: "contract-body",
content_style: ".contract-body { font-family: 'Century Schoolbook', 'sans-serif' }", content_style: ".contract-body { font-family: 'Century Schoolbook', 'sans-serif' }",
entity_encoding: 'raw',
paste_preprocess: function (plugin: any, args: any) { paste_preprocess: function (plugin: any, args: any) {
console.log(args.content) console.log(args.content)
let container = document.createElement('div'); let container = document.createElement('div');
@@ -80,7 +81,7 @@ export class RichtextTypeComponent extends FormlyFieldInput implements OnInit {
} }
} }
getInitConfig() { getInitConfig(): any {
return {...this.init_common, ...( this.multiline ? this.init_multiline : this.init_singleline)}; return {...this.init_common, ...( this.multiline ? this.init_multiline : this.init_singleline)};
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 948 B

After

Width:  |  Height:  |  Size: 318 B

View File

@@ -2,10 +2,13 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>App</title> <title>Cooper, Hillman & Toshi</title>
<base href="/"> <base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="icon" type="image/x-icon" href="favicon.ico">
<meta property="og:title" content="Cooper, Hillman & Toshi">
<meta property="og:description" content="Interface d'administration des contrats">
<meta property="og:image" content="/assets/logo.png">
</head> </head>
<body> <body>
<app-root></app-root> <app-root></app-root>

View File

@@ -354,5 +354,69 @@
<target>Mettre à jour</target> <target>Mettre à jour</target>
</segment> </segment>
</unit> </unit>
<unit id="6061331044524123789">
<segment state="initial">
<source>Authentication required</source>
<target>Authentification nécessaire</target>
</segment>
</unit>
<unit id="3624268617519726175">
<segment state="initial">
<source>Permissions too low</source>
<target>Permissions trop basses</target>
</segment>
</unit>
<unit id="6833883791906187270">
<segment state="initial">
<source>Download Link:</source>
<target>Lien de téléchargement:</target>
</segment>
</unit>
<unit id="8168599357858858911">
<segment state="initial">
<source>Preview Link:</source>
<target>Lien de prévisualisation:</target>
</segment>
</unit>
<unit id="1295614462098694869">
<segment state="initial">
<source>Preview</source>
<target>Prévisualisation</target>
</segment>
</unit>
<unit id="2445188258613609179">
<segment state="initial">
<source>Signature</source>
<target>Signature</target>
</segment>
</unit>
<unit id="8474971383445371291">
<segment state="initial">
<source>
<pc id="0" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">Cette page est à la destination exclusive de <pc id="1" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;"><ph id="2" equiv="INTERPOLATION" disp="{{ this.signatory }}"/></pc></pc>
<pc id="3" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">Si vous n&apos;êtes <pc id="4" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;">pas</pc> <ph id="5" equiv="INTERPOLATION" disp="{{ this.signatory }}"/>, veuillez <pc id="6" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;">fermer cette page immédiatement</pc> et surpprimer tous les liens en votre possession menant vers celle-ci.</pc>
<pc id="7" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">En vous maintenant et/ou en interagissant avec cette page, vous enfreignez l&apos;article L.229 du code pénal de l&apos;Etat de San Andreas pour <pc id="8" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;">usurpation d&apos;identité</pc> et vous vous exposez ainsi à une amende de 20 000$ ainsi qu&apos;à des poursuites civiles.</pc>
<pc id="9" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">Le cabinet Cooper, Hillman &amp; Toshi LLC</pc>
</source>
<target>
<pc id="0" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">Cette page est à la destination exclusive de <pc id="1" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;"><ph id="2" equiv="INTERPOLATION" disp="{{ this.signatory }}"/></pc></pc>
<pc id="3" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">Si vous n&apos;êtes <pc id="4" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;">pas</pc> <ph id="5" equiv="INTERPOLATION" disp="{{ this.signatory }}"/>, veuillez <pc id="6" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;">fermer cette page immédiatement</pc> et surpprimer tous les liens en votre possession menant vers celle-ci.</pc>
<pc id="7" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">En vous maintenant et/ou en interagissant avec cette page, vous enfreignez l&apos;article L.229 du code pénal de l&apos;Etat de San Andreas pour <pc id="8" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;">usurpation d&apos;identité</pc> et vous vous exposez ainsi à une amende de 20 000$ ainsi qu&apos;à des poursuites civiles.</pc>
<pc id="9" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">Le cabinet Cooper, Hillman &amp; Toshi LLC</pc>
</target>
</segment>
</unit>
<unit id="7430416142942514215">
<segment state="initial">
<source>Publish</source>
<target>Publier</target>
</segment>
</unit>
<unit id="2990108023996257960">
<segment state="initial">
<source>This Contract has already been signed by</source>
<target>Ce contrat a déjà été signé par</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View File

@@ -292,5 +292,55 @@
<source>100 items per page</source> <source>100 items per page</source>
</segment> </segment>
</unit> </unit>
<unit id="3624268617519726175">
<segment>
<source>Permissions too low</source>
</segment>
</unit>
<unit id="6061331044524123789">
<segment>
<source>Authentication required</source>
</segment>
</unit>
<unit id="6833883791906187270">
<segment>
<source>Download Link:</source>
</segment>
</unit>
<unit id="8168599357858858911">
<segment>
<source>Preview Link:</source>
</segment>
</unit>
<unit id="1295614462098694869">
<segment>
<source>Preview</source>
</segment>
</unit>
<unit id="2445188258613609179">
<segment>
<source>Signature</source>
</segment>
</unit>
<unit id="7430416142942514215">
<segment>
<source>Publish</source>
</segment>
</unit>
<unit id="8474971383445371291">
<segment>
<source>
<pc id="0" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">Cette page est à la destination exclusive de <pc id="1" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;"><ph id="2" equiv="INTERPOLATION" disp="{{ this.signatory }}"/></pc></pc>
<pc id="3" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">Si vous n&apos;êtes <pc id="4" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;">pas</pc> <ph id="5" equiv="INTERPOLATION" disp="{{ this.signatory }}"/>, veuillez <pc id="6" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;">fermer cette page immédiatement</pc> et surpprimer tous les liens en votre possession menant vers celle-ci.</pc>
<pc id="7" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">En vous maintenant et/ou en interagissant avec cette page, vous enfreignez l&apos;article L.229 du code pénal de l&apos;Etat de San Andreas pour <pc id="8" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;">usurpation d&apos;identité</pc> et vous vous exposez ainsi à une amende de 20 000$ ainsi qu&apos;à des poursuites civiles.</pc>
<pc id="9" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">Le cabinet Cooper, Hillman &amp; Toshi LLC</pc>
</source>
</segment>
</unit>
<unit id="2990108023996257960">
<segment>
<source>This Contract has already been signed by</source>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View File

@@ -26,3 +26,15 @@
.contract-body { .contract-body {
font-family: 'Century Schoolbook'; font-family: 'Century Schoolbook';
} }
.nav-link {
color: #D2BA6F;
--bs-nav-link-hover-color: #9c8a51;
--bs-nav-pills-link-active-color: #D2BA6F;
--bs-nav-pills-link-active-bg: #114856;
}
.nav-link.active {
border: #D2BA6F solid 2px;
border-radius: 0;
}

View File

@@ -41,6 +41,25 @@ http {
default_type text/javascript; default_type text/javascript;
} }
location /contracts/signature/ {
set $is_robot 0;
if ($http_user_agent ~* "Discordbot|googlebot|bingbot|yandex|baiduspider|twitterbot|facebookexternalhit|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|outbrain|pinterest\/0\.|pinterestbot|slackbot|vkShare|W3C_Validator|whatsapp") {
rewrite /contracts/signature/(.*) /api/v1/contract/print/opengraph/$1 last;
proxy_pass http://docker-back;
set $is_robot 1;
}
if ($is_robot = 0) {
rewrite ^ /index.html?$args last;
}
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
}
location /api/v1/ { location /api/v1/ {
proxy_pass http://docker-back/; proxy_pass http://docker-back/;
proxy_redirect off; proxy_redirect off;

View File

@@ -26,6 +26,26 @@ http {
proxy_set_header X-Forwarded-Host $server_name; proxy_set_header X-Forwarded-Host $server_name;
} }
location /contracts/signature/ {
set $is_robot 0;
if ($http_user_agent ~* "Discordbot|googlebot|bingbot|yandex|baiduspider|twitterbot|facebookexternalhit|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|outbrain|pinterest\/0\.|pinterestbot|slackbot|vkShare|W3C_Validator|whatsapp") {
rewrite /contracts/signature/(.*) /api/v1/contract/print/opengraph/$1 last;
proxy_pass http://docker-back;
break;
set $is_robot 1;
}
if ($is_robot = 0) {
proxy_pass http://docker-front;
}
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
}
location /api/v1/ { location /api/v1/ {
proxy_pass http://docker-back/; proxy_pass http://docker-back/;
proxy_redirect off; proxy_redirect off;