Better feedback on draft publication

This commit is contained in:
2023-03-10 17:27:14 +01:00
parent a52435d443
commit 1d3918db87
10 changed files with 198 additions and 73 deletions

View File

@@ -6,7 +6,7 @@ from ..core.routes import get_crud_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, Party, replace_variables_in_value from .models import Contract, ContractDraft, ContractDraftStatus, Party, replace_variables_in_value
from .schemas import ContractCreate, ContractRead, ContractUpdate from .schemas import ContractCreate, ContractRead, ContractUpdate
from ..entity.models import Entity from ..entity.models import Entity
@@ -64,6 +64,8 @@ async def create(item: ContractCreate, user=Depends(get_current_user)) -> dict:
contract_dict['provisions'] = provisions contract_dict['provisions'] = provisions
o = await Contract(**contract_dict).create() o = await Contract(**contract_dict).create()
await draft.update({"$set": {"status": ContractDraftStatus.published}})
return {"message": "Contract Successfully created", "id": o.id} return {"message": "Contract Successfully created", "id": o.id}
@@ -89,11 +91,11 @@ async def affix_signature(signature_id: str, signature_file: UploadFile = File(.
if signature.signature_affixed: if signature.signature_affixed:
raise HTTPException(status_code=400, detail="Signature already affixed") raise HTTPException(status_code=400, detail="Signature already affixed")
with open("media/signatures/{}.png".format(signature_id), "wb") as buffer: with open(f'media/signatures/{signature_id}.png', "wb") as buffer:
shutil.copyfileobj(signature_file.file, buffer) shutil.copyfileobj(signature_file.file, buffer)
update_query = {"$set": { update_query = {"$set": {
'parties.{}.signature_affixed'.format(signature_index): True f'parties.{signature_index}.signature_affixed': True
}} }}
signature.signature_affixed = True signature.signature_affixed = True
if contract.is_signed(): if contract.is_signed():

View File

@@ -19,7 +19,7 @@ class ContractStatus(str, Enum):
class ContractDraftStatus(str, Enum): class ContractDraftStatus(str, Enum):
in_progress = 'in_progress' in_progress = 'in_progress'
ready = 'ready' ready = 'ready'
created = 'created' published = 'published'
class DraftParty(BaseModel): class DraftParty(BaseModel):
@@ -29,7 +29,8 @@ class DraftParty(BaseModel):
"resource": "entity", "resource": "entity",
"schema": "Entity", "schema": "Entity",
} }
} },
default=""
) )
part: str part: str
representative_id: str = Field( representative_id: str = Field(
@@ -59,8 +60,8 @@ class Party(BaseModel):
class ProvisionGenuine(BaseModel): class ProvisionGenuine(BaseModel):
type: Literal['genuine'] = 'genuine' type: Literal['genuine'] = 'genuine'
title: str = RichtextSingleline(props={"parametrized": True}) title: str = RichtextSingleline(props={"parametrized": True}, default="")
body: str = RichtextMultiline(props={"parametrized": True}) body: str = RichtextMultiline(props={"parametrized": True}, default="")
class ContractProvisionTemplateReference(BaseModel): class ContractProvisionTemplateReference(BaseModel):
@@ -73,7 +74,8 @@ class ContractProvisionTemplateReference(BaseModel):
"displayedFields": ['title', 'body'] "displayedFields": ['title', 'body']
}, },
}, },
props={"parametrized": True} props={"parametrized": True},
default=""
) )
@@ -98,6 +100,7 @@ class ContractDraft(CrudDocument):
format="dictionary", format="dictionary",
) )
status: ContractDraftStatus = ContractDraftStatus.in_progress status: ContractDraftStatus = ContractDraftStatus.in_progress
todo: List[str] = []
class Settings(CrudDocument.Settings): class Settings(CrudDocument.Settings):
bson_encoders = { bson_encoders = {
@@ -106,6 +109,40 @@ class ContractDraft(CrudDocument):
hour=0, minute=0, second=0) hour=0, minute=0, second=0)
} }
async def check_is_ready(self):
if self.status == ContractDraftStatus.published:
return
self.todo = []
if len(self.parties) < 2:
self.todo.append('Contract must have at least two parties')
if len(self.provisions) < 1:
self.todo.append('Contract must have at least one provision')
for p in self.parties:
if not p.entity_id:
self.todo.append('All parties must have an associated entity`')
for p in self.provisions:
if p.provision.type == "genuine" and not (p.provision.title and p.provision.body):
self.todo.append('Empty genuine provision')
elif p.provision.type == "template" and not p.provision.provision_template_id:
self.todo.append('Empty template provision')
for v in self.variables:
if not (v.key and v.value):
self.todo.append('Empty variable')
if self.todo:
self.status = ContractDraftStatus.in_progress
else:
self.status = ContractDraftStatus.ready
await self.update({"$set": {
"status": self.status,
"todo": self.todo
}})
class Contract(CrudDocument): class Contract(CrudDocument):
name: str name: str

View File

@@ -84,8 +84,8 @@ async def preview_draft(draft_id: str) -> str:
return await render_print('localhost', draft) return await render_print('localhost', draft)
@print_router.get("/preview/{signature_id}", response_class=HTMLResponse) @print_router.get("/preview/signature/{signature_id}", response_class=HTMLResponse)
async def preview_contract(signature_id: str) -> str: async def preview_contract_by_signature(signature_id: str) -> str:
contract = await Contract.find_by_signature_id(signature_id) contract = await Contract.find_by_signature_id(signature_id)
for p in contract.parties: for p in contract.parties:
if p.signature_affixed: if p.signature_affixed:
@@ -94,6 +94,16 @@ async def preview_contract(signature_id: str) -> str:
return await render_print('localhost', contract) return await render_print('localhost', contract)
@print_router.get("/preview/{contract_id}", response_class=HTMLResponse)
async def preview_contract(contract_id: str) -> str:
contract = await Contract.get(contract_id)
for p in contract.parties:
if p.signature_affixed:
p.signature_png = retrieve_signature_png(f'media/signatures/{p.signature_uuid}.png')
return await render_print('localhost', contract)
@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) -> str:
contract = await Contract.get(contract_id) contract = await Contract.get(contract_id)

View File

@@ -1,7 +1,47 @@
from fastapi import APIRouter from beanie import PydanticObjectId
from fastapi import HTTPException, Depends
from ..core.routes import get_crud_router from ..core.routes import get_crud_router
from .models import ContractDraft from ..user.manager import get_current_user
from .models import ContractDraft, ContractDraftStatus
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)
del(draft_router.routes[0])
del(draft_router.routes[2])
@draft_router.post("/", response_description="Contract Draft added to the database")
async def create(item: ContractDraftCreate, user=Depends(get_current_user)) -> dict:
await item.validate_foreign_key()
o = await ContractDraft(**item.dict()).create()
await o.check_is_ready()
return {"message": "Contract Draft added successfully", "id": o.id}
@draft_router.put("/{id}", response_description="Contract Draft record updated")
async def update(id: PydanticObjectId, req: ContractDraftUpdate, user=Depends(get_current_user)) -> ContractDraftRead:
req = {k: v for k, v in req.dict().items() if v is not None}
update_query = {"$set": {
field: value for field, value in req.items()
}}
item = await ContractDraft.get(id)
if not item:
raise HTTPException(
status_code=404,
detail="Contract Draft record not found!"
)
if item.status == ContractDraftStatus.published:
raise HTTPException(
status_code=400,
detail="Contract Draft has already been published"
)
await item.update(update_query)
await item.check_is_ready()
return ContractDraftRead(**item.dict())

View File

@@ -3,7 +3,7 @@ from typing import List
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from .models import ContractDraft, DraftProvision, Party, 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
@@ -17,14 +17,12 @@ class ContractDraftRead(ContractDraft):
class ContractDraftCreate(Writer): class ContractDraftCreate(Writer):
name: str name: str
title: str title: str
parties: List[Party] parties: List[DraftParty]
provisions: List[DraftProvision] provisions: List[DraftProvision]
variables: List[DictionaryEntry] = Field( variables: List[DictionaryEntry] = Field(
default=[], default=[],
format="dictionary", format="dictionary",
) )
location: str = ""
date: datetime.date = datetime.date(1, 1, 1)
async def validate_foreign_key(self): async def validate_foreign_key(self):
return return

View File

@@ -4,5 +4,6 @@
[schema]="this.schema" [schema]="this.schema"
(resourceUpdated)="this.flashService.success('Entity updated')" (resourceUpdated)="this.flashService.success('Entity updated')"
(resourceDeleted)="this.flashService.success('Entity deleted')" (resourceDeleted)="this.flashService.success('Entity deleted')"
(resourceReceived)="this.onResourceReceived($event)"
(error)="this.flashService.error($event)"> (error)="this.flashService.error($event)">
</crud-card> </crud-card>

View File

@@ -1,26 +1,32 @@
import {Component, Input} from "@angular/core"; import {Component, EventEmitter, Input, Output} from "@angular/core";
import {ActivatedRoute, ParamMap} from "@angular/router"; import {ActivatedRoute, ParamMap} from "@angular/router";
import { FlashmessagesService } from "../../../layout/flashmessages/flashmessages.service"; import { FlashmessagesService } from "../../../layout/flashmessages/flashmessages.service";
@Component({ @Component({
templateUrl: 'card.component.html', templateUrl: 'card.component.html',
selector: 'base-card', selector: 'base-card',
}) })
export class BaseCrudCardComponent { export class BaseCrudCardComponent {
@Input() resource: string | undefined; @Input() resource: string | undefined;
@Input() resource_id: string | null = null; @Input() resource_id: string | null = null;
@Input() schema: string | undefined; @Input() schema: string | undefined;
constructor( @Output() resourceReceived: EventEmitter<any> = new EventEmitter();
private route: ActivatedRoute,
public flashService: FlashmessagesService
) {}
ngOnInit(): void { constructor(
if (this.resource_id === null) { private route: ActivatedRoute,
this.route.paramMap.subscribe((params: ParamMap) => { public flashService: FlashmessagesService
this.resource_id = params.get('id') ) {}
})
ngOnInit(): void {
if (this.resource_id === null) {
this.route.paramMap.subscribe((params: ParamMap) => {
this.resource_id = params.get('id')
})
}
}
onResourceReceived(model: any) {
this.resourceReceived.emit(model);
} }
}
} }

View File

@@ -25,35 +25,54 @@ export class ContractsNewComponent extends BaseContractsComponent {
@Component({ @Component({
template:` template:`
<label>Download Link:</label> <ng-container *ngIf="this.resourceReadyToPrint; else previewLink">
<div class="input-group mb-12"> <label i18n>Download Link:</label>
<span class="input-group-text"><a href="{{ this.contractPrintLink! }}" target="_blank">{{ this.contractPrintLink! }}&nbsp;</a></span> <div class="input-group mb-12">
<button type="button" class="btn btn-light" [cdkCopyToClipboard]="this.contractPrintLink!"><i-bs name="text-paragraph"/></button> <span class="input-group-text"><a href="{{ this.contractPrintLink! }}" target="_blank">{{ this.contractPrintLink! }}</a></span>
</div> <button type="button" class="btn btn-light" [cdkCopyToClipboard]="this.contractPrintLink!"><i-bs name="text-paragraph"/></button>
</div>
</ng-container>
<ng-template #previewLink>
<label i18n>Preview Link:</label>
<div class="input-group mb-12">
<span class="input-group-text"><a href="{{ this.contractPreviewLink! }}" target="_blank">{{ this.contractPreviewLink! }}</a></span>
<button type="button" class="btn btn-light" [cdkCopyToClipboard]="this.contractPreviewLink!"><i-bs name="text-paragraph"/></button>
</div>
</ng-template>
<base-card <base-card
[resource_id]="this.resource_id" [resource_id]="this.resource_id"
[resource]="this.resource" [resource]="this.resource"
[schema]="this.schema"> [schema]="this.schema"
(resourceReceived)="this.onResourceReceived($event)">
</base-card> </base-card>
`, `,
}) })
export class ContractsCardComponent extends BaseContractsComponent{ export class ContractsCardComponent extends BaseContractsComponent{
resource_id: string | null = null; resource_id: string | null = null;
resourceReadyToPrint = false;
contractPrintLink: string | null = null; contractPrintLink: string | null = null;
constructor( contractPreviewLink: string | null = null;
private route: ActivatedRoute
) { constructor(
super() private route: ActivatedRoute
} ) {
super()
}
ngOnInit(): void { ngOnInit(): void {
if (this.resource_id === null) { if (this.resource_id === null) {
this.route.paramMap.subscribe((params: ParamMap) => { this.route.paramMap.subscribe((params: ParamMap) => {
this.resource_id = params.get('id') this.resource_id = params.get('id')
this.contractPrintLink = `${location.origin}/api/v1/contract/print/pdf/${this.resource_id}`
}) })
} }
} }
onResourceReceived(model: any): void {
this.resourceReadyToPrint = model.status != "published";
this.contractPrintLink = `${location.origin}/api/v1/contract/print/pdf/${this.resource_id}`
this.contractPreviewLink = `${location.origin}/api/v1/contract/print/preview/${this.resource_id}`
}
} }
@Component({ @Component({
@@ -119,7 +138,7 @@ export class ContractsSignatureComponent implements OnInit {
} }
getPreview() { getPreview() {
return this.sanitizer.bypassSecurityTrustResourceUrl("/api/v1/contract/print/preview/" + this.signature_id); return this.sanitizer.bypassSecurityTrustResourceUrl("/api/v1/contract/print/preview/signature/" + this.signature_id);
} }
postSignature(image: string) { postSignature(image: string) {

View File

@@ -3,7 +3,7 @@ import { FormlyFieldConfig } from "@ngx-formly/core";
import { FormGroup} from "@angular/forms"; import { FormGroup} from "@angular/forms";
import { CrudFormlyJsonschemaService } from "@common/crud/crud-formly-jsonschema.service"; import { CrudFormlyJsonschemaService } from "@common/crud/crud-formly-jsonschema.service";
import { CrudService } from "@common/crud/crud.service"; import { CrudService } from "@common/crud/crud.service";
import {ActivatedRoute, ParamMap} from "@angular/router"; import { ActivatedRoute, ParamMap, Router } from "@angular/router";
import { formatDate } from "@angular/common"; import { formatDate } from "@angular/common";
@@ -92,15 +92,20 @@ export class DraftsNewComponent extends BaseDraftsComponent implements OnInit {
template: ` template: `
<base-card <base-card
[resource]="this.resource" [resource]="this.resource"
[schema]="this.schema"> [schema]="this.schema"
(resourceReceived)="this.onResourceReceived($event)"
>
</base-card> </base-card>
<a class="btn btn-link" href="http://localhost/api/v1/contract/print/preview/draft/{{this.resource_id}}" target="_blank">Preview</a> <a class="btn btn-link" href="http://localhost/api/v1/contract/print/preview/draft/{{this.resource_id}}" target="_blank">Preview</a>
<formly-form [fields]="newContractFormfields" [form]="newContractForm" [model]="newContractModel"></formly-form> <ng-container *ngIf="this.isReadyForPublication;">
<button class="btn btn-success" (click)="publish()">Publish</button> <formly-form [fields]="newContractFormfields" [form]="newContractForm" [model]="newContractModel"></formly-form>
<button class="btn btn-success" (click)="publish()">Publish</button>
</ng-container>
` `
}) })
export class DraftsCardComponent extends BaseDraftsComponent implements OnInit { export class DraftsCardComponent extends BaseDraftsComponent implements OnInit {
resource_id: string | null = null;templateModel: {} = {}; resource_id: string | null = null;templateModel: {} = {};
isReadyForPublication = false;
newContractFormfields: FormlyFieldConfig[] = []; newContractFormfields: FormlyFieldConfig[] = [];
newContractForm: FormGroup = new FormGroup({}); newContractForm: FormGroup = new FormGroup({});
newContractModel: any = { newContractModel: any = {
@@ -132,7 +137,8 @@ export class DraftsCardComponent extends BaseDraftsComponent implements OnInit {
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private formlyJsonschema: CrudFormlyJsonschemaService, private formlyJsonschema: CrudFormlyJsonschemaService,
private crudService: CrudService private crudService: CrudService,
private router: Router,
) { ) {
super(); super();
} }
@@ -150,8 +156,12 @@ export class DraftsCardComponent extends BaseDraftsComponent implements OnInit {
} }
publish() { publish() {
this.crudService.create('contract', this.newContractModel).subscribe((templateModel) => { this.crudService.create('contract', this.newContractModel).subscribe((response: any) => {
console.log(templateModel) this.router.navigate([`../../${response.id}`], {relativeTo: this.route});
}); });
} }
onResourceReceived(model: any): void {
this.isReadyForPublication = model.status == "ready";
}
} }

View File

@@ -37,7 +37,7 @@ export class CardComponent implements OnInit {
@Output() resourceUpdated: EventEmitter<string> = new EventEmitter(); @Output() resourceUpdated: EventEmitter<string> = new EventEmitter();
@Output() resourceDeleted: EventEmitter<string> = new EventEmitter(); @Output() resourceDeleted: EventEmitter<string> = new EventEmitter();
@Output() error: EventEmitter<string> = new EventEmitter(); @Output() error: EventEmitter<string> = new EventEmitter();
@Output() resourceReceived: EventEmitter<any> = new EventEmitter();
form = new FormGroup({}); form = new FormGroup({});
fields: FormlyFieldConfig[] = []; fields: FormlyFieldConfig[] = [];
@@ -78,6 +78,7 @@ export class CardComponent implements OnInit {
next :(model: any) => { next :(model: any) => {
this.model = model; this.model = model;
this._modelLoading$.next(false); this._modelLoading$.next(false);
this.resourceReceived.emit(model);
}, },
error: (err) => this.error.emit("Error loading the model:" + err) error: (err) => this.error.emit("Error loading the model:" + err)
}); });
@@ -96,27 +97,28 @@ export class CardComponent implements OnInit {
onSubmit(model: any) { onSubmit(model: any) {
this._modelLoading$.next(true); this._modelLoading$.next(true);
if (this.isCreateForm()) { if (this.isCreateForm()) {
this.crudService.create(this.resource!, model).subscribe({ this.crudService.create(this.resource!, model).subscribe({
next: (response: any) => { next: (response: any) => {
this._modelLoading$.next(false); this._modelLoading$.next(false);
if (! this.is_modal) { if (! this.is_modal) {
this.router.navigate([`../${response.id}`], {relativeTo: this.route}); this.router.navigate([`../${response.id}`], {relativeTo: this.route});
} else { } else {
this.resourceCreated.emit(response.id) this.resourceCreated.emit(response.id)
} }
}, },
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.model = model; this.model = model;
this._modelLoading$.next(false); this._modelLoading$.next(false);
this.resourceUpdated.emit(model._id) 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)
});
} }
} }