Skip to content

Commit

Permalink
Export data_export db view into a comma-separated file format (#57)
Browse files Browse the repository at this point in the history
* Export data_export view as comma-separated file format

* Format document

* Add documentation

* Add Content-Disposition header

* Add UI test

* Reword documentation

Co-authored-by: Daniel Jovanovic <[email protected]>

* Reword documentation

Co-authored-by: Daniel Jovanovic <[email protected]>

* Fix expectation

* Fix download path

Co-authored-by: Daniel Jovanovic <[email protected]>
  • Loading branch information
flenny and danjov authored Jul 5, 2022
1 parent 35b604f commit e085bb1
Show file tree
Hide file tree
Showing 9 changed files with 115 additions and 1 deletion.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -397,3 +397,7 @@ FodyWeavers.xsd

# JetBrains Rider
*.sln.iml

# Cypress test output
src/ClientApp/cypress/screenshot
src/ClientApp/cypress/downloads
9 changes: 9 additions & 0 deletions docs/articles/einstiegsseite.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# Einstiegsseite

Die Einstiegsseite besteht aus einer Karte und einer Suchmaske.

## Karte

Zu Beginn zeigt die Karte alle Bohrungen im Kanton Solothurn an. Um in der Karte zu navigieren stehen die folgenden Werkzeuge zur Verfügung:

* Zoom In / Zoom Out ![Zoom In / Zoom Out Icon](../images/zoom-icon.png)
* Ansicht gesamter Kanton ![Ansicht gesamter Kanton Icon](../images/all-out-icon.png)
* Zurück zur letzten Ansicht ![Zurück zur letzten Ansicht Icon](../images/back-icon.png)
Expand All @@ -12,11 +15,17 @@ Das Verschieben des Kartenausschnitts geschieht durch Klicken und Ziehen im Kart
Durch Klicken auf eine Bohrung werden die wichtigsten Informationen in einem Popup-Fenster angezeigt, ohne dass ein Werkzeug ausgewählt werden muss.

## Suchmaske

Die Suchmaske besteht aus fünf Eingabefeldern nach denen die Bohrungen bzw. Standorte durchsucht werden können:

* Gemeinde
* Grundbuchnummer(n)
* Bezeichnung
* Erstellungsdatum
* Mutationsdatum

Die gefundenen Standorte werden in einer Tabelle unterhalb der Suchmaske angezeigt. Im Kartenfenster werden nur noch die Bohrungen der gefundenen Standorte angezeigt, der Kartenausschnitt passt sich automatisch den Suchergebnissen an.

## Daten exportieren

Unter dem Menüpunkt _Daten exportieren_ ![Daten exportieren](../images/file-download-icon.png) können die Daten in eine Textdatei mit kommagetrennten Werten (CSV) exportiert und heruntergeladen werden. Die exportierten Felder entsprechen denen der Datenbank View _bohrung.data_export_. Die in UTF-8 codierten Daten können anschliessend, bspw. in Excel unter _Daten_ -> _Aus Text/CSV_ geladen werden.
Binary file added docs/images/file-download-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/ClientApp/cypress/fixtures/data_export.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
standort.standort_id,standort.bezeichnung,standort.bemerkung,standort.anzbohrloch,standort.gbnummer,standort.freigabe_afu,standort.afu_usr,standort.afu_date,bohrung.bohrung_id,bohrung.bezeichnung,bohrung.bemerkung,bohrung.datum,bohrung.durchmesserbohrloch,bohrung.ablenkung,bohrung.quali,bohrung.qualibem,bohrung.quelleref,bohrung.h_quali,bohrung.X,bohrung.Y,bohrprofil.bohrprofil_id,bohrprofil.datum,bohrprofil.bemerkung,bohrprofil.kote,bohrprofil.endtiefe,bohrprofil.tektonik,bohrprofil.fmfelso,bohrprofil.fmeto,bohrprofil.quali,bohrprofil.qualibem,bohrprofil.h_quali,bohrprofil.h_tektonik,bohrprofil.h_fmeto,bohrprofil.h_fmfelso,schicht.schicht_id,schicht.tiefe,schicht.quali,schicht.qualibem,schicht.bemerkung,schicht.h_quali,codeschicht.kurztext,codeschicht.text,codeschicht.sort,vorkommnis.vorkommnis_id,vorkommnis.typ,vorkommnis.tiefe,vorkommnis.bemerkung,vorkommnis.quali,vorkommnis.qualibem,vorkommnis.h_quali,vorkommnis.h_typ
30001,Small Metal Computer,Azerbaijan,0,d1hl6younua1ltu26brynqj7ovxf4cyc02m5p3bi,f,Angelo Kägi,2021-02-15 21:50:00.853725,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
30002,Awesome Frozen Bacon,Ethiopia,3,8izqjogj6jwh7u87fh32al5hh96t5pz5p86xznf1,f,Christiane Mayer,2021-09-29 06:36:38.077446,47891,Unbranded Wooden Car,Enterprise-wide mobile infrastructure,2021-10-21,27851,9,219,It only works when I'm Chad.,Hürlimann LLC,3,2599865,1236124,52653,2021-04-24,plum,29064,27003,122,45,156,172,,12,10,5,5,,,,,,,,,,91474,37,0.059433408,feed,397,,3,2
14 changes: 14 additions & 0 deletions src/ClientApp/cypress/integration/layout.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
describe("General app tests", () => {
it("Downloads the comma-separated (CSV) file", () => {
cy.intercept("/export", (request) => {
request.reply({
headers: { "Content-Disposition": "attachment; filename=data_export.csv" },
fixture: "data_export.csv",
});
});

cy.visit("/");
cy.get('a[href*="/export"]').click();
cy.readFile("cypress/downloads/data_export.csv", "utf8").should("exist").should("contains", "Angelo Kägi");
});
});
7 changes: 7 additions & 0 deletions src/ClientApp/src/components/Layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import ListItemText from "@mui/material/ListItemText";
import HomeIcon from "@mui/icons-material/Home";
import GroupIcon from "@mui/icons-material/Group";
import InfoIcon from "@mui/icons-material/Info";
import FileDownloadIcon from "@mui/icons-material/FileDownload";
import { Footer } from "./Footer";

import { AppBar } from "./AppBar";
Expand Down Expand Up @@ -94,6 +95,12 @@ export function Layout(props) {
</ListItemIcon>
<ListItemText primary="Hilfe" />
</ListItemButton>
<ListItemButton component="a" target="_blank" href="/export">
<ListItemIcon>
<FileDownloadIcon />
</ListItemIcon>
<ListItemText primary="Daten exportieren" />
</ListItemButton>
</List>
</Drawer>
<React.Fragment>{props.children}</React.Fragment>
Expand Down
2 changes: 1 addition & 1 deletion src/ClientApp/src/setupProxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const target = env.ASPNETCORE_HTTPS_PORT
? env.ASPNETCORE_URLS.split(";")[0]
: "http://localhost:50704";

const context = ["/version", "/standort"];
const context = ["/version", "/standort", "/export"];

module.exports = function (app) {
const appProxy = createProxyMiddleware(context, {
Expand Down
36 changes: 36 additions & 0 deletions src/Controllers/ExportController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Npgsql;
using System.Text;

namespace EWS;

[ApiController]
[Route("[controller]")]
public class ExportController : ControllerBase
{
private readonly EwsContext context;

public ExportController(EwsContext context)
{
this.context = context;
}

/// <summary>
/// Asynchronously exports the bohrung.data_export database view into a comma-separated file format (CSV).
/// The first record represents the header containing the specified list of field names.
/// The output is created by the PostgreSQL COPY command.
/// </summary>
[HttpGet]
public async Task<ContentResult> GetAsync(CancellationToken cancellationToken)
{
using var connection = new NpgsqlConnection(context.Database.GetDbConnection().ConnectionString);
await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
using var reader = await connection.BeginTextExportAsync(
"COPY (SELECT * FROM bohrung.data_export) TO STDOUT WITH DELIMITER ',' CSV HEADER;",
cancellationToken).ConfigureAwait(false);

Response.Headers.ContentDisposition = "attachment; filename=data_export.csv";
return Content(await reader.ReadToEndAsync().ConfigureAwait(false), "text/csv", Encoding.UTF8);
}
}
41 changes: 41 additions & 0 deletions tests/ExportControllerTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace EWS;

[TestClass]
public class ExportControllerTest
{
private EwsContext context;

[TestInitialize]
public void TestInitialize() => context = ContextFactory.CreateContext();

[TestCleanup]
public void TestCleanup() => context.Dispose();

[TestMethod]
public async Task GetAsync()
{
var httpContext = new DefaultHttpContext();
var controller = new ExportController(context);
controller.ControllerContext.HttpContext = httpContext;
var response = await controller.GetAsync(CancellationToken.None).ConfigureAwait(false);

Assert.IsInstanceOfType(response, typeof(ContentResult));
Assert.AreEqual("text/csv; charset=utf-8", response.ContentType);
Assert.AreEqual("attachment; filename=data_export.csv", httpContext.Response.Headers["Content-Disposition"].ToString());

var expectedHeader = "standort.standort_id,standort.bezeichnung,standort.bemerkung,standort.anzbohrloch,standort.gbnummer,standort.freigabe_afu,standort.afu_usr,standort.afu_date,bohrung.bohrung_id,bohrung.bezeichnung,bohrung.bemerkung,bohrung.datum,bohrung.durchmesserbohrloch,bohrung.ablenkung,bohrung.quali,bohrung.qualibem,bohrung.quelleref,bohrung.h_quali,bohrung.X,bohrung.Y,bohrprofil.bohrprofil_id,bohrprofil.datum,bohrprofil.bemerkung,bohrprofil.kote,bohrprofil.endtiefe,bohrprofil.tektonik,bohrprofil.fmfelso,bohrprofil.fmeto,bohrprofil.quali,bohrprofil.qualibem,bohrprofil.h_quali,bohrprofil.h_tektonik,bohrprofil.h_fmeto,bohrprofil.h_fmfelso,schicht.schicht_id,schicht.tiefe,schicht.quali,schicht.qualibem,schicht.bemerkung,schicht.h_quali,codeschicht.kurztext,codeschicht.text,codeschicht.sort,vorkommnis.vorkommnis_id,vorkommnis.typ,vorkommnis.tiefe,vorkommnis.bemerkung,vorkommnis.quali,vorkommnis.qualibem,vorkommnis.h_quali,vorkommnis.h_typ";

Assert.AreEqual(expectedHeader, response.Content.Split('\n')[0]);
Assert.AreEqual(31263, response.Content.Split('\n').Length);
}
}

0 comments on commit e085bb1

Please sign in to comment.