changeset 1365:da7ee82ad5d6

merge
author Fadi Abbud <fadi.abbud@intevation.de>
date Tue, 27 Nov 2018 09:03:27 +0100
parents 0c5cbbafbd94 (current diff) 3ff916e853d4 (diff)
children a25a4d4a3e6e 7550a4707863
files client/src/components/map/contextbox/Bottlenecks.vue client/src/components/map/contextbox/Staging.vue client/src/store/imports.js client/src/store/index.js
diffstat 22 files changed, 277 insertions(+), 211 deletions(-) [+]
line wrap: on
line diff
--- a/client/src/components/map/contextbox/ImportSoundingresults.vue	Mon Nov 26 12:44:52 2018 +0100
+++ b/client/src/components/map/contextbox/ImportSoundingresults.vue	Tue Nov 27 09:03:27 2018 +0100
@@ -1,87 +1,100 @@
 <template>
-    <div>
-        <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center">
-            <font-awesome-icon icon="upload" class="mr-2"></font-awesome-icon>Import Soundingresults
-        </h6>
-        <div v-if="editState" class="ml-auto mr-auto mt-4 w-95">
-            <div class="d-flex flex-row input-group mb-4">
-                <div class="offset-r">
-                    <label for="bottleneck" class="label-text" id="bottlenecklabel">Bottleneck</label>
-                </div>
-                <input
-                    id="bottleneck"
-                    type="text"
-                    class="form-control"
-                    placeholder="Name of Bottleneck"
-                    aria-label="bottleneck"
-                    aria-describedby="bottlenecklabel"
-                    v-model="bottleneck"
-                >
-            </div>
-            <div class="d-flex flex-row input-group mb-4">
-                <div class="offset-r">
-                    <label class="label-text" for="importdate" id="importdatelabel">Date</label>
-                </div>
-                <input
-                    id="importdate"
-                    type="date"
-                    class="form-control"
-                    placeholder="Date of import"
-                    aria-label="bottleneck"
-                    aria-describedby="bottlenecklabel"
-                    v-model="importDate"
-                >
-                <div class="offset-r">
-                    <label
-                        class="label-text w-100 depthreferencelabel"
-                        for="depthreference"
-                    >Depthreference</label>
-                </div>
-                <select v-model="depthReference" class="custom-select" id="depthreference">
-                    <option
-                        v-for="option in this.$options.depthReferenceOptions"
-                        :key="option"
-                    >{{option}}</option>
-                </select>
-            </div>
-            <div class="text-left">
-                <small v-for="(message, index) in messages" :key="index">{{message}}</small>
-            </div>
+  <div>
+    <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center">
+      <font-awesome-icon icon="upload" class="mr-2"></font-awesome-icon>Import
+      Soundingresults
+    </h6>
+    <div v-if="editState" class="ml-auto mr-auto mt-4 w-95">
+      <div class="d-flex flex-row input-group mb-4">
+        <div class="offset-r">
+          <label for="bottleneck" class="label-text" id="bottlenecklabel">Bottleneck</label>
+        </div>
+        <div class="d-flex flex-column">
+          <select v-model="bottleneck" class="custom-select">
+            <option v-for="bottleneck in availableBottlenecks" :key="bottleneck">{{bottleneck}}</option>
+          </select>
+          <span class="text-left text-danger">
+            <small v-if="!bottleneck">Please select a bottleneck</small>
+          </span>
+        </div>
+      </div>
+      <div class="d-flex flex-row input-group mb-4">
+        <div class="offset-r">
+          <label class="label-text" for="importdate" id="importdatelabel">Date</label>
+        </div>
+        <div class="d-flex flex-column">
+          <input
+            id="importdate"
+            type="date"
+            class="form-control"
+            placeholder="Date of import"
+            aria-label="bottleneck"
+            aria-describedby="bottlenecklabel"
+            v-model="importDate"
+          >
+          <span class="text-left text-danger">
+            <small v-if="!importDate">Please enter a date</small>
+          </span>
+        </div>
+        <div class="offset-r">
+          <label class="label-text w-100 depthreferencelabel" for="depthreference">Depthreference</label>
+        </div>
+        <div class="d-flex flex-column">
+          <select v-model="depthReference" class="custom-select" id="depthreference">
+            <option v-for="option in this.$options.depthReferenceOptions" :key="option">{{ option }}</option>
+          </select>
+          <span class="text-left text-danger">
+            <small v-if="!depthReference">Please enter a reference</small>
+          </span>
         </div>
-        <div class="w-95 ml-auto mr-auto mt-4 mb-4">
-            <div v-if="uploadState" class="d-flex flex-row input-group mb-4">
-                <div class="custom-file">
-                    <input
-                        type="file"
-                        @change="fileSelected"
-                        class="custom-file-input"
-                        id="uploadFile"
-                    >
-                    <label class="custom-file-label" for="uploadFile">{{uploadLabel}}</label>
-                </div>
-            </div>
-            <div class="buttons text-right">
-                <a
-                    v-if="editState"
-                    download="meta.json"
-                    :href="dataLink "
-                    class="btn btn-outline-info pull-left"
-                >Download Meta.json</a>
-                <button
-                    v-if="editState"
-                    @click="deleteTempData"
-                    class="btn btn-danger"
-                    type="button"
-                >Cancel Upload</button>
-                <button
-                    :disabled="disableUpload"
-                    @click="submit"
-                    class="btn btn-info"
-                    type="button"
-                >{{uploadState?"Upload":"Confirm"}}</button>
-            </div>
+      </div>
+      <div class="text-left">
+        <small v-for="(message, index) in messages" :key="index">
+          {{
+          message
+          }}
+        </small>
+      </div>
+    </div>
+    <div class="w-95 ml-auto mr-auto mt-4 mb-4">
+      <div v-if="uploadState" class="d-flex flex-row input-group mb-4">
+        <div class="custom-file">
+          <input
+            accept=".zip"
+            type="file"
+            @change="fileSelected"
+            class="custom-file-input"
+            id="uploadFile"
+          >
+          <label class="custom-file-label" for="uploadFile">
+            {{
+            uploadLabel
+            }}
+          </label>
         </div>
+      </div>
+      <div class="buttons text-right">
+        <a
+          v-if="editState"
+          download="meta.json"
+          :href="dataLink"
+          class="btn btn-outline-info pull-left"
+        >Download Meta.json</a>
+        <button
+          v-if="editState"
+          @click="deleteTempData"
+          class="btn btn-danger"
+          type="button"
+        >Cancel Upload</button>
+        <button
+          :disabled="disableUploadButton"
+          @click="submit"
+          class="btn btn-info"
+          type="button"
+        >{{ uploadState ? "Upload" : "Confirm" }}</button>
+      </div>
     </div>
+  </div>
 </template>
 
 <script>
@@ -91,7 +104,7 @@
  * SPDX-License-Identifier: AGPL-3.0-or-later
  * License-Filename: LICENSES/AGPL-3.0.txt
  *
- * Copyright (C) 2018 by via donau 
+ * Copyright (C) 2018 by via donau
  *   – Österreichische Wasserstraßen-Gesellschaft mbH
  * Software engineering by Intevation GmbH
  *
@@ -101,6 +114,7 @@
  */
 import { HTTP } from "../../../lib/http";
 import { displayError, displayInfo } from "../../../lib/errors.js";
+import { mapState } from "vuex";
 
 const defaultLabel = "Choose .zip-file";
 const IMPORTSTATE = { UPLOAD: "UPLOAD", EDIT: "EDIT" };
@@ -173,12 +187,14 @@
         }
       })
         .then(response => {
-          const { bottleneck, date } = response.data.meta;
-          const depthReference = response.data.meta["depth-reference"];
+          if (response.data.meta) {
+            const { bottleneck, date } = response.data.meta;
+            const depthReference = response.data.meta["depth-reference"];
+            this.bottleneck = bottleneck;
+            this.depthReference = depthReference;
+            this.importDate = new Date(date).toISOString().split("T")[0];
+          }
           this.importState = IMPORTSTATE.EDIT;
-          this.bottleneck = bottleneck;
-          this.depthReference = depthReference;
-          this.importDate = new Date(date).toISOString().split("T")[0];
           this.token = response.data.token;
           this.messages = response.data.messages;
         })
@@ -222,7 +238,26 @@
         });
     }
   },
+  mounted() {
+    this.$store.dispatch("bottlenecks/loadBottlenecks");
+  },
+  watch: {
+    showContextBox() {
+      if (!this.showContextBox && this.token) this.deleteTempData();
+    }
+  },
   computed: {
+    ...mapState("application", ["showContextBox"]),
+    ...mapState("bottlenecks", ["bottlenecks"]),
+    disableUploadButton() {
+      if (this.importState === IMPORTSTATE.UPLOAD) return this.disableUpload;
+      if (!this.bottleneck || !this.importDate || !this.depthReference)
+        return true;
+      return this.disableUpload;
+    },
+    availableBottlenecks() {
+      return this.bottlenecks.map(x => x.properties.name);
+    },
     editState() {
       return this.importState === IMPORTSTATE.EDIT;
     },
--- a/client/src/components/map/contextbox/Staging.vue	Mon Nov 26 12:44:52 2018 +0100
+++ b/client/src/components/map/contextbox/Staging.vue	Tue Nov 27 09:03:27 2018 +0100
@@ -6,10 +6,10 @@
     <table class="table mb-0">
       <thead>
         <tr>
-          <th>Name</th>
+          <th>id</th>
           <th>Datatype</th>
           <th>Importdate</th>
-          <th>ImportID</th>
+          <th>Username</th>
           <th>&nbsp;</th>
           <th>&nbsp;</th>
         </tr>
@@ -17,11 +17,11 @@
       <tbody v-if="filteredData.length">
         <tr v-for="data in filteredData" :key="data.id">
           <td>
-            <a @click="zoomTo(data.location)" href="#">{{ data.name }}</a>
+            <a @click="zoomTo(data.location)" href="#">{{ data.id }}</a>
           </td>
-          <td>{{ data.type }}</td>
-          <td>{{ data.date }}</td>
-          <td>{{ data.importID }}</td>
+          <td>{{ data.kind }}</td>
+          <td>{{ data.enqueued }}</td>
+          <td>{{ data.user }}</td>
           <td>
             <button
               @click="toggleApproval(data.id, $options.STATES.APPROVED)"
@@ -68,56 +68,12 @@
  * Markus Kottländer <markus@intevation.de>
  */
 import { mapState } from "vuex";
-
+import { STATES } from "../../../store/imports.js";
 import { displayError, displayInfo } from "../../../lib/errors.js";
 
 export default {
-  STATES: {
-    NEEDSAPPROVAL: "NEEDSAPPROVAL",
-    APPROVED: "APPROVED",
-    REJECTED: "REJECTED"
-  },
   data() {
-    return {
-      demodata: [
-        {
-          id: 1,
-          name: "B1",
-          date: "2018-11-19 10:23",
-          location: [16.5364, 48.1471],
-          status: "NEEDSAPPROVAL",
-          importID: "123456789",
-          type: "bottleneck"
-        },
-        {
-          id: 2,
-          name: "B2",
-          date: "2018-11-19 10:24",
-          location: [16.5364, 48.1472],
-          status: "NEEDSAPPROVAL",
-          importID: "123456789",
-          type: "bottleneck"
-        },
-        {
-          id: 3,
-          name: "s1",
-          date: "2018-11-13 10:25",
-          location: [16.5364, 48.1473],
-          status: "NEEDSAPPROVAL",
-          importID: "987654321",
-          type: "soundingresult"
-        },
-        {
-          id: 4,
-          name: "s2",
-          date: "2018-11-13 10:26",
-          location: [16.5364, 48.1474],
-          status: "NEEDSAPPROVAL",
-          importID: "987654321",
-          type: "soundingresult"
-        }
-      ]
-    };
+    return {};
   },
   mounted() {
     this.$store.dispatch("imports/getStaging").catch(error => {
@@ -130,46 +86,22 @@
   },
   computed: {
     ...mapState("application", ["searchQuery"]),
+    ...mapState("imports", ["staging"]),
     filteredData() {
-      return this.demodata.filter(data => {
-        const nameFound = data.name
-          .toLowerCase()
-          .includes(this.searchQuery.toLowerCase());
-        const dateFound = data.date
-          .toLowerCase()
-          .includes(this.searchQuery.toLowerCase());
-        const locationFound = data.location.find(coord => {
-          return coord
-            .toString()
-            .toLowerCase()
-            .includes(this.searchQuery.toLowerCase());
-        });
-        const statusFound = data.status
-          .toLowerCase()
-          .includes(this.searchQuery.toLowerCase());
-        const importIDFound = data.importID
-          .toLowerCase()
-          .includes(this.searchQuery.toLowerCase());
-        const typeFound = data.type
-          .toLowerCase()
-          .includes(this.searchQuery.toLowerCase());
-
-        return (
-          nameFound ||
-          dateFound ||
-          locationFound ||
-          statusFound ||
-          importIDFound ||
-          typeFound
+      return this.staging.filter(data => {
+        const result = [data.id + "", data.enqueued, data.kind, data.user].some(
+          x => x.toLowerCase().includes(this.searchQuery.toLowerCase())
         );
+        return result;
       });
     }
   },
+  STATES: STATES,
   methods: {
     confirmReview() {
-      const message = this.demodata
+      const message = this.staging
         .map(x => {
-          return x.name + ": " + x.status;
+          return x.id + ": " + x.status;
         })
         .join("\n");
       displayInfo({
@@ -178,15 +110,16 @@
       });
     },
     needsApproval(item) {
-      return item.status === this.$options.STATES.NEEDSAPPROVAL;
+      return item.status === STATES.NEEDSAPPROVAL;
     },
     isRejected(item) {
-      return item.status === this.$options.STATES.REJECTED;
+      return item.status === STATES.REJECTED;
     },
     isApproved(item) {
-      return item.status === this.$options.STATES.APPROVED;
+      return item.status === STATES.APPROVED;
     },
     zoomTo(coordinates) {
+      if (!coordinates) return;
       this.$store.commit("map/moveMap", {
         coordinates: coordinates,
         zoom: 17,
@@ -194,14 +127,10 @@
       });
     },
     toggleApproval(id, newStatus) {
-      const stagedResult = this.demodata.find(e => {
-        return e.id === id;
+      this.$store.commit("imports/toggleApproval", {
+        id: id,
+        newStatus: newStatus
       });
-      if (stagedResult.status === newStatus) {
-        stagedResult.status = this.$options.STATES.NEEDSAPPROVAL;
-      } else {
-        stagedResult.status = newStatus;
-      }
     }
   }
 };
--- a/client/src/store/imports.js	Mon Nov 26 12:44:52 2018 +0100
+++ b/client/src/store/imports.js	Tue Nov 27 09:03:27 2018 +0100
@@ -30,7 +30,7 @@
   };
 };
 
-export default {
+const imports = {
   init,
   namespaced: true,
   state: init(),
@@ -39,19 +39,21 @@
       state.imports = imports;
     },
     setStaging: (state, staging) => {
-      state.staging = staging;
+      const enriched = staging.map(x => {
+        return { ...x, status: STATES.NEEDSAPPROVAL };
+      });
+      state.staging = enriched;
     },
 
     toggleApproval: (state, change) => {
-      throw "Not implemented!";
-      const { id, newState } = change;
-      const stagedResult = this.state.staging.find(e => {
+      const { id, newStatus } = change;
+      const stagedResult = state.staging.find(e => {
         return e.id === id;
       });
-      if (stagedResult.status === newState) {
-        stagedResult.status = this.$options.STATES.NEEDSAPPROVAL;
+      if (stagedResult.status === newStatus) {
+        stagedResult.status = STATES.NEEDSAPPROVAL;
       } else {
-        stagedResult.status = newState;
+        stagedResult.status = newStatus;
       }
     }
   },
@@ -101,3 +103,5 @@
     }
   }
 };
+
+export { imports, STATES };
--- a/client/src/store/index.js	Mon Nov 26 12:44:52 2018 +0100
+++ b/client/src/store/index.js	Tue Nov 27 09:03:27 2018 +0100
@@ -21,7 +21,7 @@
 import map from "./map";
 import fairwayprofile from "./fairway";
 import bottlenecks from "./bottlenecks";
-import imports from "./imports";
+import { imports } from "./imports";
 
 Vue.use(Vuex);
 
--- a/docker/Dockerfile.backend	Mon Nov 26 12:44:52 2018 +0100
+++ b/docker/Dockerfile.backend	Tue Nov 27 09:03:27 2018 +0100
@@ -1,5 +1,7 @@
 FROM ubuntu:bionic
 LABEL authors="tom.gottfried@intevation.de"
+LABEL description="Contains software from gemma, for right holders and\
+ licensing infos, see https://hg.intevation.de/gemma ."
 
 RUN sed -i 's/\(deb.*\)$/\1 universe/' /etc/apt/sources.list
 
--- a/docker/Dockerfile.db	Mon Nov 26 12:44:52 2018 +0100
+++ b/docker/Dockerfile.db	Tue Nov 27 09:03:27 2018 +0100
@@ -1,5 +1,7 @@
 FROM ubuntu:bionic
 LABEL authors="tom.gottfried@intevation.de"
+LABEL description="Contains software from gemma, for right holders and\
+ licensing infos, see https://hg.intevation.de/gemma ."
 
 RUN apt-get update &&\
     apt-get -y install --no-install-recommends curl gnupg
--- a/docker/Dockerfile.geoserv	Mon Nov 26 12:44:52 2018 +0100
+++ b/docker/Dockerfile.geoserv	Tue Nov 27 09:03:27 2018 +0100
@@ -1,5 +1,7 @@
 FROM ubuntu:bionic
 LABEL authors="tom@intevation.de"
+LABEL description="Contains software from gemma, for right holders and\
+ licensing infos, see https://hg.intevation.de/gemma ."
 
 RUN sed -i 's/\(deb.*\)$/\1 universe/' /etc/apt/sources.list
 
--- a/docker/Dockerfile.spa	Mon Nov 26 12:44:52 2018 +0100
+++ b/docker/Dockerfile.spa	Tue Nov 27 09:03:27 2018 +0100
@@ -1,5 +1,7 @@
 FROM ubuntu:bionic
 LABEL authors="tom@intevation.de"
+LABEL description="Contains software from gemma, for right holders and\
+ licensing infos, see https://hg.intevation.de/gemma ."
 
 RUN sed -i 's/\(deb.*\)$/\1 universe/' /etc/apt/sources.list
 
--- a/docker/README.md	Mon Nov 26 12:44:52 2018 +0100
+++ b/docker/README.md	Tue Nov 27 09:03:27 2018 +0100
@@ -34,7 +34,7 @@
 
 ## Create ER diagrams
 
-Assuming you have installed postgresql_autodoc and graphviz on a machine
+Assuming you have installed `postgresql_autodoc` and `graphviz` on a machine
 from wich you can reach your docker host, you can use the following:
 
 - ER diagram with waterway related tables:
--- a/pkg/auth/middleware.go	Mon Nov 26 12:44:52 2018 +0100
+++ b/pkg/auth/middleware.go	Tue Nov 27 09:03:27 2018 +0100
@@ -26,16 +26,26 @@
 	tokenKey
 )
 
+// GetSession returns the session stored in the context of the request.
 func GetSession(req *http.Request) (*Session, bool) {
 	session, ok := req.Context().Value(sessionKey).(*Session)
 	return session, ok
 }
 
+// GetToken returns the session token associated with given request.
 func GetToken(req *http.Request) (string, bool) {
 	token, ok := req.Context().Value(tokenKey).(string)
 	return token, ok
 }
 
+// SessionMiddleware constructs a middleware to enforce the existence
+// of the header X-Gemma-Auth in the incoming request and checks
+// if a session is bound to it.
+// Ihe the checks fail the constructed handler issues an http.StatusUnauthorized
+// back to the invokation stacks and prevents the execution of the
+// nested http.Handler next.
+// Inside the http.Handler next calls to GetSession and GetToken are valid
+// to fetch the respective information.
 func SessionMiddleware(next http.Handler) http.Handler {
 
 	return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
@@ -63,6 +73,10 @@
 	})
 }
 
+// SessionChecker constructs a middleware to check invariants about a session
+// before calling the nested http.Handler next.
+// This is useful when creating specialized middleware e.g. to enforce
+// a role system.
 func SessionChecker(next http.Handler, check func(*Session) bool) http.Handler {
 	return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
 		if claims, ok := GetSession(req); !ok || !check(claims) {
@@ -73,12 +87,15 @@
 	})
 }
 
+// HasRole is a checker function fitting into SessionChecker to check
+// if the user is logged in with at least one of list of given roles.
 func HasRole(roles ...string) func(*Session) bool {
 	return func(session *Session) bool {
 		return session.Roles.HasAny(roles...)
 	}
 }
 
+// EnsureRole is a macro function to stitch SessionChecker and HasRole together.
 func EnsureRole(roles ...string) func(http.Handler) http.Handler {
 	return func(handler http.Handler) http.Handler {
 		return SessionMiddleware(SessionChecker(handler, HasRole(roles...)))
--- a/pkg/auth/opendb.go	Mon Nov 26 12:44:52 2018 +0100
+++ b/pkg/auth/opendb.go	Tue Nov 27 09:03:27 2018 +0100
@@ -27,10 +27,14 @@
 )
 
 var (
+	// ErrNoMetamorphUser is returned if no metamorphic user is configured.
 	ErrNoMetamorphUser = errors.New("No metamorphic user configured")
-	ErrNotLoggedIn     = errors.New("Not logged in")
+	// ErrNotLoggedIn is returned if there is the user is not logged in.
+	ErrNotLoggedIn = errors.New("Not logged in")
 )
 
+// OpenDB opens up a database connection with a given username and password.
+// The other credentials are taken from the configuration.
 func OpenDB(user, password string) (*sql.DB, error) {
 
 	// To ease SSL config ride a bit on parsing.
@@ -74,7 +78,7 @@
 	return db, nil
 }
 
-func MetamorphConn(ctx context.Context, user string) (*sql.Conn, error) {
+func metamorphConn(ctx context.Context, user string) (*sql.Conn, error) {
 	db, err := mm.open()
 	if err != nil {
 		return nil, err
@@ -102,6 +106,8 @@
 WHERE oid IN (SELECT oid FROM cte) AND rolname <> current_user
 AND EXISTS (SELECT 1 FROM users.list_users WHERE username = current_user)`
 
+// AllOtherRoles loggs in as user with password and returns a list
+// of all roles the logged in user has in the system.
 func AllOtherRoles(user, password string) (Roles, error) {
 	db, err := OpenDB(user, password)
 	if err != nil {
@@ -126,8 +132,12 @@
 	return roles, rows.Err()
 }
 
+// RunAs runs a given function fn with a database connection impersonated
+// as the given role.
+// To make this work a metamorphic user has to be configured in
+// the system configuration.
 func RunAs(ctx context.Context, role string, fn func(*sql.Conn) error) error {
-	conn, err := MetamorphConn(ctx, role)
+	conn, err := metamorphConn(ctx, role)
 	if err != nil {
 		return err
 	}
@@ -135,6 +145,8 @@
 	return fn(conn)
 }
 
+// RunAsSessionUser is a convinience wrapper araound which extracts
+// the logged in user from a session and calls RunAs with it.
 func RunAsSessionUser(req *http.Request, fn func(*sql.Conn) error) error {
 	token, ok := GetToken(req)
 	if !ok {
--- a/pkg/auth/session.go	Mon Nov 26 12:44:52 2018 +0100
+++ b/pkg/auth/session.go	Tue Nov 27 09:03:27 2018 +0100
@@ -24,18 +24,27 @@
 	"gemma.intevation.de/gemma/pkg/misc"
 )
 
+// Roles is a list of roles a logged in user has.
 type Roles []string
 
+// Session stores the informations about a logged in user.
 type Session struct {
-	ExpiresAt int64  `json:"expires"`
-	User      string `json:"user"`
-	Roles     Roles  `json:"roles"`
+	// ExpiresAt is a unix timestamp when the session
+	// of the user expires.
+	ExpiresAt int64 `json:"expires"`
+
+	// User is the login name of the user.
+	User string `json:"user"`
+
+	// Roles is the list of roles of the user.
+	Roles Roles `json:"roles"`
 
 	// private fields for managing expiration.
 	access time.Time
 	mu     sync.Mutex
 }
 
+// Has checks if a certain role is amongst the roles.
 func (r Roles) Has(role string) bool {
 	for _, x := range r {
 		if x == role {
@@ -45,6 +54,7 @@
 	return false
 }
 
+// HasAny checks if any of the given roles is in the role list.
 func (r Roles) HasAny(roles ...string) bool {
 	for _, y := range roles {
 		if r.Has(y) {
@@ -59,7 +69,8 @@
 	maxTokenValid    = time.Hour * 3
 )
 
-func NewSession(user, password string, roles Roles) *Session {
+// newSession creates a new session.
+func newSession(user, password string, roles Roles) *Session {
 
 	// Create the Claims
 	return &Session{
@@ -137,23 +148,27 @@
 	return access
 }
 
-func GenerateSessionKey() string {
+func generateSessionKey() string {
 	return base64.URLEncoding.EncodeToString(
 		common.GenerateRandomKey(sessionKeyLength))
 }
 
+// ErrInvalidRole is returned if a given role does not exsist in this system.
 var ErrInvalidRole = errors.New("Invalid role")
 
+// GenerateSession creates a new session for a given user and password
+// backed by the roles of this user in the database.
 func GenerateSession(user, password string) (string, *Session, error) {
 	roles, err := AllOtherRoles(user, password)
 	if err != nil {
 		return "", nil, err
 	}
+	// TODO: Make this a configuration.
 	if !roles.HasAny("sys_admin", "waterway_admin", "waterway_user") {
 		return "", nil, ErrInvalidRole
 	}
-	token := GenerateSessionKey()
-	session := NewSession(user, password, roles)
+	token := generateSessionKey()
+	session := newSession(user, password, roles)
 	Sessions.Add(token, session)
 	return token, session, nil
 }
--- a/pkg/auth/store.go	Mon Nov 26 12:44:52 2018 +0100
+++ b/pkg/auth/store.go	Tue Nov 27 09:03:27 2018 +0100
@@ -22,11 +22,14 @@
 	bolt "github.com/etcd-io/bbolt"
 )
 
+// ErrNoSuchToken is returned if a given token does not
+// exists th the session store.
 var ErrNoSuchToken = errors.New("No such token")
 
 // Sessions is the global connection pool.
 var Sessions *SessionStore
 
+// SessionStore encapsulates a set of currently active sessions.
 type SessionStore struct {
 	storage  *bolt.DB
 	sessions map[string]*Session
@@ -35,6 +38,11 @@
 
 var sessionsBucket = []byte("sessions")
 
+// NewSessionStore creates a new session store.
+// If the filename is empty the session are only hold in memory.
+// If the filename is not empty the sessions are mirrored to
+// a file with this name. Use the later option if you want
+// a persistent session store.
 func NewSessionStore(filename string) (*SessionStore, error) {
 
 	ss := &SessionStore{
@@ -125,6 +133,8 @@
 	}
 }
 
+// Delete removes a session identified by its token from the
+// session store. Returns true if there was such s session.
 func (ss *SessionStore) Delete(token string) bool {
 	res := make(chan bool)
 	ss.cmds <- func() {
@@ -156,6 +166,9 @@
 	}
 }
 
+// Add puts a session into the session store identified by
+// a given token. An old session with the same key will
+// be replaced.
 func (ss *SessionStore) Add(token string, session *Session) {
 	res := make(chan struct{})
 
@@ -173,6 +186,9 @@
 	<-res
 }
 
+// Renew refreshes a session. It takes an old token to
+// identify a session and returns a new token with the
+// freshed up one.
 func (ss *SessionStore) Renew(token string) (string, error) {
 
 	type result struct {
@@ -189,7 +205,7 @@
 		} else {
 			delete(ss.sessions, token)
 			ss.remove(token)
-			newToken := GenerateSessionKey()
+			newToken := generateSessionKey()
 			// TODO: Ensure that this is not racy!
 			session.ExpiresAt = time.Now().Add(maxTokenValid).Unix()
 			ss.sessions[newToken] = session
@@ -202,6 +218,8 @@
 	return r.newToken, r.err
 }
 
+// Session returns the session associated with given token.
+// Returns nil if no matching session was found.
 func (ss *SessionStore) Session(token string) *Session {
 	res := make(chan *Session)
 	ss.cmds <- func() {
@@ -217,6 +235,7 @@
 	return <-res
 }
 
+// Logout removes all sessions of a given user from the session store.
 func (ss *SessionStore) Logout(user string) {
 	ss.cmds <- func() {
 		for token, session := range ss.sessions {
@@ -228,6 +247,8 @@
 	}
 }
 
+// Shutdown closes the session store.
+// If using the persistent mode the backing session database is closed.
 func (ss *SessionStore) Shutdown() error {
 	if db := ss.storage; db != nil {
 		log.Println("info: shutdown persistent session store.")
--- a/pkg/common/errors.go	Mon Nov 26 12:44:52 2018 +0100
+++ b/pkg/common/errors.go	Tue Nov 27 09:03:27 2018 +0100
@@ -19,6 +19,7 @@
 	"strings"
 )
 
+// ToError concats a slice of errors to a single error.
 func ToError(errs []error) error {
 	var b strings.Builder
 	for i, err := range errs {
--- a/pkg/common/random.go	Mon Nov 26 12:44:52 2018 +0100
+++ b/pkg/common/random.go	Tue Nov 27 09:03:27 2018 +0100
@@ -21,6 +21,8 @@
 	"math/big"
 )
 
+// GenerateRandomKey generates a cryptographically secure random key
+// of a given length.
 func GenerateRandomKey(length int) []byte {
 	k := make([]byte, length)
 	if _, err := io.ReadFull(rand.Reader, k); err != nil {
@@ -29,6 +31,9 @@
 	return k
 }
 
+// RandomString generates a cryptographically secure password
+// of a given length which consists of alpha numeric characters
+// and at least one 'special' one.
 func RandomString(n int) string {
 
 	const (
--- a/pkg/common/zip.go	Mon Nov 26 12:44:52 2018 +0100
+++ b/pkg/common/zip.go	Tue Nov 27 09:03:27 2018 +0100
@@ -18,6 +18,8 @@
 	"strings"
 )
 
+// FindInZIP scans a ZIP file directory for a file that ends with
+// case insensitive string. Returns only the first match.
 func FindInZIP(z *zip.ReadCloser, needle string) *zip.File {
 	needle = strings.ToLower(needle)
 	for _, straw := range z.File {
--- a/pkg/controllers/importqueue.go	Mon Nov 26 12:44:52 2018 +0100
+++ b/pkg/controllers/importqueue.go	Tue Nov 27 09:03:27 2018 +0100
@@ -44,7 +44,7 @@
 SELECT true FROM Waterway.imports WHERE id = $1`
 
 	selectHasNoRunningImportSQL = `
-SELECT true FROM Waterway.imports
+SELECT true FROM waterway.imports
 WHERE id = $1 AND state <> 'running'::waterway.import_state`
 
 	selectImportLogsSQL = `
@@ -305,7 +305,9 @@
 
 const (
 	isPendingSQL = `
-SELECT state = 'pending'::waterway.import_state, kind WHERE id = $1`
+SELECT state = 'pending'::waterway.import_state, kind
+FROM waterway.imports
+WHERE id = $1`
 
 	reviewSQL = `
 UPDATE waterway.imports SET
@@ -367,15 +369,17 @@
 			}
 		}
 
-		if _, err = tx.ExecContext(ctx, deleteImportTrackSQL, id); err != nil {
-			return
-		}
 	} else {
 		if _, err = tx.ExecContext(ctx, deleteImportDataSQL, id); err != nil {
 			return
 		}
 	}
 
+	// Remove the import track
+	if _, err = tx.ExecContext(ctx, deleteImportTrackSQL, id); err != nil {
+		return
+	}
+
 	// Log the decision and set the final state.
 	session, _ := auth.GetSession(req)
 	who := session.User
@@ -392,5 +396,14 @@
 	if err = tx.Commit(); err != nil {
 		return
 	}
+
+	result := struct {
+		Message string `json:"message"`
+	}{
+		Message: fmt.Sprintf("Import #%d successfully changed to state '%s'.",
+			id, state),
+	}
+
+	jr = JSONResult{Result: &result}
 	return
 }
--- a/pkg/models/sr.go	Mon Nov 26 12:44:52 2018 +0100
+++ b/pkg/models/sr.go	Tue Nov 27 09:03:27 2018 +0100
@@ -53,6 +53,10 @@
 WHERE bn.objnam = $1 AND sr.date_info = $2`
 )
 
+func (srd SoundingResultDate) MarshalJSON() ([]byte, error) {
+	return json.Marshal(srd.Format(SoundingResultDateFormat))
+}
+
 func (srd *SoundingResultDate) UnmarshalJSON(data []byte) error {
 	var s string
 	if err := json.Unmarshal(data, &s); err != nil {
--- a/schema/auth.sql	Mon Nov 26 12:44:52 2018 +0100
+++ b/schema/auth.sql	Tue Nov 27 09:03:27 2018 +0100
@@ -39,7 +39,9 @@
 GRANT INSERT, UPDATE, DELETE ON
     users.templates, users.user_templates TO waterway_admin;
 GRANT INSERT, UPDATE, DELETE ON
-    waterway.imports, waterway.import_logs, waterway.track_imports TO waterway_admin;
+    waterway.imports, waterway.import_logs, waterway.track_imports,
+    waterway.sounding_results
+    TO waterway_admin;
 
 --
 -- Extended privileges for sys_admin
--- a/schema/auth_tests.sql	Mon Nov 26 12:44:52 2018 +0100
+++ b/schema/auth_tests.sql	Tue Nov 27 09:03:27 2018 +0100
@@ -10,7 +10,6 @@
 
 -- Author(s):
 --  * Tom Gottfried <tom@intevation.de>
---  * Sascha Teichmann<sascha.teichmann@intevation.de>
 
 --
 -- pgTAP test script for privileges and RLS policies
--- a/schema/gemma.sql	Mon Nov 26 12:44:52 2018 +0100
+++ b/schema/gemma.sql	Tue Nov 27 09:03:27 2018 +0100
@@ -586,11 +586,9 @@
 $$
 BEGIN
     EXECUTE format('DELETE FROM %s WHERE id = $1', OLD.relation) USING OLD.key;
+    RETURN NULL;
 END;
 $$
 LANGUAGE plpgsql;
 
-CREATE TRIGGER delete_import AFTER DELETE ON waterway.track_imports
-   FOR EACH ROW EXECUTE PROCEDURE waterway.del_import();
-
 COMMIT;
--- a/schema/install-db.sh	Mon Nov 26 12:44:52 2018 +0100
+++ b/schema/install-db.sh	Tue Nov 27 09:03:27 2018 +0100
@@ -12,6 +12,7 @@
 # Author(s):
 #  * Sascha Wilde <wilde@intevation.de>
 #  * Tom Gottfried <tom@intevation.de>
+#  * Sascha L. Teichmann <sascha.teichmann@intevation.de>
 
 ME=`basename "$0"`
 BASEDIR=`dirname "$0"`