11 Commits

18 changed files with 109 additions and 96 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,5 +1,5 @@
import datetime import datetime
from typing import List, Literal from typing import List, Literal, Optional
from enum import Enum from enum import Enum
from pydantic import BaseModel, Field, validator from pydantic import BaseModel, Field, validator
@@ -52,10 +52,10 @@ 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 ProvisionGenuine(BaseModel): class ProvisionGenuine(BaseModel):
@@ -181,7 +181,7 @@ class Contract(CrudDocument):
lawyer: Entity = Field(title="Avocat en charge") lawyer: Entity = Field(title="Avocat en charge")
location: str = Field(title="Lieu") location: str = Field(title="Lieu")
date: datetime.date = Field(title="Date") date: datetime.date = Field(title="Date")
label: str = None label: Optional[str] = None
@validator("label", always=True) @validator("label", always=True)
def generate_label(cls, v, values, **kwargs): def generate_label(cls, v, values, **kwargs):
@@ -190,7 +190,7 @@ class Contract(CrudDocument):
for p in values['parties']: for p in values['parties']:
contract_label = contract_label + f" - {p.entity.label}" contract_label = contract_label + f" - {p.entity.label}"
contract_label = contract_label + f" {values['date'].strftime('%m/%d/%Y')}" contract_label = contract_label + f" - {values['date'].strftime('%m/%d/%Y')}"
return contract_label return contract_label
return v return v

View File

@@ -130,7 +130,7 @@ async def create_pdf(contract_id: str) -> str:
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): def retrieve_signature_png(filepath):

View File

@@ -117,6 +117,10 @@ 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;

View File

@@ -3,8 +3,8 @@ 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_pagination import Page, Params, add_pagination
from fastapi_paginate.ext.motor import paginate from fastapi_pagination.ext.beanie import paginate
from ..user.manager import get_current_user, get_current_superuser from ..user.manager import get_current_user, get_current_superuser
@@ -79,8 +79,7 @@ def get_crud_router(model, model_create, model_read, model_update):
sort = parse_sort(sort_by) sort = parse_sort(sort_by)
query = parse_query(query, model_read) query = parse_query(query, model_read)
collection = model.get_motor_collection() items = paginate(model.find(query), Params(**{'size': size, 'page': page}))
items = paginate(collection, query, Params(**{'size': size, 'page': page}), sort=sort)
return await items return await items
@router.put("/{id}", response_description="{} record updated".format(model.__name__)) @router.put("/{id}", response_description="{} record updated".format(model.__name__))

View File

@@ -23,8 +23,8 @@ 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:

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

@@ -78,23 +78,12 @@ export class ContractsCardComponent extends BaseContractsComponent{
@Component({ @Component({
template: ` template: `
<ngb-accordion #acc="ngbAccordion" activeIds="ngb-panel-1"> <div>
<ngb-panel>
<ng-template ngbPanelTitle>
<span i18n>Preview</span>
</ng-template>
<ng-template ngbPanelContent>
<iframe width="100%" <iframe width="100%"
[src]="getPreview()" [src]="getPreview()"
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> 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>
</ngb-panel> </div>
<ngb-panel>
<ng-template ngbPanelTitle>
<span i18n>Signature</span>
</ng-template>
<ng-template ngbPanelContent>
<ng-container *ngIf="this.affixed"><ng-container i18n>This Contract has already been signed by</ng-container> {{ this.signatory }}</ng-container>
<div class="row" *ngIf="!this.affixed"> <div class="row" *ngIf="!this.affixed">
<signature-drawer class="col-7" <signature-drawer class="col-7"
(signatureDrawn$)="postSignature($event)"></signature-drawer> (signatureDrawn$)="postSignature($event)"></signature-drawer>
@@ -104,11 +93,7 @@ export class ContractsCardComponent extends BaseContractsComponent{
<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>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> <p>Le cabinet Cooper, Hillman & Toshi LLC</p>
</div> </div>
</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;

View File

@@ -21,7 +21,6 @@
(filterChange)="onFilterChange($event)" (filterChange)="onFilterChange($event)"
></crud-list-filter-list> ></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-hover"> <table class="table table-striped table-hover">
@@ -31,6 +30,9 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngIf="loading$ | async">
<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"> <tr *ngFor="let row of listData$ | async" (click)="onRowClick(row._id)" (auxclick)="onRowMiddleClick(row._id);" class="table-row-link">
<td class="text-truncate" *ngFor="let col of this.displayedColumns" style="max-width: 150px;"> <td class="text-truncate" *ngFor="let col of this.displayedColumns" style="max-width: 150px;">
<ngb-highlight [result]="getColumnValue(row, col.path)" [term]="searchTerm"></ngb-highlight> <ngb-highlight [result]="getColumnValue(row, col.path)" [term]="searchTerm"></ngb-highlight>
@@ -40,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

@@ -68,7 +68,6 @@ export class ListComponent implements OnInit {
if (parsedParams.hasOwnProperty('searchFilters')) { if (parsedParams.hasOwnProperty('searchFilters')) {
parsedParams['searchFilters'] = JSON.parse(parsedParams['searchFilters']); parsedParams['searchFilters'] = JSON.parse(parsedParams['searchFilters']);
} }
this._total$.next(this.page * this.pageSize);
this._set(parsedParams) this._set(parsedParams)
}); });
} }

View File

@@ -4,12 +4,17 @@ import { FieldArrayType } from '@ngx-formly/core';
@Component({ @Component({
selector: 'formly-array-type', selector: 'formly-array-type',
template: ` template: `
<div> <div class="mb-3">
<ngb-accordion #acc="ngbAccordion" activeIds="ngb-panel-0">
<ngb-panel id="ngb-panel-0">
<ng-template ngbPanelTitle>
<label *ngIf="props.label" class="form-label">{{ props.label }}</label> <label *ngIf="props.label" class="form-label">{{ props.label }}</label>
<p *ngIf="props.description">{{ props.description }}</p> <p *ngIf="props.description">{{ props.description }}</p>
<div class="alert alert-danger" role="alert" *ngIf="showError && formControl.errors"> <div class="alert alert-danger" role="alert" *ngIf="showError && formControl.errors">
<formly-validation-message [field]="field"></formly-validation-message> <formly-validation-message [field]="field"></formly-validation-message>
</div> </div>
</ng-template>
<ng-template ngbPanelContent>
<div class="row row-cols-1 row-cols-md-{{this.itemsPerRow}} g-1"> <div class="row row-cols-1 row-cols-md-{{this.itemsPerRow}} g-1">
<div *ngFor="let entry of field.fieldGroup; let i = index" class="col"> <div *ngFor="let entry of field.fieldGroup; let i = index" class="col">
<div class="card"> <div class="card">
@@ -29,7 +34,10 @@ import { FieldArrayType } from '@ngx-formly/core';
</div> </div>
</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> <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>
</ng-template>
</ngb-panel>
</ngb-accordion>
</div> </div>
`, `,
}) })

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

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

@@ -36,4 +36,5 @@
.nav-link.active { .nav-link.active {
border: #D2BA6F solid 2px; border: #D2BA6F solid 2px;
border-radius: 0;
} }