Skip to content

Commit

Permalink
Merge pull request #773 from geonetwork/DF/add-csv-support
Browse files Browse the repository at this point in the history
[Datafeeder] Add CSV support
  • Loading branch information
f-necas authored Jun 26, 2024
2 parents 27089d8 + bea1f36 commit 4e89bca
Show file tree
Hide file tree
Showing 29 changed files with 646 additions and 27 deletions.
6 changes: 6 additions & 0 deletions apps/datafeeder/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { PublicationLockGuard } from './router/publication-lock.guard'
import { PublicationStatusGuard } from './router/publication-status.guard'
import { UploadProgressGuard } from './router/upload-progress.guard'
import { UploadStatusGuard } from './router/upload-status.guard'
import { DatasetValidationCsvPageComponent } from './presentation/pages/dataset-validation-csv-page/dataset-validation-csv-page'

const routes: Routes = [
{ path: '', component: UploadDataPageComponent },
Expand All @@ -24,6 +25,11 @@ const routes: Routes = [
component: DatasetValidationPageComponent,
canActivate: [UploadStatusGuard, PublicationLockGuard],
},
{
path: ':id/validation-csv',
component: DatasetValidationCsvPageComponent,
canActivate: [UploadStatusGuard, PublicationLockGuard],
},
{
path: ':id/step/:stepId',
component: FormsPageComponent,
Expand Down
2 changes: 2 additions & 0 deletions apps/datafeeder/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { DATAFEEDER_STATE_KEY, reducer } from './store/datafeeder.reducer'
import { FeatureAuthModule } from '@geonetwork-ui/feature/auth'
import { MatIconModule } from '@angular/material/icon'
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
import { DatasetValidationCsvPageComponent } from './presentation/pages/dataset-validation-csv-page/dataset-validation-csv-page'

export function apiConfigurationFactory() {
return new Configuration({
Expand All @@ -54,6 +55,7 @@ export function apiConfigurationFactory() {
UploadDataRulesComponent,
AnalysisProgressPageComponent,
DatasetValidationPageComponent,
DatasetValidationCsvPageComponent,
DataImportValidationMapPanelComponent,
UploadDataErrorDialogComponent,
UploadDataBackgroundComponent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,11 @@ import { Component, Input } from '@angular/core'
})
export class UploadDataRulesComponent {
@Input() maxFileSizeMb: number
@Input() acceptedFileFormats = ['SHP', 'GeoJSON', 'GeoPackage', 'Spatialite']
@Input() acceptedFileFormats = [
'SHP',
'GeoJSON',
'GeoPackage',
'Spatialite',
'CSV',
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,13 @@ export class UploadDataComponent {
haveRights = false
uploading = false
// Edge use uncommon 'application/x-zip-compressed' mime type
acceptedMimeType = ['.zip', 'application/zip', 'application/x-zip-compressed']
acceptedMimeType = [
'.zip',
'application/zip',
'application/x-zip-compressed',
'text/csv',
'application/csv',
]

@Input() maxFileSizeMb: number
@Output() errors$ = new EventEmitter<UploadDataError>()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
import { NO_ERRORS_SCHEMA } from '@angular/core'
import {
ComponentFixture,
discardPeriodicTasks,
fakeAsync,
flush,
flushMicrotasks,
TestBed,
tick,
} from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { ActivatedRoute, Router } from '@angular/router'
import {
AnalysisStatusEnumApiModel,
Expand All @@ -25,7 +17,7 @@ const jobMock: UploadJobStatusApiModel = {
jobId: JOB_ID,
status: AnalysisStatusEnumApiModel.Done,
progress: 1,
datasets: [{}],
datasets: [{ format: 'SHAPEFILE' }],
}
const jobMockNoDS: UploadJobStatusApiModel = {
jobId: JOB_ID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import { EMPTY, firstValueFrom, Observable, timer } from 'rxjs'
import { DatafeederFacade } from '../../../store/datafeeder.facade'
import { expand, switchMap } from 'rxjs/operators'
import { UploadProgressGuard } from '../../../router/upload-progress.guard'

const { Pending, Analyzing, Done } = AnalysisStatusEnumApiModel

Expand Down Expand Up @@ -51,9 +52,16 @@ export class AnalysisProgressPageComponent implements OnInit {

onJobFinish(job: UploadJobStatusApiModel) {
const done = job.status === Done && job.datasets?.length > 0
this.router.navigate([done ? 'validation' : '/'], {
relativeTo: this.activatedRoute,
queryParams: done ? {} : { error: 'analysis' },
})
this.router.navigate(
[
done
? UploadProgressGuard.getRedirectPage(job.datasets[0].format)
: '/',
],
{
relativeTo: this.activatedRoute,
queryParams: done ? {} : { error: 'analysis' },
}
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
:host {
display: flex;
flex: 1;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<div class="flex-1 flex items-center dataset-validation-bg">
<div class="container mx-auto pt-16 pb-16 w-full h-full flex flex-col">
<div class="text-5xl font-bold" translate>
datafeeder.datasetValidation.title
</div>
<div class="pb-4 pt-4">
<gn-ui-step-bar
[steps]="numberOfSteps"
[currentStep]="1"
type="primary"
></gn-ui-step-bar>
</div>
<div
class="text-2xl font-bold"
translate
[translateParams]="{ number: numOfEntities }"
>
datafeeder.datasetValidation.datasetInformation
</div>

<div class="flex flex-col gap-10 pb-10 pt-5 flex-1">
<div class="flex gap-4 w-full">
<div>
<gn-ui-dropdown-selector
[title]="'datafeeder.validation.csv.delimiter' | translate"
[choices]="delimiterChoices"
(selectValue)="selectDelimiter($event)"
[selected]="csvDelimiter"
[extraBtnClass]="'secondary min-w-full'"
ariaName="search-sort-by"
>
</gn-ui-dropdown-selector>
</div>
<div>
<gn-ui-dropdown-selector
[title]="'datafeeder.validation.csv.quoteChar' | translate"
[choices]="quoteCharChoices"
(selectValue)="selectQuoteChar($event)"
[selected]="quoteChar"
[extraBtnClass]="'secondary min-w-full'"
ariaName="search-sort-by"
>
</gn-ui-dropdown-selector>
</div>
<div>
<gn-ui-dropdown-selector
[title]="'datafeeder.validation.csv.lat.field' | translate"
[choices]="latLngChoices"
(selectValue)="selectLatLng($event, true)"
[selected]="latField"
[extraBtnClass]="'secondary min-w-full' + (latLngValid ? '' : ' !border-red-500')"
ariaName="search-sort-by"
>
</gn-ui-dropdown-selector>
</div>
<div>
<gn-ui-dropdown-selector
[title]="'datafeeder.validation.csv.lng.field' | translate"
[choices]="latLngChoices"
(selectValue)="selectLatLng($event, false)"
[selected]="lngField"
[extraBtnClass]="'secondary min-w-full' + (latLngValid ? '' : ' !border-red-500')"
ariaName="search-sort-by"
>
</gn-ui-dropdown-selector>
</div>
</div>
<div>
<div class="pb-2" translate>
datafeeder.datasetValidationCsv.lineNumbers
</div>
<div
class="relative overflow-x-auto shadow-md sm:rounded-lg border rounded-2xl"
>
<table
class="bg-white w-full table-auto border-separate border-spacing-0"
>
<tbody>
<tr
*ngFor="let row of csvData; let isFirstRow = first"
class="{{isFirstRow ? 'uppercase font-bold' : ''}} rounded-2xl bg-white border-b bg-secondary-white"
>
<td
*ngFor="let value of row; let isOdd = odd; let columnIndex = index;"
class="{{isOdd? 'bg-slate-100' : ''}} {{isFirstRow ? 'border-b' : ''}} text-sm px-2 py-2 border-spacing-0 border-separate max-w-[200px] whitespace-nowrap text-ellipsis overflow-hidden"
[title]="value"
>
{{value}}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="flex items-center justify-between">
<p
class="text-sm"
[innerHTML]="'datafeeder.datasetValidationCsv.explicitLineNumbers' | translate"
></p>
<gn-ui-button
(buttonClick)="submitValidation()"
[disabled]="!isValid()"
type="primary"
extraClass="rounded-full px-20"
>
<span class="uppercase text-white font-bold" translate>
datafeeder.datasetValidation.submitButton
</span>
</gn-ui-button>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { NO_ERRORS_SCHEMA } from '@angular/core'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { ActivatedRoute, Router } from '@angular/router'
import {
AnalysisStatusEnumApiModel,
UploadJobStatusApiModel,
} from '@geonetwork-ui/data-access/datafeeder'
import { of } from 'rxjs'
import { WizardService } from '@geonetwork-ui/feature/editor'
import { DatafeederFacade } from '../../../store/datafeeder.facade'
import { DatasetValidationCsvPageComponent } from './dataset-validation-csv-page'
import { UtilI18nModule } from '@geonetwork-ui/util/i18n'
import { TranslateModule } from '@ngx-translate/core'

const jobMock: UploadJobStatusApiModel = {
jobId: '1234',
status: AnalysisStatusEnumApiModel.Done,
progress: 100,
datasets: [
{
name: 'f_name',
featureCount: 36,
format: 'CSV',
options: {
quoteChar: '"',
csv: 'IlllYXIiLCJNYWtlIiwiTW9kZWwiLCJMZW5ndGgiCiIxOTk3IiwiRm9yZCIsIkUzNTAiLCIyLjM1IgoiMjAwMCIsIk1lcmN1cnkiLCJDb3VnYXIiLCIyLjM4Ig==',
},
},
],
}

const facadeMock = {
upload$: of(jobMock),
}

const wizardServiceMock = {
getConfigurationStepNumber: jest.fn(() => 6),
initialize: jest.fn(),
getWizardFieldData: jest.fn(() => null),
}

const activatedRouteMock = {
params: of({ id: 1 }),
}

const routerMock = {
navigate: jest.fn(),
}

describe('DatasetValidationCsvPageComponent', () => {
let component: DatasetValidationCsvPageComponent
let fixture: ComponentFixture<DatasetValidationCsvPageComponent>

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [DatasetValidationCsvPageComponent],
imports: [UtilI18nModule, TranslateModule.forRoot()],
schemas: [NO_ERRORS_SCHEMA],
providers: [
{
provide: WizardService,
useValue: wizardServiceMock,
},
{
provide: DatafeederFacade,
useValue: facadeMock,
},
{ provide: ActivatedRoute, useValue: activatedRouteMock },
{ provide: Router, useValue: routerMock },
],
}).compileComponents()
})

it('should create', () => {
createComponent()
expect(component).toBeTruthy()
})

describe('Job DONE', () => {
beforeEach(() => {
createComponent()
fixture.detectChanges()
})

it('should create', () => {
expect(component).toBeTruthy()
})

it('should contain the correct csvData', () => {
expect(component.csvData).toEqual([
['Year', 'Make', 'Model', 'Length'],
['1997', 'Ford', 'E350', '2.35'],
['2000', 'Mercury', 'Cougar', '2.38'],
])
})
})

describe('Job ERROR', () => {
beforeEach(() => {
jobMock.status = AnalysisStatusEnumApiModel.Error
createComponent()
})

it('route to validation page', () => {
expect(routerMock.navigate).toHaveBeenCalledWith(['/'], {
relativeTo: activatedRouteMock,
queryParams: { error: 'analysis' },
})
})
})

function createComponent() {
fixture = TestBed.createComponent(DatasetValidationCsvPageComponent)
component = fixture.componentInstance
fixture.detectChanges()
}
})
Loading

0 comments on commit 4e89bca

Please sign in to comment.