Add support for timezone export
This commit is contained in:
@@ -860,6 +860,12 @@
|
|||||||
"type": "[]string",
|
"type": "[]string",
|
||||||
"required": true,
|
"required": true,
|
||||||
"title": "Fields to export"
|
"title": "Fields to export"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "timezone",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"title": "Convert times to this timezone"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -190,6 +190,12 @@
|
|||||||
"required": true,
|
"required": true,
|
||||||
"title": "Fields to export",
|
"title": "Fields to export",
|
||||||
"type": "[]string"
|
"type": "[]string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "timezone",
|
||||||
|
"required": false,
|
||||||
|
"title": "Convert times to this timezone",
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"path": [
|
"path": [
|
||||||
|
|||||||
@@ -27,12 +27,14 @@ type (
|
|||||||
w FlatWriter
|
w FlatWriter
|
||||||
ff []field
|
ff []field
|
||||||
u userFinder
|
u userFinder
|
||||||
|
tz string
|
||||||
}
|
}
|
||||||
|
|
||||||
structuredEncoder struct {
|
structuredEncoder struct {
|
||||||
w StructuredEncoder
|
w StructuredEncoder
|
||||||
ff []field
|
ff []field
|
||||||
u userFinder
|
u userFinder
|
||||||
|
tz string
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -49,15 +51,40 @@ func MakeFields(nn ...string) []field {
|
|||||||
return ff
|
return ff
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func preprocessHeader(hh []field, tz string) []field {
|
||||||
|
nhh := make([]field, 0)
|
||||||
|
|
||||||
|
// We need to prepare additional header fields for exporting
|
||||||
|
if tz != "" && tz != "UTC" {
|
||||||
|
for _, f := range hh {
|
||||||
|
switch f.name {
|
||||||
|
case "createdAt",
|
||||||
|
"updatedAt",
|
||||||
|
"deletedAt":
|
||||||
|
|
||||||
|
nhh = append(nhh, f, field{name: f.name + "_date"}, field{name: f.name + "_time"})
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
nhh = append(nhh, f)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return hh
|
||||||
|
}
|
||||||
|
return nhh
|
||||||
|
}
|
||||||
|
|
||||||
func MultiValueField(name string) field {
|
func MultiValueField(name string) field {
|
||||||
return field{name: name, encodeAllMulti: true}
|
return field{name: name, encodeAllMulti: true}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFlatWriter(w FlatWriter, header bool, u userFinder, ff ...field) *flatWriter {
|
func NewFlatWriter(w FlatWriter, header bool, u userFinder, tz string, ff ...field) *flatWriter {
|
||||||
f := &flatWriter{
|
f := &flatWriter{
|
||||||
w: w,
|
w: w,
|
||||||
ff: ff,
|
ff: preprocessHeader(ff, tz),
|
||||||
u: u,
|
u: u,
|
||||||
|
tz: tz,
|
||||||
}
|
}
|
||||||
|
|
||||||
if header {
|
if header {
|
||||||
@@ -80,11 +107,13 @@ func (enc flatWriter) writeHeader() {
|
|||||||
_ = enc.w.Write(ss)
|
_ = enc.w.Write(ss)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStructuredEncoder(w StructuredEncoder, u userFinder, ff ...field) *structuredEncoder {
|
func NewStructuredEncoder(w StructuredEncoder, u userFinder, tz string, ff ...field) *structuredEncoder {
|
||||||
return &structuredEncoder{
|
return &structuredEncoder{
|
||||||
w: w,
|
w: w,
|
||||||
|
// No need for additional timezone headers, since the output is structured
|
||||||
ff: ff,
|
ff: ff,
|
||||||
u: u,
|
u: u,
|
||||||
|
tz: tz,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,15 +13,17 @@ type (
|
|||||||
w io.Writer
|
w io.Writer
|
||||||
ff []field
|
ff []field
|
||||||
u userFinder
|
u userFinder
|
||||||
|
tz string
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewExcelizeEncoder(w io.Writer, header bool, u userFinder, ff ...field) *excelizeEncoder {
|
func NewExcelizeEncoder(w io.Writer, header bool, u userFinder, tz string, ff ...field) *excelizeEncoder {
|
||||||
enc := &excelizeEncoder{
|
enc := &excelizeEncoder{
|
||||||
f: excelize.NewFile(),
|
f: excelize.NewFile(),
|
||||||
w: w,
|
w: w,
|
||||||
ff: ff,
|
ff: preprocessHeader(ff, tz),
|
||||||
u: u,
|
u: u,
|
||||||
|
tz: tz,
|
||||||
}
|
}
|
||||||
|
|
||||||
if header {
|
if header {
|
||||||
|
|||||||
+95
-12
@@ -8,15 +8,44 @@ import (
|
|||||||
"github.com/cortezaproject/corteza-server/compose/types"
|
"github.com/cortezaproject/corteza-server/compose/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
parsedTime struct {
|
||||||
|
field string
|
||||||
|
value string
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// Time formatter
|
// Time formatter
|
||||||
//
|
//
|
||||||
// Takes ptr to time.Time so we can conver both cases (value + ptr)
|
// Takes ptr to time.Time so we can conver both cases (value + ptr)
|
||||||
func fmtTime(tp *time.Time) string {
|
// The function also generates additional fields with included timezone.
|
||||||
|
func fmtTime(field string, tp *time.Time, tz string) (pt []*parsedTime, err error) {
|
||||||
if tp == nil {
|
if tp == nil {
|
||||||
return ""
|
return pt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return tp.UTC().Format(time.RFC3339)
|
pt = append(pt, &parsedTime{
|
||||||
|
field: field,
|
||||||
|
value: tp.UTC().Format(time.RFC3339),
|
||||||
|
})
|
||||||
|
if tz == "" || tz == "UTC" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loc, err := time.LoadLocation(tz)
|
||||||
|
if err != nil {
|
||||||
|
return pt, err
|
||||||
|
}
|
||||||
|
tt := tp.In(loc)
|
||||||
|
pt = append(pt,
|
||||||
|
&parsedTime{
|
||||||
|
field: field + "_date",
|
||||||
|
value: tt.Format("2006-01-02"),
|
||||||
|
}, &parsedTime{
|
||||||
|
field: field + "_time",
|
||||||
|
value: tt.Format("15:04:05"),
|
||||||
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func fmtUint64(u uint64) string {
|
func fmtUint64(u uint64) string {
|
||||||
@@ -49,6 +78,12 @@ func fmtSysUser(u uint64, finder userFinder) (string, error) {
|
|||||||
func (enc flatWriter) Record(r *types.Record) (err error) {
|
func (enc flatWriter) Record(r *types.Record) (err error) {
|
||||||
var out = make([]string, len(enc.ff))
|
var out = make([]string, len(enc.ff))
|
||||||
|
|
||||||
|
procTime := func(d []string, pts []*parsedTime, base int) {
|
||||||
|
for i, p := range pts {
|
||||||
|
d[base+i] = p.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for f, field := range enc.ff {
|
for f, field := range enc.ff {
|
||||||
switch field.name {
|
switch field.name {
|
||||||
case "recordID", "ID":
|
case "recordID", "ID":
|
||||||
@@ -68,21 +103,33 @@ func (enc flatWriter) Record(r *types.Record) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case "createdAt":
|
case "createdAt":
|
||||||
out[f] = fmtTime(&r.CreatedAt)
|
tt, err := fmtTime("createdAt", &r.CreatedAt, enc.tz)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
procTime(out, tt, f)
|
||||||
case "updatedBy":
|
case "updatedBy":
|
||||||
out[f], err = fmtSysUser(r.UpdatedBy, enc.u)
|
out[f], err = fmtSysUser(r.UpdatedBy, enc.u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case "updatedAt":
|
case "updatedAt":
|
||||||
out[f] = fmtTime(r.UpdatedAt)
|
tt, err := fmtTime("updatedAt", r.UpdatedAt, enc.tz)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
procTime(out, tt, f)
|
||||||
case "deletedBy":
|
case "deletedBy":
|
||||||
out[f], err = fmtSysUser(r.DeletedBy, enc.u)
|
out[f], err = fmtSysUser(r.DeletedBy, enc.u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case "deletedAt":
|
case "deletedAt":
|
||||||
out[f] = fmtTime(r.DeletedAt)
|
tt, err := fmtTime("deletedAt", r.DeletedAt, enc.tz)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
procTime(out, tt, f)
|
||||||
default:
|
default:
|
||||||
vv := r.Values.FilterByName(field.name)
|
vv := r.Values.FilterByName(field.name)
|
||||||
// @todo support for field.encodeAllMulti
|
// @todo support for field.encodeAllMulti
|
||||||
@@ -106,6 +153,12 @@ func (enc structuredEncoder) Record(r *types.Record) (err error) {
|
|||||||
c int
|
c int
|
||||||
)
|
)
|
||||||
|
|
||||||
|
procTime := func(d map[string]interface{}, pts []*parsedTime) {
|
||||||
|
for _, p := range pts {
|
||||||
|
d[p.field] = p.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, f := range enc.ff {
|
for _, f := range enc.ff {
|
||||||
switch f.name {
|
switch f.name {
|
||||||
case "recordID", "ID":
|
case "recordID", "ID":
|
||||||
@@ -128,7 +181,11 @@ func (enc structuredEncoder) Record(r *types.Record) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case "createdAt":
|
case "createdAt":
|
||||||
out[f.name] = fmtTime(&r.CreatedAt)
|
tt, err := fmtTime("createdAt", &r.CreatedAt, enc.tz)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
procTime(out, tt)
|
||||||
case "updatedBy":
|
case "updatedBy":
|
||||||
out[f.name], err = fmtSysUser(r.UpdatedBy, enc.u)
|
out[f.name], err = fmtSysUser(r.UpdatedBy, enc.u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -138,7 +195,11 @@ func (enc structuredEncoder) Record(r *types.Record) (err error) {
|
|||||||
if r.UpdatedAt == nil {
|
if r.UpdatedAt == nil {
|
||||||
out[f.name] = nil
|
out[f.name] = nil
|
||||||
} else {
|
} else {
|
||||||
out[f.name] = fmtTime(r.UpdatedAt)
|
tt, err := fmtTime("updatedAt", r.UpdatedAt, enc.tz)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
procTime(out, tt)
|
||||||
}
|
}
|
||||||
|
|
||||||
case "deletedBy":
|
case "deletedBy":
|
||||||
@@ -150,7 +211,11 @@ func (enc structuredEncoder) Record(r *types.Record) (err error) {
|
|||||||
if r.DeletedAt == nil {
|
if r.DeletedAt == nil {
|
||||||
out[f.name] = nil
|
out[f.name] = nil
|
||||||
} else {
|
} else {
|
||||||
out[f.name] = fmtTime(r.DeletedAt)
|
tt, err := fmtTime("deletedAt", r.DeletedAt, enc.tz)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
procTime(out, tt)
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -182,6 +247,12 @@ func (enc *excelizeEncoder) Record(r *types.Record) (err error) {
|
|||||||
enc.row++
|
enc.row++
|
||||||
var u string
|
var u string
|
||||||
|
|
||||||
|
procTime := func(pts []*parsedTime, base int) {
|
||||||
|
for i, p := range pts {
|
||||||
|
_ = enc.f.SetCellStr(enc.sheet(), enc.pos(base+i), p.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for p, f := range enc.ff {
|
for p, f := range enc.ff {
|
||||||
p++
|
p++
|
||||||
switch f.name {
|
switch f.name {
|
||||||
@@ -204,7 +275,11 @@ func (enc *excelizeEncoder) Record(r *types.Record) (err error) {
|
|||||||
}
|
}
|
||||||
_ = enc.f.SetCellStr(enc.sheet(), enc.pos(p), u)
|
_ = enc.f.SetCellStr(enc.sheet(), enc.pos(p), u)
|
||||||
case "createdAt":
|
case "createdAt":
|
||||||
_ = enc.f.SetCellStr(enc.sheet(), enc.pos(p), fmtTime(&r.CreatedAt))
|
tt, err := fmtTime("createdAt", &r.CreatedAt, enc.tz)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
procTime(tt, p)
|
||||||
case "updatedBy":
|
case "updatedBy":
|
||||||
u, err = fmtSysUser(r.UpdatedBy, enc.u)
|
u, err = fmtSysUser(r.UpdatedBy, enc.u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -212,7 +287,11 @@ func (enc *excelizeEncoder) Record(r *types.Record) (err error) {
|
|||||||
}
|
}
|
||||||
_ = enc.f.SetCellStr(enc.sheet(), enc.pos(p), u)
|
_ = enc.f.SetCellStr(enc.sheet(), enc.pos(p), u)
|
||||||
case "updatedAt":
|
case "updatedAt":
|
||||||
_ = enc.f.SetCellStr(enc.sheet(), enc.pos(p), fmtTime(r.UpdatedAt))
|
tt, err := fmtTime("updatedAt", r.UpdatedAt, enc.tz)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
procTime(tt, p)
|
||||||
case "deletedBy":
|
case "deletedBy":
|
||||||
u, err = fmtSysUser(r.DeletedBy, enc.u)
|
u, err = fmtSysUser(r.DeletedBy, enc.u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -220,7 +299,11 @@ func (enc *excelizeEncoder) Record(r *types.Record) (err error) {
|
|||||||
}
|
}
|
||||||
_ = enc.f.SetCellStr(enc.sheet(), enc.pos(p), u)
|
_ = enc.f.SetCellStr(enc.sheet(), enc.pos(p), u)
|
||||||
case "deletedAt":
|
case "deletedAt":
|
||||||
_ = enc.f.SetCellStr(enc.sheet(), enc.pos(p), fmtTime(r.DeletedAt))
|
tt, err := fmtTime("deletedAt", r.DeletedAt, enc.tz)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
procTime(tt, p)
|
||||||
default:
|
default:
|
||||||
vv := r.Values.FilterByName(f.name)
|
vv := r.Values.FilterByName(f.name)
|
||||||
if len(vv) > 0 {
|
if len(vv) > 0 {
|
||||||
|
|||||||
@@ -441,15 +441,15 @@ func (ctrl *Record) Export(ctx context.Context, r *request.RecordExport) (interf
|
|||||||
switch strings.ToLower(r.Ext) {
|
switch strings.ToLower(r.Ext) {
|
||||||
case "json", "jsonl", "ldjson", "ndjson":
|
case "json", "jsonl", "ldjson", "ndjson":
|
||||||
contentType = "application/jsonl"
|
contentType = "application/jsonl"
|
||||||
recordEncoder = encoder.NewStructuredEncoder(json.NewEncoder(w), uf, ff...)
|
recordEncoder = encoder.NewStructuredEncoder(json.NewEncoder(w), uf, r.Timezone, ff...)
|
||||||
|
|
||||||
case "csv":
|
case "csv":
|
||||||
contentType = "text/csv"
|
contentType = "text/csv"
|
||||||
recordEncoder = encoder.NewFlatWriter(csv.NewWriter(w), true, uf, ff...)
|
recordEncoder = encoder.NewFlatWriter(csv.NewWriter(w), true, uf, r.Timezone, ff...)
|
||||||
|
|
||||||
case "xlsx":
|
case "xlsx":
|
||||||
contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||||
recordEncoder = encoder.NewExcelizeEncoder(w, true, uf, ff...)
|
recordEncoder = encoder.NewExcelizeEncoder(w, true, uf, r.Timezone, ff...)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
http.Error(w, "unsupported format ("+r.Ext+")", http.StatusBadRequest)
|
http.Error(w, "unsupported format ("+r.Ext+")", http.StatusBadRequest)
|
||||||
|
|||||||
@@ -529,6 +529,10 @@ type RecordExport struct {
|
|||||||
rawFields []string
|
rawFields []string
|
||||||
Fields []string
|
Fields []string
|
||||||
|
|
||||||
|
hasTimezone bool
|
||||||
|
rawTimezone string
|
||||||
|
Timezone string
|
||||||
|
|
||||||
hasFilename bool
|
hasFilename bool
|
||||||
rawFilename string
|
rawFilename string
|
||||||
Filename string
|
Filename string
|
||||||
@@ -557,6 +561,7 @@ func (r RecordExport) Auditable() map[string]interface{} {
|
|||||||
|
|
||||||
out["filter"] = r.Filter
|
out["filter"] = r.Filter
|
||||||
out["fields"] = r.Fields
|
out["fields"] = r.Fields
|
||||||
|
out["timezone"] = r.Timezone
|
||||||
out["filename"] = r.Filename
|
out["filename"] = r.Filename
|
||||||
out["ext"] = r.Ext
|
out["ext"] = r.Ext
|
||||||
out["namespaceID"] = r.NamespaceID
|
out["namespaceID"] = r.NamespaceID
|
||||||
@@ -609,6 +614,11 @@ func (r *RecordExport) Fill(req *http.Request) (err error) {
|
|||||||
r.Fields = parseStrings(val)
|
r.Fields = parseStrings(val)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if val, ok := get["timezone"]; ok {
|
||||||
|
r.hasTimezone = true
|
||||||
|
r.rawTimezone = val
|
||||||
|
r.Timezone = val
|
||||||
|
}
|
||||||
r.hasFilename = true
|
r.hasFilename = true
|
||||||
r.rawFilename = chi.URLParam(req, "filename")
|
r.rawFilename = chi.URLParam(req, "filename")
|
||||||
r.Filename = chi.URLParam(req, "filename")
|
r.Filename = chi.URLParam(req, "filename")
|
||||||
@@ -1783,6 +1793,21 @@ func (r *RecordExport) GetFields() []string {
|
|||||||
return r.Fields
|
return r.Fields
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasTimezone returns true if timezone was set
|
||||||
|
func (r *RecordExport) HasTimezone() bool {
|
||||||
|
return r.hasTimezone
|
||||||
|
}
|
||||||
|
|
||||||
|
// RawTimezone returns raw value of timezone parameter
|
||||||
|
func (r *RecordExport) RawTimezone() string {
|
||||||
|
return r.rawTimezone
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTimezone returns casted value of timezone parameter
|
||||||
|
func (r *RecordExport) GetTimezone() string {
|
||||||
|
return r.Timezone
|
||||||
|
}
|
||||||
|
|
||||||
// HasFilename returns true if filename was set
|
// HasFilename returns true if filename was set
|
||||||
func (r *RecordExport) HasFilename() bool {
|
func (r *RecordExport) HasFilename() bool {
|
||||||
return r.hasFilename
|
return r.hasFilename
|
||||||
|
|||||||
Reference in New Issue
Block a user