the export file name uses browser time zone, the transaction time in exported file uses time zone in transaction

This commit is contained in:
MaysWind
2021-03-25 23:36:52 +08:00
parent 62d4ba605d
commit ffcbca01d9
9 changed files with 59 additions and 14 deletions
+1
View File
@@ -157,6 +157,7 @@ func startWebServer(c *cli.Context) error {
if config.EnableDataExport {
dataRoute := apiRoute.Group("/data")
dataRoute.Use(bindMiddleware(middlewares.HeaderInQueryString))
dataRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString))
{
dataRoute.GET("/export.csv", bindCsv(api.DataManagements.ExportDataHandler))
+13 -4
View File
@@ -47,6 +47,15 @@ func (a *DataManagementsApi) ExportDataHandler(c *core.Context) ([]byte, string,
return nil, "", errs.ErrDataExportNotAllowed
}
timezone := time.Local
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.WarnfWithRequestId(c, "[data_managements.ExportDataHandler] cannot get client timezone offset, because %s", err.Error())
} else {
timezone = time.FixedZone("Client Timezone", int(utcOffset)*60)
}
uid := c.GetCurrentUid()
accounts, err := a.accounts.GetAllAccountsByUid(uid)
@@ -88,14 +97,14 @@ func (a *DataManagementsApi) ExportDataHandler(c *core.Context) ([]byte, string,
return nil, "", errs.ErrOperationFailed
}
result, err := a.exporter.GetOutputContent(uid, allTransactions, accountMap, categoryMap, tagMap, tagIndexs)
result, err := a.exporter.GetOutputContent(uid, timezone, allTransactions, accountMap, categoryMap, tagMap, tagIndexs)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get csv format exported data for \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.Or(err, errs.ErrOperationFailed)
}
fileName := a.getFileName()
fileName := a.getFileName(timezone)
return result, fileName, nil
}
@@ -150,8 +159,8 @@ func (a *DataManagementsApi) ClearDataHandler(c *core.Context) (interface{}, *er
return true, nil
}
func (a *DataManagementsApi) getFileName() string {
currentTime := utils.FormatToLongDateTimeWithoutSecond(time.Now())
func (a *DataManagementsApi) getFileName(timezone *time.Location) string {
currentTime := utils.FormatUnixTimeToLongDateTimeWithoutSecond(time.Now().Unix(), timezone)
currentTime = strings.Replace(currentTime, "-", "_", -1)
currentTime = strings.Replace(currentTime, " ", "_", -1)
currentTime = strings.Replace(currentTime, ":", "_", -1)
+3 -1
View File
@@ -1,6 +1,8 @@
package cli
import (
"time"
"github.com/urfave/cli/v2"
"github.com/mayswind/lab/pkg/errs"
@@ -176,7 +178,7 @@ func (a *UserDataCli) ExportTransaction(c *cli.Context, uid int64) ([]byte, erro
return nil, err
}
result, err := a.csvExporter.GetOutputContent(uid, allTransactions, accountMap, categoryMap, tagMap, tagIndexs)
result, err := a.csvExporter.GetOutputContent(uid, time.Local, allTransactions, accountMap, categoryMap, tagMap, tagIndexs)
if err != nil {
log.BootErrorf("[user_data.ExportTransaction] failed to get csv format exported data for \"uid:%d\", because %s", uid, err.Error())
+2 -2
View File
@@ -12,7 +12,7 @@ const requestIdFieldKey = "REQUEST_ID"
const tokenClaimsFieldKey = "TOKEN_CLAIMS"
const responseErrorFieldKey = "RESPONSE_ERROR"
const clientTimezoneOffsetHeaderName = "X-Timezone-Offset"
const ClientTimezoneOffsetHeaderName = "X-Timezone-Offset"
// Context represents the request and response context
type Context struct {
@@ -71,7 +71,7 @@ func (c *Context) GetCurrentUid() int64 {
// GetClientTimezoneOffset returns the client timezone offset
func (c *Context) GetClientTimezoneOffset() (int16, error) {
value := c.GetHeader(clientTimezoneOffsetHeaderName)
value := c.GetHeader(ClientTimezoneOffsetHeaderName)
offset, err := strconv.Atoi(value)
if err != nil {
+4 -2
View File
@@ -3,6 +3,7 @@ package exporters
import (
"fmt"
"strings"
"time"
"github.com/mayswind/lab/pkg/models"
"github.com/mayswind/lab/pkg/utils"
@@ -17,7 +18,7 @@ const csvHeaderLine = "Time,Type,Category,Sub Category,Account,Amount,Account2,A
const csvDataLineFormat = "%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n"
// GetOutputContent returns the exported csv data
func (e *CSVFileExporter) GetOutputContent(uid int64, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexs map[int64][]int64) ([]byte, error) {
func (e *CSVFileExporter) GetOutputContent(uid int64, timezone *time.Location, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexs map[int64][]int64) ([]byte, error) {
var ret strings.Builder
ret.Grow(len(transactions) * 100)
@@ -30,7 +31,8 @@ func (e *CSVFileExporter) GetOutputContent(uid int64, transactions []*models.Tra
continue
}
transactionTime := utils.FormatToLongDateTimeWithoutSecond(utils.ParseFromUnixTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime)))
transactionTimeZone := time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60)
transactionTime := utils.FormatUnixTimeToLongDateTimeWithoutSecond(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime), transactionTimeZone)
transactionType := e.getTransactionTypeName(transaction.Type)
category := e.getTransactionCategoryName(transaction.CategoryId, categoryMap)
subCategory := e.getTransactionSubCategoryName(transaction.CategoryId, categoryMap)
+6 -2
View File
@@ -1,9 +1,13 @@
package exporters
import "github.com/mayswind/lab/pkg/models"
import (
"time"
"github.com/mayswind/lab/pkg/models"
)
// DataExporter defines the structure of data exporter
type DataExporter interface {
// GetOutputContent returns the exported data
GetOutputContent(uid int64, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexs map[int64][]int64) ([]byte, error)
GetOutputContent(uid int64, timezone *time.Location, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexs map[int64][]int64) ([]byte, error)
}
+16
View File
@@ -0,0 +1,16 @@
package middlewares
import (
"github.com/mayswind/lab/pkg/core"
)
const utcOffsetQueryStringParam = "utc_offset"
// HeaderInQueryString puts some headers from query string
func HeaderInQueryString(c *core.Context) {
utcOffset, exists := c.GetQuery(utcOffsetQueryStringParam)
if exists {
c.Request.Header.Set(core.ClientTimezoneOffsetHeaderName, utcOffset)
}
}
+8 -2
View File
@@ -12,8 +12,14 @@ func FormatUnixTimeToLongDateTimeInServerTimezone(unixTime int64) string {
return ParseFromUnixTime(unixTime).Format(longDateTimeFormat)
}
// FormatToLongDateTimeWithoutSecond returns a textual representation of the time value formatted by long date time format (no second)
func FormatToLongDateTimeWithoutSecond(t time.Time) string {
// FormatUnixTimeToLongDateTimeWithoutSecond returns a textual representation of the unix time formatted by long date time format (no second)
func FormatUnixTimeToLongDateTimeWithoutSecond(unixTime int64, timezone *time.Location) string {
t := ParseFromUnixTime(unixTime)
if timezone != nil {
t = t.In(timezone)
}
return t.Format(longDateTimeWithoutSecondFormat)
}
+6 -1
View File
@@ -5,7 +5,7 @@
<f7-card>
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-button external no-chevron target="_blank" :link="`${$constants.api.baseUrlPath}/data/export.csv?token=${$user.getToken()}`">{{ $t('Export Data') }}</f7-list-button>
<f7-list-button external no-chevron target="_blank" :link="`${$constants.api.baseUrlPath}/data/export.csv?token=${$user.getToken()}&utc_offset=${currentTimezoneOffsetMinutes}`">{{ $t('Export Data') }}</f7-list-button>
<f7-list-button color="red" @click="clearData(null)">{{ $t('Clear User Data') }}</f7-list-button>
</f7-list>
</f7-card-content>
@@ -31,6 +31,11 @@ export default {
showInputPasswordSheetForClearData: false,
};
},
computed: {
currentTimezoneOffsetMinutes() {
return this.$utilities.getTimezoneOffsetMinutes();
}
},
methods: {
clearData(password) {
const self = this;