23 Commits

Author SHA1 Message Date
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
46ac3295e3 Merge branch 'master' of git.dorfsvald.net:ewandor/cht-lawfirm 2023-03-16 15:36:53 +01:00
2349f4c804 Adding a makefile 2023-03-16 15:36:36 +01:00
334150bc0f Preparing dockers for prod 2023-03-16 15:03:06 +01:00
ewandor
da19ef652e Merge branch 'master' of git.dorfsvald.net:ewandor/cht-lawfirm 2023-03-16 03:47:47 +01:00
ewandor
015fc00f6f using root when locating assets in contract print 2023-03-16 03:47:29 +01:00
e22c197d3a Involving gittea image registry 2023-03-16 02:55:55 +01:00
becab58c73 Adding templates to migration scripts 2023-03-16 02:24:35 +01:00
70a863f6e1 Correcting nginx configuration for prod 2023-03-16 02:24:21 +01:00
2b55d206e2 Naming containers and freezing mongo version to 4.4.19 2023-03-16 02:23:54 +01:00
f35182233b Adding a paste filter on richtext editor 2023-03-15 15:45:11 +01:00
43474c960f Merge remote-tracking branch 'origin/fix/default-date-contract-creation' 2023-03-15 15:19:21 +01:00
b3566b39b8 Removing a typo 2023-03-15 15:16:09 +01:00
7bebc05e08 fulltext search use each word separately 2023-03-15 15:15:47 +01:00
6b49b688ac Removing unused imports 2023-03-14 19:09:49 +01:00
7dbe4a1716 Changing db password 2023-03-14 19:06:13 +01:00
701ac8e1dc Merge branch 'master' of git.dorfsvald.net:ewandor/cht-lawfirm 2023-03-14 19:00:18 +01:00
2ba34a675d Production env for the front 2023-03-14 18:59:56 +01:00
23 changed files with 268 additions and 48 deletions

12
Makefile Normal file
View File

@@ -0,0 +1,12 @@
publish:
git checkout $(TAG)
docker build -f back/Dockerfile.prod -t git.dorfsvald.net/ewandor/cht-lawfirm-back-prod back
docker tag git.dorfsvald.net/ewandor/cht-lawfirm-back-prod:latest git.dorfsvald.net/ewandor/cht-lawfirm-back-prod:$(TAG)
docker push git.dorfsvald.net/ewandor/cht-lawfirm-back-prod:latest
docker push git.dorfsvald.net/ewandor/cht-lawfirm-back-prod:$(TAG)
docker build -f front/Dockerfile.prod -t git.dorfsvald.net/ewandor/cht-lawfirm-nginx-prod front
docker tag git.dorfsvald.net/ewandor/cht-lawfirm-nginx-prod:latest git.dorfsvald.net/ewandor/cht-lawfirm-nginx-prod:$(TAG)
docker push git.dorfsvald.net/ewandor/cht-lawfirm-nginx-prod:latest
docker push git.dorfsvald.net/ewandor/cht-lawfirm-nginx-prod:$(TAG)
git switch -

View File

@@ -5,13 +5,10 @@ RUN apt update && apt install -y xfonts-base xfonts-75dpi python3-pip python3-cf
WORKDIR /code WORKDIR /code
# copy both 'package.json' and 'package-lock.json' (if available)
COPY ./requirements.txt /code/requirements.txt COPY ./requirements.txt /code/requirements.txt
# install project dependencies
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
# copy project files and folders to the current working directory (i.e. 'app' folder)
COPY ./app /code/app COPY ./app /code/app
EXPOSE 8000 EXPOSE 8000

15
back/Dockerfile.prod Normal file
View File

@@ -0,0 +1,15 @@
FROM python:3.10
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/*
WORKDIR /code
COPY ./requirements.txt /code/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
COPY ./app /code/app
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

View File

@@ -81,7 +81,7 @@ async def render_css(root_url, contract):
async def preview_draft(draft_id: str, request: Request) -> str: async def preview_draft(draft_id: str, request: Request) -> str:
draft = await build_model(await ContractDraft.get(draft_id)) draft = await build_model(await ContractDraft.get(draft_id))
return await render_print(f'{request.url.scheme}://{request.url.hostname}', 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)
@@ -91,7 +91,7 @@ async def preview_contract_by_signature(signature_id: str, request: Request) ->
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')
return await render_print(f'{request.url.scheme}://{request.url.hostname}', contract) return await render_print('', contract)
@print_router.get("/preview/{contract_id}", response_class=HTMLResponse) @print_router.get("/preview/{contract_id}", response_class=HTMLResponse)
@@ -101,7 +101,7 @@ async def preview_contract(contract_id: str, request: Request) -> str:
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')
return await render_print(f'{request.url.scheme}://{request.url.hostname}', contract) return await render_print('', contract)
@print_router.get("/pdf/{contract_id}", response_class=FileResponse) @print_router.get("/pdf/{contract_id}", response_class=FileResponse)

View File

@@ -39,7 +39,10 @@ def parse_query(query: str, model):
or_array = [] or_array = []
for field in model.Settings.fulltext_search: for field in model.Settings.fulltext_search:
or_array.append(RegEx(field, value, 'i')) 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] operand = Or(or_array) if len(or_array) > 1 else or_array[0]
elif operator == 'eq': elif operator == 'eq':

View File

@@ -5,10 +5,11 @@ from beanie import init_beanie
from .user import User, AccessToken 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 .order.models import Order
from .contract.models import ContractDraft, Contract from .contract.models import ContractDraft, Contract
# from .order.models import Order
DATABASE_URL = "mongodb://root:example@mongo:27017/" DB_PASSWORD = "IBO3eber0mdw2R9pnInLdtFykQFY2f06"
DATABASE_URL = f"mongodb://root:{DB_PASSWORD}@mongo:27017/"
async def init_db(): async def init_db():

View File

@@ -1,5 +1,5 @@
import uuid import uuid
from typing import Any, Dict, Generic, Optional from typing import Any
from bson import ObjectId from bson import ObjectId
from fastapi import Depends from fastapi import Depends

View File

@@ -3,10 +3,10 @@ import asyncio
import json import json
from os import path from os import path
from app.db import init_db, Entity, Order, Contract, User, AccessToken from app.db import init_db, Entity, Contract, ContractTemplate, ProvisionTemplate, User
models = [Entity, Order, Contract, User] models = [Entity, Contract, User, ContractTemplate, ProvisionTemplate]
async def handle_migration(args): async def handle_migration(args):

27
docker-compose.prod.yml Normal file
View File

@@ -0,0 +1,27 @@
version: "3.9"
services:
back:
image: git.dorfsvald.net/ewandor/cht-lawfirm-back-prod:latest
restart: always
volumes:
- ${ROOT_PATH}/back/media:/code/media
nginx:
image: git.dorfsvald.net/ewandor/cht-lawfirm-nginx-prod:latest
restart: always
ports:
- "3820:80"
mongo:
image: "mongo:4.4.19"
restart: always
ports:
- "27017:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: IBO3eber0mdw2R9pnInLdtFykQFY2f06
volumes:
- database:/data/db
volumes:
database:

View File

@@ -0,0 +1,42 @@
version: "3.9"
services:
back:
build:
context: ${ROOT_PATH}/back
restart: always
ports:
- "8000:8000"
volumes:
- ${ROOT_PATH}/back/app:/code/app
- ${ROOT_PATH}/back/media:/code/media
front:
build:
context: ${ROOT_PATH}/front
restart: always
ports:
- "4200:4200"
volumes:
- ${ROOT_PATH}/front/app/src:/app/src
- ${ROOT_PATH}/front/app/public:/app/public
nginx:
build:
context: ${ROOT_PATH}/nginx
restart: always
ports:
- "3820:80"
mongo:
image: "mongo:4.4.18"
restart: always
ports:
- "27017:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: IBO3eber0mdw2R9pnInLdtFykQFY2f06
volumes:
- database:/data/db
volumes:
database:

View File

@@ -3,6 +3,7 @@ services:
back: back:
build: build:
context: ./back context: ./back
image: cht-lawfirm-back-dev
restart: always restart: always
ports: ports:
- "8000:8000" - "8000:8000"
@@ -13,6 +14,7 @@ services:
front: front:
build: build:
context: ./front context: ./front
image: cht-lawfirm-front-dev
restart: always restart: always
ports: ports:
- "4200:4200" - "4200:4200"
@@ -23,18 +25,19 @@ services:
nginx: nginx:
build: build:
context: ./nginx context: ./nginx
image: cht-lawfirm-nginx-dev
restart: always restart: always
ports: ports:
- "80:80" - "80:80"
mongo: mongo:
image: "mongo:4.4.18" image: "mongo:4.4.19"
restart: always restart: always
ports: ports:
- "27017:27017" - "27017:27017"
environment: environment:
MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example MONGO_INITDB_ROOT_PASSWORD: IBO3eber0mdw2R9pnInLdtFykQFY2f06
volumes: volumes:
- database:/data/db - database:/data/db

View File

@@ -1,22 +1,11 @@
FROM node:lts-alpine FROM node:lts-alpine
# install simple http server for serving static content
RUN npm install -g http-server
# make the 'app' folder the current working directory
WORKDIR /app WORKDIR /app
RUN npm install -g @angular/cli http-server
RUN npm install -g @angular/cli
# copy both 'package.json' and 'package-lock.json' (if available)
COPY app/package*.json ./ COPY app/package*.json ./
# install project dependencies
RUN npm install RUN npm install
# copy project files and folders to the current working directory (i.e. 'app' folder)
COPY app/ . COPY app/ .
# build app for production with minification
RUN npm run build RUN npm run build
EXPOSE 4200 EXPOSE 4200

17
front/Dockerfile.prod Normal file
View File

@@ -0,0 +1,17 @@
FROM node:lts-alpine AS builder
WORKDIR /app
RUN npm install -g @angular/cli
COPY app/package*.json ./
RUN npm install
COPY app/ .
RUN npm run build --prod
FROM nginx:alpine
COPY nginx.prod.conf /etc/nginx/nginx.conf
COPY --from=builder /app/dist/app/fr/ /usr/share/nginx/html/

View File

@@ -13,7 +13,8 @@
"sourceLocale": "en-US", "sourceLocale": "en-US",
"locales": { "locales": {
"fr": { "fr": {
"translation": "src/locale/messages.fr.xlf" "translation": "src/locale/messages.fr.xlf",
"baseHref": ""
} }
} }
}, },
@@ -63,9 +64,6 @@
"extractLicenses": false, "extractLicenses": false,
"sourceMap": true, "sourceMap": true,
"namedChunks": true "namedChunks": true
},
"fr": {
"localize": ["fr"]
} }
}, },
"defaultConfiguration": "production" "defaultConfiguration": "production"
@@ -80,7 +78,7 @@
"browserTarget": "app:build:development" "browserTarget": "app:build:development"
}, },
"fr": { "fr": {
"browserTarget": "App:build:fr" "browserTarget": "app:build:fr"
} }
}, },
"defaultConfiguration": "development" "defaultConfiguration": "development"

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

@@ -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 {
@@ -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([`../../${response.id}`], {relativeTo: this.route}),
error: (err) => this.flashService.error(err)
}); });
} }

View File

@@ -112,10 +112,10 @@ export class CardComponent implements OnInit {
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.model = model;
this._modelLoading$.next(false);
this.resourceUpdated.emit(model._id); this.resourceUpdated.emit(model._id);
this.resourceReceived.emit(model); this.resourceReceived.emit(model);
this.model = model;
this._modelLoading$.next(false);
}, },
error: (err) => this.error.emit("Error updating the entity:" + err) error: (err) => this.error.emit("Error updating the entity:" + err)
}); });

View File

@@ -6,7 +6,7 @@ import { ListComponent } from "./list/list.component";
const routes: Routes = [ const routes: Routes = [
{ path: '', component: ListComponent }, { path: '', component: ListComponent },
{ path: ':id', component: CardComponent }, { path: ':id', component: CardComponent },
];; ];
@NgModule({ @NgModule({
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],

View File

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

View File

@@ -19,15 +19,15 @@
<span class="col col-form-label" i18n *ngIf="loading$ | async">Loading...</span> <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 *ngFor="let row of listData$ | async" (click)="onRowClick(row._id)" (auxclick)="onRowMiddleClick(row._id);" class="table-row-link">
<td *ngFor="let col of this.displayedColumns"> <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>

View File

@@ -100,7 +100,7 @@ export class ListComponent implements OnInit {
parent = parent[key]; parent = parent[key];
} }
} }
return parent; return parent.replace(/<[^>]*>/g, '');
} }
private _search() { private _search() {
@@ -144,10 +144,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)
window.open(newUrl, '_blank');
}
onCreate() { onCreate() {
this.router.navigate([`../new`], {relativeTo: this.route}); this.router.navigate([`../new`], {relativeTo: this.route});
} }

View File

@@ -1,6 +1,7 @@
import {Component, OnInit} from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { FormlyFieldInput } from "@ngx-formly/bootstrap/input"; import { FormlyFieldInput } from "@ngx-formly/bootstrap/input";
@Component({ @Component({
selector: 'formly-richtext-type', selector: 'formly-richtext-type',
template: ` template: `
@@ -42,11 +43,20 @@ export class RichtextTypeComponent extends FormlyFieldInput implements OnInit {
statusbar: false, statusbar: false,
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) {
console.log(args.content)
let container = document.createElement('div');
container.innerHTML = args.content.trim();
cleanPastedElement(container)
console.log(container.innerHTML);
args.content = container.innerHTML;
}
} }
init_multiline = { init_multiline = {
plugins: 'lists image imagetools table code searchreplace autoresize', plugins: 'lists image imagetools table code searchreplace paste autoresize',
menubar: 'edit insert format tools table', menubar: 'edit insert format tools table',
menu: { menu: {
edit: { title: 'Edit', items: 'undo redo | cut copy paste | selectall | searchreplace' }, edit: { title: 'Edit', items: 'undo redo | cut copy paste | selectall | searchreplace' },
@@ -59,7 +69,7 @@ export class RichtextTypeComponent extends FormlyFieldInput implements OnInit {
} }
init_singleline = { init_singleline = {
plugins: 'autoresize', plugins: 'paste autoresize',
menubar: '', menubar: '',
toolbar: 'undo redo | bold italic underline', toolbar: 'undo redo | bold italic underline',
} }
@@ -71,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)};
} }
@@ -92,7 +102,49 @@ export class RichtextTypeComponent extends FormlyFieldInput implements OnInit {
} }
} }
} }
}
function cleanPastedElement(htmlElement: HTMLElement): string {
if (! htmlElement.innerHTML) {
return "";
}
let innerHtml = ""
for(let i = 0; i < htmlElement.childNodes.length; i++){
const childNode = htmlElement.childNodes[i] as HTMLElement
if (childNode.nodeName == "#text") {
innerHtml += childNode.nodeValue;
} else {
innerHtml += cleanPastedElement(childNode);
}
}
htmlElement.innerHTML = innerHtml
} if (htmlElement.tagName == "SPAN") {
let text = htmlElement.innerHTML
const style = htmlElement.style
if (style.fontWeight == "700") {
let strong = document.createElement('b');
strong.innerHTML = text
text = strong.outerHTML;
}
if (style.textDecoration == "underline") {
let underline = document.createElement('u');
underline.innerHTML = text;
text = underline.outerHTML;
}
if (style.fontStyle == "italic") {
let italic = document.createElement('em');
italic.innerHTML = text;
text = italic.outerHTML;
}
return text;
}
htmlElement.style.removeProperty("line-height")
htmlElement.style.removeProperty("margin")
return htmlElement.outerHTML
}

53
front/nginx.prod.conf Normal file
View File

@@ -0,0 +1,53 @@
worker_processes 1;
events { worker_connections 1024; }
http {
sendfile on;
upstream docker-back {
server back:8000;
}
types {
module js;
}
include /etc/nginx/mime.types;
server {
listen 80;
gzip on;
gzip_http_version 1.1;
gzip_disable "MSIE [1-6]\.";
gzip_min_length 256;
gzip_vary on;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;
gzip_comp_level 9;
root /usr/share/nginx/html;
location / {
try_files $uri $uri/ /index.html?$args;
}
location ~* ^.+\.css$ {
default_type text/css;
}
location ~* ^.+\.js$ {
default_type text/javascript;
}
location /api/v1/ {
proxy_pass http://docker-back/;
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;
}
}
}