This is an automated email from the ASF dual-hosted git repository.

klesh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git


The following commit(s) were added to refs/heads/main by this push:
     new 2edda2047 feat: generate JQL with user's time zone (#4954)
2edda2047 is described below

commit 2edda2047120af1f4995d9139f25d867a1895025
Author: Liang Zhang <[email protected]>
AuthorDate: Sun Apr 23 15:49:51 2023 +0800

    feat: generate JQL with user's time zone (#4954)
---
 backend/core/runner/db.go                          |  10 +-
 backend/core/runner/db_test.go                     |  68 ++++++++++++++
 backend/plugins/jira/tasks/issue_collector.go      | 101 +++++++++++++++++++--
 backend/plugins/jira/tasks/issue_collector_test.go |  22 ++++-
 4 files changed, 186 insertions(+), 15 deletions(-)

diff --git a/backend/core/runner/db.go b/backend/core/runner/db.go
index b5ef01bb0..bca6cc153 100644
--- a/backend/core/runner/db.go
+++ b/backend/core/runner/db.go
@@ -85,7 +85,7 @@ func NewGormDbEx(configReader config.ConfigReader, logger 
log.Logger, sessionCon
        var db *gorm.DB
        switch strings.ToLower(u.Scheme) {
        case "mysql":
-               dbUrl = fmt.Sprintf("%s@tcp(%s)%s?%s", getUserString(u), 
u.Host, u.Path, u.RawQuery)
+               dbUrl = fmt.Sprintf("%s@tcp(%s)%s?%s", getUserString(u), 
u.Host, u.Path, addLocal(u.Query()))
                db, err = gorm.Open(mysql.Open(dbUrl), dbConfig)
        case "postgresql", "postgres", "pg":
                db, err = gorm.Open(postgres.Open(dbUrl), dbConfig)
@@ -114,3 +114,11 @@ func getUserString(u *url.URL) string {
        }
        return userString
 }
+
+// addLocal adds loc=Local to the query string if it's not already there
+func addLocal(query url.Values) string {
+       if query.Get("loc") == "" {
+               query.Set("loc", "Local")
+       }
+       return query.Encode()
+}
diff --git a/backend/core/runner/db_test.go b/backend/core/runner/db_test.go
new file mode 100644
index 000000000..550a8fc3e
--- /dev/null
+++ b/backend/core/runner/db_test.go
@@ -0,0 +1,68 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package runner
+
+import (
+       "net/url"
+       "testing"
+)
+
+func Test_addLocal(t *testing.T) {
+       values, _ := url.ParseQuery("charset=utf8mb4&parseTime=True")
+       type args struct {
+               query url.Values
+       }
+       tests := []struct {
+               name string
+               args args
+               want string
+       }{
+               {
+                       name: "test add local",
+                       args: args{
+                               query: values,
+                       },
+                       want: "charset=utf8mb4&loc=Local&parseTime=True",
+               },
+               {
+                       name: "test add local",
+                       args: args{
+                               query: url.Values{
+                                       "local": []string{"abc"},
+                               },
+                       },
+                       want: "loc=Local&local=abc",
+               },
+               {
+                       name: "test add local",
+                       args: args{
+                               query: url.Values{
+                                       "loc": []string{"abc"},
+                               },
+                       },
+                       want: "loc=abc",
+               },
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       if got := addLocal(tt.args.query); got != tt.want {
+                               t.Errorf("addLocal() = %v, want %v", got, 
tt.want)
+                       }
+               })
+       }
+}
diff --git a/backend/plugins/jira/tasks/issue_collector.go 
b/backend/plugins/jira/tasks/issue_collector.go
index 64d2d77e2..19f9259c9 100644
--- a/backend/plugins/jira/tasks/issue_collector.go
+++ b/backend/plugins/jira/tasks/issue_collector.go
@@ -20,6 +20,8 @@ package tasks
 import (
        "encoding/json"
        "fmt"
+       "github.com/apache/incubator-devlake/core/dal"
+       "github.com/apache/incubator-devlake/plugins/jira/models"
        "io"
        "net/http"
        "net/url"
@@ -44,7 +46,7 @@ var CollectIssuesMeta = plugin.SubTaskMeta{
 
 func CollectIssues(taskCtx plugin.SubTaskContext) errors.Error {
        data := taskCtx.GetData().(*JiraTaskData)
-
+       logger := taskCtx.GetLogger()
        collectorWithState, err := 
api.NewStatefulApiCollector(api.RawDataSubTaskArgs{
                Ctx: taskCtx,
                /*
@@ -68,7 +70,13 @@ func CollectIssues(taskCtx plugin.SubTaskContext) 
errors.Error {
        // IMPORTANT: we have to keep paginated data in a consistence order to 
avoid data-missing, if we sort issues by
        //  `updated`, issue will be jumping between pages if it got updated 
during the collection process
        incremental := collectorWithState.IsIncremental()
-       jql := buildJQL(data.TimeAfter, 
collectorWithState.LatestState.LatestSuccessStart, incremental)
+       loc, err := getTimeZone(taskCtx)
+       if err != nil {
+               logger.Info("failed to get timezone, err: %v", err)
+       } else {
+               logger.Info("got user's timezone: %v", loc.String())
+       }
+       jql := buildJQL(data.TimeAfter, 
collectorWithState.LatestState.LatestSuccessStart, incremental, loc)
 
        err = collectorWithState.InitCollector(api.ApiCollectorArgs{
                ApiClient:   data.ApiClient,
@@ -137,22 +145,95 @@ func CollectIssues(taskCtx plugin.SubTaskContext) 
errors.Error {
 }
 
 // buildJQL build jql based on timeAfter and incremental mode
-func buildJQL(timeAfter, latestSuccessStart *time.Time, isIncremental bool) 
string {
+func buildJQL(timeAfter, latestSuccessStart *time.Time, isIncremental bool, 
location *time.Location) string {
        jql := "ORDER BY created ASC"
        var moment time.Time
        if timeAfter != nil {
                moment = *timeAfter
        }
        // if isIncremental is true, we should not collect data before 
latestSuccessStart
-       if isIncremental {
-               // subtract 24 hours to avoid missing data due to time zone 
difference
-               latest := latestSuccessStart.Add(-24 * time.Hour)
-               if latest.After(moment) {
-                       moment = latest
-               }
+       if isIncremental && latestSuccessStart.After(moment) {
+               moment = *latestSuccessStart
        }
        if !moment.IsZero() {
-               jql = fmt.Sprintf("updated >= '%s' %s", 
moment.In(time.UTC).Format("2006/01/02 15:04"), jql)
+               if location != nil {
+                       moment = moment.In(location)
+               } else {
+                       moment = moment.In(time.UTC).Add(-24 * time.Hour)
+               }
+               jql = fmt.Sprintf("updated >= '%s' %s", 
moment.Format("2006/01/02 15:04"), jql)
        }
        return jql
 }
+
+// getTimeZone get user's timezone from jira API
+func getTimeZone(taskCtx plugin.SubTaskContext) (*time.Location, errors.Error) 
{
+       data := taskCtx.GetData().(*JiraTaskData)
+       connectionId := data.Options.ConnectionId
+       var conn models.JiraConnection
+       err := taskCtx.GetDal().First(&conn, dal.Where("id = ?", connectionId))
+       if err != nil {
+               return nil, err
+       }
+       var resp *http.Response
+       var path string
+       var query url.Values
+       if data.JiraServerInfo.DeploymentType == models.DeploymentServer {
+               path = "api/2/user"
+               query = url.Values{"username": []string{conn.Username}}
+       } else {
+               path = "api/3/user"
+               var accountId string
+               accountId, err = getAccountId(data.ApiClient, conn.Username)
+               if err != nil {
+                       return nil, err
+               }
+               query = url.Values{"accountId": []string{accountId}}
+       }
+       resp, err = data.ApiClient.Get(path, query, nil)
+       if err != nil {
+               return nil, err
+       }
+       defer resp.Body.Close()
+       var timeZone struct {
+               TimeZone string `json:"timeZone"`
+       }
+       err = errors.Convert(json.NewDecoder(resp.Body).Decode(&timeZone))
+       if err != nil {
+               return nil, err
+       }
+       tz, err := errors.Convert01(time.LoadLocation(timeZone.TimeZone))
+       if err != nil {
+               return nil, err
+       }
+       if tz == nil {
+               return nil, errors.Default.New(fmt.Sprintf("invalid time zone: 
%s", timeZone.TimeZone))
+       }
+       return tz, nil
+}
+
+func getAccountId(client *api.ApiAsyncClient, username string) (string, 
errors.Error) {
+       resp, err := client.Get("api/3/user/picker", url.Values{"query": 
[]string{username}}, nil)
+       if err != nil {
+               return "", err
+       }
+       defer resp.Body.Close()
+       var accounts struct {
+               Users []struct {
+                       AccountID   string `json:"accountId"`
+                       AccountType string `json:"accountType"`
+                       HTML        string `json:"html"`
+                       DisplayName string `json:"displayName"`
+               } `json:"users"`
+               Total  int    `json:"total"`
+               Header string `json:"header"`
+       }
+       err = errors.Convert(json.NewDecoder(resp.Body).Decode(&accounts))
+       if err != nil {
+               return "", err
+       }
+       if len(accounts.Users) == 0 {
+               return "", errors.Default.New("no user found")
+       }
+       return accounts.Users[0].AccountID, nil
+}
diff --git a/backend/plugins/jira/tasks/issue_collector_test.go 
b/backend/plugins/jira/tasks/issue_collector_test.go
index 3dba84ed1..c9891943d 100644
--- a/backend/plugins/jira/tasks/issue_collector_test.go
+++ b/backend/plugins/jira/tasks/issue_collector_test.go
@@ -27,10 +27,12 @@ func Test_buildJQL(t *testing.T) {
        timeAfter := base
        add48 := base.Add(48 * time.Hour)
        minus48 := base.Add(-48 * time.Hour)
+       loc, _ := time.LoadLocation("Asia/Shanghai")
        type args struct {
                timeAfter          *time.Time
                latestSuccessStart *time.Time
                isIncremental      bool
+               location           *time.Location
        }
        tests := []struct {
                name string
@@ -51,8 +53,9 @@ func Test_buildJQL(t *testing.T) {
                                timeAfter:          nil,
                                latestSuccessStart: &add48,
                                isIncremental:      true,
+                               location:           loc,
                        },
-                       want: "updated >= '2021/02/04 04:05' ORDER BY created 
ASC",
+                       want: "updated >= '2021/02/05 12:05' ORDER BY created 
ASC",
                },
                {
                        name: "test incremental",
@@ -60,8 +63,9 @@ func Test_buildJQL(t *testing.T) {
                                timeAfter:          &base,
                                latestSuccessStart: nil,
                                isIncremental:      false,
+                               location:           loc,
                        },
-                       want: "updated >= '2021/02/03 04:05' ORDER BY created 
ASC",
+                       want: "updated >= '2021/02/03 12:05' ORDER BY created 
ASC",
                },
                {
                        name: "test incremental",
@@ -72,6 +76,16 @@ func Test_buildJQL(t *testing.T) {
                        },
                        want: "updated >= '2021/02/04 04:05' ORDER BY created 
ASC",
                },
+               {
+                       name: "test incremental",
+                       args: args{
+                               timeAfter:          &timeAfter,
+                               latestSuccessStart: &add48,
+                               isIncremental:      true,
+                               location:           loc,
+                       },
+                       want: "updated >= '2021/02/05 12:05' ORDER BY created 
ASC",
+               },
                {
                        name: "test incremental",
                        args: args{
@@ -79,13 +93,13 @@ func Test_buildJQL(t *testing.T) {
                                latestSuccessStart: &minus48,
                                isIncremental:      true,
                        },
-                       want: "updated >= '2021/02/03 04:05' ORDER BY created 
ASC",
+                       want: "updated >= '2021/02/02 04:05' ORDER BY created 
ASC",
                },
        }
 
        for _, tt := range tests {
                t.Run(tt.name, func(t *testing.T) {
-                       if got := buildJQL(tt.args.timeAfter, 
tt.args.latestSuccessStart, tt.args.isIncremental); got != tt.want {
+                       if got := buildJQL(tt.args.timeAfter, 
tt.args.latestSuccessStart, tt.args.isIncremental, tt.args.location); got != 
tt.want {
                                t.Errorf("buildJQL() = %v, want %v", got, 
tt.want)
                        }
                })

Reply via email to