diff --git a/.gitignore b/.gitignore index 00ffeb6..7711c37 100644 --- a/.gitignore +++ b/.gitignore @@ -397,3 +397,7 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml + +# Cypress test output +src/ClientApp/cypress/screenshot +src/ClientApp/cypress/downloads diff --git a/docs/articles/einstiegsseite.md b/docs/articles/einstiegsseite.md index e0879a4..84c6b60 100644 --- a/docs/articles/einstiegsseite.md +++ b/docs/articles/einstiegsseite.md @@ -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) @@ -12,7 +15,9 @@ 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 @@ -20,3 +25,7 @@ Die Suchmaske besteht aus fünf Eingabefeldern nach denen die Bohrungen bzw. Sta * 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. diff --git a/docs/images/file-download-icon.png b/docs/images/file-download-icon.png new file mode 100644 index 0000000..2c291d4 Binary files /dev/null and b/docs/images/file-download-icon.png differ diff --git a/src/ClientApp/cypress/fixtures/data_export.csv b/src/ClientApp/cypress/fixtures/data_export.csv new file mode 100644 index 0000000..999490a --- /dev/null +++ b/src/ClientApp/cypress/fixtures/data_export.csv @@ -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 diff --git a/src/ClientApp/cypress/integration/layout.spec.js b/src/ClientApp/cypress/integration/layout.spec.js new file mode 100644 index 0000000..811e037 --- /dev/null +++ b/src/ClientApp/cypress/integration/layout.spec.js @@ -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"); + }); +}); diff --git a/src/ClientApp/src/components/Layout.js b/src/ClientApp/src/components/Layout.js index a853f89..061ddb7 100644 --- a/src/ClientApp/src/components/Layout.js +++ b/src/ClientApp/src/components/Layout.js @@ -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"; @@ -94,6 +95,12 @@ export function Layout(props) { + + + + + + {props.children} diff --git a/src/ClientApp/src/setupProxy.js b/src/ClientApp/src/setupProxy.js index f6f88d3..2eb805b 100644 --- a/src/ClientApp/src/setupProxy.js +++ b/src/ClientApp/src/setupProxy.js @@ -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, { diff --git a/src/Controllers/ExportController.cs b/src/Controllers/ExportController.cs new file mode 100644 index 0000000..4b32b90 --- /dev/null +++ b/src/Controllers/ExportController.cs @@ -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; + } + + /// + /// 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. + /// + [HttpGet] + public async Task 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); + } +} diff --git a/tests/ExportControllerTest.cs b/tests/ExportControllerTest.cs new file mode 100644 index 0000000..9c3b5c4 --- /dev/null +++ b/tests/ExportControllerTest.cs @@ -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); + } +}