changeset 2675:43e1abaf9ee3 import-overview-rework

Merged default into import-overview-rework branch.
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Fri, 15 Mar 2019 10:18:49 +0100
parents df2a31e8f257 (diff) b219ca1514f4 (current diff)
children fe93c48f76c9
files
diffstat 2 files changed, 161 insertions(+), 97 deletions(-) [+]
line wrap: on
line diff
--- a/pkg/controllers/importqueue.go	Fri Mar 15 09:34:06 2019 +0100
+++ b/pkg/controllers/importqueue.go	Fri Mar 15 10:18:49 2019 +0100
@@ -14,6 +14,7 @@
 package controllers
 
 import (
+	"context"
 	"database/sql"
 	"encoding/json"
 	"fmt"
@@ -21,6 +22,7 @@
 	"net/http"
 	"strconv"
 	"strings"
+	"time"
 
 	"github.com/gorilla/mux"
 	"github.com/jackc/pgx/pgtype"
@@ -32,13 +34,12 @@
 
 const (
 	warningSQLPrefix = `
-`
-	selectImportsSQL = `
 WITH warned AS (
   SELECT distinct(import_id) AS id
   FROM import.import_logs
   WHERE kind = 'warn'::log_type
-)
+)`
+	selectImportsSQL = warningSQLPrefix + `
 SELECT
   imports.id AS id,
   state::varchar,
@@ -46,21 +47,21 @@
   kind,
   username,
   signer,
-  summary,
-  EXISTS (
-    SELECT true FROM warned
-	WHERE warned.id = imports.id
-  ) AS has_warnings
-`
-	withoutWarningsSQL = `
+  summary IS NOT NULL AS has_summary,
+  imports.id IN (SELECT id FROM warned) AS has_warnings
 FROM import.imports
+WHERE
 `
-	withWarningsSQL = `
-FROM import.imports JOIN warned ON imports.id = warned.id
+	selectBeforeSQL = warningSQLPrefix + `
+SELECT enqueued FROM import.imports
+WHERE
 `
-
-	selectHasImportSQL = `
-SELECT true FROM import.imports WHERE id = $1`
+	selectAfterSQL = warningSQLPrefix + `
+SELECT enqueued FROM import.imports
+WHERE
+`
+	selectImportSummaySQL = `
+SELECT summary FROM import.imports WHERE id = $1`
 
 	selectHasNoRunningImportSQL = `
 SELECT true FROM import.imports
@@ -126,93 +127,127 @@
 	return &ta
 }
 
-func queryImportListStmt(
-	conn *sql.Conn,
-	req *http.Request,
-) (*sql.Rows, error) {
+type filterBuilder struct {
+	stmt    strings.Builder
+	args    []interface{}
+	hasCond bool
+}
+
+func (fb *filterBuilder) arg(format string, v ...interface{}) {
+	indices := make([]interface{}, len(v))
+	for i := range indices {
+		indices[i] = len(fb.args) + i + 1
+	}
+	fmt.Fprintf(&fb.stmt, format, indices...)
+	fb.args = append(fb.args, v...)
+}
 
-	var (
-		stmt   strings.Builder
-		args   []interface{}
-		states *pgtype.TextArray
-		kinds  *pgtype.TextArray
-		ids    *pgtype.Int8Array
-	)
+func (fb *filterBuilder) cond(format string, v ...interface{}) {
+	if fb.hasCond {
+		fb.stmt.WriteString(" AND ")
+	} else {
+		fb.hasCond = true
+	}
+	fb.arg(format, v...)
+}
 
-	arg := func(format string, v interface{}) {
-		fmt.Fprintf(&stmt, format, len(args)+1)
-		args = append(args, v)
-	}
+func buildFilters(req *http.Request) (l, b, a *filterBuilder, err error) {
+
+	l = new(filterBuilder)
+	a = new(filterBuilder)
+	b = new(filterBuilder)
 
-	var warnings bool
+	var noBefore, noAfter bool
 
-	switch warn := strings.ToLower(req.FormValue("warnings")); warn {
-	case "1", "t", "true":
-		warnings = true
-	}
+	l.stmt.WriteString(selectImportsSQL)
+	a.stmt.WriteString(selectAfterSQL)
+	b.stmt.WriteString(selectBeforeSQL)
 
 	if st := req.FormValue("states"); st != "" {
-		states = toTextArray(st, imports.ImportStateNames)
+		states := toTextArray(st, imports.ImportStateNames)
+		l.cond(" state = ANY($%d) ", states)
+		a.cond(" state = ANY($%d) ", states)
+		b.cond(" state = ANY($%d) ", states)
 	}
 
 	if ks := req.FormValue("kinds"); ks != "" {
-		kinds = toTextArray(ks, imports.ImportKindNames())
+		kinds := toTextArray(ks, imports.ImportKindNames())
+		l.cond(" kind = ANY($%d) ", kinds)
+		a.cond(" kind = ANY($%d) ", kinds)
+		b.cond(" kind = ANY($%d) ", kinds)
 	}
 
 	if idss := req.FormValue("ids"); idss != "" {
-		ids = toInt8Array(idss)
+		ids := toInt8Array(idss)
+		l.cond(" id = ANY($%d) ", ids)
+		a.cond(" id = ANY($%d) ", ids)
+		b.cond(" id = ANY($%d) ", ids)
 	}
 
-	stmt.WriteString(selectImportsSQL)
-	if warnings {
-		stmt.WriteString(withWarningsSQL)
+	if from := req.FormValue("from"); from != "" {
+		var fromTime time.Time
+		if fromTime, err = time.Parse(models.ImportTimeFormat, from); err != nil {
+			return
+		}
+		l.cond(" enqueued >= $%d ", fromTime)
+		b.cond(" enqueued < $%d", fromTime)
 	} else {
-		stmt.WriteString(withoutWarningsSQL)
+		noBefore = true
 	}
 
-	if states != nil || kinds != nil || ids != nil {
-		stmt.WriteString(" WHERE ")
+	if to := req.FormValue("to"); to != "" {
+		var toTime time.Time
+		if toTime, err = time.Parse(models.ImportTimeFormat, to); err != nil {
+			return
+		}
+		l.cond(" enqueued <= $%d ", toTime)
+		a.cond(" enqueued > $%d", toTime)
+	} else {
+		noAfter = true
 	}
 
-	if states != nil {
-		arg(" state = ANY($%d) ", states)
-	}
-
-	if states != nil && (kinds != nil || ids != nil) {
-		stmt.WriteString("AND")
-	}
-
-	if kinds != nil {
-		arg(" kind = ANY($%d) ", kinds)
+	switch warn := strings.ToLower(req.FormValue("warnings")); warn {
+	case "1", "t", "true":
+		l.cond(" id IN (SELECT id FROM warned) ")
+		a.cond(" id IN (SELECT id FROM warned) ")
+		b.cond(" id IN (SELECT id FROM warned) ")
 	}
 
-	if (states != nil || kinds != nil) && ids != nil {
-		stmt.WriteString("AND")
+	if !l.hasCond {
+		l.stmt.WriteString(" TRUE ")
 	}
-
-	if ids != nil {
-		arg(" id = ANY($%d) ", ids)
+	if !b.hasCond {
+		b.stmt.WriteString(" TRUE ")
+	}
+	if !a.hasCond {
+		a.stmt.WriteString(" TRUE ")
 	}
 
-	stmt.WriteString(" ORDER BY enqueued DESC ")
+	l.stmt.WriteString(" ORDER BY enqueued DESC ")
+	a.stmt.WriteString(" ORDER BY enqueued LIMIT 1")
+	b.stmt.WriteString(" ORDER BY enqueued LIMIT 1")
 
-	if lim := req.FormValue("limit"); lim != "" {
-		limit, err := strconv.ParseInt(lim, 10, 64)
-		if err != nil {
-			return nil, err
-		}
-		arg(" LIMIT $%d ", limit)
+	if noBefore {
+		b = nil
 	}
+	if noAfter {
+		a = nil
+	}
+	return
+}
 
-	if ofs := req.FormValue("offset"); ofs != "" {
-		offset, err := strconv.ParseInt(ofs, 10, 64)
-		if err != nil {
-			return nil, err
-		}
-		arg(" OFFSET $%d ", offset)
+func neighbored(ctx context.Context, conn *sql.Conn, fb *filterBuilder) *models.ImportTime {
+
+	var when time.Time
+	err := conn.QueryRowContext(ctx, fb.stmt.String(), fb.args...).Scan(&when)
+	switch {
+	case err == sql.ErrNoRows:
+		return nil
+	case err != nil:
+		log.Printf("warn: %v\n", err)
+		return nil
 	}
-
-	return conn.QueryContext(req.Context(), stmt.String(), args...)
+	return &models.ImportTime{when}
 }
 
 func listImports(
@@ -221,27 +256,35 @@
 	conn *sql.Conn,
 ) (jr JSONResult, err error) {
 
+	var list, before, after *filterBuilder
+
+	if list, before, after, err = buildFilters(req); err != nil {
+		return
+	}
+
+	ctx := req.Context()
+
 	var rows *sql.Rows
-	rows, err = queryImportListStmt(conn, req)
-	if err != nil {
+	if rows, err = conn.QueryContext(ctx, list.stmt.String(), list.args...); err != nil {
 		return
 	}
 	defer rows.Close()
 
 	imports := make([]*models.Import, 0, 20)
 
-	var signer, summary sql.NullString
+	var signer sql.NullString
 
 	for rows.Next() {
 		var it models.Import
+		var enqueued time.Time
 		if err = rows.Scan(
 			&it.ID,
 			&it.State,
-			&it.Enqueued,
+			&enqueued,
 			&it.Kind,
 			&it.User,
 			&signer,
-			&summary,
+			&it.Summary,
 			&it.Warnings,
 		); err != nil {
 			return
@@ -249,12 +292,7 @@
 		if signer.Valid {
 			it.Signer = signer.String
 		}
-		if summary.Valid {
-			if err = json.NewDecoder(
-				strings.NewReader(summary.String)).Decode(&it.Summary); err != nil {
-				return
-			}
-		}
+		it.Enqueued = models.ImportTime{enqueued}
 		imports = append(imports, &it)
 	}
 
@@ -262,11 +300,25 @@
 		return
 	}
 
+	var prev, next *models.ImportTime
+
+	if before != nil {
+		prev = neighbored(ctx, conn, before)
+	}
+
+	if after != nil {
+		next = neighbored(ctx, conn, after)
+	}
+
 	jr = JSONResult{
 		Result: struct {
-			Imports []*models.Import `json:"imports"`
+			Prev    *models.ImportTime `json:"prev,omitempty"`
+			Next    *models.ImportTime `json:"next,omitempty"`
+			Imports []*models.Import   `json:"imports"`
 		}{
 			Imports: imports,
+			Prev:    prev,
+			Next:    next,
 		},
 	}
 	return
@@ -283,8 +335,8 @@
 	id, _ := strconv.ParseInt(mux.Vars(req)["id"], 10, 64)
 
 	// Check if he have such a import job first.
-	var dummy bool
-	err = conn.QueryRowContext(ctx, selectHasImportSQL, id).Scan(&dummy)
+	var summary sql.NullString
+	err = conn.QueryRowContext(ctx, selectImportSummaySQL, id).Scan(&summary)
 	switch {
 	case err == sql.ErrNoRows:
 		err = JSONError{
@@ -296,6 +348,14 @@
 		return
 	}
 
+	var sum interface{}
+	if summary.Valid {
+		if err = json.NewDecoder(
+			strings.NewReader(summary.String)).Decode(&sum); err != nil {
+			return
+		}
+	}
+
 	// We have it -> generate log entries.
 	var rows *sql.Rows
 	rows, err = conn.QueryContext(ctx, selectImportLogsSQL, id)
@@ -320,8 +380,10 @@
 
 	jr = JSONResult{
 		Result: struct {
+			Summary interface{}              `json:"summary,omitempty"`
 			Entries []*models.ImportLogEntry `json:"entries"`
 		}{
+			Summary: sum,
 			Entries: entries,
 		},
 	}
--- a/pkg/models/import.go	Fri Mar 15 09:34:06 2019 +0100
+++ b/pkg/models/import.go	Fri Mar 15 10:18:49 2019 +0100
@@ -19,18 +19,20 @@
 	"time"
 )
 
+const ImportTimeFormat = "2006-01-02T15:04:05.000"
+
 type (
 	ImportTime struct{ time.Time }
 
 	Import struct {
-		ID       int64       `json:"id"`
-		State    string      `json:"state"`
-		Enqueued ImportTime  `json:"enqueued"`
-		Kind     string      `json:"kind"`
-		User     string      `json:"user"`
-		Signer   string      `json:"signer,omitempty"`
-		Summary  interface{} `json:"summary,omitempty"`
-		Warnings bool        `json:"warnings,omitempty"`
+		ID       int64      `json:"id"`
+		State    string     `json:"state"`
+		Enqueued ImportTime `json:"enqueued"`
+		Kind     string     `json:"kind"`
+		User     string     `json:"user"`
+		Signer   string     `json:"signer,omitempty"`
+		Summary  bool       `json:"summary,omitempty"`
+		Warnings bool       `json:"warnings,omitempty"`
 	}
 
 	ImportLogEntry struct {
@@ -62,7 +64,7 @@
 }
 
 func (it ImportTime) MarshalJSON() ([]byte, error) {
-	return json.Marshal(it.Format("2006-01-02T15:04:05.000"))
+	return json.Marshal(it.Format(ImportTimeFormat))
 }
 
 func (it *ImportTime) Scan(x interface{}) error {