20 Commits

Author SHA1 Message Date
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
24 changed files with 210 additions and 111 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,8 +1,8 @@
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 from pydantic import BaseModel, Field, validator
from beanie.operators import ElemMatch from beanie.operators import ElemMatch
from ..core.models import CrudDocument, RichtextSingleline, RichtextMultiline, DictionaryEntry from ..core.models import CrudDocument, RichtextSingleline, RichtextMultiline, DictionaryEntry
@@ -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,6 +181,19 @@ class Contract(CrudDocument):
lawyer: Entity = Field(title="Avocat en charge") lawyer: Entity = Field(title="Avocat en charge")
location: str = Field(title="Lieu") location: str = Field(title="Lieu")
date: datetime.date = Field(title="Date") date: datetime.date = Field(title="Date")
label: Optional[str] = None
@validator("label", always=True)
def generate_label(cls, v, values, **kwargs):
if not v:
contract_label = values['title']
for p in values['parties']:
contract_label = contract_label + f" - {p.entity.label}"
contract_label = contract_label + f" - {values['date'].strftime('%m/%d/%Y')}"
return contract_label
return v
class Settings(CrudDocument.Settings): class Settings(CrudDocument.Settings):
fulltext_search = ['name', 'title'] fulltext_search = ['name', 'title']

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):
@@ -139,3 +139,18 @@ def retrieve_signature_png(filepath):
base64_utf8_str = base64.b64encode(b_content).decode('utf-8') base64_utf8_str = base64.b64encode(b_content).decode('utf-8')
ext = filepath.split('.')[-1] ext = filepath.split('.')[-1]
return f'data:image/{ext};base64,{base64_utf8_str}' return f'data:image/{ext};base64,{base64_utf8_str}'
@print_router.get("/opengraph/{signature_id}", response_class=HTMLResponse)
async def get_signature_opengraph(signature_id: str, request: Request) -> str:
contract = await Contract.find_by_signature_id(signature_id)
signature = contract.get_signature(signature_id)
template = templates.get_template("opengraph.html")
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

@@ -3,10 +3,10 @@ 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, get_current_user_and_firm
def parse_sort(sort_by): def parse_sort(sort_by):
@@ -63,28 +63,27 @@ def get_crud_router(model, model_create, model_read, model_update):
router = APIRouter() router = APIRouter()
@router.post("/", response_description="{} added to the database".format(model.__name__)) @router.post("/", response_description="{} added to the database".format(model.__name__))
async def create(item: model_create, user=Depends(get_current_user)) -> dict: async def create(instance: str, firm: str, item: model_create, user=Depends(get_current_user)) -> dict:
await item.validate_foreign_key() await item.validate_foreign_key()
o = await model(**item.dict()).create() o = await model(**item.dict()).create()
return {"message": "{} added successfully".format(model.__name__), "id": o.id} return {"message": "{} added successfully".format(model.__name__), "id": o.id}
@router.get("/{id}", response_description="{} record retrieved".format(model.__name__)) @router.get("/{id}", response_description="{} record retrieved".format(model.__name__))
async def read_id(id: PydanticObjectId, user=Depends(get_current_user)) -> model_read: async def read_id(instance: str, firm: str, id: PydanticObjectId, user=Depends(get_current_user)) -> model_read:
item = await model.get(id) item = await model.get(id)
return model_read(**item.dict()) return model_read(**item.dict())
@router.get("/", response_model=Page[model_read], response_description="{} records retrieved".format(model.__name__)) @router.get("/", response_model=Page[model_read], response_description="{} records retrieved".format(model.__name__))
async def read_list(size: int = 50, page: int = 1, sort_by: str = None, query: str = None, async def read_list(instance: str, firm: str, size: int = 50, page: int = 1, sort_by: str = None, query: str = None,
user=Depends(get_current_user)) -> Page[model_read]: user=Depends(get_current_user_and_firm)) -> Page[model_read]:
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__))
async def update(id: PydanticObjectId, req: model_update, user=Depends(get_current_user)) -> model_read: async def update(instance: str, firm: str, 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} req = {k: v for k, v in req.dict().items() if v is not None}
update_query = {"$set": { update_query = {"$set": {
field: value for field, value in req.items() field: value for field, value in req.items()
@@ -101,7 +100,7 @@ def get_crud_router(model, model_create, model_read, model_update):
return model_read(**item.dict()) return model_read(**item.dict())
@router.delete("/{id}", response_description="{} record deleted from the database".format(model.__name__)) @router.delete("/{id}", response_description="{} record deleted from the database".format(model.__name__))
async def delete(id: PydanticObjectId, user=Depends(get_current_superuser)) -> dict: async def delete(instance: str, firm: str, id: PydanticObjectId, user=Depends(get_current_superuser)) -> dict:
item = await model.get(id) item = await model.get(id)
if not item: if not item:

View File

@@ -6,7 +6,6 @@ from .user import User, AccessToken
from .entity.models import Entity from .entity.models import Entity
from .template.models import ContractTemplate, ProvisionTemplate from .template.models import ContractTemplate, ProvisionTemplate
from .contract.models import ContractDraft, Contract 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/"

View File

@@ -23,13 +23,13 @@ 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 '{} {}'.format(self.firstname, self.lastname) return '{} {}'.format(self.firstname, self.lastname)

View File

@@ -17,10 +17,12 @@ async def on_startup():
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

@@ -109,6 +109,9 @@ fastapi_users = FastAPIUsers[User, uuid.UUID](
get_current_user = fastapi_users.current_user(active=True) get_current_user = fastapi_users.current_user(active=True)
get_current_superuser = fastapi_users.current_user(active=True, superuser=True) get_current_superuser = fastapi_users.current_user(active=True, superuser=True)
def get_current_user_and_firm(user=Depends(get_current_user)):
return user
def get_auth_router(): def get_auth_router():
return fastapi_users.get_auth_router(auth_backend) return fastapi_users.get_auth_router(auth_backend)

View File

@@ -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,37 +78,22 @@ export class ContractsCardComponent extends BaseContractsComponent{
@Component({ @Component({
template: ` template: `
<ngb-accordion #acc="ngbAccordion" activeIds="ngb-panel-1"> <div>
<ngb-panel> <iframe width="100%"
<ng-template ngbPanelTitle> [src]="getPreview()"
<span i18n>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 i18n>Signature</span> <p>Le cabinet Cooper, Hillman & Toshi LLC</p>
</ng-template> </div>
<ng-template ngbPanelContent> </div>`
<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">
<signature-drawer class="col-7"
(signatureDrawn$)="postSignature($event)"></signature-drawer>
<div class="col-5" i18n>
<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;

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

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

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

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;