Initial commit for gisaf/fastapi

This commit is contained in:
phil 2024-02-17 12:35:03 +05:30
commit adce44722f
1361 changed files with 42521 additions and 0 deletions

View file

@ -0,0 +1,6 @@
.item {
border: 1px solid grey;
margin: 0 3px;
padding: 0 2px;
border-radius: 3px;
}

View file

@ -0,0 +1,17 @@
<div class='item'>
<div class='path'>
{{ file.path }}
</div>
<div class='url'>
{{ file.url }}
</div>
<div class='status'>
{{ file.status }}
</div>
<div class='store'>
{{ file.store }}
</div>
<div class='time'>
{{ file.time }}
</div>
</div>

View file

@ -0,0 +1,25 @@
import { Component, OnInit, Input,
ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { AdminDataService } from '../../admin-data.service'
import { AdminBasketFile } from '../data.service'
@Component({
selector: 'gisaf-admin-basket-item',
templateUrl: './basket-item.component.html',
styleUrls: ['./basket-item.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AdminBasketItemComponent implements OnInit {
constructor(
public adminDataService: AdminDataService,
private route: ActivatedRoute,
private cdr: ChangeDetectorRef,
) {}
@Input() file: AdminBasketFile
ngOnInit() {
}
}

View file

@ -0,0 +1,19 @@
import { Injectable } from '@angular/core'
import { Router, Resolve, RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router'
import { Observable, forkJoin } from 'rxjs'
import { map } from 'rxjs/operators'
import { AdminBasket, AdminBasketDataService } from './data.service'
@Injectable()
export class BasketResolver implements Resolve<object> {
constructor(
private router: Router,
private basketDataService: AdminBasketDataService,
) {}
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<AdminBasket> {
var name = route.paramMap.get('name')
return this.basketDataService.getBasket(name)
}
}

View file

@ -0,0 +1,30 @@
.container {
display: flex;
flex-direction: row;
justify-content: space-evenly;
}
.container mat-form-field {
flex: 1 1 0;
}
.container form {
border: 1px solid grey;
padding: 2px 1em;
border-radius: 0.6em;
margin-bottom: 3px;
background-color: #424242;
}
.importButtonGroup {
flex: 5 1 0;
display: inline-block;
}
mat-form-field.store {
width: 18em;
}
mat-form-field.status {
width: 4em;
}

View file

@ -0,0 +1,76 @@
<div class='container'>
<form [formGroup]="formGroup">
<mat-form-field [hidden]="is_upload_field_hidden('project')">
<mat-label>Project</mat-label>
<mat-select formControlName="project">
<mat-option
*ngFor="let item of basket.projects || adminDataService.surveyMeta.projects"
[value]="item.name">
{{ item.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field [hidden]="is_upload_field_hidden('surveyor')">
<mat-label>Surveyor</mat-label>
<mat-select formControlName="surveyor">
<mat-option
*ngFor="let item of adminDataService.surveyMeta.surveyors"
[value]="item.name">
{{ item.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field [hidden]="is_upload_field_hidden('equipment')">
<mat-label>Equipment</mat-label>
<mat-select formControlName="equipment">
<mat-option
*ngFor="let item of adminDataService.surveyMeta.equipments"
[value]="item.name">
{{ item.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class='store' [hidden]="is_upload_field_hidden('store_line_work')">
<mat-label>Store</mat-label>
<mat-select formControlName="store_line_work">
<mat-option
*ngFor="let item of adminDataService.surveyMeta.stores_line_work"
[value]="item.name">
{{ item.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class='store' [hidden]="is_upload_field_hidden('store_misc')">
<mat-label>Store</mat-label>
<mat-select formControlName="store_misc">
<mat-option
*ngFor="let item of adminDataService.surveyMeta.stores_misc"
[value]="item.name">
{{ item.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class='status' [hidden]="is_upload_field_hidden('status')">
<mat-label>Status</mat-label>
<mat-select formControlName="status">
<mat-option
*ngFor="let item of adminDataService.surveyMeta.statuses"
[value]="item">
{{ item }}
</mat-option>
</mat-select>
</mat-form-field>
<div class='importButtonGroup'>
<button mat-button (click)="fileInput.click()" [disabled]="!formGroup.valid">
<mat-icon>file_upload</mat-icon>
Add file
</button>
<mat-checkbox formControlName='autoImport'
matTooltip='Automatically import the content of the file in the database when its added to the basket'>
Auto import
</mat-checkbox>
</div>
<input hidden (change)="onFileUpload()" #fileInput type="file" id="file">
</form>
</div>

View file

@ -0,0 +1,115 @@
import { Component, OnInit, Input, ViewChild, ElementRef,
ChangeDetectorRef, ChangeDetectionStrategy, SimpleChanges, OnChanges } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { UntypedFormGroup, UntypedFormControl, Validators } from '@angular/forms'
import { MatTableDataSource } from '@angular/material/table'
import { AdminDataService } from '../../admin-data.service'
import { AdminBasketFile, AdminBasket } from '../data.service'
import { MatSnackBar } from '@angular/material/snack-bar'
import { HtmlSnackbarComponent } from '../../../custom-snackbar/custom-snackbar.component'
@Component({
selector: 'gisaf-admin-basket-upload',
templateUrl: './basket-upload.component.html',
styleUrls: ['./basket-upload.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AdminBasketUploadComponent implements OnInit, OnChanges {
constructor(
public adminDataService: AdminDataService,
private cdr: ChangeDetectorRef,
private http: HttpClient,
private snackBar: MatSnackBar,
) {}
@ViewChild('fileInput') fileInput: ElementRef
@Input() basket: AdminBasket
@Input() dataSource: MatTableDataSource<object>
//upload_fields = ['store', 'status', 'project', 'surveyor', 'equipment']
formGroup: UntypedFormGroup = new UntypedFormGroup({})
ngOnInit() {
let defaults = this.adminDataService.surveyMeta.defaults
this.formGroup = new UntypedFormGroup({
'store_misc': new UntypedFormControl(defaults['store_misc'], [Validators.required]),
'store_line_work': new UntypedFormControl(defaults['store_line_work'], [Validators.required]),
'status': new UntypedFormControl(defaults['status'], [Validators.required]),
'project': new UntypedFormControl(defaults['project'], [Validators.required]),
'surveyor': new UntypedFormControl(defaults['surveyor'], [Validators.required]),
'equipment': new UntypedFormControl(defaults['equipment'], [Validators.required]),
'autoImport': new UntypedFormControl(true),
})
this.setupRequired()
}
ngOnChanges(changes: SimpleChanges) {
this.setupRequired()
}
setupRequired() {
for (let field in this.formGroup.controls) {
if (this.basket.uploadFields.includes(field)) {
this.formGroup.controls[field].setValidators(Validators.required)
}
else {
this.formGroup.controls[field].clearValidators()
}
this.formGroup.controls[field].updateValueAndValidity({onlySelf: true})
}
this.formGroup.updateValueAndValidity({onlySelf: true})
}
onFileUpload() {
const formData = new FormData()
formData.append('file', <File>this.fileInput.nativeElement.files[0])
let fg = this.formGroup.getRawValue()
for (let field in fg) {
if (this.basket.uploadFields.indexOf(field) != -1) {
formData.append(field, fg[field])
}
}
formData.append('autoImport', this.formGroup.get('autoImport').value)
this.http.post('upload/basket/' + this.basket.name, formData).subscribe(
resp => {
let importResult = resp['import_result']
const importTime = resp['time'] || (importResult && importResult['time'])
const fileImport = new AdminBasketFile(
resp['id'],
resp['dir'],
resp['name'],
resp['url'],
resp['md5'],
importTime && new Date(importTime),
resp['comment'],
resp['status'],
resp['store'],
resp['project'],
resp['surveyor'],
resp['equipment'],
)
this.dataSource.data.push(fileImport)
this.dataSource.data = this.dataSource.data
let msg = 'File ' + fileImport.name + ' added to basket'
if (importResult) {
this.snackBar.openFromComponent(HtmlSnackbarComponent, {
data: importResult
//duration: 3000
})
}
else {
this.snackBar.open(msg, 'Close')
}
this.cdr.markForCheck()
}
)
}
is_upload_field_hidden(fname: string) {
return this.basket.uploadFields.indexOf(fname)==-1
}
}

View file

@ -0,0 +1,66 @@
h1 {
margin-top: 0;
margin-bottom: 0;
}
gisaf-admin-basket-upload {
flex: 1 1 0;
}
.filter button {
min-width: 1em!important;
}
.cdk-column-delete, .cdk-column-import {
max-width: 4em;
width: 3em;
}
.cdk-column-name>span {
cursor: pointer;
}
.even {
background-color: #f5f5f512;
}
.tools {
display: flex;
}
.tools .filter {
width: 10em;
}
.tools .upload {
flex: 1 1 0;
}
:host ::ng-deep th.mat-mdc-header-cell, :host ::ng-deep td.mat-mdc-cell, :host ::ng-deep td.mat-mdc-footer-cell {
padding: 0 0.5em;
}
:host ::ng-deep td.mat-mdc-cell {
border-bottom-style: inherit;
}
table.content {
width: 100%;
}
.mat-column-delete, .mat-column-import {
width: 3em;
text-align: center;
}
th.mat-mdc-header-cell:first-of-type, td.mat-mdc-cell:first-of-type, td.mat-mdc-footer-cell:first-of-type {
padding-left: inherit;
}
th.mat-mdc-header-cell:last-of-type, td.mat-mdc-cell:last-of-type, td.mat-mdc-footer-cell:last-of-type {
padding-right: inherit;
}
td .mat-mdc-button {
min-width: inherit ! important;
}

View file

@ -0,0 +1,107 @@
<div fxFlexFill fxLayout='column'>
<h1>Basket: {{ basket.name }}</h1>
<div class='tools'>
<mat-form-field class='filter'>
<mat-label>Filter table</mat-label>
<input matInput
(input)="applyFilter()"
matTooltip="Filter the items of the basket from the table"
[(ngModel)]="filterText"/>
<button mat-button matSuffix mat-icon-button
*ngIf="filterText"
aria-label="Clear"
(click)="filterText='';applyFilter()"
>
<mat-icon>close</mat-icon>
</button>
</mat-form-field>
<gisaf-admin-basket-upload [basket]='basket' [dataSource]='dataSource'>
</gisaf-admin-basket-upload>
</div>
<table mat-table matSort [dataSource]="dataSource" class='content'>
<ng-container matColumnDef="delete">
<th mat-header-cell *matHeaderCellDef>
<form [formGroup]="unlockDeleteFormGroup">
<mat-checkbox formControlName='canDelete' matTooltip='Unlock delete buttons'>
</mat-checkbox>
</form>
</th>
<td mat-cell *matCellDef="let item">
<button mat-button (click)="deleteItem(item)" matTooltip="Delete"
[disabled]="!unlockDeleteFormGroup.controls['canDelete'].value">
<mat-icon aria-label="delete">delete</mat-icon>
</button>
</td>
</ng-container>
<ng-container matColumnDef="import">
<th mat-header-cell *matHeaderCellDef>Import</th>
<td mat-cell *matCellDef="let item">
<button mat-button (click)="importItem(item, $event.ctrlKey)"
matTooltip="Import to the database. Press 'Control' key for a dry run (no actual change will happen)"
>
<mat-icon aria-label="import">input</mat-icon>
</button>
</td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Name</th>
<td mat-cell *matCellDef="let item">
<span (click)="download(item)" [title]="item.dir">{{ item.name }}</span>
</td>
</ng-container>
<ng-container matColumnDef="store">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Store</th>
<td mat-cell *matCellDef="let item">{{ item.store }}</td>
</ng-container>
<ng-container matColumnDef="time">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Import time</th>
<td mat-cell *matCellDef="let item">{{ isDate(item.time) ? (item.time | date:'dd/MM/yyyy, HH:mm:ss') : 'never' }}</td>
</ng-container>
<ng-container matColumnDef="url">
<th mat-header-cell *matHeaderCellDef mat-sort-header>URL</th>
<td mat-cell *matCellDef="let item">{{ item.url }}</td>
</ng-container>
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Status</th>
<td mat-cell *matCellDef="let item">{{ item.status }}</td>
</ng-container>
<ng-container matColumnDef="project">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Project</th>
<td mat-cell *matCellDef="let item">{{ item.project }}</td>
</ng-container>
<ng-container matColumnDef="surveyor">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Surveyor</th>
<td mat-cell *matCellDef="let item">{{ item.surveyor }}</td>
</ng-container>
<ng-container matColumnDef="equipment">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Equipment</th>
<td mat-cell *matCellDef="let item">{{ item.equipment }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="getColumns();sticky:true"></tr>
<tr mat-row [ngClass]="{even: even}"
*matRowDef="let row; let even = even; columns:getColumns()"
></tr>
</table>
<!--
<gisaf-admin-basket-item [file]='file' *ngFor='let file of basket.file'>
</gisaf-admin-basket-item>
-->
<mat-paginator
class="paginator"
[pageSizeOptions]="[10, 20, 50, 100]"
[pageIndex]='0'
[pageSize]='10'
showFirstLastButtons
>
</mat-paginator>
</div>

View file

@ -0,0 +1,107 @@
import { Component, OnInit, Input, ViewChild, ElementRef,
ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { UntypedFormGroup, UntypedFormControl } from '@angular/forms'
import { SelectionModel } from '@angular/cdk/collections'
import { MatPaginator } from '@angular/material/paginator'
import { MatSnackBar } from '@angular/material/snack-bar'
import { MatSort } from '@angular/material/sort'
import { MatTableDataSource } from '@angular/material/table'
import { AdminDataService } from '../admin-data.service'
import { AdminBasketDataService, AdminBasket, AdminBasketFile } from './data.service'
import { HtmlSnackbarComponent } from '../../custom-snackbar/custom-snackbar.component'
@Component({
selector: 'gisaf-admin-basket',
templateUrl: './basket.component.html',
styleUrls: ['./basket.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AdminBasketComponent implements OnInit {
constructor(
public adminDataService: AdminDataService,
public adminBasketDataService: AdminBasketDataService,
private route: ActivatedRoute,
private snackBar: MatSnackBar,
private cdr: ChangeDetectorRef,
) {}
basket: AdminBasket
dataSource: MatTableDataSource<object>
@ViewChild(MatPaginator, {static: true}) paginator: MatPaginator
@ViewChild(MatSort, {static: true}) sort: MatSort
selection = new SelectionModel(true, [])
unlockDeleteFormGroup: UntypedFormGroup = new UntypedFormGroup({})
columns: string[] = [
'name',
'status',
'time',
'store',
'project',
'surveyor',
'equipment',
'import',
'delete',
]
filterText: string
ngOnInit() {
this.route.data.subscribe(
(basket: object) => {
this.basket = basket['basket']
this.dataSource = new MatTableDataSource(this.basket.files)
this.dataSource.sort = this.sort
this.dataSource.paginator = this.paginator
this.cdr.markForCheck()
}
)
this.unlockDeleteFormGroup = new UntypedFormGroup({
'canDelete': new UntypedFormControl(),
})
}
getColumns() {
return this.columns.filter(
col => this.basket.columns.indexOf(col) != -1
)
}
applyFilter() {
this.dataSource.filter = this.filterText.trim().toLowerCase()
}
download(item: AdminBasketFile) {
window.open('/download/basket/' + this.basket.name + '/' + item.id + '/' + item.name)
}
importItem(item: AdminBasketFile, dryRun: boolean) {
this.adminBasketDataService.importItem(this.basket.name, item.id, dryRun).subscribe(
resp => {
this.basket.files.find(row => row.id == item.id).time = new Date(resp.time)
this.snackBar.openFromComponent(HtmlSnackbarComponent, {
data: resp,
//duration: 3000
})
this.cdr.markForCheck()
}
)
}
deleteItem(item: AdminBasketFile) {
this.adminBasketDataService.deleteItem(this.basket.name, item.id).subscribe(
id => {
let dsi = this.dataSource.data.findIndex(fi => fi['id'] == id)
this.dataSource.data.splice(dsi, 1)
// Force Angular change detection (??)
this.dataSource.data = this.dataSource.data
this.cdr.markForCheck()
}
)
}
isDate(val: any) {
return val instanceof Date && isFinite(<any>val)
}
}

View file

@ -0,0 +1,223 @@
import { Injectable } from '@angular/core'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'
import { Apollo, gql } from 'apollo-angular'
import { Project } from '../admin-data.service'
export class AdminBasketFile {
constructor(
public id: number,
public dir: string,
public name: string,
public url: string,
public md5: string,
public time: Date,
public comment: string,
public status: string,
public store: string,
public project: string,
public surveyor: string,
public equipment: string,
public import_result?: string,
) {}
}
export class AdminBasket {
constructor(
public name: string,
public files?: AdminBasketFile[],
public columns?: string[],
public uploadFields?: string[],
public projects?: Project[],
) {}
}
export class BasketImportResult {
constructor(
public time: Date,
public message: string,
public details?: string,
) {}
}
export class AdminBasketUploadFieldData {
constructor(
public stores: string[],
public statuses: string[],
public projects: string[],
public surveyors: string[],
public equipments: string[],
) {}
}
const getAdminBasketsQuery = gql`
query admin_baskets {
admin_baskets {
name
}
}
`
const getAdminBasketUploadFieldDataQuery = gql`
query admin_basket_upload_field_data {
admin_basket_upload_field_data {
store
status
project
surveyor
equipment
}
}
`
const getAdminBasketQuery = gql`
query admin_basket ($name: String!) {
admin_basket (name: $name) {
name
files {
id
name
dir
url
md5
time
comment
status
store
project
surveyor
equipment
}
columns
uploadFields
projects
}
}
`
const deleteAdminBasketItemMutation = gql`
mutation deleteBasketItem ($basket: String!, $id: Int!) {
deleteBasketItem (basket: $basket, id: $id) {
result
}
}
`
const importAdminBasketItemMutation = gql`
mutation importBasketItem ($basket: String!, $id: Int!, $dryRun: Boolean) {
importBasketItem (basket: $basket, id: $id, dryRun: $dryRun) {
result {
message
time
details
}
}
}
`
@Injectable()
export class AdminBasketDataService {
constructor(
private apollo: Apollo,
) {}
getBaskets(): Observable<AdminBasket[]> {
// Get the list a basket names
return this.apollo.query({
query: getAdminBasketsQuery,
}).pipe(map(
res => res['data']['admin_baskets'].map(
(data: object) => new AdminBasket(
data['name'],
)
)
))
}
getBasketUploadFieldData(): Observable<AdminBasketUploadFieldData> {
// Get the list a basket names
return this.apollo.query({
query: getAdminBasketUploadFieldDataQuery,
}).pipe(map(
res => res['data']['admin_basket_upload_field_data'].map(
(data: object) => new AdminBasketUploadFieldData(
data['store'],
data['status'],
data['project'],
data['surveyor'],
data['equipment'],
)
)
))
}
getBasket(name: string): Observable<AdminBasket> {
// Get all info and content of a basket
return this.apollo.query({
query: getAdminBasketQuery,
variables: {
name: name
}
}).pipe(map(
res => {
let data = res['data']['admin_basket']
return new AdminBasket(
data['name'],
data['files'].map(file => new AdminBasketFile(
file['id'],
file['dir'],
file['name'],
file['url'],
file['md5'],
new Date(file['time']),
file['comment'],
file['status'],
file['store'],
file['project'],
file['surveyor'],
file['equipment'],
)),
data['columns'],
data['uploadFields'],
// XXX: the proejct id isn't actually used in the UI,
// but required in class definition
data['projects'] && data['projects'].map(
(projectName: string) => new Project(undefined, projectName)
),
)
}
))
}
importItem(basket: string, id: number, dryRun: boolean=false): Observable<BasketImportResult> {
return this.apollo.mutate({
mutation: importAdminBasketItemMutation,
variables: {
basket: basket,
id: id,
dryRun: dryRun
}
}).pipe(map(
resp => new BasketImportResult(
resp['data']['importBasketItem']['result']['time'],
resp['data']['importBasketItem']['result']['message'],
JSON.parse(resp['data']['importBasketItem']['result']['details']),
)
))
}
deleteItem(basket: string, id: number) {
return this.apollo.mutate({
mutation: deleteAdminBasketItemMutation,
variables: {
basket: basket,
id: id,
}
}).pipe(map(
resp => resp['data']['deleteBasketItem']['result']
))
}
}