the export file name uses browser time zone, the transaction time in exported file uses time zone in transaction
This commit is contained in:
@@ -157,6 +157,7 @@ func startWebServer(c *cli.Context) error {
|
|||||||
|
|
||||||
if config.EnableDataExport {
|
if config.EnableDataExport {
|
||||||
dataRoute := apiRoute.Group("/data")
|
dataRoute := apiRoute.Group("/data")
|
||||||
|
dataRoute.Use(bindMiddleware(middlewares.HeaderInQueryString))
|
||||||
dataRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString))
|
dataRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString))
|
||||||
{
|
{
|
||||||
dataRoute.GET("/export.csv", bindCsv(api.DataManagements.ExportDataHandler))
|
dataRoute.GET("/export.csv", bindCsv(api.DataManagements.ExportDataHandler))
|
||||||
|
|||||||
@@ -47,6 +47,15 @@ func (a *DataManagementsApi) ExportDataHandler(c *core.Context) ([]byte, string,
|
|||||||
return nil, "", errs.ErrDataExportNotAllowed
|
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()
|
uid := c.GetCurrentUid()
|
||||||
|
|
||||||
accounts, err := a.accounts.GetAllAccountsByUid(uid)
|
accounts, err := a.accounts.GetAllAccountsByUid(uid)
|
||||||
@@ -88,14 +97,14 @@ func (a *DataManagementsApi) ExportDataHandler(c *core.Context) ([]byte, string,
|
|||||||
return nil, "", errs.ErrOperationFailed
|
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 {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get csv format exported data for \"uid:%d\", because %s", uid, err.Error())
|
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)
|
return nil, "", errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
fileName := a.getFileName()
|
fileName := a.getFileName(timezone)
|
||||||
|
|
||||||
return result, fileName, nil
|
return result, fileName, nil
|
||||||
}
|
}
|
||||||
@@ -150,8 +159,8 @@ func (a *DataManagementsApi) ClearDataHandler(c *core.Context) (interface{}, *er
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *DataManagementsApi) getFileName() string {
|
func (a *DataManagementsApi) getFileName(timezone *time.Location) string {
|
||||||
currentTime := utils.FormatToLongDateTimeWithoutSecond(time.Now())
|
currentTime := utils.FormatUnixTimeToLongDateTimeWithoutSecond(time.Now().Unix(), timezone)
|
||||||
currentTime = strings.Replace(currentTime, "-", "_", -1)
|
currentTime = strings.Replace(currentTime, "-", "_", -1)
|
||||||
currentTime = strings.Replace(currentTime, " ", "_", -1)
|
currentTime = strings.Replace(currentTime, " ", "_", -1)
|
||||||
currentTime = strings.Replace(currentTime, ":", "_", -1)
|
currentTime = strings.Replace(currentTime, ":", "_", -1)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
|
||||||
"github.com/mayswind/lab/pkg/errs"
|
"github.com/mayswind/lab/pkg/errs"
|
||||||
@@ -176,7 +178,7 @@ func (a *UserDataCli) ExportTransaction(c *cli.Context, uid int64) ([]byte, erro
|
|||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
log.BootErrorf("[user_data.ExportTransaction] failed to get csv format exported data for \"uid:%d\", because %s", uid, err.Error())
|
log.BootErrorf("[user_data.ExportTransaction] failed to get csv format exported data for \"uid:%d\", because %s", uid, err.Error())
|
||||||
|
|||||||
+2
-2
@@ -12,7 +12,7 @@ const requestIdFieldKey = "REQUEST_ID"
|
|||||||
const tokenClaimsFieldKey = "TOKEN_CLAIMS"
|
const tokenClaimsFieldKey = "TOKEN_CLAIMS"
|
||||||
const responseErrorFieldKey = "RESPONSE_ERROR"
|
const responseErrorFieldKey = "RESPONSE_ERROR"
|
||||||
|
|
||||||
const clientTimezoneOffsetHeaderName = "X-Timezone-Offset"
|
const ClientTimezoneOffsetHeaderName = "X-Timezone-Offset"
|
||||||
|
|
||||||
// Context represents the request and response context
|
// Context represents the request and response context
|
||||||
type Context struct {
|
type Context struct {
|
||||||
@@ -71,7 +71,7 @@ func (c *Context) GetCurrentUid() int64 {
|
|||||||
|
|
||||||
// GetClientTimezoneOffset returns the client timezone offset
|
// GetClientTimezoneOffset returns the client timezone offset
|
||||||
func (c *Context) GetClientTimezoneOffset() (int16, error) {
|
func (c *Context) GetClientTimezoneOffset() (int16, error) {
|
||||||
value := c.GetHeader(clientTimezoneOffsetHeaderName)
|
value := c.GetHeader(ClientTimezoneOffsetHeaderName)
|
||||||
offset, err := strconv.Atoi(value)
|
offset, err := strconv.Atoi(value)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package exporters
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/mayswind/lab/pkg/models"
|
"github.com/mayswind/lab/pkg/models"
|
||||||
"github.com/mayswind/lab/pkg/utils"
|
"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"
|
const csvDataLineFormat = "%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n"
|
||||||
|
|
||||||
// GetOutputContent returns the exported csv data
|
// 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
|
var ret strings.Builder
|
||||||
|
|
||||||
ret.Grow(len(transactions) * 100)
|
ret.Grow(len(transactions) * 100)
|
||||||
@@ -30,7 +31,8 @@ func (e *CSVFileExporter) GetOutputContent(uid int64, transactions []*models.Tra
|
|||||||
continue
|
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)
|
transactionType := e.getTransactionTypeName(transaction.Type)
|
||||||
category := e.getTransactionCategoryName(transaction.CategoryId, categoryMap)
|
category := e.getTransactionCategoryName(transaction.CategoryId, categoryMap)
|
||||||
subCategory := e.getTransactionSubCategoryName(transaction.CategoryId, categoryMap)
|
subCategory := e.getTransactionSubCategoryName(transaction.CategoryId, categoryMap)
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
package exporters
|
package exporters
|
||||||
|
|
||||||
import "github.com/mayswind/lab/pkg/models"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mayswind/lab/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
// DataExporter defines the structure of data exporter
|
// DataExporter defines the structure of data exporter
|
||||||
type DataExporter interface {
|
type DataExporter interface {
|
||||||
// GetOutputContent returns the exported data
|
// 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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,8 +12,14 @@ func FormatUnixTimeToLongDateTimeInServerTimezone(unixTime int64) string {
|
|||||||
return ParseFromUnixTime(unixTime).Format(longDateTimeFormat)
|
return ParseFromUnixTime(unixTime).Format(longDateTimeFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FormatToLongDateTimeWithoutSecond returns a textual representation of the time value formatted by long date time format (no second)
|
// FormatUnixTimeToLongDateTimeWithoutSecond returns a textual representation of the unix time formatted by long date time format (no second)
|
||||||
func FormatToLongDateTimeWithoutSecond(t time.Time) string {
|
func FormatUnixTimeToLongDateTimeWithoutSecond(unixTime int64, timezone *time.Location) string {
|
||||||
|
t := ParseFromUnixTime(unixTime)
|
||||||
|
|
||||||
|
if timezone != nil {
|
||||||
|
t = t.In(timezone)
|
||||||
|
}
|
||||||
|
|
||||||
return t.Format(longDateTimeWithoutSecondFormat)
|
return t.Format(longDateTimeWithoutSecondFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<f7-card>
|
<f7-card>
|
||||||
<f7-card-content class="no-safe-areas" :padding="false">
|
<f7-card-content class="no-safe-areas" :padding="false">
|
||||||
<f7-list>
|
<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-button color="red" @click="clearData(null)">{{ $t('Clear User Data') }}</f7-list-button>
|
||||||
</f7-list>
|
</f7-list>
|
||||||
</f7-card-content>
|
</f7-card-content>
|
||||||
@@ -31,6 +31,11 @@ export default {
|
|||||||
showInputPasswordSheetForClearData: false,
|
showInputPasswordSheetForClearData: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
currentTimezoneOffsetMinutes() {
|
||||||
|
return this.$utilities.getTimezoneOffsetMinutes();
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
clearData(password) {
|
clearData(password) {
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|||||||
Reference in New Issue
Block a user