From 48011e32da29b84e18139c81c8eb0e06530756f1 Mon Sep 17 00:00:00 2001 From: Adrien Gallou Date: Sun, 26 Feb 2023 16:47:54 +0100 Subject: [PATCH 1/3] =?UTF-8?q?premi=C3=A8re=20version=20du=20changement?= =?UTF-8?q?=20de=20point=20d'entr=C3=A9e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gotoggl/gotoggl.go | 146 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 136 insertions(+), 10 deletions(-) diff --git a/gotoggl/gotoggl.go b/gotoggl/gotoggl.go index 31ae5fc..6ddae5f 100644 --- a/gotoggl/gotoggl.go +++ b/gotoggl/gotoggl.go @@ -9,6 +9,8 @@ import ( "net/http" "strconv" "time" + "strings" + "io" ) var _ = json.Unmarshal @@ -40,22 +42,19 @@ func (d *Duration) UnmarshalJSON(data []byte) error { // TimeEntry contains the data returned for a single time entry. type TimeEntry struct { - Id int Description string - WorkspaceId int `json:"wid"` ProjectId int `json:"pid"` - Guid string - Billable bool Start time.Time - Stop time.Time - Duration Duration - DurOnly bool - UserId int `json:"uid"` - CreatedWith string `json:"created_with"` + Duration time.Duration Tags []string - At string } +type Me struct { + Id int + DefaultWorkspaceId int `json:"default_workspace_id"` +} + + // TimeEntryResponse is a wrapper for the data returned by /time_entries type TimeEntryResponse struct { Data TimeEntry @@ -81,9 +80,100 @@ func (tes *TimeEntriesService) Current() (TimeEntry, error) { return TimeEntry{}, nil } +type searchRequest struct { + EndDate string `json:"end_date"` + StartDate string `json:"start_date"` +} + + +type searchTimeEntry struct { + Description string + ProjectId int `json:"project_id"` + Billable bool + Start time.Time + TagIds []int `json:"tag_ids"` + TimeEntries []searchTimeEntryDetail `json:"time_entries"` +} + +type searchTimeEntryDetail struct { + Start time.Time + Seconds int `json:"seconds"` +} + + // Range returns time entries started in a specific time range. Only the first // 1000 found time entries are returned. There is no pagination. func (tes *TimeEntriesService) Range(start, end time.Time) ([]TimeEntry, error) { + // On récupère le workspace par défaut + me := Me{} + pathMe := fmt.Sprintf("me") + errMe := tes.client.GET(pathMe, &me) + if errMe != nil { + return nil, fmt.Errorf("Couldn't get me: %v\n", errMe) + } + + fmt.Println(fmt.Sprintf("Default workspace ID found : %d", me.DefaultWorkspaceId)) + + + aa := searchRequest{ + EndDate: end.Format("2006-01-02"), + StartDate: start.Format("2006-01-02"), + } + + s, errM := json.Marshal(aa) + if errM != nil { + return nil, errM + + } + + + searchTimeEntries := []searchTimeEntry{} +// t0 := start.Format(time.RFC3339) +// t1 := end.Format(time.RFC3339) + path := fmt.Sprintf("workspace/%d/search/time_entries", me.DefaultWorkspaceId) + err := tes.client.POST(path, strings.NewReader(string(s)), &searchTimeEntries) + if err != nil { + return nil, fmt.Errorf("Couldn't get time entries: %v\n", err) + } + +//fmt.Printf("%#v", searchTimeEntries) + + timeEntries := []TimeEntry{} + + for _, searchTimeEntry := range searchTimeEntries { + te := TimeEntry{} +//fmt.Printf("%#v", searchTimeEntry) + + + te.Description = searchTimeEntry.Description + te.ProjectId = searchTimeEntry.ProjectId + + for _, searchTimeEntryDetail := range searchTimeEntry.TimeEntries { +fmt.Println(fmt.Sprintf("-----------")) + + + + te.Start = searchTimeEntryDetail.Start +fmt.Println(fmt.Sprintf("%ds", searchTimeEntryDetail.Seconds)) + d, _ := time.ParseDuration(fmt.Sprintf("%ds", searchTimeEntryDetail.Seconds)) + te.Duration = d + } + +fmt.Println(fmt.Sprintf("%#v", searchTimeEntry.TagIds)) + + te.Tags = append(te.Tags, "Développement") //TODO corriger + + + timeEntries = append(timeEntries, te) + } + + + return timeEntries, nil + + + + +/* timeEntries := []TimeEntry{} t0 := start.Format(time.RFC3339) t1 := end.Format(time.RFC3339) @@ -93,6 +183,7 @@ func (tes *TimeEntriesService) Range(start, end time.Time) ([]TimeEntry, error) return nil, fmt.Errorf("Couldn't get time entries: %v\n", err) } return timeEntries, nil +*/ } type User struct { @@ -191,6 +282,41 @@ func (c *Client) GET(path string, response interface{}) error { return nil } +func (c *Client) POST(path string, body io.Reader, response interface{}) error { + if len(path) > 0 && path[0] == '/' { + log.Print("Warning: Do not include / at the start of path") + } +// url := TogglApi+path + url := "https://api.track.toggl.com/reports/api/v3/" + path +//log.Print(url) + + req, _ := http.NewRequest("POST", url, body) + + req.SetBasicAuth(c.ApiKey, "api_token") + resp, err := c.client.Do(req) + if err != nil { + return fmt.Errorf("GET couldn't do request %v: %v\n", path, err) + } + defer func() { + resp.Body.Close() + }() + buf, err := ioutil.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("GET to %v couldn't read response body: %v\n", req.URL, err) + } + if len(buf) == 0 { + return fmt.Errorf("GET to %v response had length zero.\n", req.URL) + } + if err := json.Unmarshal(buf, &response); err != nil { + return fmt.Errorf("GET couldn't unmarshal response: %v (Response was %v)\n", err, string(buf)) + } + if resp.StatusCode < 200 || resp.StatusCode >= 400 { + return fmt.Errorf("GET got wrong status code %v\n", resp.Status) + } + return nil +} + + /* type TogglTimeEntry struct { From e5d87293523ff57de4b8eb1c786ad00f1150a320 Mon Sep 17 00:00:00 2001 From: Adrien Gallou Date: Sun, 26 Feb 2023 17:21:37 +0100 Subject: [PATCH 2/3] =?UTF-8?q?on=20r=C3=A9cup=C3=A8re=20bien=20le=20libel?= =?UTF-8?q?l=C3=A9=20du=20tag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gotoggl/gotoggl.go | 45 +++++++++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/gotoggl/gotoggl.go b/gotoggl/gotoggl.go index 6ddae5f..1045aa0 100644 --- a/gotoggl/gotoggl.go +++ b/gotoggl/gotoggl.go @@ -98,9 +98,15 @@ type searchTimeEntry struct { type searchTimeEntryDetail struct { Start time.Time Seconds int `json:"seconds"` +} + +type TagItem struct { + Id int `json:"id"` + Name string `json:"name"` } + // Range returns time entries started in a specific time range. Only the first // 1000 found time entries are returned. There is no pagination. func (tes *TimeEntriesService) Range(start, end time.Time) ([]TimeEntry, error) { @@ -115,6 +121,14 @@ func (tes *TimeEntriesService) Range(start, end time.Time) ([]TimeEntry, error) fmt.Println(fmt.Sprintf("Default workspace ID found : %d", me.DefaultWorkspaceId)) + // on liste tous les ids + allTags := []TagItem{} + errTags := tes.client.GET(fmt.Sprintf("workspaces/%d/tags", me.DefaultWorkspaceId), &allTags) + if errTags != nil { + return nil, fmt.Errorf("Couldn't get tags: %v\n", errTags) + } + + // on prépare le body pour récupérer les time entries aa := searchRequest{ EndDate: end.Format("2006-01-02"), StartDate: start.Format("2006-01-02"), @@ -126,43 +140,46 @@ func (tes *TimeEntriesService) Range(start, end time.Time) ([]TimeEntry, error) } - + // on recherche les time entries searchTimeEntries := []searchTimeEntry{} -// t0 := start.Format(time.RFC3339) -// t1 := end.Format(time.RFC3339) path := fmt.Sprintf("workspace/%d/search/time_entries", me.DefaultWorkspaceId) err := tes.client.POST(path, strings.NewReader(string(s)), &searchTimeEntries) if err != nil { return nil, fmt.Errorf("Couldn't get time entries: %v\n", err) } -//fmt.Printf("%#v", searchTimeEntries) + // on met en forme les timeEntries timeEntries := []TimeEntry{} for _, searchTimeEntry := range searchTimeEntries { te := TimeEntry{} -//fmt.Printf("%#v", searchTimeEntry) - te.Description = searchTimeEntry.Description te.ProjectId = searchTimeEntry.ProjectId for _, searchTimeEntryDetail := range searchTimeEntry.TimeEntries { -fmt.Println(fmt.Sprintf("-----------")) - - - te.Start = searchTimeEntryDetail.Start -fmt.Println(fmt.Sprintf("%ds", searchTimeEntryDetail.Seconds)) d, _ := time.ParseDuration(fmt.Sprintf("%ds", searchTimeEntryDetail.Seconds)) te.Duration = d } -fmt.Println(fmt.Sprintf("%#v", searchTimeEntry.TagIds)) - - te.Tags = append(te.Tags, "Développement") //TODO corriger + var tags []string + for _, tagId := range searchTimeEntry.TagIds { + var tagName string + for _, allTag := range allTags { + if (allTag.Id == tagId) { + tagName = allTag.Name + } + } + if (0 == len(tagName)) { + panic(fmt.Sprintf("Name not found for tag id %d", tagId)) + } + + tags = append(tags, tagName) //TODO corriger + } + te.Tags = tags timeEntries = append(timeEntries, te) } From 77d0699381994997dc9f69af94a554caa81a5960 Mon Sep 17 00:00:00 2001 From: Adrien Gallou Date: Sun, 26 Feb 2023 17:30:48 +0100 Subject: [PATCH 3/3] nettoyage / factorisation --- gotoggl/gotoggl.go | 152 +++------------------------------------------ 1 file changed, 9 insertions(+), 143 deletions(-) diff --git a/gotoggl/gotoggl.go b/gotoggl/gotoggl.go index 1045aa0..cda8fcf 100644 --- a/gotoggl/gotoggl.go +++ b/gotoggl/gotoggl.go @@ -7,7 +7,6 @@ import ( "io/ioutil" "log" "net/http" - "strconv" "time" "strings" "io" @@ -24,22 +23,6 @@ const ( UserAgent = "github.com/roessland/gotoggl" ) -// Duration encapsulates the standard Duration in an anonymous field. Toggl -// returns durations in seconds, but time.Duration uses nanoseconds. Therefore -// we have to implement a custom UnmarshalJSON. -type Duration struct{ time.Duration } - -// UnmarshalJSON loads a Toggl duration into a Go duration. Toggl durations are -// given in seconds. -func (d *Duration) UnmarshalJSON(data []byte) error { - seconds, err := strconv.ParseInt(string(data), 10, 64) - if err != nil { - fmt.Errorf("Couldn't unmarshal toggl.Duration: %v\n", err) - } - d.Duration = time.Duration(seconds * int64(time.Second)) - return nil -} - // TimeEntry contains the data returned for a single time entry. type TimeEntry struct { Description string @@ -54,7 +37,6 @@ type Me struct { DefaultWorkspaceId int `json:"default_workspace_id"` } - // TimeEntryResponse is a wrapper for the data returned by /time_entries type TimeEntryResponse struct { Data TimeEntry @@ -68,24 +50,11 @@ type TimeEntriesService struct { client *Client } -// Get returns details of a single time entry -func (tes *TimeEntriesService) Get(id int) (TimeEntry, error) { - panic("Get() Not yet implemented") - return TimeEntry{}, nil -} - -// Current returns running time entry -func (tes *TimeEntriesService) Current() (TimeEntry, error) { - panic("Current() not yet implemented") - return TimeEntry{}, nil -} - type searchRequest struct { EndDate string `json:"end_date"` StartDate string `json:"start_date"` } - type searchTimeEntry struct { Description string ProjectId int `json:"project_id"` @@ -105,8 +74,6 @@ type TagItem struct { Name string `json:"name"` } - - // Range returns time entries started in a specific time range. Only the first // 1000 found time entries are returned. There is no pagination. func (tes *TimeEntriesService) Range(start, end time.Time) ([]TimeEntry, error) { @@ -143,7 +110,7 @@ func (tes *TimeEntriesService) Range(start, end time.Time) ([]TimeEntry, error) // on recherche les time entries searchTimeEntries := []searchTimeEntry{} path := fmt.Sprintf("workspace/%d/search/time_entries", me.DefaultWorkspaceId) - err := tes.client.POST(path, strings.NewReader(string(s)), &searchTimeEntries) + err := tes.client.POSTOnV3(path, strings.NewReader(string(s)), &searchTimeEntries) if err != nil { return nil, fmt.Errorf("Couldn't get time entries: %v\n", err) } @@ -176,7 +143,7 @@ func (tes *TimeEntriesService) Range(start, end time.Time) ([]TimeEntry, error) panic(fmt.Sprintf("Name not found for tag id %d", tagId)) } - tags = append(tags, tagName) //TODO corriger + tags = append(tags, tagName) } te.Tags = tags @@ -186,21 +153,6 @@ func (tes *TimeEntriesService) Range(start, end time.Time) ([]TimeEntry, error) return timeEntries, nil - - - - -/* - timeEntries := []TimeEntry{} - t0 := start.Format(time.RFC3339) - t1 := end.Format(time.RFC3339) - path := fmt.Sprintf("me/time_entries?start_date=%s&end_date=%s", t0, t1) - err := tes.client.GET(path, &timeEntries) - if err != nil { - return nil, fmt.Errorf("Couldn't get time entries: %v\n", err) - } - return timeEntries, nil -*/ } type User struct { @@ -268,46 +220,20 @@ func NewClient(apiKey string) *Client { return c } -// GET does a GET operation to the main API (not the reports API) and -// unmarshals the result into the given interface. func (c *Client) GET(path string, response interface{}) error { - if len(path) > 0 && path[0] == '/' { - log.Print("Warning: Do not include / at the start of path") - } - req, _ := http.NewRequest("GET", TogglApi+path, nil) - req.SetBasicAuth(c.ApiKey, "api_token") - resp, err := c.client.Do(req) - if err != nil { - return fmt.Errorf("GET couldn't do request %v: %v\n", path, err) - } - defer func() { - resp.Body.Close() - }() - buf, err := ioutil.ReadAll(resp.Body) - if err != nil { - return fmt.Errorf("GET to %v couldn't read response body: %v\n", req.URL, err) - } - if len(buf) == 0 { - return fmt.Errorf("GET to %v response had length zero.\n", req.URL) - } - if err := json.Unmarshal(buf, &response); err != nil { - return fmt.Errorf("GET couldn't unmarshal response: %v (Response was %v)\n", err, string(buf)) - } - if resp.StatusCode < 200 || resp.StatusCode >= 400 { - return fmt.Errorf("GET got wrong status code %v\n", resp.Status) - } - return nil + return c.genericDoRequest("GET", TogglApi, path, nil, response) } -func (c *Client) POST(path string, body io.Reader, response interface{}) error { +func (c *Client) POSTOnV3(path string, body io.Reader, response interface{}) error { + return c.genericDoRequest("POST", "https://api.track.toggl.com/reports/api/v3/", path, body, response) +} + +func (c *Client) genericDoRequest(method string, endpoint string, path string, body io.Reader, response interface{}) error { if len(path) > 0 && path[0] == '/' { log.Print("Warning: Do not include / at the start of path") } -// url := TogglApi+path - url := "https://api.track.toggl.com/reports/api/v3/" + path -//log.Print(url) - req, _ := http.NewRequest("POST", url, body) + req, _ := http.NewRequest(method, endpoint + path, body) req.SetBasicAuth(c.ApiKey, "api_token") resp, err := c.client.Do(req) @@ -334,64 +260,4 @@ func (c *Client) POST(path string, body io.Reader, response interface{}) error { } -/* -type TogglTimeEntry struct { - Id int - Description string - WorkspaceId int `json:"wid"` - ProjectId int `json:"pid"` - Guid string - Billable bool - Start time.Time - Stop time.Time - Duration int - DurOnly bool - UserId int `json:"uid"` - CreatedWith string `json:"created_with"` - Tags []string - At string -} - -type TogglTimeEntryResponse struct { - Data TogglTimeEntry -} - -type TogglProject struct { - ID int - GUID string - WID int - CID int - Name string - Billable bool - IsPrivate bool `json:"is_private"` - Active bool - Template bool - At time.Time - CreatedAt time.Time `json:"created_at"` - Color string - AutoEstimates bool `json:"auto_estimates"` - ActualHours int `json:"actual_hours"` -} - -type TogglProjectResponse struct { - Data TogglProject -} - -type TogglProjectSummary struct { - Id int - // Items []??? - Time int // Duration in milliseconds - Title struct { - Client string - Color string - HexColor string `json:"hex_color"` - Project string - } - // TotalCurrencies []Currency `json:"total_currencies"` -} - -type TogglProjectSummariesResponse struct { - Data []TogglProjectSummary -} -*/