# HG changeset patch # User Sascha L. Teichmann # Date 1625651080 -7200 # Node ID 47c2ca05e8ec2c80908743439ba4881bdc780df7 # Parent 4a6feb5d3727f18afbf04cd810f866686f2427ce# Parent dcc692a333c087be84d7a3b65f934d4a792e1116 Merged extented-report branch back into default. diff -r 4a6feb5d3727 -r 47c2ca05e8ec client/.env --- a/client/.env Sun Jul 04 11:37:37 2021 +0200 +++ b/client/.env Wed Jul 07 11:44:40 2021 +0200 @@ -15,4 +15,4 @@ VUE_APP_SILENCE_TRANSLATIONWARNINGS = #Url of user manual -VUE_APP_USER_MANUAL_URL= +VUE_APP_USER_MANUAL_URL= \ No newline at end of file diff -r 4a6feb5d3727 -r 47c2ca05e8ec client/src/components/App.vue --- a/client/src/components/App.vue Sun Jul 04 11:37:37 2021 +0200 +++ b/client/src/components/App.vue Wed Jul 07 11:44:40 2021 +0200 @@ -1,5 +1,5 @@ @@ -139,7 +153,8 @@ mixins: [sortTable], data() { return { - sortColumn: "user" // overriding the sortTable mixin's empty default value + sortColumn: "user", // overriding the sortTable mixin's empty default value + reportToggled: false }; }, components: { @@ -147,15 +162,18 @@ Spacer: () => import("@/components/Spacer") }, computed: { - ...mapGetters("usermanagement", [ - "isUserDetailsVisible", - "users", - "currentUser" - ]), + ...mapGetters("usermanagement", ["isUserDetailsVisible", "users"]), ...mapState("application", ["showSidebar"]), + ...mapState("usermanagement", ["currentUser"]), usersLabel() { return this.$gettext("Users"); }, + reportsLabel() { + return this.$gettext("DQL Report"); + }, + receivesReportLabel() { + return this.$gettext("User receives Data Quality Report"); + }, sendMailLabel() { return this.$gettext("Send testmail"); }, @@ -197,6 +215,37 @@ } }, methods: { + toggleReport(user) { + HTTP.patch( + `/users/${user.user}`, + { + reports: user.reports + }, + { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "application/json; charset=UTF-8" + } + } + ) + .then(() => { + if (this.currentUser && this.currentUser.user === user.user) { + this.reportToggled = !this.reportToggled; + } + }) + .catch(error => { + let message = "Backend not reachable"; + if (error.response) { + const { status, data } = error.response; + message = `${status}: ${data.message || data}`; + } + displayError({ + title: this.$gettext("Backend Error"), + message: message + }); + user.reports = !user.reports; + }); + }, sendTestMail(user) { HTTP.get("/testmail/" + encodeURIComponent(user), { headers: { diff -r 4a6feb5d3727 -r 47c2ca05e8ec client/src/store/importschedule.js --- a/client/src/store/importschedule.js Sun Jul 04 11:37:37 2021 +0200 +++ b/client/src/store/importschedule.js Wed Jul 07 11:44:40 2021 +0200 @@ -31,7 +31,9 @@ SOUNDINGRESULTS: "soundingresults", APPROVEDGAUGEMEASUREMENTS: "approvedgaugemeasurements", WATERWAYPROFILES: "waterwayprofiles", - FAIRWAYMARKS: "fairwaymarks" + FAIRWAYMARKS: "fairwaymarks", + REPORT: "report", + STATSUPDATE: "statsupdate" }; const KINDIMPORTTYPE = { @@ -44,7 +46,9 @@ fd: "fairwaydimension", wg: "waterwaygauges", dmv: "distancemarksvirtual", - dma: "distancemarksashore" + dma: "distancemarksashore", + report: "report", + statsupdate: "statsupdate" }; const IMPORTTYPEKIND = { @@ -57,7 +61,9 @@ fairwaydimension: "fd", waterwaygauges: "wg", distancemarksvirtual: "dmv", - distancemarksashore: "dma" + distancemarksashore: "dma", + report: "report", + statsupdate: "statsupdate" }; const FAIRWAYMARKKINDS = { @@ -110,7 +116,9 @@ sourceOrganization: null, trys: null, waitRetry: null, - selectedMark: null + selectedMark: null, + statsUpdate: null, + reportName: null }; }; @@ -123,6 +131,7 @@ const init = () => { return { schedules: [], + availableReports: null, importScheduleDetailVisible: false, currentSchedule: initializeCurrentSchedule(), mode: MODES.LIST @@ -134,6 +143,9 @@ namespaced: true, state: init(), mutations: { + setAvailableReports: (state, value) => { + state.availableReports = value; + }, setEditMode: state => { state.mode = MODES.EDIT; }, @@ -271,9 +283,33 @@ sourceOrganization ); } + if (kind === IMPORTTYPES.STATSUPDATE) { + const { name } = config; + Vue.set(state.currentSchedule, "statsUpdate", name); + } + if (kind === IMPORTTYPES.REPORT) { + const { name } = config; + Vue.set(state.currentSchedule, "reportName", name); + } } }, actions: { + loadAvailableReports({ commit }) { + return new Promise((resolve, reject) => { + HTTP.get("/data/reports", { + headers: { + "X-Gemma-Auth": localStorage.getItem("token") + } + }) + .then(response => { + commit("setAvailableReports", response.data.reports); + resolve(response); + }) + .catch(error => { + reject(error); + }); + }); + }, loadSchedule({ commit }, id) { return new Promise((resolve, reject) => { HTTP.get("/imports/config/" + id, { diff -r 4a6feb5d3727 -r 47c2ca05e8ec example_conf.toml --- a/example_conf.toml Sun Jul 04 11:37:37 2021 +0200 +++ b/example_conf.toml Wed Jul 07 11:44:40 2021 +0200 @@ -88,3 +88,4 @@ # Schema for "Testclient imports" # schema-dirs = "$PATH_TO_SCHEMATA" # published-config ="$PATH/pub-config.json" +# report-path = "$PATH_TO_XSLX_AND_YAML_PAIRS" diff -r 4a6feb5d3727 -r 47c2ca05e8ec go.mod --- a/go.mod Sun Jul 04 11:37:37 2021 +0200 +++ b/go.mod Wed Jul 07 11:44:40 2021 +0200 @@ -3,39 +3,39 @@ go 1.13 require ( + github.com/360EntSecGroup-Skylar/excelize/v2 v2.4.0 + github.com/PaesslerAG/gval v1.1.0 github.com/cockroachdb/apd v1.1.0 // indirect - github.com/etcd-io/bbolt v1.3.3 github.com/fatih/structs v1.1.0 github.com/fogleman/contourmap v0.0.0-20190814184649-9f61d36c4199 + github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/gofrs/uuid v3.2.0+incompatible // indirect - github.com/gorilla/mux v1.7.4 + github.com/gorilla/mux v1.8.0 github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 // indirect github.com/jackc/pgx v3.6.2+incompatible github.com/jonas-p/go-shp v0.1.2-0.20190401125246-9fd306ae10a6 github.com/lib/pq v1.2.0 // indirect + github.com/magiconair/properties v1.8.5 // indirect github.com/mitchellh/go-homedir v1.1.0 - github.com/pelletier/go-toml v1.6.0 // indirect - github.com/pkg/errors v0.9.1 // indirect + github.com/mitchellh/mapstructure v1.4.1 // indirect + github.com/pelletier/go-toml v1.9.1 // indirect github.com/rs/cors v1.7.0 github.com/sergi/go-diff v1.0.0 github.com/shopspring/decimal v0.0.0-20190905144223-a36b5d85f337 // indirect - github.com/spf13/afero v1.2.2 // indirect + github.com/spf13/afero v1.6.0 // indirect github.com/spf13/cast v1.3.1 // indirect - github.com/spf13/cobra v0.0.6 + github.com/spf13/cobra v1.1.3 github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/spf13/viper v1.6.2 - github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e - github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 // indirect - go.etcd.io/bbolt v1.3.3 // indirect - golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 // indirect - golang.org/x/net v0.0.0-20200301022130-244492dfa37a - golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 // indirect - golang.org/x/text v0.3.3 // indirect - gonum.org/v1/gonum v0.7.0 + github.com/spf13/viper v1.7.1 + github.com/tidwall/rtree v1.2.8 + go.etcd.io/bbolt v1.3.5 + golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect + golang.org/x/net v0.0.0-20210525063256-abc453219eb5 + golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea // indirect + gonum.org/v1/gonum v0.9.1 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df - gopkg.in/ini.v1 v1.54.0 // indirect + gopkg.in/ini.v1 v1.62.0 // indirect gopkg.in/robfig/cron.v1 v1.2.0 - gopkg.in/yaml.v2 v2.2.8 // indirect + gopkg.in/yaml.v2 v2.4.0 ) diff -r 4a6feb5d3727 -r 47c2ca05e8ec go.sum --- a/go.sum Sun Jul 04 11:37:37 2021 +0200 +++ b/go.sum Wed Jul 07 11:44:40 2021 +0200 @@ -1,39 +1,70 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= +github.com/360EntSecGroup-Skylar/excelize/v2 v2.4.0 h1:X+2CWGf5W1tm2+W7Y/LLrAPLFSNlHATnqDudGoIzaxY= +github.com/360EntSecGroup-Skylar/excelize/v2 v2.4.0/go.mod h1:p9lGPoVX3HYEbFRfjgrPWaaKsHe/2u4EM9DB/qoctgU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af h1:wVe6/Ea46ZMeNkQjjBW6xcqyQA/j5e0D6GytH95g0gQ= +github.com/PaesslerAG/gval v1.1.0 h1:k3RuxeZDO3eejD4cMPSt+74tUSvTnbGvLx0df4mdwFc= +github.com/PaesslerAG/gval v1.1.0/go.mod h1:y/nm5yEyTeX6av0OfKJNp9rBNj2XrGhAf5+v24IBN1I= +github.com/PaesslerAG/jsonpath v0.1.0 h1:gADYeifvlqK3R3i2cR5B4DGgxLXIPb3TRTH1mGi0jPI= +github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/etcd-io/bbolt v1.3.3 h1:gSJmxrs37LgTqR/oyJBWok6k6SvXEUerFTbltIhXkBM= -github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fogleman/contourmap v0.0.0-20190814184649-9f61d36c4199 h1:kufr0u0RIG5ACpjFsPRbbuHa0FhMWsS3tnSFZ2hf07s= github.com/fogleman/contourmap v0.0.0-20190814184649-9f61d36c4199/go.mod h1:mqaaaP4j7nTF8T/hx5OCljA7BYWHmrH2uh+Q023OchE= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= -github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= +github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= +github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= +github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -45,20 +76,50 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= -github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc= @@ -68,13 +129,17 @@ github.com/jonas-p/go-shp v0.1.2-0.20190401125246-9fd306ae10a6 h1:h5O7ee4tlSPVjdC75eSLX7jXZiHftthuHio/GtrhaSM= github.com/jonas-p/go-shp v0.1.2-0.20190401125246-9fd306ae10a6/go.mod h1:MRIhyxDQ6VVp0oYeD7yPGr5RSTNScUFKCDsI5DR7PtI= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -83,27 +148,44 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= +github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4= -github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= -github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= +github.com/pelletier/go-toml v1.9.1 h1:a6qW1EVNZWH9WGI6CsYdD8WAylkoXBS5yv0XHlh17Tc= +github.com/pelletier/go-toml v1.9.1/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= +github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= @@ -113,10 +195,18 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/richardlehane/mscfb v1.0.3 h1:rD8TBkYWkObWO0oLDFCbwMeZ4KoalxQy+QgniCj3nKI= +github.com/richardlehane/mscfb v1.0.3/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +github.com/richardlehane/msoleps v1.0.1 h1:RfrALnSNXzmXLbGct/P2b4xkFz4e8Gmj/0Vj9M9xC1o= +github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shopspring/decimal v0.0.0-20190905144223-a36b5d85f337 h1:Da9XEUfFxgyDOqUfwgoTDcWzmnlOnCGi6i4iPS+8Fbw= @@ -129,126 +219,226 @@ github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= -github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= +github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.6 h1:breEStsVwemnKh2/s6gMvSdMEkwW0sK8vGStnlVBMCs= -github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= -github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= +github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M= +github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= -github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= -github.com/spf13/viper v1.6.2 h1:7aKfF+e8/k68gda3LOjo5RxiUqddoFxVq4BKBPrxk5E= -github.com/spf13/viper v1.6.2/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= +github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e h1:+NL1GDIUOKxVfbp2KoJQD9cTQ6dyP2co9q4yzmT9FZo= -github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e/go.mod h1:/h+UnNGt0IhNNJLkGikcdcJqm66zGD/uJGMRxK/9+Ao= -github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 h1:Otn9S136ELckZ3KKDyCkxapfufrqDqwmGjcHfAyXRrE= -github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563/go.mod h1:mLqSmt7Dv/CNneF2wfcChfN1rvapyQr01LGKnKex0DQ= +github.com/tidwall/cities v0.1.0 h1:CVNkmMf7NEC9Bvokf5GoSsArHCKRMTgLuubRTHnH0mE= +github.com/tidwall/cities v0.1.0/go.mod h1:lV/HDp2gCcRcHJWqgt6Di54GiDrTZwh1aG2ZUPNbqa4= +github.com/tidwall/geoindex v1.4.4 h1:hdwzy5qNtK75i7nus59Ibr+SwcH4F2v65bw4txrLJ9M= +github.com/tidwall/geoindex v1.4.4/go.mod h1:rvVVNEFfkJVWGUdEfU8QaoOg/9zFX0h9ofWzA60mz1I= +github.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8= +github.com/tidwall/lotsa v1.0.2/go.mod h1:X6NiU+4yHA3fE3Puvpnn1XMDrFZrE9JO2/w+UMuqgR8= +github.com/tidwall/rtree v1.2.8 h1:KzqIidAdzviaRM3BQoAHMym1zy2HYE2ta+UOPse9Pyo= +github.com/tidwall/rtree v1.2.8/go.mod h1:S+JSsqPTI8LfWA4xHBo5eXzie8WJLVFeppAutSegl6M= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk= +github.com/xuri/efp v0.0.0-20210322160811-ab561f5b45e3 h1:EpI0bqf/eX9SdZDwlMmahKM+CDBgNbsXMhsN28XrM8o= +github.com/xuri/efp v0.0.0-20210322160811-ab561f5b45e3/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= -go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= +go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM= -golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20210415154028-4f45737414dc/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2 h1:y102fOLFqhV41b+4GPiJoa0k/x+pJcEi2/HB1Y5T6fU= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136 h1:A1gGSx58LAGVHUUsOf7IiR0u8Xb6W51gRwfDBhkdcaw= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk= +golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5 h1:wjuX4b5yYQnEQHzd+CBcrcC6OVR2J1CN6mUy0oSxIPo= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea h1:+WiDlPBBaO+h9vPNZi8uJ3k4BkKQB7Iow3aqwHVA5hI= +golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= -gonum.org/v1/gonum v0.7.0 h1:Hdks0L0hgznZLG9nzXb8vZ0rRvqNvAcgAp84y7Mwkgw= -gonum.org/v1/gonum v0.7.0/go.mod h1:L02bwd0sqlsvRv41G7wGWFCsVNZFv/k1xzGIxeANHGM= +gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/gonum v0.9.1 h1:HCWmqqNoELL0RAQeKBXWtkp04mGk8koafcB4He6+uhc= +gonum.org/v1/gonum v0.9.1/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= +gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= -gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.54.0 h1:oM5ElzbIi7gwLnNbPX2M25ED1vSAK3B6dex50eS/6Fs= -gopkg.in/ini.v1 v1.54.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= +gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/robfig/cron.v1 v1.2.0 h1:PSJsm0uPEND0Rumxxbo7qNb7bxQUTIWDIdpPS59/tcw= gopkg.in/robfig/cron.v1 v1.2.0/go.mod h1:3I22DCB+7VAStCIqyArwi2xY9a7IioCiNjrsnCqs+HE= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff -r 4a6feb5d3727 -r 47c2ca05e8ec pkg/auth/store.go --- a/pkg/auth/store.go Sun Jul 04 11:37:37 2021 +0200 +++ b/pkg/auth/store.go Wed Jul 07 11:44:40 2021 +0200 @@ -20,7 +20,7 @@ "time" "gemma.intevation.de/gemma/pkg/config" - bolt "github.com/etcd-io/bbolt" + bolt "go.etcd.io/bbolt" ) // ErrNoSuchToken is returned if a given token does not diff -r 4a6feb5d3727 -r 47c2ca05e8ec pkg/config/config.go --- a/pkg/config/config.go Sun Jul 04 11:37:37 2021 +0200 +++ b/pkg/config/config.go Wed Jul 07 11:44:40 2021 +0200 @@ -122,6 +122,9 @@ // SOAPTimeout is the timeout till a SOAP request is canceled. func SOAPTimeout() time.Duration { return viper.GetDuration("soap-timeout") } +// ReportPath is a path where report templates are stored. +func ReportPath() string { return viper.GetString("report-path") } + var ( proxyKeyOnce sync.Once proxyKey []byte @@ -290,6 +293,8 @@ str("published-config", "", "path to a config file served to client.") + str("report-path", "", "path to a report templates.") + d("soap-timeout", 3*time.Minute, "Timeout till a SOAP request is canceled.") } diff -r 4a6feb5d3727 -r 47c2ca05e8ec pkg/controllers/importconfig.go --- a/pkg/controllers/importconfig.go Sun Jul 04 11:37:37 2021 +0200 +++ b/pkg/controllers/importconfig.go Wed Jul 07 11:44:40 2021 +0200 @@ -30,6 +30,11 @@ mw "gemma.intevation.de/gemma/pkg/middleware" ) +// RolesRequierer enforces roles when storing schedules. +type RolesRequierer interface { + RequiresRoles() auth.Roles +} + func runImportConfig(req *http.Request) (jr mw.JSONResult, err error) { id, _ := strconv.ParseInt(mux.Vars(req)["id"], 10, 64) @@ -262,12 +267,23 @@ return } config := ctor() + + session, _ := auth.GetSession(req) + + if r, ok := config.(RolesRequierer); ok { + if roles := r.RequiresRoles(); len(roles) > 0 && !session.Roles.HasAny(roles...) { + err = mw.JSONError{ + Code: http.StatusUnauthorized, + Message: fmt.Sprintf( + "Not allowed to add config for kind %s", string(cfg.Kind)), + } + return + } + } if err = json.Unmarshal(cfg.Config, config); err != nil { return } - session, _ := auth.GetSession(req) - pc := imports.PersistentConfig{ User: session.User, Kind: string(cfg.Kind), diff -r 4a6feb5d3727 -r 47c2ca05e8ec pkg/controllers/report.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/controllers/report.go Wed Jul 07 11:44:40 2021 +0200 @@ -0,0 +1,179 @@ +// This is Free Software under GNU Affero General Public License v >= 3.0 +// without warranty, see README.md and license for details. +// +// SPDX-License-Identifier: AGPL-3.0-or-later +// License-Filename: LICENSES/AGPL-3.0.txt +// +// Copyright (C) 2021 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann + +package controllers + +import ( + "database/sql" + "fmt" + "log" + "net/http" + "os" + "path/filepath" + "sort" + "strings" + + "gemma.intevation.de/gemma/pkg/config" + "gemma.intevation.de/gemma/pkg/middleware" + "gemma.intevation.de/gemma/pkg/xlsx" + + mw "gemma.intevation.de/gemma/pkg/middleware" + + "github.com/360EntSecGroup-Skylar/excelize/v2" + "github.com/gorilla/mux" +) + +func listReports(req *http.Request) (jr mw.JSONResult, err error) { + path := config.ReportPath() + if path == "" { + err = mw.JSONError{ + Code: http.StatusNotFound, + Message: http.StatusText(http.StatusNotFound), + } + return + } + + // This would be easier with Go 1.16+. + + dir, err := os.Open(path) + if err != nil { + log.Printf("error: %v\n", err) + err = mw.JSONError{ + Code: http.StatusInternalServerError, + Message: "Listing report templates failed.", + } + return + } + defer dir.Close() + files, err := dir.Readdirnames(-1) + if err != nil { + log.Printf("error: %v\n", err) + err = mw.JSONError{ + Code: http.StatusInternalServerError, + Message: "Listing report templates failed.", + } + return + } + + pairs := map[string]int{} + +all: + for _, file := range files { + var mask int + switch { + case strings.HasSuffix(file, ".xlsx"): + mask = 1 + case strings.HasSuffix(file, ".yaml"): + mask = 2 + default: + continue all + } + basename := filepath.Base(file) + name := strings.TrimSuffix(basename, filepath.Ext(basename)) + pairs[name] |= mask + } + + var reports []string + for name, mask := range pairs { + if mask == 3 { + reports = append(reports, name) + } + } + sort.Strings(reports) + + out := struct { + Reports []string `json:"reports"` + }{ + Reports: reports, + } + jr = mw.JSONResult{Result: out} + return +} + +func report(rw http.ResponseWriter, req *http.Request) { + + path := config.ReportPath() + if path == "" { + http.NotFound(rw, req) + return + } + + if stat, err := os.Stat(path); err != nil { + if os.IsNotExist(err) { + log.Printf("error: report dir '%s' does not exists.\n", path) + http.NotFound(rw, req) + } else { + log.Printf("error: %v\n", err) + http.Error(rw, "Error: "+err.Error(), http.StatusInternalServerError) + } + return + } else if !stat.Mode().IsDir() { + log.Printf("error: report dir '%s' is not a directory.\n", path) + http.NotFound(rw, req) + return + } + + vars := mux.Vars(req) + name := vars["name"] + + xlsxFilename := filepath.Join(path, name+".xlsx") + yamlFilename := filepath.Join(path, name+".yaml") + + for _, check := range []string{xlsxFilename, yamlFilename} { + if _, err := os.Stat(check); err != nil { + if os.IsNotExist(err) { + http.NotFound(rw, req) + } else { + log.Printf("error: %v\n", err) + http.Error(rw, "Error: "+err.Error(), http.StatusInternalServerError) + } + return + } + } + + template, err := excelize.OpenFile(xlsxFilename) + if err != nil { + log.Printf("error: %v\n", err) + http.Error(rw, "Error: "+err.Error(), http.StatusInternalServerError) + return + } + + action, err := xlsx.ActionFromFile(yamlFilename) + if err != nil { + http.Error(rw, "Error: "+err.Error(), http.StatusInternalServerError) + log.Printf("error: %v\n", err) + return + } + + ctx := req.Context() + conn := middleware.GetDBConn(req) + + tx, err := conn.BeginTx(ctx, &sql.TxOptions{ReadOnly: true}) + defer tx.Rollback() + + if err := action.Execute(ctx, tx, template); err != nil { + log.Printf("error: %v\n", err) + http.Error(rw, "Error: "+err.Error(), http.StatusInternalServerError) + return + } + rw.Header().Set( + "Content-Disposition", + fmt.Sprintf("attachment; filename=%s.xlsx", name)) + rw.Header().Set( + "Content-Type", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + + if _, err := template.WriteTo(rw); err != nil { + log.Printf("error: %v\n", err) + } +} diff -r 4a6feb5d3727 -r 47c2ca05e8ec pkg/controllers/routes.go --- a/pkg/controllers/routes.go Sun Jul 04 11:37:37 2021 +0200 +++ b/pkg/controllers/routes.go Wed Jul 07 11:44:40 2021 +0200 @@ -68,6 +68,11 @@ Handle: updateUser, })).Methods(http.MethodPut) + api.Handle("/users/{user:.+}", any(&mw.JSONHandler{ + Input: func(*http.Request) interface{} { return new(models.UserPatch) }, + Handle: patchUser, + })).Methods(http.MethodPatch) + api.Handle("/users/{user:.+}", sysAdmin(&mw.JSONHandler{ Handle: deleteUser, })).Methods(http.MethodDelete) @@ -259,6 +264,12 @@ NoConn: true, })).Methods(http.MethodPost) + api.Handle("/imports/{kind:report|statsupdate}", sysAdmin(&mw.JSONHandler{ + Input: importModel, + Handle: manualImport, + NoConn: true, + })).Methods(http.MethodPost) + // Import scheduler configuration api.Handle("/imports/config/{id:[0-9]+}/run", waterwayAdmin(&mw.JSONHandler{ @@ -322,6 +333,28 @@ NoConn: true, })).Methods(http.MethodPut) + // Handler for reporting + + api.Handle("/data/reports", + waterwayAdmin(&mw.JSONHandler{ + Handle: listReports, + NoConn: true, + })).Methods(http.MethodGet) + + api.Handle("/data/report/{name:"+models.SafePathExp+"}", waterwayAdmin( + mw.DBConn(http.HandlerFunc(report)))).Methods(http.MethodGet) + + // Handler for update scripts + api.Handle("/data/stats-updates", + sysAdmin(&mw.JSONHandler{ + Handle: listStatsUpdates, + })).Methods(http.MethodGet) + + api.Handle("/data/stats-updates/{name:.+}", + sysAdmin(&mw.JSONHandler{ + Handle: statsUpdates, + })).Methods(http.MethodGet) + // Handler to serve data to the client. api.Handle("/data/{type:availability|fairway}/{kind:stretch|section|bottleneck}/{name:.+}", any( diff -r 4a6feb5d3727 -r 47c2ca05e8ec pkg/controllers/statsupdates.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/controllers/statsupdates.go Wed Jul 07 11:44:40 2021 +0200 @@ -0,0 +1,114 @@ +// This is Free Software under GNU Affero General Public License v >= 3.0 +// without warranty, see README.md and license for details. +// +// SPDX-License-Identifier: AGPL-3.0-or-later +// License-Filename: LICENSES/AGPL-3.0.txt +// +// Copyright (C) 2021 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann + +package controllers + +import ( + "database/sql" + "log" + "net/http" + + mw "gemma.intevation.de/gemma/pkg/middleware" + "github.com/gorilla/mux" +) + +const ( + listStatsUpdatesSQL = `SELECT name FROM sys_admin.stats_updates ORDER BY name` + statsUpdatesScriptSQL = `SELECT script FROM sys_admin.stats_updates WHERE name = $1` +) + +func listStatsUpdates(req *http.Request) (jr mw.JSONResult, err error) { + + ctx := req.Context() + conn := mw.JSONConn(req) + + rows, err2 := conn.QueryContext(ctx, listStatsUpdatesSQL) + if err2 != nil { + log.Printf("error: %v\n", err2) + err = mw.JSONError{ + Code: http.StatusInternalServerError, + Message: "Listing stats update failed.", + } + return + } + defer rows.Close() + + names := []string{} + + for rows.Next() { + var name string + if err2 := rows.Scan(&name); err2 != nil { + log.Printf("error: %v\n", err2) + err = mw.JSONError{ + Code: http.StatusInternalServerError, + Message: "Fetching stats update names failed.", + } + return + } + names = append(names, name) + } + + if err2 := rows.Err(); err2 != nil { + log.Printf("error: %v\n", err2) + err = mw.JSONError{ + Code: http.StatusInternalServerError, + Message: "Fetching stats update names failed.", + } + return + } + + out := struct { + StatsUpdates []string `json:"stats-updates"` + }{ + StatsUpdates: names, + } + jr = mw.JSONResult{Result: out} + return +} + +func statsUpdates(req *http.Request) (jr mw.JSONResult, err error) { + name := mux.Vars(req)["name"] + + ctx := req.Context() + conn := mw.JSONConn(req) + + var script string + err2 := conn.QueryRowContext(ctx, statsUpdatesScriptSQL, name).Scan(&script) + switch { + case err2 == sql.ErrNoRows: + err = mw.JSONError{ + Code: http.StatusNotFound, + Message: "No such update script.", + } + return + case err2 != nil: + log.Printf("error: %v\n", err2) + err = mw.JSONError{ + Code: http.StatusInternalServerError, + Message: "Loading update script failed.", + } + return + } + + if _, err2 := conn.ExecContext(ctx, script); err2 != nil { + log.Printf("error: %v\n", err2) + err = mw.JSONError{ + Code: http.StatusInternalServerError, + Message: "Loading update script failed.", + } + return + } + + jr = mw.JSONResult{Result: map[string]interface{}{}} + return +} diff -r 4a6feb5d3727 -r 47c2ca05e8ec pkg/controllers/user.go --- a/pkg/controllers/user.go Sun Jul 04 11:37:37 2021 +0200 +++ b/pkg/controllers/user.go Wed Jul 07 11:44:40 2021 +0200 @@ -21,6 +21,8 @@ "fmt" "log" "net/http" + "strconv" + "strings" "text/template" "time" @@ -37,22 +39,22 @@ const ( createUserSQL = `INSERT INTO users.list_users - VALUES ($1, $2, $3, $4, NULL, $5)` + VALUES ($1, $2, $3, $4, NULL, $5, $6)` createUserExtentSQL = `INSERT INTO users.list_users VALUES ($1, $2, $3, $4, - ST_MakeBox2D(ST_Point($5, $6), ST_Point($7, $8)), $9)` + ST_MakeBox2D(ST_Point($5, $6), ST_Point($7, $8)), $9, $10)` updateUserUnprivSQL = `UPDATE users.list_users SET (pw, map_extent, email_address) = ($2, ST_MakeBox2D(ST_Point($3, $4), ST_Point($5, $6)), $7) WHERE username = $1` updateUserSQL = `UPDATE users.list_users - SET (rolname, username, pw, country, map_extent, email_address) - = ($2, $3, $4, $5, NULL, $6) + SET (rolname, username, pw, country, map_extent, email_address, report_reciever) + = ($2, $3, $4, $5, NULL, $6, $7) WHERE username = $1` updateUserExtentSQL = `UPDATE users.list_users - SET (rolname, username, pw, country, map_extent, email_address) - = ($2, $3, $4, $5, ST_MakeBox2D(ST_Point($6, $7), ST_Point($8, $9)), $10) + SET (rolname, username, pw, country, map_extent, email_address, report_reciever) + = ($2, $3, $4, $5, ST_MakeBox2D(ST_Point($6, $7), ST_Point($8, $9)), $10, $11) WHERE username = $1` deleteUserSQL = `DELETE FROM users.list_users WHERE username = $1` @@ -63,7 +65,8 @@ country, email_address, ST_XMin(map_extent), ST_YMin(map_extent), - ST_XMax(map_extent), ST_YMax(map_extent) + ST_XMax(map_extent), ST_YMax(map_extent), + report_reciever FROM users.list_users` listUserSQL = `SELECT @@ -71,7 +74,8 @@ country, email_address, ST_XMin(map_extent), ST_YMin(map_extent), - ST_XMax(map_extent), ST_YMax(map_extent) + ST_XMax(map_extent), ST_YMax(map_extent), + report_reciever FROM users.list_users WHERE username = $1` ) @@ -80,23 +84,30 @@ testSysadminNotifyMailTmpl = template.Must( template.New("sysadmin").Parse(`Dear {{ .User }}, -this is a test email for the Gemma System Errors notification service. You +this is a test email from the Gemma System Errors notification service. You recieved this mail, because a System Administrator triggered the test mail sending function at {{ .Timestamp }}. -When a critical system error is detected an automated mail will be send to -the address: {{ .Email }} with details on the error condition.`)) +When a critical system error is detected an automated mail will be sent to +{{ .Email }} with details on the error condition.`)) testWWAdminNotifyMailTmpl = template.Must( template.New("waterwayadmin").Parse(`Dear {{ .User }}, -this is a test email for the Gemma System Imports notification service. You +this is a test email from the Gemma System Mail notification service. You recieved this mail, because a System Administrator triggered the test mail sending function at {{ .Timestamp }}. -When the status of an data import managed by you changes an automated mail will -be send to the address: {{ .Email }} with details on the new import status -(inkluding import errors) and details on the concerned import.`)) +When the status of a data import managed by you changes an automated mail will +be sent to {{ .Email }} with details on the new import status +(including import errors) and details on the corresponding import.`)) + + testWWUserNotifyMailTmpl = template.Must( + template.New("waterwayuser").Parse(`Dear {{ .User }}, + +this is a test email from the Gemma System Mail notification service. You +recieved this mail, because a System Administrator triggered the test mail +sending function at {{ .Timestamp }}.`)) ) func deleteUser(req *http.Request) (jr mw.JSONResult, err error) { @@ -181,6 +192,7 @@ newUser.Password, newUser.Country, newUser.Email, + newUser.Reports, ) } else { res, err = db.ExecContext( @@ -194,6 +206,7 @@ newUser.Extent.X1, newUser.Extent.Y1, newUser.Extent.X2, newUser.Extent.Y2, newUser.Email, + newUser.Reports, ) } } else { @@ -241,6 +254,137 @@ return } +func patchUser(req *http.Request) (jr mw.JSONResult, err error) { + + user := models.UserName(mux.Vars(req)["user"]) + if !user.IsValid() { + err = mw.JSONError{ + Code: http.StatusBadRequest, + Message: "error: user invalid", + } + return + } + + s, ok := auth.GetSession(req) + if !ok { + err = mw.JSONError{ + Code: http.StatusUnauthorized, + Message: "error: not logged in", + } + return + } + + priv := s.Roles.Has("sys_admin") + + if !priv && s.User != string(user) { + err = mw.JSONError{ + Code: http.StatusUnauthorized, + Message: "error: not allowed to modify someone else", + } + return + } + + var ( + columns []string + positions []string + args []interface{} + ) + + update := func(column string, value interface{}) { + columns = append(columns, column) + positions = append(positions, "$"+strconv.Itoa(len(positions)+1)) + args = append(args, value) + } + + updateBox := func(column string, extent *models.BoundingBox) { + columns = append(columns, column) + pos := len(positions) + position := fmt.Sprintf("ST_MakeBox2D(ST_Point($%d, $%d), ST_Point($%d, $%d))", + pos+1, pos+2, pos+3, pos+4) + positions = append(positions, position) + args = append(args, extent.X1, extent.Y1, extent.X2, extent.Y2) + } + + patch := mw.JSONInput(req).(*models.UserPatch) + + if patch.User != nil && priv { + update("user", *patch.User) + } + if patch.Role != nil && priv { + update("rolname", *patch.Role) + } + if patch.Password != nil { + update("pw", *patch.Password) + } + if patch.Email != nil { + update("email_address", *patch.Email) + } + if patch.Country != nil && priv { + update("country", *patch.Country) + } + if patch.Reports != nil && priv { + update("report_reciever", *patch.Reports) + } + if patch.Extent != nil { + updateBox("map_extent", patch.Extent) + } + + var colsS, posS string + + switch len(columns) { + case 0: // Nothing to do + jr = mw.JSONResult{ + Code: http.StatusCreated, + Result: struct { + Result string `json:"result"` + }{"success"}, + } + return + case 1: // No brackets if there is only one argument. + colsS = columns[0] + posS = positions[0] + default: + colsS = "(" + strings.Join(columns, ",") + ")" + posS = "(" + strings.Join(positions, ",") + ")" + } + + stmt := fmt.Sprintf( + `UPDATE users.list_users SET %s = %s WHERE username = $%d`, + colsS, + posS, + len(positions)+1) + + args = append(args, user) + + db := mw.JSONConn(req) + + var res sql.Result + if res, err = db.ExecContext(req.Context(), stmt, args...); err != nil { + return + } + + if n, err2 := res.RowsAffected(); err2 == nil && n == 0 { + err = mw.JSONError{ + Code: http.StatusNotFound, + Message: fmt.Sprintf("Cannot find user %s.", user), + } + return + } + + if patch.User != nil && *patch.User != user { + // Running in a go routine should not be necessary. + go func() { auth.Sessions.Logout(string(user)) }() + } + + jr = mw.JSONResult{ + Code: http.StatusCreated, + Result: struct { + Result string `json:"result"` + }{"success"}, + } + return +} + func createUser(req *http.Request) (jr mw.JSONResult, err error) { user := mw.JSONInput(req).(*models.User) @@ -256,6 +400,7 @@ user.Password, user.Country, user.Email, + user.Reports, ) } else { _, err = db.ExecContext( @@ -268,6 +413,7 @@ user.Extent.X1, user.Extent.Y1, user.Extent.X2, user.Extent.Y2, user.Email, + user.Reports, ) } @@ -307,6 +453,7 @@ &user.Email, &user.Extent.X1, &user.Extent.Y1, &user.Extent.X2, &user.Extent.Y2, + &user.Reports, ); err != nil { return } @@ -343,6 +490,7 @@ &result.Email, &result.Extent.X1, &result.Extent.Y1, &result.Extent.X2, &result.Extent.Y2, + &result.Reports, ) switch { @@ -382,6 +530,7 @@ &userData.Email, &userData.Extent.X1, &userData.Extent.Y1, &userData.Extent.X2, &userData.Extent.Y2, + &userData.Reports, ) switch { @@ -408,19 +557,18 @@ } var bodyTmpl *template.Template - if userData.Role == "sys_admin" { + switch userData.Role { + case "sys_admin": subject = "Gemma: Sysadmin Notification TEST" bodyTmpl = testSysadminNotifyMailTmpl - } else if userData.Role == "waterway_admin" { + case "waterway_admin": subject = "Gemma: Waterway Admin Notification TEST" bodyTmpl = testWWAdminNotifyMailTmpl - } else { - err = mw.JSONError{ - Code: http.StatusBadRequest, - Message: "Test mails can only be generated for admin roles.", - } - return + default: + subject = "Gemma: Waterway User Notification TEST" + bodyTmpl = testWWUserNotifyMailTmpl } + var buf bytes.Buffer if err := bodyTmpl.Execute(&buf, &tmplVars); err != nil { log.Printf("error: %v\n", err) diff -r 4a6feb5d3727 -r 47c2ca05e8ec pkg/imports/modelconvert.go --- a/pkg/imports/modelconvert.go Sun Jul 04 11:37:37 2021 +0200 +++ b/pkg/imports/modelconvert.go Wed Jul 07 11:44:40 2021 +0200 @@ -4,7 +4,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later // License-Filename: LICENSES/AGPL-3.0.txt // -// Copyright (C) 2018, 2020 by via donau +// Copyright (C) 2018, 2020, 2021 by via donau // – Österreichische Wasserstraßen-Gesellschaft mbH // Software engineering by Intevation GmbH // @@ -45,6 +45,8 @@ DSECJobKind: func() interface{} { return new(models.SectionDelete) }, DSTJobKind: func() interface{} { return new(models.StretchDelete) }, DSRJobKind: func() interface{} { return new(models.SoundingResultDelete) }, + ReportJobKind: func() interface{} { return FindJobCreator(ReportJobKind).Create() }, + StatsUpdateJobKind: func() interface{} { return FindJobCreator(StatsUpdateJobKind).Create() }, } // ImportModelForJobKind returns the constructor function to diff -r 4a6feb5d3727 -r 47c2ca05e8ec pkg/imports/report.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/imports/report.go Wed Jul 07 11:44:40 2021 +0200 @@ -0,0 +1,279 @@ +// This is Free Software under GNU Affero General Public License v >= 3.0 +// without warranty, see README.md and license for details. +// +// SPDX-License-Identifier: AGPL-3.0-or-later +// License-Filename: LICENSES/AGPL-3.0.txt +// +// Copyright (C) 2021 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann + +package imports + +import ( + "bytes" + "context" + "database/sql" + "errors" + "fmt" + "log" + "os" + "path/filepath" + "strings" + "text/template" + "time" + + "gemma.intevation.de/gemma/pkg/auth" + "gemma.intevation.de/gemma/pkg/common" + "gemma.intevation.de/gemma/pkg/config" + "gemma.intevation.de/gemma/pkg/misc" + "gemma.intevation.de/gemma/pkg/models" + "gemma.intevation.de/gemma/pkg/xlsx" + + "github.com/360EntSecGroup-Skylar/excelize/v2" +) + +type Report struct { + models.QueueConfigurationType + Name models.SafePath `json:"name"` +} + +const ReportJobKind JobKind = "report" + +type reportJobCreator struct{} + +const ( + selectReportUsersSQL = ` +SELECT username, email_address +FROM users.list_users +WHERE report_reciever +ORDER BY country, username` + + selectCurrentUserSQL = ` +SELECT current_user, email_address +FROM users.list_users +WHERE username = current_user` +) + +var reportMailTmpl = template.Must(template.New("report-mail"). + Parse(`Dear {{ .Receiver }} + +this is an automatically generated report from the Gemma system. +You got this mail because you are listed as a report receiver. +If you received it without consent please +contact {{ .Admin }} under {{ .AdminEmail }}. + +Find attached {{ .Attachment }} containing the {{ .Report }} report from {{ .When }}. + +Kind Regards`)) + +func init() { RegisterJobCreator(ReportJobKind, reportJobCreator{}) } + +func (reportJobCreator) Description() string { return "report" } + +func (reportJobCreator) AutoAccept() bool { return true } + +func (reportJobCreator) Create() Job { return new(Report) } + +func (reportJobCreator) Depends() [2][]string { return [2][]string{{}, {}} } + +func (reportJobCreator) StageDone(context.Context, *sql.Tx, int64, Feedback) error { + return nil +} + +// RequiresRoles enforces to be a sys_admin to run this . +func (*Report) RequiresRoles() auth.Roles { return auth.Roles{"sys_admin"} } + +func (r *Report) Description() (string, error) { return string(r.Name), nil } + +func (*Report) CleanUp() error { return nil } + +func (r *Report) MarshalAttributes(attrs common.Attributes) error { + if err := r.QueueConfigurationType.MarshalAttributes(attrs); err != nil { + return err + } + attrs.Set("name", string(r.Name)) + return nil +} + +func (r *Report) UnmarshalAttributes(attrs common.Attributes) error { + if err := r.QueueConfigurationType.UnmarshalAttributes(attrs); err != nil { + return err + } + name, found := attrs.Get("name") + if !found { + return errors.New("missing 'name' attribute") + } + r.Name = models.SafePath(name) + if !r.Name.Valid() { + return fmt.Errorf("'%s' is not a safe path", name) + } + return nil +} + +func (r *Report) loadTemplate() (*excelize.File, *xlsx.Action, error) { + path := config.ReportPath() + if path == "" { + return nil, nil, errors.New("no report dir configured") + } + + if stat, err := os.Stat(path); err != nil { + if os.IsNotExist(err) { + return nil, nil, fmt.Errorf("report dir '%s' does not exists", path) + } + return nil, nil, err + } else if !stat.Mode().IsDir() { + return nil, nil, fmt.Errorf("report dir '%s' is not a directory", path) + } + + xlsxFilename := filepath.Join(path, string(r.Name)+".xlsx") + yamlFilename := filepath.Join(path, string(r.Name)+".yaml") + + for _, check := range []string{xlsxFilename, yamlFilename} { + if _, err := os.Stat(check); err != nil { + if os.IsNotExist(err) { + return nil, nil, fmt.Errorf("'%s' does not exists", check) + } + return nil, nil, err + } + } + + template, err := excelize.OpenFile(xlsxFilename) + if err != nil { + return nil, nil, err + } + + action, err := xlsx.ActionFromFile(yamlFilename) + if err != nil { + return nil, nil, err + } + + return template, action, nil +} + +func (r *Report) Do( + ctx context.Context, + importID int64, + conn *sql.Conn, + feedback Feedback, +) (interface{}, error) { + + start := time.Now() + + feedback.Info("Generating report %s.", r.Name) + + template, action, err := r.loadTemplate() + if err != nil { + return nil, err + } + + tx, err := conn.BeginTx(ctx, &sql.TxOptions{ReadOnly: true}) + if err != nil { + return nil, err + } + defer tx.Rollback() + + // Fetch receivers + var users []misc.EmailReceiver + + if err := func() error { + rows, err := tx.QueryContext(ctx, selectReportUsersSQL) + if err != nil { + return err + } + defer rows.Close() + + for rows.Next() { + var u misc.EmailReceiver + if err := rows.Scan(&u.Name, &u.Address); err != nil { + return err + } + users = append(users, u) + } + return rows.Err() + }(); err != nil { + return nil, err + } + + if len(users) == 0 { + feedback.Warn("No users found to send reports to.") + return nil, nil + } + + // Fetch admin who is responsible for the report. + var admin misc.EmailReceiver + if err := tx.QueryRowContext( + ctx, selectCurrentUserSQL).Scan(&admin.Name, &admin.Address); err != nil { + log.Printf("error: Cannot find sender: %v\n") + return nil, fmt.Errorf("cannot find sender: %v", err) + } + + // Generate the actual report. + if err := action.Execute(ctx, tx, template); err != nil { + log.Printf("error: %v\n", err) + return nil, fmt.Errorf("Generating report failed: %v", err) + } + + var buf bytes.Buffer + if _, err := template.WriteTo(&buf); err != nil { + log.Printf("error: %v\n", err) + return nil, fmt.Errorf("generating report failed: %v", err) + } + + feedback.Info("Sending report to %d receiver(s).", len(users)) + + now := start.UTC().Format("2006-01-02") + + attached := string(r.Name) + "-" + now + ".xlsx" + + body := func(u misc.EmailReceiver) (string, error) { + fill := struct { + Receiver string + Attachment string + Report string + When string + Admin string + AdminEmail string + }{ + Receiver: u.Name, + Attachment: attached, + Report: string(r.Name), + When: now, + Admin: admin.Name, + AdminEmail: admin.Address, + } + var sb strings.Builder + if err := reportMailTmpl.Execute(&sb, &fill); err != nil { + return "", err + } + return sb.String(), nil + } + + errorHandler := func(r misc.EmailReceiver, err error) error { + // We do not terminate the sending of the emails if + // sending failed. We only log it. + feedback.Warn("Sending report to %s failed: %v", r.Name, err) + return nil + } + + if err := misc.SendMailToAll( + users, + "Report "+string(r.Name)+" from "+now, + body, + []misc.EmailAttachment{{ + Name: attached, + Content: buf.Bytes(), + }}, + errorHandler, + ); err != nil { + return nil, err + } + + feedback.Info("Generating and sending report took %v.", + time.Since(start)) + + return nil, nil +} diff -r 4a6feb5d3727 -r 47c2ca05e8ec pkg/imports/statsupdate.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/imports/statsupdate.go Wed Jul 07 11:44:40 2021 +0200 @@ -0,0 +1,116 @@ +// This is Free Software under GNU Affero General Public License v >= 3.0 +// without warranty, see README.md and license for details. +// +// SPDX-License-Identifier: AGPL-3.0-or-later +// License-Filename: LICENSES/AGPL-3.0.txt +// +// Copyright (C) 2021 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann + +package imports + +import ( + "context" + "database/sql" + "errors" + "fmt" + "time" + + "gemma.intevation.de/gemma/pkg/auth" + "gemma.intevation.de/gemma/pkg/common" + "gemma.intevation.de/gemma/pkg/models" +) + +type StatsUpdate struct { + models.QueueConfigurationType + Name string `json:"name"` +} + +const StatsUpdateJobKind JobKind = "statsupdate" + +type statsUpdateJobCreator struct{} + +func init() { RegisterJobCreator(StatsUpdateJobKind, statsUpdateJobCreator{}) } + +func (statsUpdateJobCreator) Description() string { return "statsupdate" } + +func (statsUpdateJobCreator) AutoAccept() bool { return true } + +func (statsUpdateJobCreator) Create() Job { return new(StatsUpdate) } + +func (statsUpdateJobCreator) Depends() [2][]string { return [2][]string{{}, {}} } + +func (statsUpdateJobCreator) StageDone(context.Context, *sql.Tx, int64, Feedback) error { + return nil +} + +// RequiresRoles enforces to be a sys_admin to run this . +func (*StatsUpdate) RequiresRoles() auth.Roles { return auth.Roles{"sys_admin"} } + +func (su *StatsUpdate) Description() (string, error) { return su.Name, nil } + +func (*StatsUpdate) CleanUp() error { return nil } + +func (su *StatsUpdate) MarshalAttributes(attrs common.Attributes) error { + if err := su.QueueConfigurationType.MarshalAttributes(attrs); err != nil { + return err + } + attrs.Set("name", su.Name) + return nil +} + +func (su *StatsUpdate) UnmarshalAttributes(attrs common.Attributes) error { + if err := su.QueueConfigurationType.UnmarshalAttributes(attrs); err != nil { + return err + } + name, found := attrs.Get("name") + if !found { + return errors.New("missing 'name' attribute") + } + su.Name = name + return nil +} + +const loadUpdateStatsScriptSQL = `SELECT script FROM sys_admin.stats_updates WHERE name = $1` + +func (su *StatsUpdate) Do( + ctx context.Context, + importID int64, + conn *sql.Conn, + feedback Feedback, +) (interface{}, error) { + + start := time.Now() + + feedback.Info("Running stats update %s.", su.Name) + + tx, err := conn.BeginTx(ctx, nil) + if err != nil { + return nil, err + } + defer tx.Rollback() + + var script string + switch err := tx.QueryRowContext(ctx, loadUpdateStatsScriptSQL, su.Name).Scan(&script); { + case err == sql.ErrNoRows: + return nil, fmt.Errorf("no update script found for '%s'", su.Name) + case err != nil: + return nil, err + } + + if _, err := tx.ExecContext(ctx, script); err != nil { + return nil, err + } + + if err := tx.Commit(); err != nil { + return nil, err + } + + feedback.Info("Running stats update took %v.", time.Since(start)) + + return nil, nil +} diff -r 4a6feb5d3727 -r 47c2ca05e8ec pkg/mesh/polygon.go --- a/pkg/mesh/polygon.go Sun Jul 04 11:37:37 2021 +0200 +++ b/pkg/mesh/polygon.go Wed Jul 07 11:44:40 2021 +0200 @@ -44,7 +44,7 @@ IntersectionOverlaps ) -func (ls lineSegment) Rect(interface{}) ([]float64, []float64) { +func (ls lineSegment) Rect() ([2]float64, [2]float64) { var min, max [2]float64 @@ -64,14 +64,14 @@ max[1] = ls[1] } - return min[:], max[:] + return min, max } func (p *Polygon) Indexify() { indices := make([]*rtree.RTree, len(p.rings)) for i := range indices { - index := rtree.New(nil) + index := new(rtree.RTree) indices[i] = index rng := p.rings[i] @@ -83,7 +83,8 @@ } else { ls = []float64{rng[i], rng[i+1], rng[0], rng[1]} } - index.Insert(ls) + min, max := ls.Rect() + index.Insert(min, max, ls) } } @@ -217,9 +218,11 @@ return IntersectionOutSide } + min, max := box.Rect() + for _, index := range p.indices { var intersects bool - index.Search(box, func(item rtree.Item) bool { + index.Search(min, max, func(_, _ [2]float64, item interface{}) bool { if item.(lineSegment).intersects(box) { intersects = true return false @@ -252,9 +255,10 @@ func (p *Polygon) IntersectionWithTriangle(t *Triangle) IntersectionType { box := t.BBox() + min, max := box.Rect() for _, index := range p.indices { var intersects bool - index.Search(box, func(item rtree.Item) bool { + index.Search(min, max, func(_, _ [2]float64, item interface{}) bool { ls := item.(lineSegment) other := make(lineSegment, 4) for i := range t { diff -r 4a6feb5d3727 -r 47c2ca05e8ec pkg/mesh/vertex.go --- a/pkg/mesh/vertex.go Sun Jul 04 11:37:37 2021 +0200 +++ b/pkg/mesh/vertex.go Wed Jul 07 11:44:40 2021 +0200 @@ -784,8 +784,8 @@ } // Rect returns the bounding box of this box as separated coordinates. -func (a Box2D) Rect(interface{}) ([]float64, []float64) { - return []float64{a.X1, a.Y1}, []float64{a.X2, a.Y2} +func (a Box2D) Rect() ([2]float64, [2]float64) { + return [2]float64{a.X1, a.Y1}, [2]float64{a.X2, a.Y2} } // Intersects checks if two Box2Ds intersect. diff -r 4a6feb5d3727 -r 47c2ca05e8ec pkg/misc/mail.go --- a/pkg/misc/mail.go Sun Jul 04 11:37:37 2021 +0200 +++ b/pkg/misc/mail.go Wed Jul 07 11:44:40 2021 +0200 @@ -14,11 +14,23 @@ package misc import ( + "io" + gomail "gopkg.in/gomail.v2" "gemma.intevation.de/gemma/pkg/config" ) +type EmailReceiver struct { + Name string + Address string +} + +type EmailAttachment struct { + Name string + Content []byte +} + // SendMail sends an email to a given address with a given subject // and body. // The credentials to contact the SMPT server are taken from the @@ -41,3 +53,54 @@ return d.DialAndSend(m) } + +func SendMailToAll( + receivers []EmailReceiver, + subject string, + body func(EmailReceiver) (string, error), + attachments []EmailAttachment, + errorHandler func(EmailReceiver, error) error, +) error { + + d := gomail.Dialer{ + Host: config.MailHost(), + Port: int(config.MailPort()), + Username: config.MailUser(), + Password: config.MailPassword(), + LocalName: config.MailHelo(), + SSL: config.MailPort() == 465, + } + + s, err := d.Dial() + if err != nil { + return err + } + defer s.Close() + + m := gomail.NewMessage() + for _, r := range receivers { + m.SetHeader("From", config.MailFrom()) + m.SetAddressHeader("To", r.Address, r.Name) + m.SetHeader("Subject", subject) + b, err := body(r) + if err != nil { + return err + } + m.SetBody("text/plain", b) + for _, at := range attachments { + content := at.Content + m.Attach(at.Name, gomail.SetCopyFunc(func(w io.Writer) error { + _, err := w.Write(content) + return err + })) + } + + if err := gomail.Send(s, m); err != nil { + if err = errorHandler(r, err); err != nil { + return err + } + } + m.Reset() + } + return nil +} diff -r 4a6feb5d3727 -r 47c2ca05e8ec pkg/models/common.go --- a/pkg/models/common.go Sun Jul 04 11:37:37 2021 +0200 +++ b/pkg/models/common.go Wed Jul 07 11:44:40 2021 +0200 @@ -18,6 +18,7 @@ "encoding/json" "errors" "fmt" + "regexp" "strings" "time" @@ -40,6 +41,9 @@ Country string // UniqueCountries is a list of unique countries. UniqueCountries []Country + + // SafePath should only contain chars that directory traversal safe. + SafePath string ) func (d Date) MarshalJSON() ([]byte, error) { @@ -149,3 +153,25 @@ } return b.String() } + +const SafePathExp = "[a-zA-Z0-9_-]+" + +var safePathRegExp = regexp.MustCompile("^" + SafePathExp + "$") + +func (sp SafePath) Valid() bool { + return safePathRegExp.MatchString(string(sp)) +} + +// UnmarshalJSON ensures that the given string only consist +// of runes that are directory traversal safe. +func (sp *SafePath) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if c := SafePath(s); c.Valid() { + *sp = c + return nil + } + return fmt.Errorf("'%s' is not a safe path", s) +} diff -r 4a6feb5d3727 -r 47c2ca05e8ec pkg/models/user.go --- a/pkg/models/user.go Sun Jul 04 11:37:37 2021 +0200 +++ b/pkg/models/user.go Wed Jul 07 11:44:40 2021 +0200 @@ -46,9 +46,21 @@ Password string `json:"password,omitempty"` Email Email `json:"email"` Country Country `json:"country"` + Reports bool `json:"reports"` Extent *BoundingBox `json:"extent"` } + // UserPatch is used to send only partial updates. + UserPatch struct { + User *UserName `json:"user,omitempty"` + Role *Role `json:"role,omitempty"` + Password *string `json:"password,omitempty"` + Email *Email `json:"email,omitempty"` + Country *Country `json:"country,omitempty"` + Reports *bool `json:"reports,omitempty"` + Extent *BoundingBox `json:"extent,omitempty"` + } + // PWResetUser is send to request a password reset for a user. PWResetUser struct { User string `json:"user"` diff -r 4a6feb5d3727 -r 47c2ca05e8ec pkg/xlsx/handlebars.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/xlsx/handlebars.go Wed Jul 07 11:44:40 2021 +0200 @@ -0,0 +1,83 @@ +// This is Free Software under GNU Affero General Public License v >= 3.0 +// without warranty, see README.md and license for details. +// +// SPDX-License-Identifier: AGPL-3.0-or-later +// License-Filename: LICENSES/AGPL-3.0.txt +// +// Copyright (C) 2021 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann + +package xlsx + +import "strings" + +func handlebars(s string, replace func(string) string) string { + + var ( + out, repl strings.Builder + mode int + ) + + for _, c := range s { + switch mode { + case 0: + if c == '{' { + mode = 1 + } else { + out.WriteRune(c) + } + case 1: + if c == '{' { + mode = 2 + } else { + out.WriteByte('{') + out.WriteRune(c) + mode = 0 + } + case 2: + switch c { + case '\\': + mode = 3 + case '}': + mode = 4 + default: + repl.WriteRune(c) + } + case 3: + repl.WriteRune(c) + mode = 2 + case 4: + if c == '}' { + out.WriteString(replace(repl.String())) + repl.Reset() + mode = 0 + } else { + repl.WriteByte('}') + repl.WriteRune(c) + mode = 2 + } + } + } + + switch mode { + case 1: + out.WriteByte('{') + case 2: + out.WriteString("{{") + out.WriteString(repl.String()) + case 3: + out.WriteString("{{") + out.WriteString(repl.String()) + out.WriteByte('\\') + case 4: + out.WriteString("{{") + out.WriteString(repl.String()) + out.WriteByte('}') + } + + return out.String() +} diff -r 4a6feb5d3727 -r 47c2ca05e8ec pkg/xlsx/sql.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/xlsx/sql.go Wed Jul 07 11:44:40 2021 +0200 @@ -0,0 +1,110 @@ +// This is Free Software under GNU Affero General Public License v >= 3.0 +// without warranty, see README.md and license for details. +// +// SPDX-License-Identifier: AGPL-3.0-or-later +// License-Filename: LICENSES/AGPL-3.0.txt +// +// Copyright (C) 2021 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann + +package xlsx + +import ( + "context" + "database/sql" + "fmt" + "strconv" + "strings" +) + +type sqlResult struct { + columns []string + rows [][]interface{} +} + +func (sr *sqlResult) find(column string) int { + for i, name := range sr.columns { + if name == column { + return i + } + } + return -1 +} + +func replaceStmt(stmt string) (string, []string) { + + var names []string + + add := func(name string) int { + for i, n := range names { + if n == name { + return i + 1 + } + } + names = append(names, name) + return len(names) + } + + replace := func(s string) string { + n := add(strings.TrimSpace(s)) + return "$" + strconv.Itoa(n) + } + + out := handlebars(stmt, replace) + return out, names +} + +func query( + ctx context.Context, + tx *sql.Tx, + stmt string, + eval func(string) (interface{}, error), +) (*sqlResult, error) { + + nstmt, nargs := replaceStmt(stmt) + args := make([]interface{}, len(nargs)) + for i, n := range nargs { + var err error + if args[i], err = eval(n); err != nil { + return nil, err + } + } + + rs, err := tx.QueryContext(ctx, nstmt, args...) + if err != nil { + return nil, fmt.Errorf("SQL failed: '%s': %v", nstmt, err) + } + defer rs.Close() + + columns, err := rs.Columns() + if err != nil { + return nil, err + } + var rows [][]interface{} + + ptrs := make([]interface{}, len(columns)) + + for rs.Next() { + row := make([]interface{}, len(columns)) + for i := range row { + ptrs[i] = &row[i] + } + if err := rs.Scan(ptrs...); err != nil { + return nil, err + } + rows = append(rows, row) + } + + if err := rs.Err(); err != nil { + return nil, err + } + + return &sqlResult{ + columns: columns, + rows: rows, + }, nil +} diff -r 4a6feb5d3727 -r 47c2ca05e8ec pkg/xlsx/templater.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/xlsx/templater.go Wed Jul 07 11:44:40 2021 +0200 @@ -0,0 +1,672 @@ +// This is Free Software under GNU Affero General Public License v >= 3.0 +// without warranty, see README.md and license for details. +// +// SPDX-License-Identifier: AGPL-3.0-or-later +// License-Filename: LICENSES/AGPL-3.0.txt +// +// Copyright (C) 2021 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann + +package xlsx + +import ( + "bufio" + "context" + "database/sql" + "errors" + "fmt" + "io" + "log" + "os" + "strconv" + "strings" + + "github.com/360EntSecGroup-Skylar/excelize/v2" + "gopkg.in/yaml.v2" + + "github.com/PaesslerAG/gval" +) + +type Action struct { + Type string `yaml:"type"` + Actions []*Action `yaml:"actions"` + Location []string `yaml:"location"` + Source string `yaml:"source"` + Destination string `yaml:"destination"` + Statement string `yaml:"statement"` + Vars []string `yaml:"vars"` + Name string `yaml:"name"` + Expr string `yaml:"expr"` +} + +type frame struct { + res *sqlResult + index int +} + +type sheetAxis struct { + sheet string + axis string +} + +type cellValue struct { + value string + err error +} + +type executor struct { + ctx context.Context + tx *sql.Tx + template *excelize.File + keep map[string]bool + expressions map[string]gval.Evaluable + sourceSheet string + destinationSheet string + frames []frame + // Fetching formulas out of cells is very expensive so we cache them. + formulaCache map[sheetAxis]cellValue +} + +type area struct { + x1 int + y1 int + x2 int + y2 int + mc excelize.MergeCell +} + +func mergeCellToArea(mc excelize.MergeCell) (area, error) { + x1, y1, err := excelize.CellNameToCoordinates(mc.GetStartAxis()) + if err != nil { + return area{}, err + } + x2, y2, err := excelize.CellNameToCoordinates(mc.GetEndAxis()) + if err != nil { + return area{}, err + } + return area{ + x1: x1, + y1: y1, + x2: x2, + y2: y2, + mc: mc, + }, nil +} + +func (a *area) contains(x, y int) bool { + return a.x1 <= x && x <= a.x2 && a.y1 <= y && y <= a.y2 +} + +func ActionFromFile(filename string) (*Action, error) { + f, err := os.Open(filename) + if err != nil { + return nil, err + } + defer f.Close() + return ActionFromReader(f) +} + +func ActionFromReader(r io.Reader) (*Action, error) { + action := new(Action) + err := yaml.NewDecoder(bufio.NewReader(r)).Decode(action) + return action, err +} + +func (a *Action) Execute( + ctx context.Context, + tx *sql.Tx, + template *excelize.File, +) error { + + //if f, err := os.Create("cpu.prof"); err == nil { + // pprof.StartCPUProfile(f) + // defer pprof.StopCPUProfile() + //} + + e := executor{ + ctx: ctx, + tx: tx, + template: template, + keep: map[string]bool{}, + expressions: map[string]gval.Evaluable{}, + formulaCache: map[sheetAxis]cellValue{}, + } + + oldSheets := template.GetSheetList() + + if err := e.dispatch(a); err != nil { + return err + } + + for _, sheet := range oldSheets { + if !e.keep[sheet] { + template.DeleteSheet(sheet) + } + } + return nil +} + +var reused int + +func (e *executor) getCellFormula(sheet, axis string) (string, error) { + var ( + k = sheetAxis{sheet: sheet, axis: axis} + v cellValue + ok bool + ) + if v, ok = e.formulaCache[k]; !ok { + v.value, v.err = e.template.GetCellFormula(sheet, axis) + e.formulaCache[k] = v + } + return v.value, v.err +} + +func (e *executor) setCellFormula(sheet, axis, formula string) { + e.formulaCache[sheetAxis{sheet: sheet, axis: axis}] = cellValue{value: formula} + e.template.SetCellFormula(sheet, axis, formula) +} + +func (e *executor) dispatch(action *Action) error { + if len(action.Vars) > 0 { + e.pushVars(action.Vars) + defer e.popFrame() + } + switch action.Type { + case "sheet": + return e.sheet(action) + case "copy": + return e.copy(action) + case "select": + return e.sel(action) + case "assign": + return e.assign(action) + case "": + return e.actions(action) + } + return fmt.Errorf("unknown type '%s'", action.Type) +} + +func (e *executor) pushVars(vars []string) { + e.frames = append(e.frames, frame{ + res: &sqlResult{ + columns: vars, + rows: [][]interface{}{make([]interface{}, len(vars))}, + }, + }) +} + +func (e *executor) popFrame() { + n := len(e.frames) + e.frames[n-1].res = nil + e.frames = e.frames[:n-1] +} + +func (e *executor) assign(action *Action) error { + if action.Name == "" { + return errors.New("missing name in assign") + } + if action.Expr == "" { + return errors.New("missing expr in assign") + } + + for i := len(e.frames) - 1; i >= 0; i-- { + fr := &e.frames[i] + if idx := fr.res.find(action.Name); idx >= 0 { + f, err := e.expr(action.Expr) + if err != nil { + return err + } + value, err := f(e.ctx, e.vars()) + if err != nil { + return err + } + fr.res.rows[fr.index][idx] = value + break + } + } + return e.actions(action) +} + +func order(a, b int) (int, int) { + if a < b { + return a, b + } + return b, a +} + +func (e *executor) copy(action *Action) error { + if n := len(action.Location); !(n == 1 || n == 2) { + return fmt.Errorf("length location = %d (expect 1 or 2)", + len(action.Location)) + } + + vars := e.vars() + + var err error + expand := func(s string) string { + if err == nil { + s, err = e.expand(s, vars) + } + return s + } + split := func(s string) (int, int) { + var x, y int + if err == nil { + x, y, err = excelize.CellNameToCoordinates(s) + } + return x, y + } + + var location []string + if len(action.Location) == 1 { + location = []string{action.Location[0], action.Location[0]} + } else { + location = action.Location + } + + var destination string + if action.Destination == "" { + destination = location[0] + } else { + destination = action.Destination + } + + var ( + s1 = expand(location[0]) + s2 = expand(location[1]) + d1 = expand(destination) + sx1, sy1 = split(s1) + sx2, sy2 = split(s2) + dx1, dy1 = split(d1) + ) + if err != nil { + return err + } + sx1, sx2 = order(sx1, sx2) + sy1, sy2 = order(sy1, sy2) + + var areas []area + + //log.Println("merged cells") + if mcs, err := e.template.GetMergeCells(e.sourceSheet); err == nil { + areas = make([]area, 0, len(mcs)) + for _, mc := range mcs { + if a, err := mergeCellToArea(mc); err == nil { + areas = append(areas, a) + } + } + } + + for y, i := sy1, 0; y <= sy2; y, i = y+1, i+1 { + nextX: + for x, j := sx1, 0; x <= sx2; x, j = x+1, j+1 { + + // check if cell is part of a merged cell + for k := range areas { + area := &areas[k] + + if area.contains(x, y) { + ofsX := x - area.x1 + ofsY := y - area.y1 + + sx := dx1 + j - ofsX + sy := dy1 + i - ofsY + ex := sx + (area.x2 - area.x1) + ey := sy + (area.y2 - area.y1) + + // Copy over attributes + for l := 0; l <= area.x2-area.x1; l++ { + for m := 0; m <= area.y2-area.y1; m++ { + src, err1 := excelize.CoordinatesToCellName(area.x1+l, area.y1+m) + dst, err2 := excelize.CoordinatesToCellName(sx+l, sy+m) + if err1 != nil || err2 != nil { + continue + } + if s, err := e.template.GetCellStyle(e.sourceSheet, src); err == nil { + e.template.SetCellStyle(e.destinationSheet, dst, dst, s) + } + if s, err := e.getCellFormula(e.sourceSheet, src); err == nil { + e.setCellFormula(e.destinationSheet, dst, s) + } + } + } + + dst, err := excelize.CoordinatesToCellName(sx, sy) + if err != nil { + continue nextX + } + + // Copy over expanded text + if v, err := e.typedExpand(area.mc.GetCellValue(), vars); err == nil { + e.template.SetCellValue(e.destinationSheet, dst, v) + } + + // Finally merge the cells + if end, err := excelize.CoordinatesToCellName(ex, ey); err == nil { + e.template.MergeCell(e.destinationSheet, dst, end) + } + + continue nextX + } + } + + // Regular cell + + src, err := excelize.CoordinatesToCellName(x, y) + if err != nil { + continue + } + dst, err := excelize.CoordinatesToCellName(dx1+j, dy1+i) + if err != nil { + continue + } + + cn, err := excelize.ColumnNumberToName(x) + if err != nil { + continue + } + + cw, err := e.template.GetColWidth(e.sourceSheet, cn) + if err != nil { + continue + } + + rh, err := e.template.GetRowHeight(e.sourceSheet, y) + if err != nil { + continue + } + + dc, err := excelize.ColumnNumberToName(dx1 + j) + if err != nil { + continue + } + + if e.template.SetColWidth(e.destinationSheet, dc, dc, cw) != nil { + continue + } + + if e.template.SetRowHeight(e.destinationSheet, dy1+i, rh) != nil { + continue + } + + if s, err := e.template.GetCellStyle(e.sourceSheet, src); err == nil { + e.template.SetCellStyle(e.destinationSheet, dst, dst, s) + } + if s, err := e.getCellFormula(e.sourceSheet, src); err == nil { + e.setCellFormula(e.destinationSheet, dst, s) + } + if s, err := e.template.GetCellValue(e.sourceSheet, src); err == nil { + if v, err := e.typedExpand(s, vars); err == nil { + e.template.SetCellValue(e.destinationSheet, dst, v) + } + } + } + } + + return nil +} + +func (e *executor) sel(action *Action) error { + vars := e.vars() + + eval := func(x string) (interface{}, error) { + f, err := e.expr(x) + if err != nil { + return nil, err + } + return f(e.ctx, vars) + } + + res, err := query(e.ctx, e.tx, action.Statement, eval) + if err != nil { + return err + } + + e.frames = append(e.frames, frame{res: res}) + defer e.popFrame() + + for i := range res.rows { + e.frames[len(e.frames)-1].index = i + if err := e.actions(action); err != nil { + return err + } + } + + return nil +} + +func (e *executor) actions(action *Action) error { + for _, a := range action.Actions { + if err := e.dispatch(a); err != nil { + return err + } + } + return nil +} + +func (e *executor) sheet(action *Action) error { + + vars := e.vars() + source, err := e.expand(action.Source, vars) + if err != nil { + return err + } + + srcIdx := e.template.GetSheetIndex(source) + if srcIdx == -1 { + return fmt.Errorf("sheet '%s' not found", source) + } + + destination := action.Destination + if destination == "" { // same as source + e.keep[source] = true + destination = source + } else { // new sheet + destination, err = e.expand(destination, vars) + if err != nil { + return err + } + dstIdx := e.template.NewSheet(destination) + if len(action.Actions) == 0 { + // Only copy if there are no explicit instructions. + if err := e.template.CopySheet(srcIdx, dstIdx); err != nil { + return err + } + } + } + + if len(action.Actions) > 0 { + pSrc, pDst := e.sourceSheet, e.destinationSheet + defer func() { + e.sourceSheet, e.destinationSheet = pSrc, pDst + }() + e.sourceSheet, e.destinationSheet = source, destination + return e.actions(action) + } + + // Simple filling + + // "{{" only as a quick filter + result, err := e.template.SearchSheet(destination, "{{", true) + if err != nil { + return err + } + for _, axis := range result { + value, err := e.template.GetCellValue(destination, axis) + if err != nil { + return err + } + nvalue, err := e.typedExpand(value, vars) + if err != nil { + return err + } + if err := e.template.SetCellValue(destination, axis, nvalue); err != nil { + return err + } + } + + return nil +} + +func columnToNum(col interface{}) interface{} { + var name string + switch v := col.(type) { + case string: + name = v + default: + name = fmt.Sprintf("%v", col) + } + num, err := excelize.ColumnNameToNumber(name) + if err != nil { + log.Printf("error: invalid column name '%v'\n", col) + return 1 + } + return num +} + +func asInt(i interface{}) (int, error) { + switch v := i.(type) { + case int: + return v, nil + case int8: + return int(v), nil + case int16: + return int(v), nil + case int32: + return int(v), nil + case int64: + return int(v), nil + case float32: + return int(v), nil + case float64: + return int(v), nil + case string: + return strconv.Atoi(v) + default: + return 0, fmt.Errorf("invalid int '%v'", i) + } +} + +func coord2cell(ix, iy interface{}) interface{} { + x, err := asInt(ix) + if err != nil { + log.Printf("error: invalid x value: %v\n", err) + return "A1" + } + y, err := asInt(iy) + if err != nil { + log.Printf("error: invalid y value: %v\n", err) + return "A1" + } + + cell, err := excelize.CoordinatesToCellName(x, y) + if err != nil { + log.Printf("error: invalid cell coord (%d, %d)\n", x, y) + return "A1" + } + return cell +} + +var templateLang = gval.Full( + gval.Function("column2num", columnToNum), + gval.Function("coord2cell", coord2cell), +) + +func (e *executor) expr(x string) (gval.Evaluable, error) { + if f := e.expressions[x]; f != nil { + return f, nil + } + f, err := templateLang.NewEvaluable(x) + if err != nil { + return nil, err + } + e.expressions[x] = f + return f, nil +} + +func (e *executor) vars() map[string]interface{} { + vars := map[string]interface{}{} + if len(e.frames) > 0 { + vars["row_number"] = e.frames[len(e.frames)-1].index + } + for i := len(e.frames) - 1; i >= 0; i-- { + fr := &e.frames[i] + for j, n := range fr.res.columns { + if _, found := vars[n]; !found { + vars[n] = fr.res.rows[fr.index][j] + } + } + } + return vars +} + +func (e *executor) expand( + str string, + vars map[string]interface{}, +) (string, error) { + + var err error + + replace := func(s string) string { + if err != nil { + return "" + } + var eval gval.Evaluable + if eval, err = e.expr(strings.TrimSpace(s)); err != nil { + return "" + } + s, err = eval.EvalString(e.ctx, vars) + if err != nil { + log.Printf("error: '%s' '%s' %v\n", str, s, err) + } + return s + } + + str = handlebars(str, replace) + return str, err +} + +func (e *executor) typedExpand( + str string, + vars map[string]interface{}, +) (interface{}, error) { + + var ( + err error + repCount int + last interface{} + ) + + replace := func(s string) string { + if err != nil { + return "" + } + var eval gval.Evaluable + if eval, err = e.expr(strings.TrimSpace(s)); err != nil { + return "" + } + repCount++ + last, err = eval(e.ctx, vars) + if err != nil { + log.Printf("error: '%s' '%s' %v\n", str, s, err) + } + return fmt.Sprintf("%v", last) + } + + nstr := handlebars(str, replace) + + if err != nil { + return nil, err + } + + if repCount == 1 && + strings.HasPrefix(str, "{{") && + strings.HasSuffix(str, "}}") { + return last, nil + } + return nstr, nil +} diff -r 4a6feb5d3727 -r 47c2ca05e8ec report-templates/data-quality-report.xlsx Binary file report-templates/data-quality-report.xlsx has changed diff -r 4a6feb5d3727 -r 47c2ca05e8ec report-templates/data-quality-report.yaml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/report-templates/data-quality-report.yaml Wed Jul 07 11:44:40 2021 +0200 @@ -0,0 +1,119 @@ +actions: +# One sheet per CC +- type: select + statement: > + SELECT cc FROM + (VALUES ('AT'), ('SK'), ('HU'), ('HR'), ('RS'), ('BG'), ('RO') ) + AS t (cc); + actions: + - type: sheet + source: CCTmpl + destination: "{{ cc }}" + vars: [last_row, column_number, offset] + actions: + # Header: + - type: copy + location: [A1,A5] + # BN names + - type: select + statement: > + SELECT DISTINCT objnam AS bnnam + FROM waterway.dqr_bottleneck_stats + WHERE cc = {{ cc }} ORDER BY objnam; + actions: + - type: assign + name: last_row + expr: (row_number ?? 0) + 6 + - type: copy + location: [A6] + destination: A{{ last_row }} + - type: copy + location: [A7] + destination: A{{ (last_row ?? 6) + 1 }} + # Gen Months + - type: select + statement: > + SELECT to_char(d, 'Month YYYY') AS month, d::date + FROM generate_series( '2019-10-01'::date, + now() - interval '1 day', + '1 month'::interval ) d; + actions: + - type: assign + name: column_number + expr: column2num("B") + (row_number ?? 0) * 2 + - type: copy + location: [B4,C5] + destination: '{{ coord2cell(column_number, 4) }}' + # BN SR-Count + - type: select + statement: > + SELECT objnam, srcnt, fwacnt + FROM waterway.dqr_bottleneck_stats + WHERE cc = {{ cc }} AND month = {{ d }} + ORDER BY objnam; + actions: + - type: assign + name: last_row + expr: (row_number ?? 0) + 6 + - type: copy + location: [B6,C6] + destination: '{{ coord2cell(column_number, last_row) }}' + - type: copy + location: [B7,C7] + destination: '{{ coord2cell(column_number, (last_row ?? 0) + 1) }}' + - type: assign + name: offset + expr: (last_row ?? 0) + 3 + #-------------------------------------------------------------------------- + # GAUGES + # Header: + - type: copy + location: [A9] + destination: A{{ offset }} + # Gauges names + - type: select + statement: > + SELECT DISTINCT objname AS gnam FROM waterway.dqr_gauge_stats + WHERE cc = {{ cc }} ORDER BY objname; + actions: + - type: assign + name: last_row + expr: (row_number ?? 0) + offset + 1 + - type: copy + location: [A10] + destination: A{{ last_row }} + - type: copy + location: [A11] + destination: A{{ (last_row ?? 10) + 1 }} + # Gen Months + - type: select + statement: > + SELECT to_char(d, 'Month YYYY') AS month, d::date + FROM generate_series( '2019-10-01'::date, + now() - interval '1 day', + '1 month'::interval ) d; + actions: + - type: assign + name: column_number + expr: column2num("B") + (row_number ?? 0) + - type: copy + location: [B9] + destination: '{{ coord2cell(column_number, offset) }}' + # BN SR-Count + - type: select + statement: > + SELECT objname, daynodata + FROM waterway.dqr_gauge_stats + WHERE cc = {{ cc }} AND month = {{ d }} + ORDER BY objname; + actions: + - type: assign + name: last_row + expr: (row_number ?? 0) + offset + 1 + - type: copy + location: [B10] + destination: '{{ coord2cell(column_number, last_row) }}' + - type: copy + location: [B11] + destination: '{{ coord2cell(column_number, (last_row ?? 10) + 1) }}' + diff -r 4a6feb5d3727 -r 47c2ca05e8ec schema/auth.sql --- a/schema/auth.sql Sun Jul 04 11:37:37 2021 +0200 +++ b/schema/auth.sql Wed Jul 07 11:44:40 2021 +0200 @@ -62,6 +62,7 @@ GRANT UPDATE ON sys_admin.published_services TO sys_admin; GRANT INSERT, DELETE, UPDATE ON sys_admin.password_reset_requests TO sys_admin; GRANT DELETE ON import.imports, import.import_logs TO sys_admin; +GRANT SELECT, INSERT, DELETE, UPDATE ON sys_admin.stats_updates TO sys_admin; -- -- Privileges assigned directly to metamorph diff -r 4a6feb5d3727 -r 47c2ca05e8ec schema/gemma.sql --- a/schema/gemma.sql Sun Jul 04 11:37:37 2021 +0200 +++ b/schema/gemma.sql Wed Jul 07 11:44:40 2021 +0200 @@ -384,7 +384,8 @@ -- keep username length compatible with role identifier country char(2) NOT NULL REFERENCES countries, map_extent box2d NOT NULL, - email_address varchar NOT NULL + email_address varchar NOT NULL, + report_reciever boolean NOT NULL DEFAULT false ) ; @@ -445,6 +446,12 @@ UNIQUE (group_name, schema, name, ord), FOREIGN KEY(schema, name) REFERENCES published_services ) + + -- Table to store scripts which updates aggregated data. + CREATE TABLE stats_updates ( + name varchar PRIMARY key, + script TEXT NULL + ) ; @@ -492,7 +499,8 @@ CAST('' AS varchar) AS pw, p.country, p.map_extent, - p.email_address + p.email_address, + p.report_reciever FROM internal.user_profiles p JOIN pg_roles u ON p.username = u.rolname JOIN pg_auth_members a ON u.oid = a.member diff -r 4a6feb5d3727 -r 47c2ca05e8ec schema/install-db.sh --- a/schema/install-db.sh Sun Jul 04 11:37:37 2021 +0200 +++ b/schema/install-db.sh Wed Jul 07 11:44:40 2021 +0200 @@ -126,6 +126,7 @@ -f "$BASEDIR/roles.sql" \ -f "$BASEDIR/isrs.sql" \ -f "$BASEDIR/gemma.sql" \ + -f "$BASEDIR/reports.sql" \ -f "$BASEDIR/geo_functions.sql" \ -f "$BASEDIR/search_functions.sql" \ -f "$BASEDIR/geonames.sql" \ diff -r 4a6feb5d3727 -r 47c2ca05e8ec schema/reports.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/schema/reports.sql Wed Jul 07 11:44:40 2021 +0200 @@ -0,0 +1,89 @@ +-- This is Free Software under GNU Affero General Public License v >= 3.0 +-- without warranty, see README.md and license for details. + +-- SPDX-License-Identifier: AGPL-3.0-or-later +-- License-Filename: LICENSES/AGPL-3.0.txt + +-- Copyright (C) 2021 by via donau +-- – Österreichische Wasserstraßen-Gesellschaft mbH +-- Software engineering by Intevation GmbH + +-- Author(s): +-- * Sascha Wilde + + +-- Materialized Views with statistical data for data quality reports +CREATE MATERIALIZED VIEW waterway.dqr_gauge_stats AS +WITH d AS ( SELECT ym::date, d::date + FROM generate_series( '2019-10-01'::date, + now() - interval '1 day', + '1 month'::interval ) ym, + generate_series( ym, + ( ym + interval '1 month' + - interval '1 day'), + '1 day'::interval ) d ), + g AS ( SELECT d.ym, d.d AS day, (location).country_code AS cc, + objname, array_agg(distinct(location)) AS locations + FROM waterway.gauges,d + GROUP BY objname,d.d,(location).country_code,d.ym ), + measure AS ( + SELECT g.ym, g.cc, g.objname, g.day, + CASE WHEN count(measure_date) = 0 + THEN 1 ELSE 0 END AS missing + FROM g + LEFT OUTER JOIN waterway.gauge_measurements gm + ON ARRAY[location] <@ g.locations + AND g.day <= measure_date + AND measure_date < (g.day + interval '1 day') + GROUP BY g.objname,g.day,g.ym,g.cc ) + SELECT cc, ym AS month, objname, sum(missing) AS daynodata + FROM measure + GROUP BY cc, ym, objname; + +CREATE MATERIALIZED VIEW waterway.dqr_bottleneck_stats AS +WITH d AS ( SELECT ym::date + FROM generate_series( '2019-10-01'::date, + now() - interval '1 day', + '1 month'::interval ) ym), + bn AS ( SELECT DISTINCT objnam, responsible_country AS cc + FROM waterway.bottlenecks ), + bid AS (SELECT objnam, array_agg(distinct(bottleneck_id)) AS ids + FROM waterway.bottlenecks GROUP BY objnam) + SELECT bn.cc, d.ym AS month, bid.objnam, + COALESCE(count(distinct(sr.date_info)),0) AS srcnt, + COALESCE(count(distinct(efa.measure_date)),0) AS fwacnt + FROM bn, bid + CROSS JOIN d + LEFT OUTER JOIN waterway.sounding_results sr + ON ARRAY[sr.bottleneck_id] <@ bid.ids + AND d.ym <= sr.date_info + AND sr.date_info < (d.ym + interval '1 month') + LEFT OUTER JOIN waterway.fairway_availability fa + ON ARRAY[fa.bottleneck_id] <@ bid.ids + LEFT OUTER JOIN waterway.effective_fairway_availability efa + ON fairway_availability_id = fa.id + AND d.ym <= efa.measure_date + AND efa.measure_date < (d.ym + interval '1 month') + AND efa.measure_type = 'Measured' + WHERE bid.objnam = bn.objnam + GROUP BY bn.cc, d.ym, bid.objnam; + +-- We need a wrapper procedure with owner rights for +-- the refresh, as (from the PGSQL manual): "REFRESH MATERIALIZED VIEW +-- completely replaces the contents of a materialized view. To execute +-- this command you must be the owner of the materialized view."" + +CREATE OR REPLACE PROCEDURE sys_admin.update_dqr_stats() +LANGUAGE plpgsql AS $$ +BEGIN + EXECUTE 'REFRESH MATERIALIZED VIEW waterway.dqr_bottleneck_stats'; + EXECUTE 'REFRESH MATERIALIZED VIEW waterway.dqr_gauge_stats'; +END; +$$ SECURITY DEFINER; + +GRANT EXECUTE ON PROCEDURE sys_admin.update_dqr_stats() TO sys_admin; + +-- Config update statement +INSERT INTO sys_admin.stats_updates + VALUES ('Data quality report', + 'CALL sys_admin.update_dqr_stats();'); diff -r 4a6feb5d3727 -r 47c2ca05e8ec schema/updates/1450/01.report_reciever.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/schema/updates/1450/01.report_reciever.sql Wed Jul 07 11:44:40 2021 +0200 @@ -0,0 +1,101 @@ +ALTER TABLE internal.user_profiles + ADD COLUMN report_reciever boolean NOT NULL DEFAULT false; + +CREATE OR REPLACE VIEW users.list_users WITH (security_barrier) AS + SELECT + r.rolname, + p.username, + CAST('' AS varchar) AS pw, + p.country, + p.map_extent, + p.email_address, + p.report_reciever + FROM internal.user_profiles p + JOIN pg_roles u ON p.username = u.rolname + JOIN pg_auth_members a ON u.oid = a.member + JOIN pg_roles r ON a.roleid = r.oid + WHERE p.username = current_user + OR pg_has_role('waterway_admin', 'MEMBER') + AND p.country = ( + SELECT country FROM internal.user_profiles + WHERE username = current_user) + AND r.rolname <> 'sys_admin' + OR pg_has_role('sys_admin', 'MEMBER') +; + +CREATE OR REPLACE FUNCTION internal.update_user() RETURNS trigger +AS $$ +DECLARE + cur_username varchar; +BEGIN + cur_username = OLD.username; + + IF NEW.username <> cur_username + THEN + EXECUTE format( + 'ALTER ROLE %I RENAME TO %I', cur_username, NEW.username); + cur_username = NEW.username; + END IF; + + UPDATE internal.user_profiles p + SET (username, country, map_extent, email_address, report_reciever) + = (NEW.username, NEW.country, NEW.map_extent, NEW.email_address, NEW.report_reciever) + WHERE p.username = cur_username; + + IF NEW.rolname <> OLD.rolname + THEN + EXECUTE format( + 'REVOKE %I FROM %I', OLD.rolname, cur_username); + EXECUTE format( + 'GRANT %I TO %I', NEW.rolname, cur_username); + END IF; + + IF NEW.pw IS NOT NULL AND NEW.pw <> '' + THEN + EXECUTE format( + 'ALTER ROLE %I PASSWORD %L', + cur_username, + internal.check_password(NEW.pw)); + END IF; + + -- Do not leak new password + NEW.pw = ''; + RETURN NEW; +END; +$$ + LANGUAGE plpgsql + SECURITY DEFINER; + +CREATE OR REPLACE FUNCTION internal.create_user() RETURNS trigger +AS $$ +BEGIN + IF NEW.map_extent IS NULL + THEN + NEW.map_extent = ST_Extent(CAST(area AS geometry)) + FROM users.stretches st + JOIN users.stretch_countries stc ON stc.stretch_id = st.id + WHERE stc.country = NEW.country; + END IF; + + IF NEW.username IS NOT NULL + -- otherwise let the constraint on user_profiles speak + THEN + EXECUTE format( + 'CREATE ROLE %I IN ROLE %I LOGIN PASSWORD %L', + NEW.username, + NEW.rolname, + internal.check_password(NEW.pw)); + END IF; + + INSERT INTO internal.user_profiles ( + username, country, map_extent, email_address, report_reciever) + VALUES (NEW.username, NEW.country, NEW.map_extent, NEW.email_address, NEW.report_reciever); + + -- Do not leak new password + NEW.pw = ''; + RETURN NEW; +END; +$$ + LANGUAGE plpgsql + SECURITY DEFINER; + diff -r 4a6feb5d3727 -r 47c2ca05e8ec schema/updates/1451/01.stats_updates.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/schema/updates/1451/01.stats_updates.sql Wed Jul 07 11:44:40 2021 +0200 @@ -0,0 +1,6 @@ +CREATE TABLE sys_admin.stats_updates ( + name varchar PRIMARY key, + script TEXT NULL +); + +GRANT SELECT, INSERT, DELETE, UPDATE ON sys_admin.stats_updates TO sys_admin; diff -r 4a6feb5d3727 -r 47c2ca05e8ec schema/updates/1452/01.report_views.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/schema/updates/1452/01.report_views.sql Wed Jul 07 11:44:40 2021 +0200 @@ -0,0 +1,58 @@ +-- Materialized Views with statistical data for data quality reports +CREATE MATERIALIZED VIEW waterway.dqr_gauge_stats AS +WITH d AS ( SELECT ym::date, d::date + FROM generate_series( '2019-10-01'::date, + now() - interval '1 day', + '1 month'::interval ) ym, + generate_series( ym, + ( ym + interval '1 month' + - interval '1 day'), + '1 day'::interval ) d ), + g AS ( SELECT d.ym, d.d AS day, (location).country_code AS cc, + objname, array_agg(distinct(location)) AS locations + FROM waterway.gauges,d + GROUP BY objname,d.d,(location).country_code,d.ym ), + measure AS ( + SELECT g.ym, g.cc, g.objname, g.day, + CASE WHEN count(measure_date) = 0 + THEN 1 ELSE 0 END AS missing + FROM g + LEFT OUTER JOIN waterway.gauge_measurements gm + ON ARRAY[location] <@ g.locations + AND g.day <= measure_date + AND measure_date < (g.day + interval '1 day') + GROUP BY g.objname,g.day,g.ym,g.cc ) + SELECT cc, ym AS month, objname, sum(missing) AS daynodata + FROM measure + GROUP BY cc, ym, objname; + +CREATE MATERIALIZED VIEW waterway.dqr_bottleneck_stats AS +WITH d AS ( SELECT ym::date + FROM generate_series( '2019-10-01'::date, + now() - interval '1 day', + '1 month'::interval ) ym), + bn AS ( SELECT DISTINCT objnam, responsible_country AS cc + FROM waterway.bottlenecks ), + bid AS (SELECT objnam, array_agg(distinct(bottleneck_id)) AS ids + FROM waterway.bottlenecks GROUP BY objnam) + SELECT bn.cc, d.ym AS month, bid.objnam, + COALESCE(count(distinct(sr.date_info)),0) AS srcnt, + COALESCE(count(distinct(efa.measure_date)),0) AS fwacnt + FROM bn, bid + CROSS JOIN d + LEFT OUTER JOIN waterway.sounding_results sr + ON ARRAY[sr.bottleneck_id] <@ bid.ids + AND d.ym <= sr.date_info + AND sr.date_info < (d.ym + interval '1 month') + LEFT OUTER JOIN waterway.fairway_availability fa + ON ARRAY[fa.bottleneck_id] <@ bid.ids + LEFT OUTER JOIN waterway.effective_fairway_availability efa + ON fairway_availability_id = fa.id + AND d.ym <= efa.measure_date + AND efa.measure_date < (d.ym + interval '1 month') + AND efa.measure_type = 'Measured' + WHERE bid.objnam = bn.objnam + GROUP BY bn.cc, d.ym, bid.objnam; + +-- Refresh access rights! +GRANT SELECT on ALL tables in schema waterway TO waterway_user; diff -r 4a6feb5d3727 -r 47c2ca05e8ec schema/updates/1453/01.update_dqr_stats.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/schema/updates/1453/01.update_dqr_stats.sql Wed Jul 07 11:44:40 2021 +0200 @@ -0,0 +1,19 @@ +-- We need a wrapper procedure with owner rights for +-- the refresh, as (from the PGSQL manual): "REFRESH MATERIALIZED VIEW +-- completely replaces the contents of a materialized view. To execute +-- this command you must be the owner of the materialized view."" + +CREATE OR REPLACE PROCEDURE sys_admin.update_dqr_stats() +LANGUAGE plpgsql AS $$ +BEGIN + EXECUTE 'REFRESH MATERIALIZED VIEW waterway.dqr_bottleneck_stats'; + EXECUTE 'REFRESH MATERIALIZED VIEW waterway.dqr_gauge_stats'; +END; +$$ SECURITY DEFINER; + +GRANT EXECUTE ON PROCEDURE sys_admin.update_dqr_stats() TO sys_admin; + +-- Config update statement +INSERT INTO sys_admin.stats_updates + VALUES ('Data quality report', + 'CALL sys_admin.update_dqr_stats();'); diff -r 4a6feb5d3727 -r 47c2ca05e8ec schema/version.sql --- a/schema/version.sql Sun Jul 04 11:37:37 2021 +0200 +++ b/schema/version.sql Wed Jul 07 11:44:40 2021 +0200 @@ -1,1 +1,1 @@ -INSERT INTO gemma_schema_version(version) VALUES (1441); +INSERT INTO gemma_schema_version(version) VALUES (1453);